我有合约
这一节写给已经有合约的接入场景。这里最关键的问题不是 proof 存不存在,而是合约靠什么数据来信任这次验证结果。答案是 receipt 加 Merkle path,而不是原始 proof。所以这条路径一定会经过 verify + aggregate。
下面这张图给你一个端到端结构,重点是数据如何从 zkVerify 到合约:
1) receipt 如何到达合约
zkVerify 在聚合完成时生成 receipt,并由 relayer 发布到目标链的 zkVerify 合约。合约内部用一个映射记录聚合结果:
mapping(uint256 => mapping(uint256 => bytes32)) public proofsAggregations;
发布入口是 submitAggregation 或 submitAggregationBatchByDomainId。前者提交单条聚合,后者批量提交,节省 gas:
function submitAggregation(uint256 _domainId,uint256 _aggregationId,bytes32 _proofsAggregation) external onlyRole(OPERATOR);function submitAggregationBatchByDomainId(uint256 _domainId,uint256[] calldata _aggregationIds,bytes32[] calldata _proofsAggregations) external onlyRole(OPERATOR);
如果你是合约使用方,你不用手动调用这些函数,但你必须理解“receipt 以什么格式写入合约”。否则你无法解释为什么合约里没有你想要的 root。
2) 我如何拿到 Merkle path
proof 被聚合后,你需要拿到它在这棵树里的位置。你可以通过事件与 RPC 拿到:
- 监听
NewAggregationReceipt事件,记录domainId、aggregationId和事件所在 block hash。 - 使用
aggregate_statementPathRPC 根据 block hash、domainId、aggregationId 和 statement 取回 Merkle path。
这一步最容易被忽略的是 block hash。Published storage 只在 receipt 生成的那个 block 存在,如果你错过它,就拿不到路径。
path = aggregate_statementPath(blockHash, domainId, aggregationId, statement)
3) 合约如何验证包含关系
合约验证的入口是 verifyProofAggregation。它会检查 aggregation 是否存在,并用 Merkle path 验证你的 leaf:
function verifyProofAggregation(uint256 _domainId,uint256 _aggregationId,bytes32 _leaf,bytes32[] calldata _merklePath,uint256 _leafCount,uint256 _index) external view returns (bool);
它的核心逻辑是对 root 做 Merkle 验证:
return Merkle.verifyProofKeccak(proofsAggregation,_merklePath,_leafCount,_index,_leaf);
你要提供的关键输入包括:aggregationId、domainId、leaf、Merkle path、leafIndex 和 numberOfLeaves。这些字段绝大部分来自聚合结果或 aggregationDetails。只有 public inputs 的 hash 需要你从 proof 生成侧拿到。
inputs = { domainId, aggregationId, leaf, merklePath, leafIndex, numberOfLeaves }
4) 一个最小的“合约消费”调用路径
- 提交 proof 并进入聚合队列。
- 监听
NewAggregationReceipt,记录 block hash。 - 使用
aggregate_statementPath拿到 Merkle path。 - 组装 leaf 与路径,调用
verifyProofAggregation。
⚠️ 注意:如果你不记录 receipt 事件所在的 block hash,后续无法计算 Merkle path。
💡 提示:合约验证失败时,先检查 leafIndex/numberOfLeaves 是否与 receipt 对应,顺序错了会直接失败。
先记住这条边界:合约端消费的是 receipt + Merkle path,不是 proof 本身。下一节会讲没有合约时如何消费验证结果。