트랜잭션 및 명령어

솔라나에서 사용자는 네트워크와 상호작용하기 위해 트랜잭션을 전송합니다. 트랜잭션은 처리할 작업을 지정하는 하나 이상의 명령어를 포함합니다. 명령어의 실행 로직은 솔라나 네트워크에 배포된 프로그램에 저장되며, 각 프로그램은 자체적인 명령어 세트를 정의합니다.

다음은 솔라나 트랜잭션 처리에 관한 주요 세부 사항입니다:

  • 트랜잭션에 여러 명령어가 포함된 경우, 명령어는 트랜잭션에 추가된 순서대로 실행됩니다.
  • 트랜잭션은 "원자적"입니다 - 모든 명령어가 성공적으로 처리되어야 하며, 그렇지 않으면 전체 트랜잭션이 실패하고 어떤 변경도 발생하지 않습니다.

트랜잭션은 본질적으로 하나 이상의 명령어를 처리하기 위한 요청입니다.

트랜잭션 간소화트랜잭션 간소화

트랜잭션은 양식이 담긴 봉투와 같습니다. 각 양식은 네트워크에 무엇을 해야 할지 알려주는 명령어입니다. 트랜잭션을 보내는 것은 양식을 처리하기 위해 봉투를 우편으로 보내는 것과 같습니다.

주요 포인트

  • 솔라나 트랜잭션은 네트워크의 프로그램을 호출하는 명령어를 포함합니다.
  • 트랜잭션은 원자적입니다 - 어떤 명령어라도 실패하면 전체 트랜잭션이 실패하고 어떤 변경도 발생하지 않습니다.
  • 트랜잭션의 명령어는 순차적으로 실행됩니다.
  • 트랜잭션 크기 제한은 1232 바이트입니다.
  • 각 명령어는 세 가지 정보가 필요합니다:
    1. 호출할 프로그램의 주소
    2. 명령어가 읽거나 쓰는 계정
    3. 명령어에 필요한 추가 데이터(예: 함수 인수)

SOL 전송 예제

아래 다이어그램은 발신자에서 수신자로 SOL을 전송하는 단일 명령어가 포함된 트랜잭션을 나타냅니다.

솔라나에서 "지갑"은 시스템 프로그램이 소유한 계정입니다. 프로그램 소유자만 계정 데이터를 변경할 수 있으므로, SOL을 전송하려면 시스템 프로그램을 호출하는 트랜잭션을 보내야 합니다.

SOL 전송SOL 전송

발신자 계정은 시스템 프로그램이 자신의 lamport 잔액을 차감할 수 있도록 트랜잭션에 서명(is_signer)해야 합니다. 발신자와 수신자 계정은 lamport 잔액이 변경되므로 쓰기 가능(is_writable)해야 합니다.

트랜잭션을 보낸 후, 시스템 프로그램이 전송 명령어를 처리합니다. 그런 다음 시스템 프로그램은 발신자와 수신자 계정 모두의 lamport 잔액을 업데이트합니다.

SOL 전송 프로세스SOL 전송 프로세스

아래 예제는 한 계정에서 다른 계정으로 SOL을 전송하는 트랜잭션을 보내는 방법을 보여줍니다.

