交易结构

摘要

一笔交易由签名和消息组成。消息包含头部、账户地址、最近区块哈希和已编译指令。最大序列化大小:1,232 字节。

一个 Transaction 有两个顶层字段:

  • signatures:签名数组
  • message:交易信息,包括待处理的指令列表
Transaction
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:消息的 header
  • account_keys:交易指令所需的 账户地址 数组
  • recent_blockhash:作为交易时间戳的 blockhash
  • instructions指令 数组
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>,
}

头部

header 字段是一个 MessageHeader 结构体,包含三个 u8 字段,用于将 account_keys 数组划分为权限组:

  • num_required_signatures:交易所需的签名总数。
  • num_readonly_signed_accounts:只读签名账户的数量。
  • num_readonly_unsigned_accounts:只读未签名账户的数量。
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,
}

显示消息头三部分的示意图显示消息头三部分的示意图

账户地址

account_keys 字段是一个经过紧凑编码的公钥数组。每个条目标识至少被交易某个指令使用的账户。该数组必须包含所有账户,并且必须遵循以下严格顺序:

  1. 签名者 + 可写
  2. 签名者 + 只读
  3. 非签名者 + 可写
  4. 非签名者 + 只读

这种严格的顺序允许将 account_keys 数组与消息头中的三个计数 header 结合, 从而无需为每个账户存储元数据标志即可确定每个账户的权限。头部计数 将数组划分为上述四个权限组。

展示账户地址数组顺序的图示展示账户地址数组顺序的图示

最近区块哈希

recent_blockhash 字段是一个 32 字节的哈希值,具有两个作用:

  1. 时间戳:证明该交易是最近创建的。
  2. 去重:防止同一笔交易被重复处理。

区块哈希在 150 个 slot 后失效。如果交易到达时区块哈希已失效,则会被以 BlockhashNotFound 拒绝,除非它是有效的 持久随机数交易

getLatestBlockhash RPC 方法 可用于获取当前区块哈希以及该区块哈希有效的最后区块高度。

指令

instructions 字段是一个经过紧凑编码的数组,包含多个 CompiledInstruction 结构体。每个 CompiledInstruction 通过索引引用 account_keys 数组中的账户,而不是完整公钥。其包含:

  1. program_id_index:在 account_keys 中的索引,用于标识要调用的程序。
  2. accounts:在 account_keys 中的索引数组,指定要传递给程序的账户。
  3. 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>,
}

指令的紧凑数组指令的紧凑数组

交易二进制格式

交易采用紧凑编码方案进行序列化。所有变长数组(签名、账户密钥、指令)都以紧凑型 u16 长度编码作为前缀。对于 0-127 的值使用 1 字节,对于更大的值使用 2-3 字节。

传统交易布局(线上传输):

字段大小描述
num_signatures1-3 字节(紧凑型 u16)签名数量
signaturesnum_signatures × 64 字节Ed25519 签名
num_required_signatures1 字节MessageHeader 字段 1
num_readonly_signed1 字节MessageHeader 字段 2
num_readonly_unsigned1 字节MessageHeader 字段 3
num_account_keys1-3 字节(紧凑型 u16)静态账户密钥数量
account_keysnum_account_keys × 32 字节公钥
recent_blockhash32 字节区块哈希
num_instructions1-3 字节(紧凑型 u16)指令数量
instructions可变已编译指令 数组

每条已编译指令会被序列化为:

字段大小描述
program_id_index1 字节账户密钥索引
num_accounts1-3 字节(紧凑型 u16)账户索引数量
account_indicesnum_accounts × 1 字节账户密钥索引
data_len1-3 字节(紧凑型 u16)instruction data 长度
datadata_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 转账示意图SOL 转账示意图

交易发送后,System Program 会处理转账指令,并更新两个账户的 lamport 余额。

SOL 转账处理流程图SOL 转账处理流程图

发送 SOL 前请验证收款方

System Program 转账会向任意账户添加 lamport。协议层面不会检查收款方是否能将 SOL 转出。lamport 只能由账户的所属程序转出,因此将 SOL 发送至代币 mint、某个程序或您不控制的 PDA 存在永久损失资金的风险 —— 只有所属程序指定的授权方才能将其取回。发送至 token account 的 SOL 只能由该账户的所有者取回,发送方无法找回。

SPL token 转账具有一定的自我保护机制:Token Program 会拒绝账户与预期 mint 不匹配的转账请求。原生 SOL 转账则没有此类保护,因此发送方在签名前必须验证收款方。完整的分类逻辑请参阅验证地址

以下示例展示了与上述图表相关的代码。请参阅 System Program 的 transfer 函数

