当前位置: 首页 > web >正文

大白话解析:多证明验证(Merkle Multi-Proof)​

P.S.关于之前发布的文章【大白话解析】 OpenZeppelin 的 MerkleProof 库:Solidity 默克尔证明验证工具全指南​​(附源代码)-CSDN博客,有朋友反馈希望重点讲解【多数据批量验证】的部分。今天我们就来深入解析这个功能,帮助大家更好地理解与应用!

​​​一. 默克尔树是啥?​​​

想象一棵​​倒过来的树​​,​​叶子节点(最底层)​​ 存放的是数据的哈希(比如 Data0L0 = hash(Data0)),​​非叶子节点​​ 是它下面两个子节点的哈希组合(比如 H0 = hash(L0 + L1))。

​根哈希(Root)​​ 就是整棵树的唯一指纹,只要数据有改动,根哈希就会变。

​我们的树长这样:​

Root (H01 + H23)/             \H01 (H0 + H1)    H23 (H2 + H3)/     \          /     \H0      H1       H2      H3/  \     /  \     /  \     /  \L0  L1   L2  L3 ...(具体数据)
  • ​L1 和 L2​​ 是我们要验证的两个数据块(比如某笔交易或文件片段)。

  • ​Root​​ 是整棵树的根哈希(相当于“总证书”)。


​二、我们要干啥?​

​目标​​:​​不拿出整棵树​​,只用 ​​少量关键信息(L1, L2 + 几个辅助节点)​​,证明 L1L2确实属于这棵树,并且它们的根哈希是对的。

​为什么有用?​

  • ​节省存储/带宽​​:不用传整棵树,只传必要的证明节点。

  • ​快速验证​​:计算几步哈希,就能确认数据是否合法。

多证明验证(Merkle Multi-Proof)的意义与价值​

多证明验证(Merkle Multi-Proof)是区块链和分布式系统中一种​​高效的数据完整性验证技术​​,它的核心意义在于​​用最小的计算和存储开销,证明多个数据块属于某个更大的数据集合(如默克尔树)​​。以下是其关键意义和实际价值:

1. 传统默克尔证明(单证明)的问题​

  • ​单证明(Single Proof)​​:只能验证​​单个叶子节点​​(比如 L1),需要提供从该叶子到根的​​完整路径​​(比如 L1的兄弟 L0、父节点 H0、祖父节点 H01等)。

  • ​缺点​​:如果要验证多个数据(比如 L1和 L2),必须​​分别提供各自的证明路径​​,导致:

    • ​冗余数据​​:多个证明可能包含重复的中间节点(比如 H01可能被多次计算)。

    • ​计算开销大​​:需要多次哈希计算,效率较低。

2. 多证明(Multi-Proof)的优化​

  • ​一次性验证多个叶子​​(比如 L1和 L2),只需提供:

    • ​这些叶子本身​​(L1L2)。

    • ​必要的“缺失节点”​​(比如 L0L3H23),帮助计算路径。

    • ​操作指令(proofFlags)​​,指导程序如何组合这些节点。

  • ​优势​​:

    • ​减少冗余​​:共享中间节点(比如 H01只需计算一次)。

    • ​降低计算量​​:合并多个验证步骤,减少哈希计算次数。
    • ​节省带宽/存储​​:不需要传输整棵树,只需关键节点。

​三、我们需要哪些材料?​

材料

是啥

例子

​要验证的叶子(leaves)​