import {
airdropFactory,
appendTransactionMessageInstructions,
createSolanaRpc,
createSolanaRpcSubscriptions,
createTransactionMessage,
generateKeyPairSigner,
getSignatureFromTransaction,
lamports,
pipe,
sendAndConfirmTransactionFactory,
setTransactionMessageFeePayerSigner,
setTransactionMessageLifetimeUsingBlockhash,
signTransactionMessageWithSigners
} from "@solana/kit";
import { getTransferSolInstruction } from "@solana-program/system";
// Create a connection to cluster
const rpc = createSolanaRpc("http://localhost:8899");
const rpcSubscriptions = createSolanaRpcSubscriptions("ws://localhost:8900");
// Generate sender and recipient keypairs
const sender = await generateKeyPairSigner();
const recipient = await generateKeyPairSigner();
const LAMPORTS_PER_SOL = 1_000_000_000n;
const transferAmount = lamports(LAMPORTS_PER_SOL / 100n); // 0.01 SOL
// Fund sender with airdrop
await airdropFactory({ rpc, rpcSubscriptions })({
recipientAddress: sender.address,
lamports: lamports(LAMPORTS_PER_SOL), // 1 SOL
commitment: "confirmed"
});
// Check balance before transfer
const { value: preBalance1 } = await rpc.getBalance(sender.address).send();
const { value: preBalance2 } = await rpc.getBalance(recipient.address).send();
// Create a transfer instruction for transferring SOL from sender to recipient
const transferInstruction = getTransferSolInstruction({
source: sender,
destination: recipient.address,
amount: transferAmount // 0.01 SOL in lamports
});
// Add the transfer instruction to a new transaction
const { value: latestBlockhash } = await rpc.getLatestBlockhash().send();
const transactionMessage = pipe(
createTransactionMessage({ version: 0 }),
(tx) => setTransactionMessageFeePayerSigner(sender, tx),
(tx) => setTransactionMessageLifetimeUsingBlockhash(latestBlockhash, tx),
(tx) => appendTransactionMessageInstructions([transferInstruction], tx)
);
// Send the transaction to the network
const signedTransaction =
await signTransactionMessageWithSigners(transactionMessage);
await sendAndConfirmTransactionFactory({ rpc, rpcSubscriptions })(
signedTransaction,
{ commitment: "confirmed" }
);
const transactionSignature = getSignatureFromTransaction(signedTransaction);
// Check balance after transfer
const { value: postBalance1 } = await rpc.getBalance(sender.address).send();
const { value: postBalance2 } = await rpc.getBalance(recipient.address).send();
console.log(
"Sender prebalance:",
Number(preBalance1) / Number(LAMPORTS_PER_SOL)
);
console.log(
"Recipient prebalance:",
Number(preBalance2) / Number(LAMPORTS_PER_SOL)
);
console.log(
"Sender postbalance:",
Number(postBalance1) / Number(LAMPORTS_PER_SOL)
);
console.log(
"Recipient postbalance:",
Number(postBalance2) / Number(LAMPORTS_PER_SOL)
);
console.log("Transaction Signature:", transactionSignature);
Click to execute the code.

클라이언트 라이브러리는 종종 프로그램 명령어 구축에 대한 세부 사항을 추상화합니다. 라이브러리를 사용할 수 없는 경우, 수동으로 명령어를 구축할 수 있습니다. 이를 위해서는 명령어의 구현 세부 사항을 알아야 합니다.

아래 예제는 전송 명령어를 수동으로 구축하는 방법을 보여줍니다. Expanded Instruction 탭은 Instruction 탭과 기능적으로 동일합니다.

  • Kit
const transferAmount = 0.01; // 0.01 SOL
const transferInstruction = getTransferSolInstruction({
source: sender,
destination: recipient.address,
amount: transferAmount * LAMPORTS_PER_SOL
});
  • Legacy
const transferAmount = 0.01; // 0.01 SOL
const transferInstruction = SystemProgram.transfer({
fromPubkey: sender.publicKey,
toPubkey: receiver.publicKey,
lamports: transferAmount * LAMPORTS_PER_SOL
});
  • Rust
let transfer_amount = LAMPORTS_PER_SOL / 100; // 0.01 SOL
let transfer_instruction =
system_instruction::transfer(&sender.pubkey(), &recipient.pubkey(), transfer_amount);

아래 섹션에서는 트랜잭션과 인스트럭션의 세부 사항을 살펴보겠습니다.

Instructions

Solana 프로그램instruction은 Solana 네트워크를 사용하는 누구나 호출할 수 있는 공개 함수로 생각할 수 있습니다.

