- 区块链
- Web3
【免费下载链接】grin
Minimal implementation of the Mimblewimble protocol.
本篇围绕 Grin 仓库中的 验证逻辑文档 展开,系统讲解 MimbleWimble 协议下"事务(Transaction)验证"、"区块(Block)验证"与"链验证(Chain validation)"三层校验规则,并结合 chain/src/pipe.rs、core/src/core/transaction.rs、core/src/core/block.rs 等源码定位每条规则的落地实现。读完本文,你将能够说清楚一个区块从进入节点到被接受(或被拒)需要依次通过哪些检查、每项检查背后的密码学/共识原理,以及各检查在代码中的对应函数与文件位置。
验证规则的整体分层
Grin 的验证体系分为两大阶段、三个层次(见 doc/validation/validation_logic.md):
- 独立验证(Standalone Validations):不需要任何其他链状态即可完成,分为两层:
- Transaction:校验事务体(transaction body)的内部一致性;
- Block:在事务校验之上,再校验区块特有的 coinbase 奖励、锁高、以及 header 中的 kernel excess 参与的整体求和。
- 链验证(Chain Validations):必须读取本地链状态(前驱区块、总难度、MMR 根、UTXO 集)才能完成的校验,包括 Header 校验与 Block 全流程处理。
这种分层的核心工程动机在源码注释中写得很明确:chain/src/pipe.rs 中validate_header的注释说明,"第一级区块验证只依赖区块头,以便尽可能便宜(cheap);各项验证按成本递增的顺序排列,以尽量缩小被 DoS 攻击的面积"。也就是说,越便宜(越省 CPU/IO)的检查放在越前面,让恶意节点提交的无效数据尽可能早地被丢弃。
独立事务验证(Transaction)
原文档对事务验证的表述是:校验 "transaction body"(总重量、排序、cut-through、range proof 批量验证、内核签名)、校验 features(禁止 coinbase)、最后校验那个大 "sum"。以下逐项对应到源码实现。
事务校验的入口是 TransactionBody::validate,它内部先调用轻量的validate_read,再补齐两项昂贵验证:
/// Validates all relevant parts of a transaction body. Checks the /// excess value against the signature as well as range proofs for each /// output. pub fn validate(&self, weighting: Weighting) -> Result<(), Error> { self.validate_read(weighting)?; // Now batch verify all those unverified rangeproofs if !self.outputs.is_empty() { let mut commits = vec![]; let mut proofs = vec![]; for x in &self.outputs { commits.push(x.commitment()); proofs.push(x.proof); } Output::batch_verify_proofs(&commits, &proofs)?; } // Verify the unverified tx kernels. TxKernel::batch_sig_verify(&self.kernels)?; Ok(()) }1. 重量校验(weight)
"Validate the total weight, including reward is lower than the consensus max weight" 对应 verify_weight。重量上限不是魔法数字,而是由三个共识常量推导的,定义在 core/src/consensus.rs:
| 常量 | 值 | 含义 |
|---|---|---|
INPUT_WEIGHT | 1 | 每个 input 的重量 |
OUTPUT_WEIGHT | 21 | 每个 output 的重量 |
KERNEL_WEIGHT | 3 | 每个 kernel 的重量 |
MAX_BLOCK_WEIGHT | 40_000 | 区块最大总重量 |
一个值得注意的细节:Weighting枚举(core/src/core/transaction.rs)区分了事务在不同场景下的重量预算:
pub enum Weighting { /// Tx represents a tx (max block weight, accounting for additional coinbase reward). AsTransaction, /// Tx representing a tx with artificially limited max_weight. AsLimitedTransaction(u64), /// Tx represents a block (max block weight). AsBlock, /// No max weight limit (skip the weight check). NoLimit, }AsTransaction场景使用global::max_tx_weight(),其本质是max_block_weight减去一个 coinbase 奖励(1 个 output + 1 个 kernel)的重量——即consensus::OUTPUT_WEIGHT + consensus::KERNEL_WEIGHT。源码注释解释得很直白:"从最大事务构建区块时,必须给额外的 coinbase 奖励(1 output + 1 kernel)留出空间"(verify_weight 注释)。区块校验(AsBlock)则直接使用MAX_BLOCK_WEIGHT。
2. 排序与唯一性校验
"Validate the sorting of inputs, outputs and kernels by their hashes" 对应 verify_sorted:
// Verify that inputs|outputs|kernels are sorted in lexicographical order // and that there are no duplicates (they are all unique within this transaction). fn verify_sorted(&self) -> Result<(), Error> { self.inputs.verify_sorted_and_unique()?; self.outputs.verify_sorted_and_unique()?; self.kernels.verify_sorted_and_unique()?; Ok(()) }强制字典序排序不仅是存储约定,它让 cut-through 检查、去重以及后续的 MMR 插入都成为确定性操作。此外这一检查在反序列化时就会执行:TransactionBody::read中读取后立即调用TransactionBody::init(..., true)完成排序校验(core/src/core/transaction.rs),违规数据会被当作CorruptedData直接拒绝,不必等到完整验证阶段。
3. Cut-through(完全抵消)校验
"Validate the outputs have all been fully cut-through (no inputs matching an included output)" 对应 verify_cut_through。由于引入 "commit only" inputs 后,inputs 与 outputs 不再保证一致的排序方式,实现上采取了一个朴素但可靠的方案:把所有 input 与 output 的 commitment 合并成一个排序向量,然后扫描相邻元素是否有相等项:
fn verify_cut_through(&self) -> Result<(), Error> { let commits = self.inputs_outputs_committed(); for pair in commits.windows(2) { if pair[0] == pair[1] { return Err(Error::CutThrough); } } Ok(()) }即同一笔事务内不允许某个 input 花费本次事务自己产生的 output——事务必须在进入区块前已经"完全 cut-through"。
(补充:当前实现还在此处加入了 verify_no_nrd_duplicates,当 NRD 特性开启时禁止同一事务/区块内出现多个 public excess 相同的 NRD kernel,这是原文档未列出的新增强化项。)
4. Features 校验(禁止 coinbase)
"Verify no output or kernel include invalid features (coinbase)" 对应 verify_features:普通事务体中不允许出现is_coinbase()标记的 output 或 kernel——coinbase 是区块独有的概念,只能由矿工在区块中创建:
fn verify_output_features(&self) -> Result<(), Error> { if self.outputs.iter().any(|x| x.is_coinbase()) { return Err(Error::InvalidOutputFeatures); } Ok(()) }5. 批量 Range Proof 验证
"Batch verify all output range proofs (but only those that aren't in the validation cache yet)" 对应validate中的Output::batch_verify_proofs(&commits, &proofs)(core/src/core/transaction.rs)。range proof 证明每个 output 的隐藏值是非负的(且落在协议允许的位数范围内),是防止"负余额铸币"的关键。文档强调的"仅验证验证缓存中没有的"这一点,在 Grin 中由验证缓存(verifier cache)机制承担——batch_verify_proofs内部会先查缓存,命中则跳过昂贵的密码学验证,未命中的合并批量验证并回填缓存。
6. 内核签名验证
"Verify all kernel signatures against the excess and the message (fee+lock_time?)" 对应TxKernel::batch_sig_verify(&self.kernels)(core/src/core/transaction.rs)。每个TxKernel的 excess 是发送方的私钥对应公钥,签名证明了持有该私钥的一方同意这笔转移(消息体包含 fee 与锁高特征,即 kernel 签名的是 excess 加上 kernel 特征所编码的消息)。同样使用批量验证以摊薄 secp256k1 批处理成本。
7. 整体大 "sum" 校验
文档中的大 "sum"——"all inputs plus reward+fee, all output commitments, all kernels plus the kernel excess"——由Committedtrait 的 verify_kernel_sums 统一实现:
fn verify_kernel_sums( &self, overage: i64, kernel_offset: BlindingFactor, ) -> Result<(Commitment, Commitment), Error> { // Sum all input|output|overage commitments. let utxo_sum = self.sum_commitments(overage)?; // Sum the kernel excesses accounting for the kernel offset. let (kernel_sum, kernel_sum_plus_offset) = self.sum_kernel_excesses(&kernel_offset)?; if utxo_sum != kernel_sum_plus_offset { return Err(Error::KernelSumMismatch); } Ok((utxo_sum, kernel_sum)) }其数学含义是 MimbleWimble 的核心等式:Σ(outputs) + overage = Σ(inputs) + Σ(kernel excesses) + kernel_offset。在 Pedersen 承诺P = vG + rH下,若两侧承诺相等,则说明 value 两侧守恒(overage即 fee − reward 的净差额,以明文计入),同时盲化因子(blinding factor)两侧也守恒——后者保证没有任何人能在不暴露金额的前提下"凭空铸造"余额。对区块而言,overage取 header.overage(),其值是-REWARD(奖励计入 output 一侧,因此 UTXO 侧需要"多出来"一份奖励价值)。
独立区块验证(Block)
区块验证 = 独立事务验证 + 四条区块级规则,入口为 Block::validate:
pub fn validate(&self, prev_kernel_offset: &BlindingFactor) -> Result<(), Error> { self.body.validate(Weighting::AsBlock)?; self.verify_kernel_lock_heights()?; self.verify_nrd_kernels_for_header_version()?; self.verify_coinbase()?; // take the kernel offset for this block (block offset minus previous) // and verify outputs and kernel sums self.verify_kernel_sums( self.header.overage(), self.block_kernel_offset(prev_kernel_offset.clone())?, )?; Ok(()) }四条规则与文档的对应关系:
(1)无未来锁高内核。"Verify no kernels have a future lock height" 对应 verify_kernel_lock_heights:任何HeightLocked内核的lock_height不得大于本区块高度,"交易不能早于其 lock_height 被包含进区块"。
(2)奖励与费用求和正确。"Check that the reward plus fees 'sums' correctly with the coinbase outputs commitments and kernels excess" 对应 verify_coinbase。它筛出所有带 coinbase 标记的 output 与 kernel,验证如下等式(对 miner 而言这就是"收入"校验):
commit_sum(coinbase outputs) + commit_value(reward(total_fees)) == commit_sum(coinbase kernel excesses)源码中用secp.commit_value(reward(self.total_fees()))构造奖励承诺(core/src/core/block.rs),若不匹配则报Error::CoinbaseSumMismatch。这意味着矿工无法把区块里的交易费用划走超出"reward + fees"的部分,因为多出来的价值承诺等式将不成立。
(此处还有文档未列出的补充检查 verify_nrd_kernels_for_header_version:NRD kernel 仅在全局特性开启且区块版本 ≥ 4(HF3 之后)时合法,属于硬分叉调度的落地校验。)
(3)大 "sum" 含 header 的 kernel excess。与事务的差别在于 kernel_offset 的来源:区块用block_kernel_offset(prev),即本区块 header 的 total_kernel_offset 减去前一块的 total_kernel_offset(空区块的特例是取零),从而把"全链累计 kernel offset"换算成"本区块贡献的 offset"后再代入verify_kernel_sums(block_kernel_offset 实现)。这正是文档 Block 一节第 4 条 "all kernels plus the kernel excess from the header" 的代码形态。
(4)"快速拒绝"前置。注意Block::validate只在链处理管道内部被调用(validate_block),在此之前反序列化阶段已经做了一轮"轻量验证":UntrustedBlock::read 在从网络流读取区块时会先执行body.validate_read(Weighting::AsBlock)(跳过 range proof 与签名验证),任何违规直接视为CorruptedData。这样即使面对完全不可信的 P2P 通道,超大、乱序、超重的区块也会在解析层被拒掉,与文档 "quick rejection check first" 的思想一脉相承。
链验证(Chain Validations)
链验证发生在 chain/src/pipe.rs 的处理管道中。文档指出:"Headers and blocks have a quick rejection check first when they've already gone through validation and have either been accepted or definitely rejected (non orphans, no local error)"。
已知区块的快速拒绝(check_known)
block 管道入口 process_block 第一步是 check_known:
// If this block has greater total difficulty than treat as unknown in current context. // If it matches current chain head (latest or previous hash) then we know about it. // If it exists in the local db then we know about it. fn check_known(header: &BlockHeader, head: &Tip, ctx: &BlockContext<'_>) -> Result<(), Error>它分两级判断(check_known_head / check_known_store):区块 hash 与链头最近两个块(last/prev)相同,或已存在于本地存储,则视为已知并快速拒绝;若存储中的已知区块高度比链头低 50 个区块以上,还会上报OldBlock(源码注释标明这是对"恶意反复推送旧区块"节点的一个不完整但可用的滥用检测)。
头部验证(Header)
文档列出的 Header 校验清单在 validate_header 中逐条落地:
| 文档规则 | 源码位置与说明 |
|---|---|
| 按硬分叉表检查 version | consensus::valid_header_version(header.height, header.version)(pipe.rs L379) |
| 时间戳不得过于超前 | 文档写作 "12 * block time";当前实现把该项前移到了头部反序列化:UntrustedBlockHeader::read 用future_time_limit(FTL,grin-server.toml可配置,默认 5 分钟,见 DEFAULT_FUTURE_TIME_LIMIT 与 config/src/comments.rs)拒绝"过于超前的时间戳" |
| 时间戳严格递增(防时间回卷攻击) | header.timestamp <= prev.timestamp→InvalidBlockTime(pipe.rs L383-L387,注释即 "prevent time warp attacks") |
| 高度连续(previous + 1) | header.height != prev.height + 1→InvalidBlockHeight(pipe.rs L373-L376) |
| 拥有 primary 或 secondary PoW 解 | validate_pow_only检查header.pow.is_primary() \|\| is_secondary()(pipe.rs L60-L76) |
| 解是合法长度的 Cuck(ar|at)oo 环 | 由ctx.pow_verifier完成(主网为 Cuckatoo,edge_bits按协议版本选择,创世头 edge_bits 为 29,见 genesis.rs);core/src/pow/lean.rs提供 lean 校验的快速路径 |
| 前驱区块头存在 | prev_header_store,缺失则报 orphan(pipe.rs L322-L330) |
| 总难度严格大于前驱 | header.total_difficulty() <= prev.total_difficulty()→DifficultyTooLow(pipe.rs L420-L422) |
| PoW 满足 header 声称的网络难度 | target_difficulty = header.total_difficulty - prev.total_difficulty,且必须满足header.pow.to_difficulty(header.height) >= target_difficulty(pipe.rs L424-L428) |
| 计算的网络难度等于 header 声称值 | 用 DifficultyIter 从历史难度重放consensus::next_difficulty,要求与target_difficulty完全相等(pipe.rs L433-L443) |
| secondary scaling factor 一致 | header.pow.secondary_scaling != next_header_info.secondary_scaling→InvalidScaling(仅HeaderVersion < 5,pipe.rs L444-L453) |
文档特别提到:"In all header difficulty calculations, the difficulty proven by the proof-of-work is subject to adjustments due to either Cuckatoo sizes or Cuckaroo scaling factor." 这一点正是to_difficulty与next_difficulty分工的原因:前者把单个解的 edge_bits 换算为当块贡献的难度(含 Cuckatoo 图尺寸/缩放调整),后者基于历史窗口重算"期望"的网络难度。二者必须相等,从而把难度调整规则(如 C31 的xpr_edge_bits衰减,见 graph_weight)变成了可验证的链上事实。
此外validate_header还利用 MMR 尺寸做了两条文档未明列的防 DoS 检查:由前后 header 的output_mmr_count/kernel_mmr_count差值得到本块 output/kernel 数量的下界(每块至少 1 个 coinbase output 与 1 个 kernel),并用TransactionBody::weight_by_iok(0, num_outputs, num_kernels)估算重量下界,超过max_block_weight即拒(pipe.rs L389-L407)。
前驱 MMR 根校验("Validate the previous header MMR root is correct against the local MMR")不发生在validate_header内,而是在随后的 header 应用阶段:process_block_header 通过txhashset::header_extending进入可回滚的 header 扩展上下文,先rewind_and_apply_header_fork回放到分叉点,再执行ext.validate_root(header)(校验 header 中携带的 MMR 根与本地 header MMR 一致),然后ext.apply_header(header)写入新叶;若总工作量未增加则ext.force_rollback()丢弃改动。fork 回放过程中每一个被重放的 header 都会再次走 denylist 校验(ctx_specific_validation),保证被回滚重放的链上 header 依然全部合法(rewind_and_apply_header_fork)。
区块全流程(Block pipeline)
文档 Block 一节的四条(完整 header 验证 → 前驱整块存在否则 orphan → 独立区块验证 → 校验 MMR 根与尺寸)对应 pipe::process_block 的顺序步骤:
1. head = ctx.batch.head() // 读取当前链头 2. check_known(...) // 快速拒绝已知块 3. validate_pow_only(...) // 廉价 PoW 预检(避免把无效 PoW 放进孤儿池) 4. prev = prev_header_store(...) // 前驱头,缺失 => orphan 5. process_block_header(&b.header, ctx) // 完整 header 验证 + 写入 header MMR 6. validate_block(b, ctx) // 独立区块验证(含 body.validate) 7. txhashset::extending(...) // 进入可回滚的链扩展单元: a. rewind_and_apply_fork(...) // 回退到分叉点并沿 fork 重放前驱块 b. verify_coinbase_maturity(...) // coinbase 输入必须已成熟(文档未列,UTXO 集层面) c. validate_utxo(...) // 每个 input 必须花费未花费的 output,禁止重复 output d. verify_block_sums(...) // 用前块 block_sums 增量验证全链 utxo/kernel 求和 e. apply_block_to_txhashset(...) // 应用区块后校验 MMR roots 与 sizes f. 若 !has_more_work(...) => force_rollback() // 未增加工作量则回滚扩展 8. add_block(...) // 无论是否更新链头,块都持久化 9. has_more_work ? update_head(...) : Ok(None) // 仅当总工作量更高才更新 body head其中几个值得展开的实现要点:
verify_block_sums是"大 sum"在链状态上的增量版本(pipe.rs L481-L505):它从 db 取前一块的BlockSums { utxo_sum, kernel_sum },用本块的 inputs/outputs/kernels + header overage/kernel offset 调用verify_kernel_sums推出新块的两笔累计和并落盘。源码注释点明其安全性来源:"We know there are no double-spends etc. if this verifies successfully."——累计和能平滑推进,说明本块没有双花、没有凭空价值。validate_utxo(pipe.rs L665-L678)把校验委托给 txhashset 的 UTXO 视图:"Every input must spend an unspent output. No duplicate outputs created." 这正是 MimbleWimble "commit-only" 设计的代价:区块校验仍需要可查询的输出集。- MMR 根与尺寸校验落在 apply_block_to_txhashset:应用区块后
ext.extension.validate_roots(&block.header)?与validate_sizes(&block.header)?,即"Validate the MMRs roots and sizes against our header"。根不匹配意味着发送方声称的状态哈希与真实状态不符,区块直接无效。 - 分叉处理与重验证:
rewind_and_apply_fork(pipe.rs L617-L663)把 header MMR 与 txhashset 一并回退到共同祖先,然后沿新 fork 逐块重放,且每一块都重新执行coinbase 成熟度检查、UTXO 校验与 block_sums 重算——保证切换分叉时状态机的一致性,而不是信任旧分叉上"曾经验证过"的事实。 - 两阶段提交与回滚:所有 MMR/状态改动都在
extending提供的子 batch 中进行,只有当区块使链总工作量增加时才提交;区块本身则始终写入外层 batch("We want to save the block to the db regardless",pipe.rs L160-L164)。这让失败或落后 fork 上的块不会污染状态,同时保留历史块供后续 fork 胜出时使用。
Header 同步批量路径
文档还隐含 header 与 block 可分离处理("header first")。对应 process_block_headers:同步时按批量逐头validate_header+ 入库,再整体应用到 header MMR;若 MMR 验证失败整批可回滚("This batch may be rolled back later if the MMR does not validate successfully")。这解释了process_block中为何允许"见过 header 但没见过整块"的情况继续走完整区块流程。
总结:一次区块接受的完整旅程
把文档骨架与源码对照后,一条区块在 Grin 节点中的验证旅程可以浓缩为三段:
- 解析期(免费防线):
UntrustedBlockHeader/UntrustedBlock反序列化时完成 FTL 时间戳、版本、edge_bits 合法、MMR 尺寸上界、重量上界、排序与 cut-through 等全部轻量检查; - 独立验证期(不依赖链状态):
Block::validate完成 body 全量校验(range proofs、内核签名批量验证)、锁高、coinbase 求和、含 header kernel excess 的大 sum; - 链验证期(依赖本地状态):
pipe管道完成已知块快速拒绝、header 难度/难度增量/secondary scaling、header MMR 根校验、coinbase 成熟度、UTXO 集合校验、block_sums增量校验、txhashset MMR 根与尺寸校验,并以"总工作量是否增加"作为是否更新链头的最终裁决。
这套按成本排序、层层收紧、且所有昂贵状态变更都可在子 batch 中回滚的设计,是 Grin 在保证 MimbleWimble 隐私语义(大 sum 验证金额与盲化因子双重守恒)的同时,抵御无效数据 DoS 的核心工程答案。如需继续深入,建议按本文引用的路径依次阅读 chain/src/pipe.rs、core/src/core/block.rs、core/src/core/transaction.rs 与 core/src/core/committed.rs,以及 chain/tests/mine_simple_chain.rs 等测试中真实走通该管道的用例。
- 区块链
- Web3
【免费下载链接】grin
Minimal implementation of the Mimblewimble protocol.
相关推荐
OmniRoute 管理认证深度解析:四类凭证、双 Scope 体系与 requireManagementAuth 校验链路
OmniRoute 管理认证深度解析:四类凭证、双 Scope 体系与 requireManagementAuth 校验链路 本篇基于 OmniRoute 官方
后端API网关LLM 网关人工智能大模型MCP 服务桌面应用如何快速掌握Bitcoin Core验证接口:深入理解区块链验证回调的事件系统
如何快速掌握Bitcoin Core验证接口:深入理解区块链验证回调的事件系统 Bitcoin Core是比特币网络的核心节点软件,其验证接口系统是确保区块链一
区块链金融科技网络密码学深入理解Ajv中的独立验证代码生成
深入理解Ajv中的独立验证代码生成 什么是独立验证代码 Ajv(Another JSON Schema Validator)提供了从JSON Schema生成独
后端API设计
创作声明:本文部分内容由AI辅助生成(AIGC),仅供参考