我们要证明的数据(比如 L1L2

[L1, L2]

​辅助证明节点(proof)​

帮助我们计算路径的“中间节点”(比如 L0, L3, H23

[L0, L3, H23]

​操作指令(proofFlags)​

告诉程序怎么组合这些节点(false= 从 proof取,true= 从 leaves取)

[false, false, false, false]

​关键点​​:

  • proofFlags控制计算顺序​​:告诉程序每一步该用哪个节点(比如先算 L1 + L0,再算 L2 + L3,最后合并到根)。

  • proof提供“缺失的拼图”​​:比如 L0L1的兄弟节点,H23是更高层的哈希。


​四、先看完整代码

// 函数功能:根据给定的一组叶子节点(leaves)、证明节点(proof)、以及证明标识(flags),
// 按顺序两两组合计算哈希,最终构建并返回 Merkle Tree 的根哈希(merkleRoot)
// 这是一个内部纯函数(internal pure),不修改状态也不依赖区块链数据
function processMultiProof(bytes32[] memory proof,          // 辅助证明节点数组,例如一些中间哈希,用来补全 Merkle Tree 分支bool[] memory proofFlags,        // 证明标识数组,长度与需要组合的次数一致(通常为层数)// proofFlags[i] == true 表示当前组合中的第2个操作数 b 应该从 leaves 或 hashes 中取// proofFlags[i] == false 表示第2个操作数 b 应该从 proof 数组中取bytes32[] memory leaves          // 原始的叶子节点数组,例如一个个数据的哈希值,通常是待打包上链的数据的 hash
) internal pure returns (bytes32 merkleRoot) {// ======================// 1. 获取输入数组的长度// ======================uint256 leavesLen = leaves.length;      // 叶子节点的数量,比如 2 个数据 => 2 个叶子uint256 proofFlagsLen = proofFlags.length; // 证明标识数组的长度,表示要进行多少次两两哈希组合,比如 4 次// ======================// 2. 安全检查:参数数量必须匹配,否则无法正确构建 Merkle Tree// ======================// 规则:leaves 的数量 + proof 中辅助节点的数量,应该刚好等于 proofFlags 的数量 + 1// 解释:每执行一次 proofFlags(即一次两两组合),就会减少一个独立节点,最终合并成一个根// 所以:leaves + proof 的总自由节点数 == proofFlags 的组合次数 + 1(最终根)if (leavesLen + proof.length != proofFlagsLen + 1) {revert MerkleProofInvalidMultiproof(); // 如果不匹配,说明参数传错了,直接报错}// ======================// 3. 创建一个数组,用于存储每一层计算出来的中间哈希结果// ======================bytes32[] memory hashes = new bytes32[](proofFlagsLen);// 这个数组将保存每次组合后的哈希,比如 hash(L1,L2), hash(H1,H2), ..., 最后一个是根哈希// 数组长度 == 证明标识的数量 == 组合的次数 == proofFlagsLen// ======================// 4. 初始化指针,用于跟踪当前处理到哪里了// ======================uint256 leafPos = 0;    // 指向 leaves 数组中下一个待处理的叶子节点位置,从第 0 个开始uint256 hashPos = 0;    // 指向 hashes 数组中下一个待使用的中间哈希位置,从第 0 个开始uint256 proofPos = 0;   // 指向 proof 数组中下一个待使用的辅助证明节点位置,从第 0 个开始// ======================// 5. 开始核心逻辑:循环 proofFlagsLen 次,每次处理一次两两组合// ======================for (uint256 i = 0; i < proofFlagsLen; i++) {// 每一次循环,我们要组合两个节点 a 和 b,然后计算 hash(a, b),存入 hashes[i]// --------------------------------------------------// 第一步:选择第一个操作数 a// --------------------------------------------------bytes32 a;if (leafPos < leavesLen) {// 如果还有未使用的叶子节点,就从 leaves 中取第 leafPos 个元素作为 aa = leaves[leafPos];leafPos++;  // 取完后,指针后移,指向下一个叶子} else {// 如果所有叶子都用完了(leafPos >= leavesLen),那就从之前算好的中间哈希中取a = hashes[hashPos];hashPos++;  // 取完后,指针后移,指向下一个中间哈希}// --------------------------------------------------// 第二步:选择第二个操作数 b// --------------------------------------------------bytes32 b;if (proofFlags[i]) {// ✅ 如果 proofFlags[i] == true,表示 b 应该从 leaves 或 hashes 中取(不是从 proof!)if (leafPos < leavesLen) {// 如果还有未使用的叶子,就取 leaves[leafPos] 作为 bb = leaves[leafPos];leafPos++;} else {// 如果叶子用完了,就从之前计算的中间哈希中取 hashes[hashPos] 作为 bb = hashes[hashPos];hashPos++;}} else {// ❌ 如果 proofFlags[i] == false,表示 b 应该从 proof 数组中取(辅助证明节点)if (proofPos < proof.length) {// 如果 proof 还有数据,取 proof[proofPos] 作为 bb = proof[proofPos];proofPos++;} else {// 如果 proof 也用完了(理论上不应该发生,前提是参数校验过了),那就从 hashes 中取b = hashes[hashPos];hashPos++;}}// --------------------------------------------------// 第三步:计算 a 和 b 的组合哈希,并存入结果数组// --------------------------------------------------// 将 a 和 b 拼接后,计算 keccak256 哈希,这就是当前层的中间哈希hashes[i] = keccak256(abi.encodePacked(a, b));// 该哈希会被用于下一层的组合(如果有),或最终成为根哈希}// ======================// 6. 校验:确保所有的 proof 节点都已经被使用(没有多余的遗留)// ======================if (proofPos != proof.length) {revert MerkleProofInvalidMultiproof(); // 如果 proof 还有剩余,说明参数不匹配,证明无效}// ======================// 7. 返回最终的 Merkle Root(根哈希)// ======================// 根哈希就是最后一次计算得到的哈希,也就是 hashes 数组中的最后一个元素// 即:hashes[proofFlagsLen - 1]unchecked {return hashes[proofFlagsLen - 1]; // 返回根哈希}
}

    五、逐步解析计算过程(一步步怎么算?)​

    ​初始状态​​:

    输入参数:leaves:      [L1, L2]          // 要验证的叶子节点(目标数据)proof:       [L0, L3, H23]     // 辅助证明节点(兄弟节点/中间哈希)proofFlags:  [false, false, true, false] // 操作指令,控制如何选择第二个操作数初始化指针和存储:leafPos = 0    // 指向leaves数组的当前位置(初始为0,即L1)hashPos = 0    // 指向hashes数组的当前位置(初始为0,尚未使用)proofPos = 0   // 指向proof数组的当前位置(初始为0,即L0)hashes: [空, 空, 空, 空]  // 用于存储中间计算结果的哈希数组,共4个位置(对应proofFlags的长度)

    参数验证

    // leavesLen + proof.length = proofFlagsLen + 1
    // 2 + 3 = 4 + 1 → 5 = 5 ✓ 验证通过

    计算步骤​​:

    1. ​第一步:算 H0(L1 + L0)​

      // 初始状态变量设置:
      proofFlags[0] = false           // 表示当前要处理的操作数 b(第二个操作数)不是来自 leaves,而是来自 proof(辅助证明节点)
      leafPos = 0                     // 当前要处理的叶子节点索引,初始为 0,表示从第 1 个叶子开始
      hashPos = 0                     // 当前要填充的哈希结果数组索引,初始为 0,表示将第一个计算的哈希存入 hashes[0]
      proofPos = 0                    // 当前要读取的 proof(辅助节点)数组索引,初始为 0// ===========================
      // 步骤 1:选择第一个操作数 a
      // ===========================
      // 判断是否还有未处理的叶子节点:leafPos (0) < leavesLen (2) → 还有叶子未处理
      // 所以:从 leaves 数组中取出第 leafPos (0) 个元素,即第一个叶子节点
      a = leaves[0] = L1                      // 第一个操作数 a 是叶子节点 L1
      // 处理完该叶子后,移动指针到下一个叶子
      leafPos 更新为 1                        // 下一步将处理第 2 个叶子(如果有的话),即 L2// ===========================
      // 步骤 2:选择第二个操作数 b
      // ===========================
      // 根据 proofFlags[0] 的值来决定 b 的来源:
      // proofFlags[0] = false → 表示 b 不是来自 leaves,而是来自 proof(即辅助证明节点)
      // 所以:从 proof 数组中按 proofPos 指定的位置取出 b
      // 当前 proofPos = 0 < proof.length = 3 → 还有可用的 proof 节点
      b = proof[0] = L0                       // 第二个操作数 b 是辅助证明节点中的第 1 个元素,即 L0
      // 处理完该 proof 节点后,移动 proof 指针到下一个
      proofPos 更新为 1                       // 下一步将从 proof[1] 取数据(即 L3)// ===========================
      // 步骤 3:计算当前层的哈希值
      // ===========================
      // 将两个操作数 a 和 b 拼接后做 keccak256 哈希运算
      // 这里是将叶子 L1 和辅助节点 L0 拼接后计算哈希
      hashes[0] = keccak256(abi.encodePacked(L1, L0)) = H0
      // 即:H0 = hash(L1 || L0)// ===========================
      // 步骤 4:更新所有状态变量
      // ===========================
      // leafPos = 1                           // 已处理第 1 个叶子,接下来处理第 2 个叶子(L2)
      // hashPos = 0                           // 当前哈希存入了 hashes[0],hashPos 暂未移动(如果后续有新哈希会递增)
      // proofPos = 1                          // 已使用 proof[0],接下来将使用 proof[1]
      // hashes: [H0, 空, 空, 空]              // 当前哈希数组中,第 0 个位置已经存好第一个中间哈希 H0,其余待填充// 总结当前阶段已完成:
      // - 从 leaves 取出 L1 作为 a
      // - 从 proof 取出 L0 作为 b(因为 proofFlags[0] == false)
      // - 计算 H0 = hash(L1 || L0),存入 hashes[0]
      // - 各指针已正确前移,准备进入下一轮计算(如处理 L2 与 L3 等)
    2. ​第二步:算 H1(L2 + L3)​

      // 当前状态变量:
      proofFlags[1] = false                // 表示当前这一层的第二个操作数 b 不是来自 leaves,而是来自 proof(辅助证明节点)
      leafPos = 1                          // 当前处理的叶子节点索引为 1,即第二个叶子 L2
      hashPos = 0                          // 初始化为 0,但目前 leaves 还没用完,暂时不会从 hashes 中取数据。// 它的含义是:如果未来 leafPos >= leaves.length,就从 hashes[hashPos] 取操作数
      proofPos = 1                         // 当前从 proof 数组中读取的位置为 1,即下一个辅助节点是 proof[1]// ================================
      // 步骤 1:选择第一个操作数 a
      // ================================
      // 判断是否还有未使用的叶子节点:leafPos (1) < leaves.length (2) → 是,还有第 2 个叶子
      // 操作:从 leaves 数组中取出第 1 个元素(索引从 0 开始,所以这是第 2 个叶子)
      a = leaves[1] = L2                   // 第一个操作数 a 是第 2 个叶子节点 L2
      // 处理完该叶子后,移动 leafPos 指针到下一个位置
      leafPos 更新为 2                     // 已经处理完全部 2 个叶子(L1 和 L2),没有更多叶子了// ================================
      // 步骤 2:选择第二个操作数 b
      // ================================
      // 根据 proofFlags[1] 的值判断 b 的来源:
      // proofFlags[1] = false → 表示 b 不是来自 leaves,而是来自 proof(辅助节点数组)
      // 检查:proofPos (1) < proof.length (3) → 是,proof 中还有可用的节点
      // 操作:从 proof 数组中取出第 proofPos (1) 个元素,即第 2 个辅助节点
      b = proof[1] = L3                    // 第二个操作数 b 是辅助证明中的第 2 个节点 L3
      // 处理完该 proof 节点后,将 proofPos 指针向后移动一位
      proofPos 更新为 2                    // 接下来将使用 proof[2](可能是 H23 或其它上层节点)// ================================
      // 步骤 3:计算当前层的哈希值
      // ================================
      // 将两个操作数 a 和 b 拼接后,计算其 keccak256 哈希值
      // 即:将叶子节点 L2 和辅助节点 L3 拼接后做哈希
      hashes[1] = keccak256(abi.encodePacked(L2, L3)) = H1
      // 即:H1 = hash(L2 || L3)// 此哈希将被存入 hashes 数组的第 1 个位置(即 hashes[1])
      // 说明我们正在逐层构建 Merkle Tree 的中间哈希// ================================
      // 步骤 4:更新所有状态变量
      // ================================
      // leafPos = 2                        // 已处理完全部叶子节点(L1 和 L2),没有更多叶子了
      // hashPos = 0                        // 仍然为 0,因为目前还未从 hashes 中取任何数据// 它的含义是:如果未来 leafPos >= leaves.length,就从 hashes[hashPos] 取数据
      // proofPos = 2                       // 已使用 proof[1],接下来将使用 proof[2]
      // hashes: [H0, H1, 空, 空]           // 现在 hashes 数组中://   - hashes[0] = H0 (来自 L1 和 L0)//   - hashes[1] = H1 (来自 L2 和 L3)//   - hashes[2] 和 hashes[3] 尚未使用/计算// ✅ 当前阶段已完成:
      // - 从 leaves 中取出了第 2 个叶子 L2 作为 a
      // - 从 proof 中取出了第 2 个辅助节点 L3 作为 b(因为 proofFlags[1] == false)
      // - 计算出第二个中间哈希 H1 = hash(L2 || L3),并将其存入 hashes[1]
      // - 各状态指针已正确更新,准备进入下一阶段(如处理上层哈希,或生成最终根)
    3. ​第三步:算 H01(H1 + H2)​​

      // 当前状态变量:
      proofFlags[2] = true                // 表示当前这一层的两个操作数 a 和 b 都不是来自 leaves 或 proof,// 而是来自之前已经计算并存储在 hashes 数组中的中间哈希值
      leafPos = 2                         // 当前叶子索引为 2,等于 leaves.length(2),说明所有叶子节点已处理完毕,// 接下来如果需要操作数,只能从 hashes 中获取
      hashPos = 0                         // 当前从 hashes 数组中取数据的起始位置,初始为 0// 用于按顺序获取之前计算好的中间哈希(如 H0, H1, ...)
      proofPos = 2                        // 当前 proof 数组读取位置为 2,但由于 proofFlags[2] = true,// 所以不会使用 proof,proofPos 为下一阶段备用// ================================
      // 步骤 1:选择第一个操作数 a
      // ================================
      // 判断是否还有未使用的叶子节点:
      // leafPos (2) >= leaves.length (2) → 是的,所有叶子已用完
      // 所以:不能从 leaves 中取 a,而需要从之前计算好的中间哈希中获取// 根据规则,从 hashes 数组中按 hashPos 指定的位置取出操作数
      // 当前 hashPos = 0 → 可用,hashes[0] 存在
      a = hashes[0] = H0                 // 第一个操作数 a 是之前计算得到的第 1 个中间哈希 H0
      // 该哈希来自 hash(L1 || L2)// 处理完该 hashes 数据后,将 hashPos 指针向后移动一位
      hashPos 更新为 1                   // 下一步将尝试从 hashes[1] 获取下一个操作数// ================================
      // 步骤 2:选择第二个操作数 b
      // ================================
      // 根据 proofFlags[2] 的值:
      // proofFlags[2] = true → 表示 b 也不是来自 leaves 或 proof,而是来自 hashes// 同样地,从 hashes 数组中按当前 hashPos 指定的位置获取操作数
      // 当前 hashPos = 1 → 可用,hashes[1] 存在
      b = hashes[1] = H1                 // 第二个操作数 b 是之前计算得到的第 2 个中间哈希 H1
      // 该哈希来自 hash(L3 || L4)// 处理完该 hashes 数据后,将 hashPos 指针向后移动一位
      hashPos 更新为 2                   // 下一步若还有操作数,将从 hashes[2] 获取// ================================
      // 步骤 3:计算当前层的哈希值
      // ================================
      // 将两个操作数 a 和 b 拼接后,计算它们的 keccak256 哈希值
      // 即:将上一层的两个中间哈希 H0 和 H1 拼接后做哈希运算
      hashes[2] = keccak256(abi.encodePacked(H0, H1)) = H01
      // 即:H01 = hash(H0 || H1)// 此哈希将被存入 hashes 数组的第 2 个位置(即 hashes[2])
      // 表示这是当前层(或当前步骤)所计算出的第 3 个中间哈希(0, 1, 2)// ================================
      // 步骤 4:更新所有状态变量
      // ================================
      // leafPos = 2                        // 所有叶子节点已处理完毕,保持为 2
      // hashPos = 2                        // 已使用 hashes[0] 和 hashes[1],下一个可用中间哈希是 hashes[2]
      // proofPos = 3                       // 虽然未使用 proof,但 proofPos 自增到 3,为后续可能使用做准备
      // hashes: [H0, H1, H01, 空]          // 当前 hashes 数组内容为://   - hashes[0] = H0    (例如:hash(L1 || L0))//   - hashes[1] = H1    (例如:hash(L2 || L3))//   - hashes[2] = H01   (当前计算:hash(H0 || H1))//   - hashes[3] = 空     (尚未使用/计算)// ✅ 当前阶段已完成:
      // - 由于所有叶子已用完(leafPos >= leaves.length),所以两个操作数 a 和 b 都是从 hashes 中获取
      // - a = hashes[0] = H0,b = hashes[1] = H1
      // - 计算得到新的中间哈希 H01 = hash(H0 || H1),存入 hashes[2]
      // - 各状态指针已正确更新:
      //     - hashPos 移动到了 2,指向下一个可能的中间哈希(hashes[2])
      //     - proofPos 更新为 3(即使未使用,也递增以备后用)
      //     - hashes 数组现在保存了三层中间结果:[H0, H1, H01, 空]
      // - 准备进入下一阶段(如继续向上构建 Merkle Tree,直到生成根哈希)
    4. ​第四步:算最终 Root​

      // 当前状态变量:
      proofFlags[3] = false                // 表示当前这一层的第二个操作数 b 不是来自 hashes,也不是来自 leaves,// 而是来自 proof(辅助证明节点数组)
      leafPos = 2                          // 当前叶子索引为 2,等于 leaves.length(2),说明所有叶子节点已处理完毕,// 如果需要操作数,只能从 hashes 或 proof 中获取
      hashPos = 2                          // 当前从 hashes 数组中取数据的起始位置为 2// 之前已经使用了 hashes[0] 和 hashes[1],现在轮到 hashes[2]
      proofPos = 2                         // 当前从 proof 数组中读取的位置为 2,// 因为 proofFlags[3] = false,所以会从中取出辅助证明节点// ================================
      // 步骤 1:选择第一个操作数 a
      // ================================
      // 判断是否还有未使用的叶子节点:
      // leafPos (2) >= leaves.length (2) → 是的,所有叶子已用完
      // 所以:不能从 leaves 中取 a,而需要从之前计算好的中间哈希中获取// 根据规则,从 hashes 数组中按 hashPos 指定的位置取出操作数
      // 当前 hashPos = 2 → 可用,hashes[2] 存在
      a = hashes[2] = H01                // 第一个操作数 a 是之前计算出的第 3 个中间哈希 H01// 该哈希可能是上一层组合 hash(H0 || H1) 的结果// 处理完该 hashes 数据后,将 hashPos 指针向后移动一位
      hashPos 更新为 3                   // 下一步若还有操作数来自 hashes,将尝试从 hashes[3] 获取// ================================
      // 步骤 2:选择第二个操作数 b
      // ================================
      // 根据 proofFlags[3] 的值:
      // proofFlags[3] = false → 表示 b 不是来自 hashes,而是来自 proof(辅助节点数组)// 检查是否还有可用的 proof 节点:
      // proofPos = 2 < proof.length (3) → 是的,proof 中还有第 3 个元素可用(即 proof[2])
      // 所以:从 proof 数组中取出第 proofPos (2) 个元素
      b = proof[2] = H23                 // 第二个操作数 b 是辅助证明中的第 3 个节点 H23// 该节点可能是上一层另一个分支的中间哈希// 处理完该 proof 节点后,将 proofPos 指针向后移动一位
      proofPos 更新为 3                  // 下一步将超出当前 proof 数组范围(如果还有后续步骤)// ================================
      // 步骤 3:计算当前层的哈希值
      // ================================
      // 将两个操作数 a 和 b 拼接后,计算它们的 keccak256 哈希值
      // 即:将上一层的两个中间哈希 H01(来自 hashes)和 H23(来自 proof)拼接后做哈希运算
      hashes[3] = keccak256(abi.encodePacked(H01, H23)) = Root
      // 即:Root = hash(H01 || H23)// 此哈希将被存入 hashes 数组的第 3 个位置(即 hashes[3])
      // 表示这是当前层(通常是最后一层)所计算出的最终根哈希// ================================
      // 步骤 4:更新所有状态变量
      // ================================
      // leafPos = 2                        // 所有叶子节点已处理完毕,保持为 2
      // hashPos = 3                        // 已使用 hashes[2],下一个可用中间哈希是 hashes[3](即Root)
      // proofPos = 3                       // 已使用 proof[2],已到达或超出当前 proof 数组范围
      // hashes: [H0, H1, H01, Root]        // 当前 hashes 数组内容为://   - hashes[0] = H0      (例如:hash(L1 || L0))//   - hashes[1] = H1      (例如:hash(L2 || L3))//   - hashes[2] = H01     (例如:hash(H0 || H1))//   - hashes[3] = Root    (最终根哈希:hash(H01 || H23))// ✅ 当前阶段已完成:
      // - 由于所有叶子已用完(leafPos >= leaves.length),第一个操作数 a 从 hashes[2] = H01 获取
      // - 由于 proofFlags[3] = false,第二个操作数 b 从 proof[2] = H23 获取
      // - 将 a 和 b 拼接后计算出最终的根哈希:Root = hash(H01 || H23),存入 hashes[3]
      // - 各状态指针已正确更新:
      //     - hashPos 移动到了 3(即Root)
      //     - proofPos 移动到了 3(表示已使用最后一个辅助证明节点)
      //     - hashes 数组完整保存了从叶子到根的完整中间过程:[H0, H1, H01, Root]
      // - 至此,Merkle Tree 的根哈希(Root)已成功计算并存储在 hashes[3]

    ​最终​​:如果算出来的 Root和区块链上的根哈希一致,就证明 L1L2是合法的!


    ​六、总结​

    ✅ ​​多证明验证的核心​​:

    • ​只用少量数据(L1, L2 + 几个辅助节点)​​,就能证明它们属于整棵树。

    • ​不需要暴露所有数据​​,节省存储和计算。

    ✅ ​​为什么安全?​

    • ​每一步哈希都是不可逆的​​,篡改任何一个叶子,根哈希都会变。

    • ​proofFlags 控制计算路径​​,确保计算顺序正确。

    ✅ ​​适用场景​​:

    • ​区块链验证​​(比如证明某个交易在区块里)

    • ​文件完整性检查​​(比如证明某个文件片段没被篡改)

    • ​去中心化存储​​(比如证明你存的数据是正确的)​

    http://www.xdnf.cn/news/18787.html

    相关文章:

  • 可视化-模块1-HTML-03
  • 基于SpringBoot的美食分享平台【2026最新】
  • 构建wezzer平台!
  • Indy HTTP Server 使用 OpenSSL 3.0
  • 知识蒸馏 Knowledge Distillation 1. 监督式微调(SFT):极大似然是前向 KL 的特例
  • Grafana k6 性能测试
  • 深度模块化剖析:构建一个健壮的、支持动态Cookie和代理的Python网络爬虫
  • 保姆级Maven安装与配置教程(Windows版)
  • 基于 MATLAB 的信号处理实战:滤波、傅里叶变换与频谱分析
  • 从文本树到结构化路径:解析有限元项目架构的自动化之道
  • 服务器硬件电路设计之 SPI 问答(四):3 线 SPI、Dual SPI 与 Qual SPI 的奥秘
  • Leetcode 3660. Jump Game IX
  • k8sday16调度器
  • MSF基础知识
  • Java 内存模型(JMM)与并发可见性:深入理解多线程编程的基石
  • Java:HashMap的使用
  • K8s 实战:六大核心控制器
  • 什么是 Nonce?
  • 电力电子simulink练习10:反激Flyback电路搭建
  • Linux 的 TCP 网络编程常用API
  • 图像均衡化详解:从直方图均衡到 CLAHE,让图片告别 “灰蒙蒙“
  • 高数 不定积分(4-3):分部积分法
  • 使用虚幻引擎5(UE5)开发类似《原神》的开放世界游戏:从技术架构到实践指南
  • 内网后渗透攻击--域控制器安全(1)
  • 单表查询-分析函数的应用
  • 重置MySQL数据库的密码指南(Windows/Linux全适配)
  • 在 Ruby 客户端里用 ES|QL
  • WSL-linux部署IndexTTS 记录(含本地 CUDA/cuDNN 编译依赖说明)
  • 鸿蒙 ArkTS 开发:Number、Boolean、String 三种核心基本数据类型详解(附实战案例)
  • 夜间跌倒检测响应速度↑150%!陌讯多模态骨架追踪算法在智慧养老院的落地实践