프로그램의 instruction을 호출하려면 세 가지 핵심 정보가 필요합니다:

  • 프로그램 ID: instruction의 실행 로직을 가진 프로그램
  • 계정: instruction이 필요로 하는 계정 목록
  • instruction data: 프로그램에서 호출할 instruction과 instruction에 필요한 인수를 지정하는 바이트 배열
Instruction
pub struct Instruction {
/// Pubkey of the program that executes this instruction.
pub program_id: Pubkey,
/// Metadata describing accounts that should be passed to the program.
pub accounts: Vec<AccountMeta>,
/// Opaque data passed to the program for its own interpretation.
pub data: Vec<u8>,
}

트랜잭션 인스트럭션트랜잭션 인스트럭션

AccountMeta

instruction에 필요한 각 계정은 다음을 포함하는 AccountMeta로 제공되어야 합니다:

  • pubkey: 계정의 주소
  • is_signer: 계정이 트랜잭션에 서명해야 하는지 여부
  • is_writable: instruction이 계정의 데이터를 수정하는지 여부
AccountMeta
pub struct AccountMeta {
/// An account's public key.
pub pubkey: Pubkey,
/// True if an `Instruction` requires a `Transaction` signature matching `pubkey`.
pub is_signer: bool,
/// True if the account data or metadata may be mutated during program execution.
pub is_writable: bool,
}

AccountMetaAccountMeta

instruction이 어떤 계정을 읽거나 쓰는지 미리 지정함으로써, 동일한 계정을 수정하지 않는 트랜잭션은 병렬로 실행될 수 있습니다.

인스트럭션 구조 예시

아래 예제를 실행하여 SOL 전송 instruction의 구조를 확인해보세요.

import { generateKeyPairSigner, lamports } from "@solana/kit";
import { getTransferSolInstruction } from "@solana-program/system";
// Generate sender and recipient keypairs
const sender = await generateKeyPairSigner();
const recipient = await generateKeyPairSigner();
// Define the amount to transfer
const LAMPORTS_PER_SOL = 1_000_000_000n;
const transferAmount = lamports(LAMPORTS_PER_SOL / 100n); // 0.01 SOL
// Create a transfer instruction for transferring SOL from sender to recipient
const transferInstruction = getTransferSolInstruction({
source: sender,
destination: recipient.address,
amount: transferAmount
});
console.log(JSON.stringify(transferInstruction, null, 2));
Click to execute the code.

다음 예제는 이전 코드 스니펫의 출력을 보여줍니다. 정확한 형식은 SDK에 따라 다르지만, 모든 Solana instruction에는 다음 정보가 필요합니다:

  • 프로그램 ID: 명령을 실행할 프로그램의 주소입니다.
  • 계정: 명령에 필요한 계정 목록입니다. 각 계정에 대해 명령은 주소, 트랜잭션에 서명해야 하는지 여부, 그리고 쓰기 가능한지 여부를 지정해야 합니다.
  • 데이터: 프로그램에게 어떤 명령을 실행할지 알려주고 명령에 필요한 인수를 포함하는 바이트 버퍼입니다.
{
"accounts": [
{
"address": "Hu28vRMGWpQXN56eaE7jRiDDRRz3vCXEs7EKHRfL6bC",
"role": 3,
"signer": {
"address": "Hu28vRMGWpQXN56eaE7jRiDDRRz3vCXEs7EKHRfL6bC",
"keyPair": {
"privateKey": {},
"publicKey": {}
}
}
},
{
"address": "2mBY6CTgeyJNJDzo6d2Umipw2aGUquUA7hLdFttNEj7p",
"role": 1
}
],
"programAddress": "11111111111111111111111111111111",
"data": {
"0": 2,
"1": 0,
"2": 0,
"3": 0,
"4": 128,
"5": 150,
"6": 152,
"7": 0,
"8": 0,
"9": 0,
"10": 0,
"11": 0
}
}

트랜잭션

