概要
交易会经过 8 个阶段:接收、签名验证、规范化、预算/年龄检查、手续费支付人验证、账户加载、指令执行和提交。
交易处理流程
当交易到达 validator 时,会经过一系列验证和执行阶段。以下内容描述了从接收到提交的完整流程,并附有 agave validator 客户端的源码参考。
1. 接收与反序列化
validator 通过 UDP/QUIC 接收交易字节。原始字节必须在单个数据包内(PACKET_DATA_SIZE
= 1,232 字节)。这些字节会被反序列化为
VersionedTransaction,其中包含签名数组和
VersionedMessage(可以是 legacy 或 v0)。
2. 签名验证(sigverify)
在进入银行阶段前,签名会在
sigverify 阶段
进行验证。对于每个索引为 i 的签名,验证器会检查 Ed25519(signatures[i],
account_keys[i], message_bytes)。如果有任何签名无效,该数据包会被丢弃。
验证过程是并行化的:validator 会将数据包批次分割为
VERIFY_PACKET_CHUNK_SIZE(128)个分块,并并行处理。
3. 规范化(Sanitize)
反序列化后的交易会被规范化,生成 SanitizedTransaction(或
RuntimeTransaction)。规范化会验证结构性不变量:
- 签名数量与头部中的
num_required_signatures匹配 - 所有指令
program_id_index和account_indices都在有效范围内 - 手续费支付人(账户索引 0)是可写签名者
RuntimeTransaction 包装器会缓存来自
TransactionMeta
的预计算元数据:消息哈希、投票交易标志、预编译签名计数(Ed25519/secp256k1/secp256r1)、计算预算指令详情,以及指令数据总长度。
4. 检查计算预算、区块哈希年龄和状态缓存
该
check_transactions
方法会对每笔交易执行多项检查:
计算预算:首先解析并验证交易的计算预算指令。费用细节根据预算上限和优先级费用计算得出。如果计算预算无效或存在冲突,交易将因计算预算解析错误(如
DuplicateInstruction、InstructionError(..., InvalidInstructionData) 或
InvalidLoadedAccountsDataSizeLimit)而失败。
区块哈希年龄:交易的 recent_blockhash 会在
BlockhashQueue
中查找。如果找到该哈希且其年龄在
MAX_PROCESSING_AGE(150 个 slot)以内,则交易继续处理。如果未找到,validator 会检查是否存在有效的
持久化随机数。
状态缓存:交易的消息哈希会与状态缓存进行比对。如果已存在,则该交易会被拒绝,并返回
AlreadyProcessed。
5. 验证随机数和手续费支付账户
SVM 中的
validate_transaction_nonce_and_fee_payer
方法负责两项验证:
随机数验证(如适用):对于随机数交易,validator 会加载随机数账户并验证:
- 账户归 System Program 所有
- 能够解析为
State::Initialized - 存储的持久化随机数与交易的
recent_blockhash匹配 - 随机数可以推进(当前持久化随机数与下一个持久化随机数不同,即该随机数尚未在当前区块中使用)
- 随机数权限人已签署该交易
如果验证通过,随机数将推进到下一个持久化随机数值。详见
validate_transaction_nonce。
手续费支付账户验证:手续费支付账户(始终为索引 0)会被加载,并由
validate_fee_payer
检查:
- 账户必须存在( lamports > 0 ),否则返回
AccountNotFound - 账户必须是 system 账户或随机数账户,否则返回
InvalidAccountForFee - lamports 必须覆盖
min_balance + total_fee,其中 system 账户的min_balance为 0,随机数账户为rent.minimum_balance(NonceState::size());否则返回InsufficientFundsForFee - 扣除手续费后,账户必须仍然满足免租金(不能从免租金变为需付租金)
此阶段将从 fee payer 中扣除手续费。扣费后的 fee
payer(以及已推进的 nonce,如适用)的快照会被保存为
RollbackAccounts,这些账户即使执行失败也会被提交。
6. 加载账户
load_transaction
会加载交易中引用的所有账户。
AccountLoader
封装了外部账户存储,并维护一个批次本地缓存,使得同一批次中前面交易修改过的账户对后续交易可见。
对于每个非 fee payer 账户,加载器会:
- 从缓存或 accounts-db 获取账户
- 如有需要,更新免租金状态
- 累加账户数据大小至
loaded_accounts_data_size_limit(默认 64 MiB)。每个账户会产生TRANSACTION_ACCOUNT_BASE_SIZE(64 字节)的基础开销,加上其数据长度
对于交易指令调用的每个 program,加载器会验证 program
account 是否存在且归属于有效的 loader(NativeLoader 或 PROGRAM_OWNERS
之一)。无效的 program 会以 ProgramAccountNotFound 或
InvalidProgramForExecution 失败。
LoaderV3(可升级)program 会自动加载其关联的 programdata 账户,该账户的数据大小也计入已加载数据的限制。
如果账户加载失败,但 fee payer 已成功验证,则该交易会变为
FeesOnly
结果:手续费仍会被收取,但不会执行任何指令。
7. 执行指令
execute_loaded_transaction
会使用所有已加载账户创建一个 TransactionContext,并调用
process_message。指令会按照消息中的顺序依次执行。每次指令调用都会创建一个
InvokeContext 并调用目标 program。
指令处理细节
运行时的
process_message
函数会遍历每条指令并调用目标 program:
- 对于每条指令,运行时会调用
prepare_next_top_level_instruction,用于构建InstructionContext。该上下文包含对指令账户(由已编译索引解析)、instruction data 和 program account 索引的引用。 - 运行时会检查该程序是否为 precompile(Ed25519、Secp256k1、Secp256r1)。预编译程序会被直接验证,无需调用 BPF 虚拟机。
- 对于其他所有程序,运行时会调用
process_instruction,从缓存中加载程序并在 BPF 虚拟机中执行。 - 指令执行完成后,运行时会验证所有指令账户的 lamport 总余额未发生变化(
UnbalancedInstruction检查)。 - 如果任何指令失败,整个交易会被回滚,所有中间状态更改都不会被提交。
每条指令都会递增指令追踪。追踪包括顶层指令及其调用的所有
CPI。总追踪长度(顶层指令加所有嵌套 CPI)不能超过 64(MAX_INSTRUCTION_TRACE_LENGTH)。超出该限制会返回
InstructionError::MaxInstructionTraceLengthExceeded。
执行结束后,运行时会验证:
- 所有账户的 lamport 总和未发生变化
- 没有账户从免租变为需付租金
8. 提交或回滚
如果执行成功,TransactionContext 修改后的账户状态会写回到 AccountLoader
的批量本地缓存。如果执行失败,只有
RollbackAccounts(已扣除手续费并推进 nonce 的手续费支付者)会被写回。手续费仍会被收取,其他账户更改都会被丢弃。
流程总结
Receive packet (UDP/QUIC)--> Deserialize into VersionedTransaction--> Sigverify (parallel Ed25519 verification)--> Sanitize (structural validation, metadata extraction)--> Parse compute budget, calculate fees--> Check blockhash age (or verify nonce account)--> Check status cache (dedup)--> Validate nonce authority and advanceability (if nonce transaction)--> Validate fee payer (load, check balance, deduct fee)--> Load all accounts (with data size limits)--> Load programs (verify loaders)--> Execute instructions sequentially--> Verify post-conditions (lamport balance, rent state)--> Commit account changes (or rollback on failure)
交易错误参考
下表列出了所有
TransactionError
变体及其发生的流程阶段:
Is this page helpful?