news 2026/10/7 2:21:30

Polkadot PVF Pre-checking Runtime API 详解:`pvfs_require_precheck` 与 `submit_pvf_check_statement`

作者头像

张小明

前端开发工程师

1.2k 24
文章封面图
Polkadot PVF Pre-checking Runtime API 详解:`pvfs_require_precheck` 与 `submit_pvf_check_statement`
  • 区块链

【免费下载链接】polkadot

Polkadot Node Implementation

项目地址:https://gitcode.com/gh_mirrors/po/polkadot
点击查看免费下载

本指南围绕 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:

  1. 新平行链上链申请(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");
  2. 已有平行链代码升级(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,校验顺序为:

  1. 会话检查:stmt.session_index < current_session返回InvalidTransaction::Stale,> current_session返回InvalidTransaction::Future——即投票必须针对当前会话;
  2. 验证者索引检查:stmt.validator_index必须落在当前活跃验证者集合范围内,否则返回自定义错误码INVALID_TX_BAD_VALIDATOR_IDX(u8 = 1);
  3. 签名检查:用stmt.signing_payload()对签名进行验签,失败返回InvalidTransaction::BadProof;
  4. 主题检查:PvfActiveVoteMap中必须存在对应代码哈希的活跃投票,否则返回INVALID_TX_BAD_SUBJECT(u8 = 2);
  5. 防重复投票:该验证者在本会话内不能重复投票,否则返回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。

会话与"近期区块"锚定:投票只对当前会话有效

投票的会话有效性在运行时与节点两层都有约束:

  1. 运行时:include_pvf_check_statement与ValidateUnsigned都会检查stmt.session_index == current_session,旧会话或未来会话的投票分别被拒绝(PvfCheckStatementStale/PvfCheckStatementFuture);
  2. 节点: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 的端到端数据流:

  1. 触发:新平行链上链或代码升级时,paraspallet 将代码哈希插入PvfActiveVoteList(同一哈希可被多个 cause 合并共享);
  2. 拉取:验证者节点的pvf-checker子系统在新区块激活时调用pvfs_require_precheck获取待检查清单;
  3. 检查:子系统把每个代码哈希通过CandidateValidationMessage::PreCheck交给 Candidate-Validation 子系统,由 PVF 的 prepare/execute 沙箱 worker 执行验证代码(相关实现见 node/pvf/src/lib.rs 与 node/pvf/execute、node/pvf/prepare);
  4. 签名与提交:得到Judgement后,子系统构造PvfCheckStatement、用验证者私钥对signing_payload()(含VCPC魔数)签名,再调用submit_pvf_check_statement把无签名交易注入交易池;
  5. 传播与上链:ValidateUnsigned校验通过后,交易经 gossip 网络传播,被区块生产者包含进区块;
  6. 裁决: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

项目地址:https://gitcode.com/gh_mirrors/po/polkadot
点击查看免费下载
上一篇:WarcraftHelper:5分钟让魔兽争霸3在现代电脑上焕发新生
下一篇:Open CoDesign Agent 运行协议加固指南:从「一次性生成器」到「可见的设计协作者」

创作声明:本文部分内容由AI辅助生成(AIGC),仅供参考

版权声明: 本文来自互联网用户投稿,该文观点仅代表作者本人,不代表本站立场。本站仅提供信息存储空间服务,不拥有所有权,不承担相关法律责任。如若内容造成侵权/违法违规/事实不符,请联系邮箱:809451989@qq.com进行投诉反馈,一经查实,立即删除!
网站建设 2026/10/7 2:20:17

[FastMCP设计、原理与应用-17]从服务器向客户端的反向通知

从通信或者消息交换模式来看&#xff0c;前面涉及的都是从客户端发送请求到服务器并得到对应的响应&#xff0c;这是典型的从客户端到服务器的请求/响应模式&#xff0c;接下来我们介绍两种从服务器向客户端的反向通信模式&#xff1a; 通知&#xff1a;服务端发送单向通知给客…

作者头像 李华
网站建设 2026/10/7 2:19:56

Seata四种分布式事务模式详解:从AT到TCC选型与实战

做后端的人迟早会碰上这么一个问题&#xff1a;明明下单接口在本地环境跑得好好的&#xff0c;一上微服务就成了“薛定谔的订单”——订单表里有一条记录&#xff0c;库存却还是满的。单体时代这种事根本不存在&#xff0c;一个数据库事务包起来&#xff0c;要么全成功要么全失…

作者头像 李华
网站建设 2026/10/7 2:18:50

银河麒麟v10用CrossOver跑Windows exe:安装配置与避坑指南

简介&#xff1a;这份PDF资料面向在银河麒麟桌面版V10系统上需要运行Windows EXE应用的个人与企业用户&#xff0c;重点讲解借助CrossOver这一基于Wine的二进制翻译兼容层完成安装的完整思路。内容涵盖CrossOver工作原理、容器选择、安装包设置&#xff0c;并以WPS与QQ两个EXE为…

作者头像 李华