Solana 트랜잭션은 다음으로 구성됩니다:

  1. 서명: 트랜잭션에 포함된 서명 배열입니다.
  2. 메시지: 원자적으로 처리될 명령 목록입니다.
Transaction
pub struct Transaction {
#[wasm_bindgen(skip)]
#[serde(with = "short_vec")]
pub signatures: Vec<Signature>,
#[wasm_bindgen(skip)]
pub message: Message,
}

트랜잭션 형식트랜잭션 형식

트랜잭션 메시지의 구조는 다음과 같습니다:

  • 메시지 헤더: 서명자 및 읽기 전용 계정의 수를 지정합니다.
  • 계정 주소: 트랜잭션의 명령에 필요한 계정 주소 배열입니다.
  • 최근 블록해시: 트랜잭션의 타임스탬프 역할을 합니다.
  • 명령: 실행될 명령 배열입니다.
Message
pub struct Message {
/// The message header, identifying signed and read-only `account_keys`.
pub header: MessageHeader,
/// All the account keys used by this transaction.
#[serde(with = "short_vec")]
pub account_keys: Vec<Pubkey>,
/// The id of a recent ledger entry.
pub recent_blockhash: Hash,
/// Programs that will be executed in sequence and committed in
/// one atomic transaction if all succeed.
#[serde(with = "short_vec")]
pub instructions: Vec<CompiledInstruction>,
}

트랜잭션 메시지트랜잭션 메시지

트랜잭션 크기

Solana 트랜잭션은 1232바이트의 크기 제한이 있습니다. 이 제한은 IPv6 최대 전송 단위(MTU) 크기인 1280바이트에서 네트워크 헤더(IPv6 40바이트 + 프래그먼트 헤더 8바이트)를 위한 48바이트를 뺀 값입니다.

트랜잭션의 총 크기(서명 및 메시지)는 이 제한 아래로 유지되어야 하며 다음을 포함합니다:

  • 서명: 각 64바이트
  • 메시지: 헤더(3바이트), 계정 키(각 32바이트), 최근 블록해시(32바이트) 및 명령

트랜잭션 형식트랜잭션 형식

메시지 헤더

메시지 헤더는 계정 권한을 정의하기 위해 3바이트를 사용합니다.

  1. 필요한 서명
  2. 읽기 전용 서명된 계정 수
  3. 읽기 전용 서명되지 않은 계정 수
MessageHeader
pub struct MessageHeader {
/// The number of signatures required for this message to be considered
/// valid. The signers of those signatures must match the first
/// `num_required_signatures` of [`Message::account_keys`].
pub num_required_signatures: u8,
/// The last `num_readonly_signed_accounts` of the signed keys are read-only
/// accounts.
pub num_readonly_signed_accounts: u8,
/// The last `num_readonly_unsigned_accounts` of the unsigned keys are
/// read-only accounts.
pub num_readonly_unsigned_accounts: u8,
}

메시지 헤더메시지 헤더

컴팩트 배열 형식

트랜잭션 메시지의 컴팩트 배열은 다음 형식으로 직렬화된 배열입니다:

  1. 배열 길이(compact-u16로 인코딩됨)
  2. 배열 항목들이 연속적으로 나열됨

컴팩트 배열 형식컴팩트 배열 형식

이 형식은 트랜잭션 메시지에서 계정 주소instruction 배열의 길이를 인코딩하는 데 사용됩니다.

계정 주소 배열

트랜잭션 메시지는 instruction에 필요한 계정 주소 배열을 포함합니다. 이 배열은 포함된 주소 수를 나타내는 compact-u16 숫자로 시작합니다. 그런 다음 주소는 메시지 헤더에 의해 결정된 권한에 따라 정렬됩니다.

  • 쓰기 가능하고 서명자인 계정
  • 읽기 전용이고 서명자인 계정
  • 쓰기 가능하고 서명자가 아닌 계정
  • 읽기 전용이고 서명자가 아닌 계정

