트랜잭션 및 명령어

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

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

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

트랜잭션은 본질적으로 하나 이상의 명령을 처리하기 위한 요청입니다. 트랜잭션을 양식이 들어 있는 봉투로 생각할 수 있습니다. 각 양식은 네트워크에게 무엇을 해야 하는지 알려주는 명령입니다. 트랜잭션을 보내는 것은 양식을 처리하기 위해 봉투를 우편으로 보내는 것과 같습니다.

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

핵심 포인트

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

SOL 전송 예제

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

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

SOL 전송SOL 전송

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

트랜잭션을 보낸 후, System Program이 전송 명령을 처리합니다. 그런 다음 System Program은 발신자와 수신자 계정 모두의 lamport 잔액을 업데이트합니다.

SOL 전송 프로세스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 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);
Console
Click to execute the code.

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

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

const transferAmount = 0.01; // 0.01 SOL
const transferInstruction = getTransferSolInstruction({
source: sender,
destination: recipient.address,
amount: transferAmount * LAMPORTS_PER_SOL
});

아래 섹션에서는 트랜잭션과 명령어의 세부 사항에 대해 살펴보겠습니다.

명령어

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

Solana 프로그램은 Solana 네트워크에서 호스팅되는 웹 서버와 같으며, 각 명령어는 사용자가 특정 작업을 수행하기 위해 호출할 수 있는 공개 API 엔드포인트와 유사합니다. 명령어를 호출하는 것은 API 엔드포인트에 POST 요청을 보내는 것과 비슷하며, 사용자가 프로그램의 비즈니스 로직을 실행할 수 있게 합니다.

Solana에서 프로그램의 명령어를 호출하려면 세 가지 정보를 포함한 Instruction를 구성해야 합니다:

  • 프로그램 ID: 호출되는 명령어에 대한 비즈니스 로직이 있는 프로그램의 주소입니다.
  • 계정: 명령어가 읽거나 쓰는 모든 계정의 목록입니다.
  • instruction data: 프로그램에서 어떤 명령어를 호출할지 지정하고 명령어에 필요한 인수를 포함하는 바이트 배열입니다.
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로 제공해야 합니다. AccountMeta는 다음을 지정합니다:

  • pubkey: 계정의 주소
  • is_signer: 계정이 트랜잭션에 서명해야 하는지 여부
  • is_writable: 명령어가 계정의 데이터를 수정하는지 여부
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,
}

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

명령어가 어떤 계정을 필요로 하는지, 그리고 어떤 계정이 쓰기 가능, 읽기 전용이어야 하는지, 또는 트랜잭션에 서명해야 하는지 알기 위해서는 프로그램에서 정의한 명령어의 구현을 참조해야 합니다.

실제로는 보통 Instruction를 직접 구성할 필요가 없습니다. 대부분의 프로그램 개발자들은 명령어를 생성해주는 헬퍼 함수가 포함된 클라이언트 라이브러리를 제공합니다.

AccountMetaAccountMeta

명령어 구조 예시

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

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));
Console
Click to execute the code.

다음 예제는 이전 코드 스니펫의 출력을 보여줍니다. 정확한 형식은 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 트랜잭션은 다음으로 구성됩니다:

  1. 서명: 트랜잭션의 명령어에 필요한 모든 서명 계정의 서명 배열입니다. 서명은 계정의 개인 키로 트랜잭션 Message에 서명하여 생성됩니다.
  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>,
}

트랜잭션 크기

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

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

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

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

메시지 헤더

메시지 헤더는 트랜잭션 내 계정의 권한을 지정합니다. 이는 엄격하게 정렬된 계정 주소와 함께 작동하여 어떤 계정이 서명자이고 어떤 계정이 쓰기 가능한지 결정합니다.

  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. 배열 항목들이 하나씩 나열됨

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

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

계정 주소 배열

트랜잭션 메시지는 명령어에 필요한 모든 계정 주소의 단일 목록을 포함합니다. 배열은 포함된 주소 수를 나타내는 compact-u16 숫자로 시작합니다.

공간을 절약하기 위해 트랜잭션은 각 계정의 권한을 개별적으로 저장하지 않습니다. 대신, header와 계정 주소의 엄격한 순서를 조합하여 권한을 결정합니다.

주소는 항상 다음과 같은 순서로 정렬됩니다:

  1. 쓰기 가능하고 서명자인 계정
  2. 읽기 전용이고 서명자인 계정
  3. 쓰기 가능하고 서명자가 아닌 계정
  4. 읽기 전용이고 서명자가 아닌 계정

header는 각 권한 그룹의 계정 수를 결정하는 데 사용되는 값을 제공합니다.

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

최근 블록해시

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

  1. 트랜잭션이 생성된 시점의 타임스탬프 역할
  2. 중복 트랜잭션 방지

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

getLatestBlockhash RPC 메서드를 사용하여 현재 블록해시와 블록해시가 유효한 마지막 블록 높이를 얻을 수 있습니다.

명령어 배열

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

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

  1. 프로그램 ID 인덱스: 계정 주소 배열에서 프로그램의 주소를 가리키는 인덱스입니다. 이는 명령어를 처리할 프로그램을 지정합니다.
  2. 계정 인덱스: 이 명령어에 필요한 계정 주소를 가리키는 인덱스 배열입니다.
  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));
Console
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
}
}
]
}

트랜잭션을 제출한 후, getTransaction RPC 메서드를 사용하여 세부 정보를 검색할 수 있습니다. 응답은 다음 스니펫과 유사한 구조를 가질 것입니다. 또는 Solana Explorer를 사용하여 트랜잭션을 검사할 수 있습니다.

"트랜잭션 서명"은 Solana에서 트랜잭션을 고유하게 식별합니다. 이 서명을 사용하여 네트워크에서 트랜잭션의 세부 정보를 조회할 수 있습니다. 트랜잭션 서명은 단순히 트랜잭션의 첫 번째 서명입니다. 첫 번째 서명은 또한 트랜잭션 수수료 지불자의 서명이기도 합니다.

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?

목차

페이지 편집