트랜잭션 및 명령어
솔라나에서 사용자는 네트워크와 상호작용하기 위해 트랜잭션을 전송합니다. 트랜잭션은 처리할 작업을 지정하는 하나 이상의 명령어를 포함합니다. 명령어의 실행 로직은 솔라나 네트워크에 배포된 프로그램에 저장되며, 각 프로그램은 자체적인 명령어 세트를 정의합니다.
다음은 솔라나 트랜잭션 처리에 관한 주요 세부 사항입니다:
- 트랜잭션에 여러 명령어가 포함된 경우, 명령어는 트랜잭션에 추가된 순서대로 실행됩니다.
- 트랜잭션은 "원자적"입니다 - 모든 명령어가 성공적으로 처리되어야 하며, 그렇지 않으면 전체 트랜잭션이 실패하고 변경 사항이 적용되지 않습니다.
트랜잭션은 본질적으로 하나 이상의 명령을 처리하기 위한 요청입니다. 트랜잭션을 양식이 들어 있는 봉투로 생각할 수 있습니다. 각 양식은 네트워크에게 무엇을 해야 하는지 알려주는 명령입니다. 트랜잭션을 보내는 것은 양식을 처리하기 위해 봉투를 우편으로 보내는 것과 같습니다.
트랜잭션 간소화
핵심 포인트
- Solana 트랜잭션은 네트워크의 프로그램을 호출하는 명령을 포함합니다.
- 트랜잭션은 원자적입니다 - 어떤 명령이라도 실패하면 전체 트랜잭션이 실패하고 변경 사항이 발생하지 않습니다.
- 트랜잭션의 명령은 순차적으로 실행됩니다.
- 트랜잭션 크기 제한은 1232 바이트입니다.
- 각 명령에는 세 가지 정보가 필요합니다:
- 호출할 프로그램의 주소
- 명령이 읽거나 쓰는 계정
- 명령에 필요한 추가 데이터(예: 함수 인수)
SOL 전송 예제
아래 다이어그램은 발신자에서 수신자로 SOL을 전송하는 단일 명령이 있는 트랜잭션을 나타냅니다.
Solana에서 "지갑"은 System Program이 소유한 계정입니다. 프로그램 소유자만 계정 데이터를 변경할 수 있으므로, SOL을 전송하려면 System Program을 호출하는 트랜잭션을 보내야 합니다.
SOL 전송
발신자 계정은 System Program이 lamport 잔액을 차감할 수 있도록 트랜잭션에
서명(is_signer
)해야 합니다. 발신자와 수신자 계정은 lamport 잔액이 변경되므로
쓰기 가능(is_writable
)해야 합니다.
트랜잭션을 보낸 후, System Program이 전송 명령을 처리합니다. 그런 다음 System Program은 발신자와 수신자 계정 모두의 lamport 잔액을 업데이트합니다.
SOL 전송 프로세스
아래 예제는 한 계정에서 다른 계정으로 SOL을 전송하는 트랜잭션을 보내는 방법을 보여줍니다. System Program의 전송 명령 소스 코드는 여기에서 확인할 수 있습니다.
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
탭과 동일합니다.
const transferAmount = 0.01; // 0.01 SOLconst transferInstruction = getTransferSolInstruction({source: sender,destination: recipient.address,amount: transferAmount * LAMPORTS_PER_SOL});
아래 섹션에서는 트랜잭션과 명령어의 세부 사항에 대해 살펴보겠습니다.
명령어
Solana 프로그램의 명령어는 Solana 네트워크를 사용하는 누구나 호출할 수 있는 공개 함수로 생각할 수 있습니다.
Solana 프로그램은 Solana 네트워크에서 호스팅되는 웹 서버와 같으며, 각 명령어는
사용자가 특정 작업을 수행하기 위해 호출할 수 있는 공개 API 엔드포인트와
유사합니다. 명령어를 호출하는 것은 API 엔드포인트에 POST
요청을 보내는 것과
비슷하며, 사용자가 프로그램의 비즈니스 로직을 실행할 수 있게 합니다.
Solana에서 프로그램의 명령어를 호출하려면 세 가지 정보를 포함한 Instruction
를
구성해야 합니다:
- 프로그램 ID: 호출되는 명령어에 대한 비즈니스 로직이 있는 프로그램의 주소입니다.
- 계정: 명령어가 읽거나 쓰는 모든 계정의 목록입니다.
- instruction data: 프로그램에서 어떤 명령어를 호출할지 지정하고 명령어에 필요한 인수를 포함하는 바이트 배열입니다.
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
로
제공해야 합니다. AccountMeta
는 다음을 지정합니다:
- pubkey: 계정의 주소
- is_signer: 계정이 트랜잭션에 서명해야 하는지 여부
- is_writable: 명령어가 계정의 데이터를 수정하는지 여부
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,}
명령어가 어떤 계정을 읽거나 쓰는지 미리 지정함으로써, 동일한 계정을 수정하지 않는 트랜잭션은 병렬로 실행될 수 있습니다.
명령어가 어떤 계정을 필요로 하는지, 그리고 어떤 계정이 쓰기 가능, 읽기 전용이어야 하는지, 또는 트랜잭션에 서명해야 하는지 알기 위해서는 프로그램에서 정의한 명령어의 구현을 참조해야 합니다.
실제로는 보통 Instruction
를 직접 구성할 필요가 없습니다. 대부분의 프로그램
개발자들은 명령어를 생성해주는 헬퍼 함수가 포함된 클라이언트 라이브러리를
제공합니다.
AccountMeta
명령어 구조 예시
아래 예제를 실행하여 SOL 전송 명령어의 구조를 확인해보세요.
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 명령어는 다음 정보를 필요로 합니다:
- 프로그램 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}}
트랜잭션
원하는 명령어를 생성한 후, 다음 단계는 Transaction
를 생성하고 명령어를
트랜잭션에 추가하는 것입니다. Solana
트랜잭션은
다음으로 구성됩니다:
- 서명: 트랜잭션의 명령어에 필요한 모든 서명 계정의
서명
배열입니다. 서명은 계정의 개인 키로 트랜잭션
Message
에 서명하여 생성됩니다. - 메시지: 트랜잭션 메시지는 원자적으로 처리될 명령어 목록을 포함합니다.
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>,}
트랜잭션 크기
솔라나 트랜잭션은 1232 바이트의 크기 제한이 있습니다. 이 제한은 IPv6 최대 전송 단위(MTU) 크기인 1280 바이트에서 네트워크 헤더(IPv6 40 바이트 + 헤더 8 바이트)를 위한 48 바이트를 뺀 값입니다.
트랜잭션의 총 크기(서명 및 메시지)는 이 제한 아래로 유지되어야 하며 다음을 포함합니다:
- 서명: 각 64 바이트
- 메시지: 헤더(3 바이트), 계정 키(각 32 바이트), 최근 블록해시(32 바이트) 및 명령어
트랜잭션 형식
메시지 헤더
메시지 헤더는 트랜잭션 내 계정의 권한을 지정합니다. 이는 엄격하게 정렬된 계정 주소와 함께 작동하여 어떤 계정이 서명자이고 어떤 계정이 쓰기 가능한지 결정합니다.
- 트랜잭션의 모든 명령어에 필요한 서명 수.
- 읽기 전용인 서명된 계정의 수.
- 읽기 전용인 서명되지 않은 계정의 수.
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로 인코딩됨)
- 배열 항목들이 하나씩 나열됨
컴팩트 배열 형식
이 형식은 트랜잭션 메시지에서 계정 주소와 명령어 배열의 길이를 인코딩하는 데 사용됩니다.
계정 주소 배열
트랜잭션 메시지는 명령어에 필요한 모든 계정 주소의 단일 목록을 포함합니다. 배열은 포함된 주소 수를 나타내는 compact-u16 숫자로 시작합니다.
공간을 절약하기 위해 트랜잭션은 각 계정의 권한을 개별적으로 저장하지 않습니다. 대신, header와 계정 주소의 엄격한 순서를 조합하여 권한을 결정합니다.
주소는 항상 다음과 같은 순서로 정렬됩니다:
- 쓰기 가능하고 서명자인 계정
- 읽기 전용이고 서명자인 계정
- 쓰기 가능하고 서명자가 아닌 계정
- 읽기 전용이고 서명자가 아닌 계정
header는 각 권한 그룹의 계정 수를 결정하는 데 사용되는 값을 제공합니다.
계정 주소의 컴팩트 배열
최근 블록해시
모든 트랜잭션은 다음 두 가지 목적으로 사용되는 최근 블록해시가 필요합니다:
- 트랜잭션이 생성된 시점의 타임스탬프 역할
- 중복 트랜잭션 방지
블록해시는 150개의 블록(400ms 블록 시간을 가정할 때 약 1분) 후에 만료되며, 이후에는 트랜잭션이 만료된 것으로 간주되어 처리될 수 없습니다.
getLatestBlockhash RPC 메서드를 사용하여 현재 블록해시와 블록해시가 유효한 마지막 블록 높이를 얻을 수 있습니다.
명령어 배열
트랜잭션 메시지는 CompiledInstruction 타입의 명령어 배열을 포함합니다. 명령어는 트랜잭션에 추가될 때 이 타입으로 변환됩니다.
메시지의 계정 주소 배열과 마찬가지로, compact-u16 길이로 시작하고 그 뒤에 명령어 데이터가 옵니다. 각 명령어는 다음을 포함합니다:
- 프로그램 ID 인덱스: 계정 주소 배열에서 프로그램의 주소를 가리키는 인덱스입니다. 이는 명령어를 처리할 프로그램을 지정합니다.
- 계정 인덱스: 이 명령어에 필요한 계정 주소를 가리키는 인덱스 배열입니다.
- 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}}]}
트랜잭션을 제출한 후, getTransaction RPC 메서드를 사용하여 세부 정보를 검색할 수 있습니다. 응답은 다음 스니펫과 유사한 구조를 가질 것입니다. 또는 Solana Explorer를 사용하여 트랜잭션을 검사할 수 있습니다.
"트랜잭션 서명"은 Solana에서 트랜잭션을 고유하게 식별합니다. 이 서명을 사용하여 네트워크에서 트랜잭션의 세부 정보를 조회할 수 있습니다. 트랜잭션 서명은 단순히 트랜잭션의 첫 번째 서명입니다. 첫 번째 서명은 또한 트랜잭션 수수료 지불자의 서명이기도 합니다.
{"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?