- 区块链
【免费下载链接】polkadot
Polkadot Node Implementation
本指南围绕 Polkadot 实现者指南(Implementers' Guide)中 runtime-api/pvf-prechecking.md 展开,完整讲解 PVF Pre-checking(验证代码预检查)投票机制所依赖的两个 runtime API:如何查询待预检查的 PVF 列表、如何提交验证者对 PVF 的判定投票,并深入结合
paraspallet 的底层实现、离线状态机与客户端 Pvf-Checker 子系统,帮助读者理解从"代码升级/上链申请"到"投票达成超级多数、裁决生效"的完整闭环。
背景:什么是 PVF Pre-checking
在 Polkadot 平行链协议中,每一条平行链都需要向中继链提交一份Validation Code(验证代码,即 PVF,Parachain Validation Function)。这份代码的质量与安全性直接决定中继链的可用性与安全性:如果某份验证代码存在漏洞、非法指令或会导致中继链验证器超时的行为,那么它一旦被启用,就可能影响整条链的共识。
为此,中继链在**新平行链上链(Onboarding)与已有平行链升级验证代码(Upgrade)**两条路径上都引入了 PVF Pre-checking 流程:验证器需要先在一个受限的沙箱环境中执行待检查的验证代码,确认其能够正常编译、执行并给出结果,然后再通过投票决定该 PVF 是否被接受。相关设计文档见 node/pvf/README.md(仓库内 PVF 子系统说明)以及 runtime-api/validation-code.md(验证代码的查询 API)。
PVF Pre-checking 的两个 runtime API 于运行时版本 v2 加入(见 primitives/src/runtime_api.rs 中/***** Added in v2 *****/注释段),它们是该流程与链上状态交互的唯一入口:
/// 返回所有需要当前活跃验证器投票的 PVF(以验证代码哈希标识)。 /// 一旦某个 PVF 获得了所需支持,它就不会再被该 API 返回。 fn pvfs_require_precheck() -> Vec<ValidationCodeHash>; /// 提交验证者对一个 PVF 的判定(通过或拒绝)。 /// 该调用以无签名交易(unsigned transaction)的形式进入交易池。 fn submit_pvf_check_statement(stmt: PvfCheckStatement, signature: ValidatorSignature);PvfCheckStatement:投票的载体类型
投票的核心数据结构是PvfCheckStatement,它承载了验证者对某个 PVF 的判定结果,定义在 roadmap/implementers-guide/src/types/pvf-prechecking.md 中,并对应着polkadot-primitives中的正式 Rust 类型(见 primitives/src/v5/mod.rs):
struct PvfCheckStatement { /// `true` 表示被检查的 PVF 通过了预检查,`false` 表示未通过。 pub accept: bool, /// 被检查的验证代码的哈希。 pub subject: ValidationCodeHash, /// 该语句仅在指定的会话(session)内有效。 pub session_index: SessionIndex, /// 发起该语句的验证者在活跃验证者集合中的索引。 pub validator_index: ValidatorIndex, }值得注意的语义约束是session_index:一条投票只在它声明的那个会话内有效。因为每个会话的活跃验证者集合不同,投票在会话切换后会自动失效。与此同时,节点侧在签名前需要生成签名负载,其格式在源码中有明确约定(见 primitives/src/v5/mod.rs):
pub fn signing_payload(&self) -> Vec<u8> { const MAGIC: [u8; 4] = *b"VCPC"; // for "validation code pre-checking" (MAGIC, self.accept, self.subject, self.session_index, self.validator_index).encode() }签名负载以魔术字节VCPC开头,再拼接语句的全部字段,用于防止跨域重放(例如把其他协议的签名数据误用作投票)。
Runtime API 一:pvfs_require_precheck(查询待投票 PVF)
该 API 的作用是返回所有需要当前活跃验证者参与投票的 PVF 的代码哈希列表。它是预检查流程的"拉取端":验证者节点周期性地调用它,拿到清单后逐一执行预检查。
在 runtime 实现层,paraspallet 直接读取存储项PvfActiveVoteList返回结果,见 runtime/parachains/src/paras/mod.rs:
/// Returns the list of PVFs (aka validation code) that require casting a vote by a validator in /// the active validator set. pub(crate) fn pvfs_require_precheck() -> Vec<ValidationCodeHash> { PvfActiveVoteList::<T>::get() }而 runtime API 的对外实现则位于 runtime/parachains/src/runtime_api_impl/v5.rs:
/// Returns the list of all PVF code hashes that require pre-checking. See /// [`paras::Pallet::pvfs_require_precheck`]. pub fn pvfs_require_precheck<T: paras::Config>() -> Vec<ValidationCodeHash> { <paras::Pallet<T>>::pvfs_require_precheck() }谁会把 PVF 加入待检查列表
当出现以下两类事件时,新的 PVF 会被插入PvfActiveVoteList:
- 新平行链上链申请(Onboarding):
schedule_para_initialize在登记平行链时,如果验证代码此前从未被检查过,就会启动一次 PVF 预检查(见 runtime/parachains/src/paras/mod.rs 的注释说明:"If the validation code is not already stored in the code storage, then a PVF pre-checking process will be initiated"); - 已有平行链代码升级(Upgrade):平行链提交新代码后,同样会触发预检查。
核心的登记逻辑位于schedule_code_upgrade/schedule_para_initialize内部(见 runtime/parachains/src/paras/mod.rs),其分支逻辑可以概括为:
- 若
PvfActiveVoteMap中没有该代码哈希的活跃投票记录:如果该代码哈希已经在CodeByHash存储中(说明它此前被检查并通过过),则直接**快速通道(fast-track)**为"已接受"状态并执行enact_pvf_accepted;否则创建新的PvfCheckActiveVoteState,并把代码哈希插入PvfActiveVoteList; - 若已有活跃投票记录:执行合并(Coalescing)——不重复开启新的投票,而是把新的"原因(cause)"追加到现有投票的
causes列表中,让多个平行链共用同一次预检查。
这也解释了为什么pvfs_require_precheck的返回值会不断收缩:一旦某 PVF 达成超级多数,它就会从PvfActiveVoteList中移除,后续调用自然不再返回它。
客户端如何消费该 API:Pvf-Checker 子系统
在节点侧,pvf-checker子系统是pvfs_require_precheck的主要消费者。它在每次激活新区块(activation)时调用该 API,见 node/core/pvf-checker/src/lib.rs:
let pending_pvfs = match runtime_api::pvfs_require_precheck(sender, leaf_hash).await { Err(runtime_api::RuntimeRequestError::NotSupported) => return None, Err(_) => { gum::debug!(target: LOG_TARGET, relay_parent = ?leaf_hash, "cannot fetch PVFs that require pre-checking from runtime API"); Vec::new() }, Ok(v) => v, };注意这里的错误处理约定:如果 runtime API 返回NotSupported(即节点连接的运行时版本低于 v2),子系统会直接返回None并放弃该 leaf 的处理;其他错误则回退为空列表。
拿到清单后,子系统对每个待检查的代码哈希发起预检查(见 node/core/pvf-checker/src/lib.rs):
async fn initiate_precheck(state: &mut State, sender: &mut impl overseer::PvfCheckerSenderTrait, relay_parent: Hash, validation_code_hash: ValidationCodeHash, metrics: &Metrics) { ... let (tx, rx) = oneshot::channel(); sender.send_message(CandidateValidationMessage::PreCheck(relay_parent, validation_code_hash, tx)).await; ... }预检查请求被发送给Candidate-Validation 子系统,由它调度 PVF 的编译与执行(涉及node/pvf中的 prepare/execute worker,见 node/pvf/src/lib.rs)。检查结果(Judgement)随后进入投票提交流程。
Runtime API 二:submit_pvf_check_statement(提交判定投票)
第二个 API 用于提交验证者对某个 PVF 的判定。关键设计点:投票以无签名交易(unsigned transaction)的形式传播与上链——验证者节点并不直接修改链上状态,而是把语句通过 gossip 网络像普通交易一样广播出去,最终由某个区块生产者将语句包含进区块,再由运行时处理。
fn submit_pvf_check_statement(stmt: PvfCheckStatement, signature: ValidatorSignature);从节点到交易池:离线提交路径
在 runtime 实现中,该 API 调用paraspallet 的submit_pvf_check_statement方法,后者借助frame_system::offchain::SubmitTransaction将Call::include_pvf_check_statement作为无签名交易提交进交易池(见 runtime/parachains/src/paras/mod.rs):
/// Submits a given PVF check statement with corresponding signature as an unsigned transaction /// into the memory pool. Ultimately, that disseminates the transaction accross the network. /// /// This function expects an offchain context and cannot be callable from the on-chain logic. pub(crate) fn submit_pvf_check_statement(stmt: PvfCheckStatement, signature: ValidatorSignature) { use frame_system::offchain::SubmitTransaction; if let Err(e) = SubmitTransaction::<T, Call<T>>::submit_unsigned_transaction( Call::include_pvf_check_statement { stmt, signature }.into(), ) { log::error!(target: LOG_TARGET, "Error submitting pvf check statement: {:?}", e); } }runtime API 层的包装见 runtime/parachains/src/runtime_api_impl/v5.rs。
无签名交易的校验:ValidateUnsigned
由于是无签名交易,运行时必须通过ValidateUnsigned预先筛选合法交易,防止垃圾交易进入交易池。该逻辑位于 runtime/parachains/src/paras/mod.rs,校验顺序为:
- 会话检查:
stmt.session_index < current_session返回InvalidTransaction::Stale,> current_session返回InvalidTransaction::Future——即投票必须针对当前会话; - 验证者索引检查:
stmt.validator_index必须落在当前活跃验证者集合范围内,否则返回自定义错误码INVALID_TX_BAD_VALIDATOR_IDX(u8 = 1); - 签名检查:用
stmt.signing_payload()对签名进行验签,失败返回InvalidTransaction::BadProof; - 主题检查:
PvfActiveVoteMap中必须存在对应代码哈希的活跃投票,否则返回INVALID_TX_BAD_SUBJECT(u8 = 2); - 防重复投票:该验证者在本会话内不能重复投票,否则返回
INVALID_TX_DOUBLE_VOTE(u8 = 3)。
自定义错误码常量定义在同文件 runtime/parachains/src/paras/mod.rs:
// custom transaction error codes const INVALID_TX_BAD_VALIDATOR_IDX: u8 = 1; const INVALID_TX_BAD_SUBJECT: u8 = 2; const INVALID_TX_DOUBLE_VOTE: u8 = 3;通过校验后,交易被赋予ValidTransaction属性:优先级取自T::UnsignedPriority,存活期(longevity)约为平均会话长度的一半,并以(session_index, validator_index, subject)三元组作为and_provides键,用于交易池内的去重与依赖管理:
ValidTransaction::with_tag_prefix("PvfPreCheckingVote") .priority(T::UnsignedPriority::get()) .longevity(TryInto::<u64>::try_into( T::NextSessionRotation::average_session_length() / 2u32.into(), ).unwrap_or(64_u64)) .and_provides((stmt.session_index, stmt.validator_index, stmt.subject)) .propagate(true) .build()注意pre_dispatch直接返回Ok,因为真正完整的校验会在include_pvf_check_statement调度(dispatch)时再执行一遍(见 runtime/parachains/src/paras/mod.rs 的注释说明)。
上链裁决:include_pvf_check_statement调度逻辑
当无签名交易被包含进区块后,运行时执行include_pvf_check_statement(call_index = 7,见 runtime/parachains/src/paras/mod.rs)。其流程如下:
ensure_none(origin)?; // 必须是无签名交易来源 let validators = shared::Pallet::<T>::active_validator_keys(); let current_session = shared::Pallet::<T>::session_index(); // 1. 会话必须等于当前会话 if stmt.session_index < current_session { return Err(Error::<T>::PvfCheckStatementStale.into()) } else if stmt.session_index > current_session { return Err(Error::<T>::PvfCheckStatementFuture.into()) } // 2. 验证者索引与公钥 let validator_index = stmt.validator_index.0 as usize; let validator_public = validators.get(validator_index) .ok_or(Error::<T>::PvfCheckValidatorIndexOutOfBounds)?; // 3. 验签 let signing_payload = stmt.signing_payload(); ensure!(signature.verify(&signing_payload[..], &validator_public), Error::<T>::PvfCheckInvalidSignature); // 4. 该 PVF 必须存在活跃投票 let mut active_vote = PvfActiveVoteMap::<T>::get(&stmt.subject) .ok_or(Error::<T>::PvfCheckSubjectInvalid)?; // 5. 防重复投票 ensure!(!active_vote.has_vote(validator_index) .ok_or(Error::<T>::PvfCheckValidatorIndexOutOfBounds)?, Error::<T>::PvfCheckDoubleVote);随后根据stmt.accept写入对应的投票位(votes_accept或votes_reject),再调用active_vote.quorum(validators.len())检查是否达成结果。若达成,则从活跃投票映射与列表中移除该代码哈希,并根据结果执行裁决:
- Accepted(通过)→
enact_pvf_accepted(见 runtime/parachains/src/paras/mod.rs):对每个 cause 触发事件PvfCheckAccepted,Onboarding 原因会推进平行链入列ActionsQueue,Upgrade 原因则根据validation_upgrade_delay与minimum_validation_upgrade_delay计算升级生效区块并写入FutureCodeUpgrades; - Rejected(拒绝)→
enact_pvf_rejected(见 runtime/parachains/src/paras/mod.rs):对每个 cause 触发事件PvfCheckRejected,同时减少验证代码的引用计数(decrease_code_ref),并回滚对应路径:Onboarding 原因移除UpcomingParasGenesis、CurrentCodeHash、ParaLifecycles;Upgrade 原因写入UpgradeGoAhead::Abort信号并移除FutureCodeHash。
投票状态机与"超级多数"判定
投票的中间状态由PvfCheckActiveVoteState描述,定义于 runtime/parachains/src/paras/mod.rs:
pub(crate) struct PvfCheckActiveVoteState<BlockNumber> { votes_accept: BitVec<u8, BitOrderLsb0>, // 每个验证者一个 bit,投"通过"置 1 votes_reject: BitVec<u8, BitOrderLsb0>, // 每个验证者一个 bit,投"拒绝"置 1 age: SessionIndex, // 该投票经历过的会话变更次数 created_at: BlockNumber, // 投票创建的区块号 causes: Vec<PvfCheckCause<BlockNumber>>, // 触发本次预检查的原因列表(至少一个) }几个值得展开的设计点:
- 位向量即选票:
votes_accept与votes_reject的长度始终等于当前活跃验证者数量,一个验证者只能在其中一方置位,置位后即不可再投票(has_vote检查,见 runtime/parachains/src/paras/mod.rs); - 会话边界重置:每次会话切换时,
reinitialize_ballots会把两个位向量清零并按新会话的验证者数量重新调整大小(见 runtime/parachains/src/paras/mod.rs),因为旧会话的投票已失效;同时age自增,供后续上链/升级延迟计算使用; - "原因合并"(Coalescing):多个平行链可以共享同一次 PVF 预检查,
causes记录所有等待该结果的平行链,裁决时逐条执行。
超级多数(supermajority)判定逻辑位于quorum方法(见 runtime/parachains/src/paras/mod.rs):
fn quorum(&self, n_validators: usize) -> Option<PvfCheckOutcome> { let accept_threshold = primitives::supermajority_threshold(n_validators); // 达到该阈值后,通过已无可能,因此判为拒绝 let reject_threshold = n_validators - accept_threshold; if self.votes_accept.count_ones() >= accept_threshold { Some(PvfCheckOutcome::Accepted) } else if self.votes_reject.count_ones() > reject_threshold { Some(PvfCheckOutcome::Rejected) } else { None } }supermajority_threshold与拜占庭阈值byzantine_threshold的关系定义在 primitives/src/v5/mod.rs:
pub const fn byzantine_threshold(n: usize) -> usize { n.saturating_sub(1) / 3 } /// The supermajority threshold of validators which represents a subset /// guaranteed to have at least f+1 honest validators. pub const fn supermajority_threshold(n: usize) -> usize { n - byzantine_threshold(n) }即通过需要至少 n - (n-1)/3 票(保证包含 f+1 个诚实验证者);而拒绝的判定更宽松——当剩余票数已不足以让"通过"达成超级多数(即拒绝票数 > n - supermajority_threshold)时,就直接判为拒绝。该函数带有单元测试,见 primitives/src/v5/mod.rs:
assert_eq!(supermajority_threshold(4), 3); assert_eq!(supermajority_threshold(5), 4); assert_eq!(supermajority_threshold(6), 5); assert_eq!(supermajority_threshold(7), 5);节点侧的投票去重与提交
在节点侧,pvf-checker子系统在提交前维护一个voted: &mut HashSet<ValidationCodeHash>,对同一代码哈希只会提交一次(见 node/core/pvf-checker/src/lib.rs)。提交前先构造语句并签名:
let stmt = PvfCheckStatement { accept: judgement.is_valid(), session_index, subject: validation_code_hash, validator_index: credentials.validator_index, }; let signature = match polkadot_node_subsystem_util::sign( keystore, &credentials.validator_key, &stmt.signing_payload(), ) { Ok(Some(signature)) => signature, ... };随后调用runtime_api::submit_pvf_check_statement(见 node/core/pvf-checker/src/lib.rs)。同时,子系统会记录投票指标on_vote_submitted、on_vote_duplicate等,相关定义见 node/core/pvf-checker/src/metrics.rs。
会话与"近期区块"锚定:投票只对当前会话有效
投票的会话有效性在运行时与节点两层都有约束:
- 运行时:
include_pvf_check_statement与ValidateUnsigned都会检查stmt.session_index == current_session,旧会话或未来会话的投票分别被拒绝(PvfCheckStatementStale/PvfCheckStatementFuture); - 节点:
pvf-checker子系统在每次新区块激活时,通过session_index_for_childruntime API 获取下一个会话索引,只有在新会话到来(state.latest_session.map_or(true, |l| l < session_index))时才重新获取签名凭据(见 node/core/pvf-checker/src/lib.rs)。签名凭据通过check_signing_credentials从当前活跃验证者列表(validatorsAPI)与本地 keystore 中匹配获得(见 node/core/pvf-checker/src/lib.rs)。
测试验证与实测路径
paraspallet 的测试套件对预检查全流程有完整的覆盖,见 runtime/parachains/src/paras/tests.rs:
- 预检查发起后,
pvfs_require_precheck()返回非空列表(测试中对 Onboarding/Upgrade 两种 cause 均有断言,见该文件中assert!(!Paras::pvfs_require_precheck().is_empty())的多处出现); - 投票达成后,列表清空(
assert!(Paras::pvfs_require_precheck().is_empty())),验证"达成所需支持后不再返回"这一语义。
此外,runtime/parachains/src/paras/benchmarking/pvf_check.rs提供了include_pvf_check_statement相关基准,用于生成WeightInfo中五个权重函数(include_pvf_check_statement、..._finalize_upgrade_accept/reject、..._finalize_onboarding_accept/reject)的实测值,接口定义见 runtime/parachains/src/paras/mod.rs。
完整数据流总结
至此,可以给出 PVF Pre-checking 的端到端数据流:
- 触发:新平行链上链或代码升级时,
paraspallet 将代码哈希插入PvfActiveVoteList(同一哈希可被多个 cause 合并共享); - 拉取:验证者节点的
pvf-checker子系统在新区块激活时调用pvfs_require_precheck获取待检查清单; - 检查:子系统把每个代码哈希通过
CandidateValidationMessage::PreCheck交给 Candidate-Validation 子系统,由 PVF 的 prepare/execute 沙箱 worker 执行验证代码(相关实现见 node/pvf/src/lib.rs 与 node/pvf/execute、node/pvf/prepare); - 签名与提交:得到
Judgement后,子系统构造PvfCheckStatement、用验证者私钥对signing_payload()(含VCPC魔数)签名,再调用submit_pvf_check_statement把无签名交易注入交易池; - 传播与上链:
ValidateUnsigned校验通过后,交易经 gossip 网络传播,被区块生产者包含进区块; - 裁决:
include_pvf_check_statement完成会话、索引、签名、主题、防重复投票五项校验后记票,达超级多数则移除该 PVF 并执行enact_pvf_accepted/enact_pvf_rejected,从此pvfs_require_precheck不再返回该代码哈希。
相关文档与源码索引
- 实现者指南:PVF Pre-checking 设计页 runtime-api/pvf-prechecking.md、类型定义页 types/pvf-prechecking.md、验证代码查询 API runtime-api/validation-code.md
- runtime API 声明:
PvfPrecheckingApi位于 primitives/src/runtime_api.rs - 类型与签名负载:
PvfCheckStatement、supermajority_threshold位于 primitives/src/v5/mod.rs 与 primitives/src/v5/mod.rs - runtime 实现:
paraspallet 的投票状态机、调度逻辑与裁决逻辑位于 runtime/parachains/src/paras/mod.rs,runtime API 包装位于 runtime/parachains/src/runtime_api_impl/v5.rs - 节点侧消费方:
pvf-checker子系统 node/core/pvf-checker/src/lib.rs 及其指标 node/core/pvf-checker/src/metrics.rs - 测试与基准:runtime/parachains/src/paras/tests.rs、runtime/parachains/src/paras/benchmarking/pvf_check.rs
- 区块链
【免费下载链接】polkadot
Polkadot Node Implementation
相关推荐
local-deep-research News API 结构化异常处理:NewsAPIException 异常层级设计与 FastAPI 集成
local deep research News API 结构化异常处理:NewsAPIException 异常层级设计与 FastAPI 集成 本文以 loc
区块链Microsoft.Extensions.Configuration.Binder 源码级解析:从反射绑定到源生成器的完整指南
Microsoft.Extensions.Configuration.Binder 源码级解析:从反射绑定到源生成器的完整指南 Microsoft.Extens
区块链Polkadot 实现者指南:`persisted_validation_data` Runtime API 与持久化验证数据详解
Polkadot 实现者指南: persisted_validation_data Runtime API 与持久化验证数据详解 本篇指南聚焦 Polkadot
区块链
创作声明:本文部分内容由AI辅助生成(AIGC),仅供参考