交易和指令

在 Solana 上,用户通过发送交易与网络交互。交易包含一个或多个指令,这些指令指定需要处理的操作。指令的执行逻辑存储在部署到 Solana 网络上的程序中,每个程序定义其自己的指令集。

以下是关于 Solana 交易处理的关键细节:

  • 如果一个交易包含多个指令,这些指令会按照添加到交易中的顺序依次执行。
  • 交易是“原子性”的——所有指令必须成功处理,否则整个交易失败且不会发生任何更改。

交易本质上是一个处理一个或多个指令的请求。

交易简化图交易简化图

交易就像一个装有表格的信封。每个表格都是一条指令,告诉网络需要执行的操作。发送交易就像邮寄信封以处理这些表格。

关键点

  • Solana 交易包含调用网络上程序的指令。
  • 交易具有原子性——如果任何指令失败,整个交易失败且不会发生任何更改。
  • 交易中的指令按顺序依次执行。
  • 交易的大小限制为 1232 字节。
  • 每条指令需要以下三部分信息:
    1. 要调用的程序地址
    2. 指令读取或写入的账户
    3. 指令所需的额外数据(例如函数参数)

SOL 转账示例

下图表示一笔包含单个指令的交易,用于将 SOL 从发送方转移到接收方。

在 Solana 上,“钱包”是由系统程序拥有的账户。只有程序所有者可以更改账户的数据,因此转移 SOL 需要发送一笔交易以调用系统程序。

SOL 转账SOL 转账

发送方账户必须签署 (is_signer) 交易,以允许系统程序扣除其 lamport 余额。由于 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);

在以下部分中,我们将详细介绍交易和指令的细节。

指令

在 Solana 程序上的一个指令可以被视为一个公共函数,任何人都可以通过 Solana 网络调用它。

调用程序的指令需要以下三个关键信息:

  • 程序 ID:包含指令执行逻辑的程序
  • 账户:指令所需的账户列表
  • 指令数据:字节数组,指定要在程序上调用的指令以及指令所需的任何参数
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

指令所需的每个账户必须作为一个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,
}

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));
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
}
}

交易

一个 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 字节,减去网络头的 48 字节(40 字节 IPv6 + 8 字节分片头)。

交易的总大小(签名和消息)必须保持在此限制之内,包括:

  • 签名:每个 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数字开头,表示包含的地址数量。然后根据消息头确定的权限对地址进行排序:

  • 可写且为签名者的账户
  • 只读且为签名者的账户
  • 可写但不是签名者的账户
  • 只读且不是签名者的账户

账户地址的紧凑数组账户地址的紧凑数组

最近区块哈希

每笔交易都需要一个 最近区块哈希,其有两个作用:

  1. 作为时间戳
  2. 防止重复交易

区块哈希在 150个区块后过期(假设区块时间为 400 毫秒,大约 1 分钟),之后交易将无法处理。

您可以使用 getLatestBlockhash RPC 方法获取当前区块哈希和区块哈希有效的最后区块高度。以下是 Solana Playground上的示例。

指令数组

交易消息包含一个 指令数组,其类型为 CompiledInstruction。指令在添加到交易时会转换为此类型。

与消息中的账户地址数组类似,它以一个 compact-u16长度开头,后跟指令数据。每个指令包含:

  1. 程序 ID 索引:一个 u8 索引,指向账户地址数组中程序的地址。这指定了将处理指令的程序。
  2. 账户索引:一个 u8 索引数组,指向此指令所需的账户地址。
  3. 指令数据:一个字节数组,指定要在程序上调用的指令以及指令所需的任何附加数据(例如函数参数)。
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?

Table of Contents

Edit Page