摘要
一笔交易由签名和消息组成。消息包含头部、账户地址、最近区块哈希和已编译指令。最大序列化大小:1,232 字节。
一个
Transaction
有两个顶层字段:
signatures:签名数组message:交易信息,包括待处理的指令列表
pub struct Transaction {pub signatures: Vec<Signature>,pub message: Message,}
展示交易两部分的示意图
交易的总序列化大小不得超过
PACKET_DATA_SIZE
(1,232 字节)。该限制等于 1,280 字节(IPv6 最小 MTU)减去 48 字节的网络头部(40 字节 IPv6 +
8 字节分片头)。1,232 字节包括 signatures 数组和
message 结构体。
展示交易格式和大小限制的示意图
签名
signatures 字段是一个经过紧凑编码的
Signature
值数组。每个 Signature 都是对序列化的 Message
进行 Ed25519 算法签名(64 字节),由签名账户的私钥签署。每个交易指令中引用的
签名账户 都需要一个签名。
数组中的第一个签名属于 手续费支付者,即支付交易基础手续费和优先级手续费的账户。这个首个签名也作为交易 ID,用于在网络上查询该交易。交易 ID 通常也被称为交易签名。
费用支付账户要求:
- 必须是消息中的第一个账户(索引为 0),并且是签名者。
- 必须是 System Program 拥有的账户或 nonce 账户(由
validate_fee_payer验证)。 - 必须持有足够的 lamports 以支付
rent_exempt_minimum + total_fee;否则交易将因InsufficientFundsForFee失败。
消息
message 字段是一个
Message
结构体,包含交易的有效载荷:
header:消息的 headeraccount_keys:交易指令所需的 账户地址 数组recent_blockhash:作为交易时间戳的 blockhashinstructions: 指令 数组
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>,}
头部
header 字段是一个
MessageHeader
结构体,包含三个 u8 字段,用于将 account_keys 数组划分为权限组:
num_required_signatures:交易所需的签名总数。num_readonly_signed_accounts:只读签名账户的数量。num_readonly_unsigned_accounts:只读未签名账户的数量。
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,}
显示消息头三部分的示意图
账户地址
account_keys
字段是一个经过紧凑编码的公钥数组。每个条目标识至少被交易某个指令使用的账户。该数组必须包含所有账户,并且必须遵循以下严格顺序:
- 签名者 + 可写
- 签名者 + 只读
- 非签名者 + 可写
- 非签名者 + 只读
这种严格的顺序允许将 account_keys 数组与消息头中的三个计数
header 结合,
从而无需为每个账户存储元数据标志即可确定每个账户的权限。头部计数
将数组划分为上述四个权限组。
展示账户地址数组顺序的图示
最近区块哈希
recent_blockhash 字段是一个 32 字节的哈希值,具有两个作用:
- 时间戳:证明该交易是最近创建的。
- 去重:防止同一笔交易被重复处理。
区块哈希在 150 个 slot 后失效。如果交易到达时区块哈希已失效,则会被以
BlockhashNotFound 拒绝,除非它是有效的
持久随机数交易。
getLatestBlockhash RPC 方法
可用于获取当前区块哈希以及该区块哈希有效的最后区块高度。
指令
instructions
字段是一个经过紧凑编码的数组,包含多个
CompiledInstruction
结构体。每个 CompiledInstruction 通过索引引用 account_keys
数组中的账户,而不是完整公钥。其包含:
program_id_index:在account_keys中的索引,用于标识要调用的程序。accounts:在account_keys中的索引数组,指定要传递给程序的账户。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>,}
指令的紧凑数组
交易二进制格式
交易采用紧凑编码方案进行序列化。所有变长数组(签名、账户密钥、指令)都以紧凑型 u16 长度编码作为前缀。对于 0-127 的值使用 1 字节,对于更大的值使用 2-3 字节。
传统交易布局(线上传输):
| 字段 | 大小 | 描述 |
|---|---|---|
num_signatures | 1-3 字节(紧凑型 u16) | 签名数量 |
signatures | num_signatures × 64 字节 | Ed25519 签名 |
num_required_signatures | 1 字节 | MessageHeader 字段 1 |
num_readonly_signed | 1 字节 | MessageHeader 字段 2 |
num_readonly_unsigned | 1 字节 | MessageHeader 字段 3 |
num_account_keys | 1-3 字节(紧凑型 u16) | 静态账户密钥数量 |
account_keys | num_account_keys × 32 字节 | 公钥 |
recent_blockhash | 32 字节 | 区块哈希 |
num_instructions | 1-3 字节(紧凑型 u16) | 指令数量 |
instructions | 可变 | 已编译指令 数组 |
每条已编译指令会被序列化为:
| 字段 | 大小 | 描述 |
|---|---|---|
program_id_index | 1 字节 | 账户密钥索引 |
num_accounts | 1-3 字节(紧凑型 u16) | 账户索引数量 |
account_indices | num_accounts × 1 字节 | 账户密钥索引 |
data_len | 1-3 字节(紧凑型 u16) | instruction data 长度 |
data | data_len 字节 | 不透明 instruction data |
大小计算
假设 PACKET_DATA_SIZE = 1,232 字节,可用空间可以这样计算:
Total = 1232 bytes- compact-u16(num_sigs) # 1 byte- num_sigs * 64 # signature bytes- 3 # message header- compact-u16(num_keys) # 1 byte- num_keys * 32 # account key bytes- 32 # recent blockhash- compact-u16(num_ixs) # 1 byte- sum(instruction_sizes) # per-instruction overhead + data
示例:SOL 转账交易
下图展示了交易和指令如何协同工作,使用户能够与网络交互。在本例中,SOL 从一个账户转账到另一个账户。
发送方账户的元数据表明它必须为该交易签名。这允许 System Program 扣除 lamport。发送方和接收方账户都必须可写,以便其 lamport 余额发生变化。为执行此指令,发送方钱包会发送包含其签名的交易,以及包含 SOL 转账指令的消息。
SOL 转账示意图
交易发送后,System Program 会处理转账指令,并更新两个账户的 lamport 余额。
SOL 转账处理流程图
下方示例展示了与上述图示相关的代码。参见 System
Program 的transfer 函数。
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);
下方示例展示了包含单个 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 查找该交易。
{"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?