계정 주소의 컴팩트 배열계정 주소의 컴팩트 배열

최근 블록해시

모든 트랜잭션은 두 가지 목적으로 사용되는 최근 블록해시가 필요합니다:

  1. 타임스탬프 역할을 함
  2. 중복 트랜잭션 방지

블록해시는 150개의 블록(400ms 블록 시간을 가정할 때 약 1분) 후에 만료되며, 이후에는 트랜잭션을 처리할 수 없습니다.

getLatestBlockhash RPC 메서드를 사용하여 현재 블록해시와 블록해시가 유효한 마지막 블록 높이를 얻을 수 있습니다. Solana Playground에서 예제를 확인하세요.

instruction 배열

트랜잭션 메시지는 CompiledInstruction 타입의 instruction 배열을 포함합니다. instruction은 트랜잭션에 추가될 때 이 타입으로 변환됩니다.

메시지의 계정 주소 배열과 마찬가지로, compact-u16 길이로 시작하고 그 뒤에 instruction 데이터가 옵니다. 각 instruction에는 다음이 포함됩니다:

  1. 프로그램 ID 인덱스: 계정 주소 배열에서 프로그램의 주소를 가리키는 u8 인덱스입니다. 이는 명령어를 처리할 프로그램을 지정합니다.
  2. 계정 인덱스: 이 명령어에 필요한 계정 주소를 가리키는 u8 인덱스의 배열입니다.
  3. Instruction Data: 프로그램에서 어떤 명령어를 호출할지 지정하고 명령어에 필요한 추가 데이터(예: 함수 인수)를 포함하는 바이트 배열입니다.
CompiledInstruction
pub struct CompiledInstruction {
/// Index into the transaction keys array indicating the program account that executes this instruction.
pub program_id_index: u8,
/// Ordered indices into the transaction keys array indicating which accounts to pass to the program.
#[serde(with = "short_vec")]
pub accounts: Vec<u8>,
/// The program input data.
#[serde(with = "short_vec")]
pub data: Vec<u8>,
}

명령어의 컴팩트 배열명령어의 컴팩트 배열

트랜잭션 구조 예시

아래 예제를 실행하여 단일 SOL 전송 명령어가 포함된 트랜잭션의 구조를 확인해보세요.

import {
createSolanaRpc,
generateKeyPairSigner,
lamports,
createTransactionMessage,
setTransactionMessageFeePayerSigner,
setTransactionMessageLifetimeUsingBlockhash,
appendTransactionMessageInstructions,
pipe,
signTransactionMessageWithSigners,
getCompiledTransactionMessageDecoder
} from "@solana/kit";
import { getTransferSolInstruction } from "@solana-program/system";
const rpc = createSolanaRpc("http://localhost:8899");
const { value: latestBlockhash } = await rpc.getLatestBlockhash().send();
// Generate sender and recipient keypairs
const sender = await generateKeyPairSigner();
const recipient = await generateKeyPairSigner();
// Define the amount to transfer
const LAMPORTS_PER_SOL = 1_000_000_000n;
const transferAmount = lamports(LAMPORTS_PER_SOL / 100n); // 0.01 SOL
// Create a transfer instruction for transferring SOL from sender to recipient
const transferInstruction = getTransferSolInstruction({
source: sender,
destination: recipient.address,
amount: transferAmount
});
// Create transaction message
const transactionMessage = pipe(
createTransactionMessage({ version: 0 }),
(tx) => setTransactionMessageFeePayerSigner(sender, tx),
(tx) => setTransactionMessageLifetimeUsingBlockhash(latestBlockhash, tx),
(tx) => appendTransactionMessageInstructions([transferInstruction], tx)
);
const signedTransaction =
await signTransactionMessageWithSigners(transactionMessage);
// Decode the messageBytes
const compiledTransactionMessage =
getCompiledTransactionMessageDecoder().decode(signedTransaction.messageBytes);
console.log(JSON.stringify(compiledTransactionMessage, null, 2));
Click to execute the code.