import { createClient, generateKeyPairSigner, lamports } from "@solana/kit";
import { solanaRpc, rpcAirdrop } from "@solana/kit-plugin-rpc";
import { generatedPayer, airdropPayer } from "@solana/kit-plugin-signer";
import { systemProgram } from "@solana-program/system";
const client = await createClient()
.use(generatedPayer())
.use(
solanaRpc({
rpcUrl: "http://localhost:8899",
rpcSubscriptionsUrl: "ws://localhost:8900"
})
)
.use(rpcAirdrop())
.use(airdropPayer(lamports(1_000_000_000n)))
.use(systemProgram());
const sender = client.payer;
const recipient = await generateKeyPairSigner();
const LAMPORTS_PER_SOL = 1_000_000_000n;
const transferAmount = lamports(LAMPORTS_PER_SOL / 100n); // 0.01 SOL
// Check balance before transfer
const { value: preBalance1 } = await client.rpc
.getBalance(sender.address)
.send();
const { value: preBalance2 } = await client.rpc
.getBalance(recipient.address)
.send();
// Create a transfer instruction for transferring SOL from sender to recipient
const transferInstruction = client.system.instructions.transferSol({
source: sender,
destination: recipient.address,
amount: transferAmount // 0.01 SOL in lamports
});
const transactionSignature = await client.sendTransaction([
transferInstruction
]);
// Check balance after transfer
const { value: postBalance1 } = await client.rpc
.getBalance(sender.address)
.send();
const { value: postBalance2 } = await client.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.context.signature);
Console
Click to execute the code.

以下示例展示了包含单条 SOL 转账指令的交易结构。

import {
createClient,
generateKeyPairSigner,
lamports,
createTransactionMessage,
setTransactionMessageFeePayerSigner,
setTransactionMessageLifetimeUsingBlockhash,
appendTransactionMessageInstructions,
pipe,
signTransactionMessageWithSigners,
getCompiledTransactionMessageDecoder
} from "@solana/kit";
import { solanaRpc, rpcAirdrop } from "@solana/kit-plugin-rpc";
import { generatedPayer, airdropPayer } from "@solana/kit-plugin-signer";
import { systemProgram } from "@solana-program/system";
const client = await createClient()
.use(generatedPayer())
.use(
solanaRpc({
rpcUrl: "http://localhost:8899",
rpcSubscriptionsUrl: "ws://localhost:8900"
})
)
.use(rpcAirdrop())
.use(airdropPayer(lamports(1_000_000_000n)))
.use(systemProgram());
const { value: latestBlockhash } = await client.rpc.getLatestBlockhash().send();
const sender = client.payer;
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 = client.system.instructions.transferSol({
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
}
}
]
}

转账前验证收款方

由于 SOL 转账可成功发送至任意账户,请在签名前核实收款方。获取账户信息,仅向 System Program 钱包(或未充值的链上曲线地址)发送;拒绝向您不控制的 mint、token account、程序及 PDA 转账。

Kit
import {
type Address,
createSolanaRpc,
fetchJsonParsedAccount,
isOffCurveAddress
} from "@solana/kit";
const rpc = createSolanaRpc("https://api.mainnet-beta.solana.com");
const SYSTEM_PROGRAM = "11111111111111111111111111111111" as Address;
/**
* Throws if `recipient` cannot safely receive native SOL.
*
* Only System Program wallets (or unfunded on-curve addresses) are safe. Any
* other account locks the lamports because no authority can debit them.
*/
async function assertSafeSolRecipient(recipient: Address): Promise<void> {
const account = await fetchJsonParsedAccount(rpc, recipient);
if (!account.exists) {
// Off-curve = a PDA with no account; reject conservatively.
if (isOffCurveAddress(recipient)) {
throw new Error(
"Recipient is a PDA with no account; SOL would be locked"
);
}
// On-curve = an unfunded wallet, safe to fund.
return;
}
if (account.programAddress !== SYSTEM_PROGRAM) {
throw new Error(
`Recipient is owned by ${account.programAddress}, not a wallet; SOL would be locked`
);
}
}
// A wallet: safe.
await assertSafeSolRecipient(
"H8sMJSCQxfKiFTCfDR3DUMLPwcRbM61LGFJ8N4dK3WjS" as Address
);
// The USDC mint: rejected before any SOL leaves the sender.
await assertSafeSolRecipient(
"EPjFWdd5AufqSSqeM2qN1xzybapC8G4wEGGkZwyTDt1v" as Address
);
Console
Click to execute the code.

此代码片段用于检查原生 SOL 收款方。如需完整分类(包括处理 SPL 代币转账、token account、ATA 及 Token-2022),请参阅 验证地址

获取交易详情

提交后,可使用交易签名和 getTransaction RPC 方法检索交易详情。

您也可以通过 Solana Explorer 查找该交易。

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
©️ 2026 Solana 基金会版权所有