交易与指令

在 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);
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 网络上的 Web 服务器,其中每个指令就像一个公共 API 端点,用户可以调用它来执行特定操作。调用指令类似于向 API 端点发送 POST 请求,允许用户执行程序的业务逻辑。

要在 Solana 上调用程序的指令,您需要构建一个包含以下三部分信息的 Instruction

  • Program ID:包含指令业务逻辑的程序地址。
  • Accounts:指令读取或写入的所有账户列表。
  • 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>,
}

Transaction InstructionTransaction Instruction

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 指令都需要以下信息:

  • Program ID:将执行指令的程序地址。
  • Accounts:指令所需的账户列表。对于每个账户,指令必须指定其地址、是否需要签署交易以及是否会被写入。
  • Data:一个字节缓冲区,用于告诉程序要执行的指令以及指令所需的任何参数。
{
"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>,
}

交易大小

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数字开头,指示其包含的地址数量。

为了节省空间,交易不会单独存储每个账户的权限。相反,它依赖于 MessageHeader 和账户地址的严格排序来确定权限。

地址始终按以下方式排序:

  1. 可写且为签名者的账户
  2. 只读且为签名者的账户
  3. 可写但不是签名者的账户
  4. 只读且不是签名者的账户

MessageHeader 提供了用于确定每个权限组账户数量的值。

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

最近区块哈希

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

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

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

您可以使用 getLatestBlockhash RPC 方法获取当前区块哈希和区块哈希有效的最后区块高度。

指令数组

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

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

  1. Program ID Index:一个索引,指向账户地址数组中程序的地址。这指定了处理该指令的程序。
  2. Account Indexes:一个索引数组,指向该指令所需的账户地址。
  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?

Table of Contents

Edit Page