요약
트랜잭션은 서명과 메시지로 구성됩니다. 메시지에는 헤더, 계정 주소, 최근 블록해시 및 컴파일된 명령어가 포함됩니다. 최대 직렬화 크기: 1,232바이트.
Transaction는
두 개의 최상위 필드를 가집니다:
signatures: 서명 배열message: 처리할 명령어 목록을 포함한 트랜잭션 정보
pub struct Transaction {pub signatures: Vec<Signature>,pub message: Message,}
트랜잭션의 두 부분을 보여주는 다이어그램
트랜잭션의 총 직렬화 크기는
PACKET_DATA_SIZE(1,232바이트)를
초과할 수 없습니다. 이 제한은 1,280바이트(IPv6 최소 MTU)에서 네트워크 헤더용
48바이트(IPv6 40바이트 + 프래그먼트 헤더 8바이트)를 뺀 값입니다. 1,232바이트에는
signatures 배열과 message 구조체가 모두
포함됩니다.
트랜잭션 형식과 크기 제한을 보여주는 다이어그램
서명
signatures 필드는
Signature
값의 컴팩트 인코딩 배열입니다. 각 Signature는 서명자 계정의 개인 키로 서명된
직렬화된 Message의 64바이트 Ed25519 서명입니다. 트랜잭션의 명령어가 참조하는
모든 서명자 계정에 대해 하나의 서명이 필요합니다.
배열의 첫 번째 서명은 트랜잭션 기본 수수료 및 우선순위 수수료를 지불하는 계정인 수수료 지불자의 것입니다. 이 첫 번째 서명은 네트워크에서 트랜잭션을 조회하는 데 사용되는 트랜잭션 ID로도 사용됩니다. 트랜잭션 ID는 일반적으로 트랜잭션 서명이라고 합니다.
수수료 지불자 요구사항:
- 메시지의 첫 번째 계정(인덱스 0)이어야 하며 서명자여야 합니다.
- 시스템 프로그램 소유 계정 또는 논스 계정이어야
합니다(
validate_fee_payer에서 검증됨). rent_exempt_minimum + total_fee를 충당할 수 있는 충분한 램포트를 보유해야 하며, 그렇지 않으면 트랜잭션이 *rsInsufficientFundsForFee*로 실패합니다.
메시지
message 필드는 트랜잭션의 페이로드를 포함하는
Message
구조체입니다:
header: 메시지 헤더account_keys: 트랜잭션의 인스트럭션에 필요한 계정 주소 배열recent_blockhash: 트랜잭션의 타임스탬프 역할을 하는 블록해시instructions: 인스트럭션 배열
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>,}
헤더
header 필드는 account_keys 배열을 권한 그룹으로 분할하는 세 개의 u8 필드를
가진
MessageHeader
구조체입니다:
num_required_signatures: 트랜잭션에 필요한 총 서명 수.num_readonly_signed_accounts: 읽기 전용인 서명된 계정의 수.num_readonly_unsigned_accounts: 읽기 전용인 서명되지 않은 계정의 수.
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,}
메시지 헤더의 세 부분을 보여주는 다이어그램
계정 주소
account_keys
필드는 공개 키의 압축 인코딩된 배열입니다. 각 항목은 트랜잭션의 인스트럭션 중
하나 이상에서 사용되는 계정을 식별합니다. 배열은 모든 계정을 포함해야 하며
다음과 같은 엄격한 순서를 따라야 합니다:
- 서명자 + 쓰기 가능
- 서명자 + 읽기 전용
- 비서명자 + 쓰기 가능
- 비서명자 + 읽기 전용
이러한 엄격한 순서는 account_keys 배열을 메시지의 header에 있는
세 개의 카운트와 결합하여 계정별 메타데이터 플래그를 저장하지 않고도 각 계정의
권한을 결정할 수 있게 합니다. 헤더 카운트는 배열을 위에 나열된 네 개의 권한
그룹으로 분할합니다.
계정 주소 배열의 순서를 보여주는 다이어그램
최근 블록해시
recent_blockhash 필드는 두 가지 목적을 제공하는 32바이트 해시입니다:
- 타임스탬프: 트랜잭션이 최근에 생성되었음을 증명합니다.
- 중복 제거: 동일한 트랜잭션이 두 번 처리되는 것을 방지합니다.
블록해시는 150개의 슬롯 후에 만료됩니다. 트랜잭션이 도착했을 때 블록해시가 더
이상 유효하지 않으면 *rsBlockhashNotFound*로 거부되며, 유효한
지속 가능한 논스 트랜잭션이 아닌
경우에 해당합니다.
getLatestBlockhash RPC 메서드를
사용하면 현재 블록해시와 블록해시가 유효한 마지막 블록 높이를 가져올 수
있습니다.
인스트럭션
instructions
필드는
CompiledInstruction
구조체의 컴팩트 인코딩 배열입니다. 각 *rsCompiledInstruction*는 전체 공개 키가
아닌 account_keys 배열의 인덱스로 계정을 참조합니다. 다음을 포함합니다:
program_id_index: 호출할 프로그램을 식별하는account_keys의 인덱스.accounts: 프로그램에 전달할 계정을 지정하는account_keys의 인덱스 배열.data: 인스트럭션 식별자와 직렬화된 인수를 포함하는 바이트 배열.
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>,}
인스트럭션의 컴팩트 배열
트랜잭션 바이너리 형식
트랜잭션은 컴팩트 인코딩 방식을 사용하여 직렬화됩니다. 모든 가변 길이 배열(서명, 계정 키, 인스트럭션)은 컴팩트-u16 길이 인코딩으로 접두사가 붙습니다. 이 형식은 0-127 값에 대해 1바이트를 사용하고 더 큰 값에 대해 2-3바이트를 사용합니다.
레거시 트랜잭션 레이아웃 (네트워크 상):
| 필드 | 크기 | 설명 |
|---|---|---|
num_signatures | 1-3바이트 (컴팩트-u16) | 서명 개수 |
signatures | num_signatures x 64바이트 | Ed25519 서명 |
num_required_signatures | 1바이트 | MessageHeader 필드 1 |
num_readonly_signed | 1바이트 | MessageHeader 필드 2 |
num_readonly_unsigned | 1바이트 | MessageHeader 필드 3 |
num_account_keys | 1-3바이트 (컴팩트-u16) | 정적 계정 키 개수 |
account_keys | num_account_keys x 32바이트 | 공개 키 |
recent_blockhash | 32바이트 | 블록해시 |
num_instructions | 1-3바이트 (컴팩트-u16) | 인스트럭션 개수 |
instructions | 가변 | 컴파일된 인스트럭션 배열 |
각 컴파일된 명령어는 다음과 같이 직렬화됩니다:
| 필드 | 크기 | 설명 |
|---|---|---|
program_id_index | 1바이트 | 계정 키 인덱스 |
num_accounts | 1-3바이트 (compact-u16) | 계정 인덱스 개수 |
account_indices | num_accounts x 1바이트 | 계정 키 인덱스 |
data_len | 1-3바이트 (compact-u16) | instruction data 길이 |
data | data_len 바이트 | 불투명 instruction data |
크기 계산
PACKET_DATA_SIZE = 1,232바이트일 때, 사용 가능한 공간은 다음과 같이 계산할
수 있습니다:
Total = 1232 bytes- compact-u16(num_sigs) # 1 byte- num_sigs * 64 # signature bytes- 3 # message header- compact-u16(num_keys) # 1 byte- num_keys * 32 # account key bytes- 32 # recent blockhash- compact-u16(num_ixs) # 1 byte- sum(instruction_sizes) # per-instruction overhead + data
예시: SOL 전송 트랜잭션
아래 다이어그램은 트랜잭션과 명령어가 함께 작동하여 사용자가 네트워크와 상호작용할 수 있도록 하는 방법을 보여줍니다. 이 예시에서는 한 계정에서 다른 계정으로 SOL이 전송됩니다.
발신자 계정의 메타데이터는 트랜잭션에 서명해야 함을 나타냅니다. 이를 통해 System Program이 lamport를 차감할 수 있습니다. lamport 잔액이 변경되려면 발신자와 수신자 계정 모두 쓰기 가능해야 합니다. 이 명령어를 실행하기 위해 발신자의 지갑은 서명과 SOL 전송 명령어가 포함된 메시지를 담은 트랜잭션을 전송합니다.
SOL 전송 다이어그램
트랜잭션이 전송된 후, System Program은 전송 명령어를 처리하고 두 계정의 lamport 잔액을 업데이트합니다.
SOL 전송 프로세스 다이어그램
아래 예시는 위 다이어그램과 관련된 코드를 보여줍니다. System Program의
transfer 함수를
참조하세요.
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 clusterconst rpc = createSolanaRpc("http://localhost:8899");const rpcSubscriptions = createSolanaRpcSubscriptions("ws://localhost:8900");// Generate sender and recipient keypairsconst 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 airdropawait airdropFactory({ rpc, rpcSubscriptions })({recipientAddress: sender.address,lamports: lamports(LAMPORTS_PER_SOL), // 1 SOLcommitment: "confirmed"});// Check balance before transferconst { 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 recipientconst transferInstruction = getTransferSolInstruction({source: sender,destination: recipient.address,amount: transferAmount // 0.01 SOL in lamports});// Add the transfer instruction to a new transactionconst { 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 networkconst signedTransaction =await signTransactionMessageWithSigners(transactionMessage);await sendAndConfirmTransactionFactory({ rpc, rpcSubscriptions })(signedTransaction,{ commitment: "confirmed" });const transactionSignature = getSignatureFromTransaction(signedTransaction);// Check balance after transferconst { 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);
다음 예시는 단일 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 keypairsconst sender = await generateKeyPairSigner();const recipient = await generateKeyPairSigner();// Define the amount to transferconst 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 recipientconst transferInstruction = getTransferSolInstruction({source: sender,destination: recipient.address,amount: transferAmount});// Create transaction messageconst transactionMessage = pipe(createTransactionMessage({ version: 0 }),(tx) => setTransactionMessageFeePayerSigner(sender, tx),(tx) => setTransactionMessageLifetimeUsingBlockhash(latestBlockhash, tx),(tx) => appendTransactionMessageInstructions([transferInstruction], tx));const signedTransaction =await signTransactionMessageWithSigners(transactionMessage);// Decode the messageBytesconst compiledTransactionMessage =getCompiledTransactionMessageDecoder().decode(signedTransaction.messageBytes);console.log(JSON.stringify(compiledTransactionMessage, null, 2));
아래 코드는 이전 코드 스니펫의 출력을 보여줍니다. 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}}]}
트랜잭션 세부 정보 가져오기
제출 후, 트랜잭션 서명과 getTransaction RPC 메서드를 사용하여 트랜잭션 세부 정보를 조회할 수 있습니다.
Solana Explorer를 사용하여 트랜잭션을 찾을 수도 있습니다.
{"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?