다음 예제는 이전 코드 스니펫에서 생성된 트랜잭션 메시지 출력을 보여줍니다. SDK에 따라 정확한 형식은 다르지만 동일한 정보를 포함합니다.

{
"version": 0,
"header": {
"numSignerAccounts": 1,
"numReadonlySignerAccounts": 0,
"numReadonlyNonSignerAccounts": 1
},
"staticAccounts": [
"HoCy8p5xxDDYTYWEbQZasEjVNM5rxvidx8AfyqA4ywBa",
"5T388jBjovy7d8mQ3emHxMDTbUF8b7nWvAnSiP3EAdFL",
"11111111111111111111111111111111"
],
"lifetimeToken": "EGCWPUEXhqHJWYBfDirq3mHZb4qDpATmYqBZMBy9TBC1",
"instructions": [
{
"programAddressIndex": 2,
"accountIndices": [0, 1],
"data": {
"0": 2,
"1": 0,
"2": 0,
"3": 0,
"4": 128,
"5": 150,
"6": 152,
"7": 0,
"8": 0,
"9": 0,
"10": 0,
"11": 0
}
}
]
}

트랜잭션을 네트워크에 전송한 후 서명을 사용하여 조회하면 다음과 같은 구조의 응답을 받게 됩니다.

message 필드에는 다음 필드가 포함됩니다:

  • header: accountKeys 배열의 주소에 대한 읽기/쓰기 및 서명자 권한을 지정합니다

  • accountKeys: 트랜잭션의 명령어에 사용된 모든 계정 주소의 배열

  • recentBlockhash: 트랜잭션의 타임스탬프로 사용되는 블록해시

  • instructions: 실행할 명령어 배열. 각 명령어의 accountprogramIdIndex는 인덱스로 accountKeys 배열을 참조합니다.

  • signatures: 트랜잭션의 명령어에 의해 서명자로 요구되는 모든 계정에 대한 서명을 포함하는 배열. 서명은 해당 계정의 개인 키를 사용하여 트랜잭션 메시지에 서명함으로써 생성됩니다.

Transaction Data
{
"blockTime": 1745196488,
"meta": {
"computeUnitsConsumed": 150,
"err": null,
"fee": 5000,
"innerInstructions": [],
"loadedAddresses": {
"readonly": [],
"writable": []
},
"logMessages": [
"Program 11111111111111111111111111111111 invoke [1]",
"Program 11111111111111111111111111111111 success"
],
"postBalances": [989995000, 10000000, 1],
"postTokenBalances": [],
"preBalances": [1000000000, 0, 1],
"preTokenBalances": [],
"rewards": [],
"status": {
"Ok": null
}
},
"slot": 13049,
"transaction": {
"message": {
"header": {
"numReadonlySignedAccounts": 0,
"numReadonlyUnsignedAccounts": 1,
"numRequiredSignatures": 1
},
"accountKeys": [
"8PLdpLxkuv9Nt8w3XcGXvNa663LXDjSrSNon4EK7QSjQ",
"7GLg7bqgLBv1HVWXKgWAm6YoPf1LoWnyWGABbgk487Ma",
"11111111111111111111111111111111"
],
"recentBlockhash": "7ZCxc2SDhzV2bYgEQqdxTpweYJkpwshVSDtXuY7uPtjf",
"instructions": [
{
"accounts": [0, 1],
"data": "3Bxs4NN8M2Yn4TLb",
"programIdIndex": 2,
"stackHeight": null
}
],
"indexToProgramIds": {}
},
"signatures": [
"3jUKrQp1UGq5ih6FTDUUt2kkqUfoG2o4kY5T1DoVHK2tXXDLdxJSXzuJGY4JPoRivgbi45U2bc7LZfMa6C4R3szX"
]
},
"version": "legacy"
}

Is this page helpful?

목차

페이지 편집