트랜잭션 및 명령어
솔라나에서 사용자는 네트워크와 상호작용하기 위해 트랜잭션을 전송합니다. 트랜잭션은 처리할 작업을 지정하는 하나 이상의 명령어를 포함합니다. 명령어의 실행 로직은 솔라나 네트워크에 배포된 프로그램에 저장되며, 각 프로그램은 자체적인 명령어 세트를 정의합니다.
다음은 솔라나 트랜잭션 처리에 관한 주요 세부 사항입니다:
- 트랜잭션에 여러 명령어가 포함된 경우, 명령어는 트랜잭션에 추가된 순서대로 실행됩니다.
- 트랜잭션은 "원자적"입니다 - 모든 명령어가 성공적으로 처리되어야 하며, 그렇지 않으면 전체 트랜잭션이 실패하고 어떤 변경도 발생하지 않습니다.
트랜잭션은 본질적으로 하나 이상의 명령어를 처리하기 위한 요청입니다.
트랜잭션 간소화
트랜잭션은 양식이 담긴 봉투와 같습니다. 각 양식은 네트워크에 무엇을 해야 할지 알려주는 명령어입니다. 트랜잭션을 보내는 것은 양식을 처리하기 위해 봉투를 우편으로 보내는 것과 같습니다.
주요 포인트
- 솔라나 트랜잭션은 네트워크의 프로그램을 호출하는 명령어를 포함합니다.
- 트랜잭션은 원자적입니다 - 어떤 명령어라도 실패하면 전체 트랜잭션이 실패하고 어떤 변경도 발생하지 않습니다.
- 트랜잭션의 명령어는 순차적으로 실행됩니다.
- 트랜잭션 크기 제한은 1232 바이트입니다.
- 각 명령어는 세 가지 정보가 필요합니다:
- 호출할 프로그램의 주소
- 명령어가 읽거나 쓰는 계정
- 명령어에 필요한 추가 데이터(예: 함수 인수)
SOL 전송 예제
아래 다이어그램은 발신자에서 수신자로 SOL을 전송하는 단일 명령어가 포함된 트랜잭션을 나타냅니다.
솔라나에서 "지갑"은 시스템 프로그램이 소유한 계정입니다. 프로그램 소유자만 계정 데이터를 변경할 수 있으므로, SOL을 전송하려면 시스템 프로그램을 호출하는 트랜잭션을 보내야 합니다.
SOL 전송
발신자 계정은 시스템 프로그램이 자신의 lamport 잔액을 차감할 수 있도록
트랜잭션에 서명(is_signer
)해야 합니다. 발신자와 수신자 계정은 lamport 잔액이
변경되므로 쓰기 가능(is_writable
)해야 합니다.
트랜잭션을 보낸 후, 시스템 프로그램이 전송 명령어를 처리합니다. 그런 다음 시스템 프로그램은 발신자와 수신자 계정 모두의 lamport 잔액을 업데이트합니다.
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 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);
클라이언트 라이브러리는 종종 프로그램 명령어 구축에 대한 세부 사항을 추상화합니다. 라이브러리를 사용할 수 없는 경우, 수동으로 명령어를 구축할 수 있습니다. 이를 위해서는 명령어의 구현 세부 사항을 알아야 합니다.
아래 예제는 전송 명령어를 수동으로 구축하는 방법을 보여줍니다.
Expanded Instruction
탭은 Instruction
탭과 기능적으로 동일합니다.
- Kit
const transferAmount = 0.01; // 0.01 SOLconst transferInstruction = getTransferSolInstruction({source: sender,destination: recipient.address,amount: transferAmount * LAMPORTS_PER_SOL});
- Legacy
const transferAmount = 0.01; // 0.01 SOLconst transferInstruction = SystemProgram.transfer({fromPubkey: sender.publicKey,toPubkey: receiver.publicKey,lamports: transferAmount * LAMPORTS_PER_SOL});
- Rust
let transfer_amount = LAMPORTS_PER_SOL / 100; // 0.01 SOLlet transfer_instruction =system_instruction::transfer(&sender.pubkey(), &recipient.pubkey(), transfer_amount);
아래 섹션에서는 트랜잭션과 인스트럭션의 세부 사항을 살펴보겠습니다.
Instructions
Solana 프로그램의 instruction은 Solana 네트워크를 사용하는 누구나 호출할 수 있는 공개 함수로 생각할 수 있습니다.
프로그램의 instruction을 호출하려면 세 가지 핵심 정보가 필요합니다:
- 프로그램 ID: instruction의 실행 로직을 가진 프로그램
- 계정: instruction이 필요로 하는 계정 목록
- instruction data: 프로그램에서 호출할 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이 계정의 데이터를 수정하는지 여부
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,}
AccountMeta
instruction이 어떤 계정을 읽거나 쓰는지 미리 지정함으로써, 동일한 계정을 수정하지 않는 트랜잭션은 병렬로 실행될 수 있습니다.
인스트럭션 구조 예시
아래 예제를 실행하여 SOL 전송 instruction의 구조를 확인해보세요.
import { generateKeyPairSigner, lamports } from "@solana/kit";import { getTransferSolInstruction } from "@solana-program/system";// 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});console.log(JSON.stringify(transferInstruction, null, 2));
다음 예제는 이전 코드 스니펫의 출력을 보여줍니다. 정확한 형식은 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 트랜잭션은 다음으로 구성됩니다:
pub struct Transaction {#[wasm_bindgen(skip)]#[serde(with = "short_vec")]pub signatures: Vec<Signature>,#[wasm_bindgen(skip)]pub 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바이트를 사용합니다.
- 필요한 서명
- 읽기 전용 서명된 계정 수
- 읽기 전용 서명되지 않은 계정 수
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,}
메시지 헤더
컴팩트 배열 형식
트랜잭션 메시지의 컴팩트 배열은 다음 형식으로 직렬화된 배열입니다:
- 배열 길이(compact-u16로 인코딩됨)
- 배열 항목들이 연속적으로 나열됨
컴팩트 배열 형식
이 형식은 트랜잭션 메시지에서 계정 주소와 instruction 배열의 길이를 인코딩하는 데 사용됩니다.
계정 주소 배열
트랜잭션 메시지는 instruction에 필요한 계정 주소 배열을 포함합니다. 이 배열은 포함된 주소 수를 나타내는 compact-u16 숫자로 시작합니다. 그런 다음 주소는 메시지 헤더에 의해 결정된 권한에 따라 정렬됩니다.
- 쓰기 가능하고 서명자인 계정
- 읽기 전용이고 서명자인 계정
- 쓰기 가능하고 서명자가 아닌 계정
- 읽기 전용이고 서명자가 아닌 계정
계정 주소의 컴팩트 배열
최근 블록해시
모든 트랜잭션은 두 가지 목적으로 사용되는 최근 블록해시가 필요합니다:
- 타임스탬프 역할을 함
- 중복 트랜잭션 방지
블록해시는 150개의 블록(400ms 블록 시간을 가정할 때 약 1분) 후에 만료되며, 이후에는 트랜잭션을 처리할 수 없습니다.
getLatestBlockhash
RPC 메서드를 사용하여
현재 블록해시와 블록해시가 유효한 마지막 블록 높이를 얻을 수 있습니다.
Solana Playground에서 예제를
확인하세요.
instruction 배열
트랜잭션 메시지는 CompiledInstruction 타입의 instruction 배열을 포함합니다. instruction은 트랜잭션에 추가될 때 이 타입으로 변환됩니다.
메시지의 계정 주소 배열과 마찬가지로, compact-u16 길이로 시작하고 그 뒤에 instruction 데이터가 옵니다. 각 instruction에는 다음이 포함됩니다:
- 프로그램 ID 인덱스: 계정 주소 배열에서 프로그램의 주소를 가리키는 u8 인덱스입니다. 이는 명령어를 처리할 프로그램을 지정합니다.
- 계정 인덱스: 이 명령어에 필요한 계정 주소를 가리키는 u8 인덱스의 배열입니다.
- Instruction 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>,}
명령어의 컴팩트 배열
트랜잭션 구조 예시
아래 예제를 실행하여 단일 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}}]}
트랜잭션을 네트워크에 전송한 후 서명을 사용하여 조회하면 다음과 같은 구조의 응답을 받게 됩니다.
message
필드에는 다음 필드가 포함됩니다:
-
header
:accountKeys
배열의 주소에 대한 읽기/쓰기 및 서명자 권한을 지정합니다 -
accountKeys
: 트랜잭션의 명령어에 사용된 모든 계정 주소의 배열 -
recentBlockhash
: 트랜잭션의 타임스탬프로 사용되는 블록해시 -
instructions
: 실행할 명령어 배열. 각 명령어의account
및programIdIndex
는 인덱스로accountKeys
배열을 참조합니다. -
signatures
: 트랜잭션의 명령어에 의해 서명자로 요구되는 모든 계정에 대한 서명을 포함하는 배열. 서명은 해당 계정의 개인 키를 사용하여 트랜잭션 메시지에 서명함으로써 생성됩니다.
{"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?