交易和指令
在 Solana 上,用户通过发送交易与网络交互。交易包含一个或多个指令,这些指令指定需要处理的操作。指令的执行逻辑存储在部署到 Solana 网络上的程序中,每个程序定义其自己的指令集。
以下是关于 Solana 交易处理的关键细节:
- 如果一个交易包含多个指令,这些指令会按照添加到交易中的顺序依次执行。
- 交易是“原子性”的——所有指令必须成功处理,否则整个交易失败且不会发生任何更改。
交易本质上是一个处理一个或多个指令的请求。
交易简化图
交易就像一个装有表格的信封。每个表格都是一条指令,告诉网络需要执行的操作。发送交易就像邮寄信封以处理这些表格。
关键点
- Solana 交易包含调用网络上程序的指令。
- 交易具有原子性——如果任何指令失败,整个交易失败且不会发生任何更改。
- 交易中的指令按顺序依次执行。
- 交易的大小限制为 1232 字节。
- 每条指令需要以下三部分信息:
- 要调用的程序地址
- 指令读取或写入的账户
- 指令所需的额外数据(例如函数参数)
SOL 转账示例
下图表示一笔包含单个指令的交易,用于将 SOL 从发送方转移到接收方。
在 Solana 上,“钱包”是由系统程序拥有的账户。只有程序所有者可以更改账户的数据,因此转移 SOL 需要发送一笔交易以调用系统程序。
SOL 转账
发送方账户必须签署 (is_signer
) 交易,以允许系统程序扣除其 lamport 余额。由于 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);
在以下部分中,我们将详细介绍交易和指令的细节。
指令
在 Solana 程序上的一个指令可以被视为一个公共函数,任何人都可以通过 Solana 网络调用它。
调用程序的指令需要以下三个关键信息:
- 程序 ID:包含指令执行逻辑的程序
- 账户:指令所需的账户列表
- 指令数据:字节数组,指定要在程序上调用的指令以及指令所需的任何参数
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
:指令是否修改账户数据
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
通过预先指定指令读取或写入的账户,不修改相同账户的交易可以并行执行。
示例指令结构
运行以下示例以查看 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}}
交易
一个 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 字节,减去网络头的 48 字节(40 字节 IPv6 + 8 字节分片头)。
交易的总大小(签名和消息)必须保持在此限制之内,包括:
- 签名:每个 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数字开头,表示包含的地址数量。然后根据消息头确定的权限对地址进行排序:
- 可写且为签名者的账户
- 只读且为签名者的账户
- 可写但不是签名者的账户
- 只读且不是签名者的账户
账户地址的紧凑数组
最近区块哈希
每笔交易都需要一个 最近区块哈希,其有两个作用:
- 作为时间戳
- 防止重复交易
区块哈希在 150个区块后过期(假设区块时间为 400 毫秒,大约 1 分钟),之后交易将无法处理。
您可以使用 getLatestBlockhash
RPC 方法获取当前区块哈希和区块哈希有效的最后区块高度。以下是
Solana Playground上的示例。
指令数组
交易消息包含一个 指令数组,其类型为 CompiledInstruction。指令在添加到交易时会转换为此类型。
与消息中的账户地址数组类似,它以一个 compact-u16长度开头,后跟指令数据。每个指令包含:
- 程序 ID 索引:一个 u8 索引,指向账户地址数组中程序的地址。这指定了将处理指令的程序。
- 账户索引:一个 u8 索引数组,指向此指令所需的账户地址。
- 指令数据:一个字节数组,指定要在程序上调用的指令以及指令所需的任何附加数据(例如函数参数)。
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?