摘要:跨链桥的风险常常不在“签名算法被攻破”,而在验证语义写错

跨链桥最危险的漏洞通常不是密码学原语本身失效,而是验证合约把“看起来像证明”的字节串误当成了“绑定到正确链、正确消息、正确验证者集合、正确状态根的证明”。签名伪造、Merkle 证明欺诈、空证明(Empty Witness)、验证者集合时序错配和非标准编码解析,都会把桥的信任根从密码学安全降级成实现细节安全。本文的核心结论是:跨链桥验证器必须同时检查四个绑定关系:消息和源链状态绑定、签名和验证者集合绑定、Merkle 叶子和树域绑定、验证者更新和时间窗口绑定。少一个绑定,攻击者不必破坏哈希或私钥,也可能让链上合约接受错误提现、错误铸造或错误释放。

问题边界:只讨论防御和取证,不提供攻击模板

本文讨论跨链桥验证层的防御模型,不提供可复现漏洞利用步骤、真实目标筛选方法、伪造证明 payload 或私钥绕过细节。系统角色包括源链事件、目标链验证合约、桥中继器、多签/Guardian/Validator 集合、Merkle 或向量承诺证明、签名解析库、升级治理和跨链交换路由器。攻击者能力被抽象为:提交任意字节格式的消息和证明、重放旧验证者集合、诱导编码边界、利用实现对空值或重复值的错误处理,但不假设能破解安全哈希或获得诚实验证者私钥。

跨链桥常见信任根有多签、轻客户端、IBC 类向量承诺、ZK light client、TEE、MPC 和 optimistic fraud proof。本文重点讨论多签/证明验证型桥,因为历史事故中大量损失并非来自“链本身回滚”,而是来自目标链验证逻辑接受了不该接受的证明。对 AllSwap 这类无托管跨链交换,桥验证错误会影响资产释放、退款归因和路由风险定价。

桥验证状态机:证明必须绑定到完整上下文

一笔跨链释放可以抽象为:

`M = (srcChainId, srcBlock, emitter, nonce, token, amount, recipient, dstChainId, action)`

目标链验证器接受的不是裸消息,而是 `Proof(M, C)`,其中 `C` 是验证上下文:源链状态根、验证者集合编号、签名域、链 ID、合约地址、消息类型和过期窗口。安全验证器的状态机应接近:

```text verify_bridge_message(M, proof, ctx): require(ctx.dstChainId == localChainId) require(ctx.verifierAddress == this) require(valid_validator_set(ctx.validatorSetId, ctx.time)) require(verify_signatures(hash_domain(M, ctx), proof.signatures, ctx.validatorSet)) require(verify_inclusion(M, proof.merkle, ctx.stateRoot)) require(!consumed(M.nonce, M.emitter, M.srcChainId)) mark_consumed(...) execute_release_or_mint(M) ```

这段伪代码的重点是顺序和绑定。签名不能只签 `amount + recipient`;Merkle 证明不能只证明某个叶子在某棵树里;验证者集合不能只按当前状态读取;nonce 不能只在目标链本地递增。每个字段都要回答一个问题:这条消息来自哪条链、由哪个桥合约发出、在什么状态根下存在、由哪一代验证者确认、是否已经被消费、目标链是否就是当前链。

Merkle 空证明与叶子二义性:证明的“形状”也要被验证

Merkle 证明安全的前提不是“有一串 sibling hash 就行”,而是树结构、叶子编码、排序规则、空值语义和域分离都明确。空证明风险出现在验证器把缺失路径、默认零哈希、空 sibling 列表或未初始化 root 误解释成有效存在证明。Nomad 事故的核心教训之一就是:默认值和初始化状态若被验证逻辑当成可信 root,会让大量消息获得错误通过条件。

防御侧可以把 Merkle 验证拆成三个不变量:

`leaf = H(LEAF_DOMAIN || canonical_encode(M))`

`node = H(NODE_DOMAIN || left || right)`

`root == recompute(leaf, siblings, path, tree_height)`

这里 `LEAF_DOMAIN` 和 `NODE_DOMAIN` 必须不同,避免叶子和内部节点二义性;`canonical_encode` 必须唯一,避免可变长度字段拼接产生同一字节串;`tree_height`、`path` 和 sibling 数量必须匹配,避免短证明、空证明和路径截断。若树支持非存在证明,还必须把存在证明和非存在证明的类型域分开,不能让“空 witness”自动落入存在验证路径。

二次原像攻击在实际桥漏洞中常常不是攻破哈希函数,而是利用编码二义性:例如 `H(a || b)` 无长度前缀时,`a` 和 `b` 的边界不唯一;叶子没有域标签时,一个内部节点哈希可能被当成叶子;排序 Merkle tree 没有严格处理重复叶子时,成员证明和位置证明混淆。OpenZeppelin MerkleProof 文档也提醒过叶子长度和内部节点拼接可能造成歧义,桥验证器更不能忽视这些边界。

签名解析:阈值不是签名数量,而是唯一有效权重

多签桥通常要求 `k-of-n` 验证者签名。错误实现会把阈值检查简化成“有效签名数量 ≥ k”,却没有严格检查唯一验证者、签名域、验证者集合版本和编码规范。安全的阈值函数应更接近:

`weight = Σ votingPower(v_i) for unique v_i where Verify(pk_i, domain_hash(M, ctx), sig_i)`

然后检查 `weight >= threshold(ctx.validatorSetId)`。如果同一验证者的签名可以重复计数,或可变长度公钥数组允许同一 key 以不同编码出现,阈值就会被逻辑层削弱。若 ECDSA 未处理低 `s`、`v` 范围或地址恢复边界,签名可塑性会污染去重;若 Ed25519 或 DER 编码解析允许非规范长度、前导零、未清理内存或空公钥,验证器可能把错误输入当成合法身份。

EIP-712 的价值在桥场景里不是“更好读的签名”,而是域分离:签名应绑定链 ID、验证合约、消息类型和版本。EIP-2 对 ECDSA 可塑性的约束也说明同一个数学签名若有多种编码表示,会给去重和阈值统计带来风险。RFC 8032 对 Ed25519 编码和验证规则给出规范边界;桥合约若自己实现或移植签名库,必须把非规范编码拒绝在验证入口,而不是在业务逻辑后半段再处理。

实现上还要避免“解析成功”和“身份有效”混在一起。签名库返回 recovered address 或验证通过,只说明该字节串满足曲线验证;桥验证器还必须检查该公钥是否属于指定 `validatorSetId`、是否在该高度区间有效、是否已被本证明计数、是否满足最小权重和是否没有被治理移除。把这些检查拆成单独状态,有助于审计和监控:`decoded`、`curveValid`、`memberValid`、`uniqueSigner`、`thresholdReached`。任何一步失败都不应进入 Merkle 或执行阶段。

对可变长度数组也要保守。签名数量、签名顺序、validator index bitmap、公钥数组和权重数组必须有一致的长度约束。若验证器允许中继器提交任意顺序签名,应先排序或按 validator index 去重;若使用 bitmap,应检查 bitmap 不超出集合长度;若使用权重,应确保权重来自链上集合而不是证明输入。阈值安全来自“链上已知集合中的唯一权重”,不是来自证明字节里自称的 signer 列表。

验证者更新:历史集合不能被“复活重签”

验证者集合更新是桥系统最容易被低估的状态机。假设第 `e` 代验证者负责确认源链高度区间 `[h_e_start, h_e_end)`。目标链验证器收到消息时,不能简单使用“当前验证者集合”,也不能让中继器随意声明一个旧集合。正确逻辑需要把消息高度、签名时间、验证者集合 ID 和更新日志原子绑定:

`validSet(M, V_e) := h_e_start <= M.srcBlock < h_e_end and updateLog(e) finalized before proofTime`

如果更新日志不是原子执行,就可能出现三类错误:旧验证者集合被用于签署新高度消息;新验证者集合被用于追认旧消息;中继器在更新前后选择对自己有利的集合。所谓“历史验证者复活重签”并不要求旧验证者私钥全部泄露,只要协议允许过期集合参与当前消息,就扩大了攻击面。

防御策略包括:验证者更新必须有单调 epoch;每条消息必须声明源链高度或事件序号;验证器必须拒绝过期集合签署新区间;更新日志本身要经过相同或更强信任根确认;跨链消息消费要在验证者更新和消息执行之间保持确定顺序。对高价值桥,还应将验证者更新事件和大额释放分开延迟处理,避免治理更新和资金释放在同一风险窗口内耦合。

更严格的桥会把验证者更新看成二阶段提交。第一阶段只记录 pending set 和生效高度;第二阶段在足够源链最终性后激活集合。这样做降低了重组和中继器选择性提交带来的时序错配。代价是更新延迟更长,极端情况下会影响紧急轮换。工程上可以给大额消息设置更长的 validator-set finality window,而给小额消息使用普通窗口。关键是让风险窗口显式存在,而不是藏在“当前集合”这个模糊变量后面。

审计时应检查三个日志:验证者集合更新日志、消息证明日志和消费日志。三者必须单调且可关联。若更新日志允许跳号,消息证明日志不带源高度,消费日志只按目标链 nonce 去重,就很难在事故后判断哪一代验证者对哪条消息负责。桥的可恢复性很大程度取决于这些日志能否让资金释放被准确归因。

证明 schema:桥需要机器可检查的语义,而不是口头约定

跨链桥常把证明对象做成一段紧凑字节串,前端、SDK、中继器和合约各自理解字段含义。风险在于字段语义没有被 schema 强制约束。一个更稳健的证明 schema 至少应包含:

- `proofType`:多签、Merkle inclusion、IBC commitment、ZK proof 或 optimistic proof; - `sourceDomain`:源链、源合约、源高度、消息类型; - `targetDomain`:目标链、验证合约、动作类型; - `validatorContext`:validatorSetId、生效区间、阈值规则; - `stateContext`:状态根、树高度、路径规则、叶子域; - `consumptionKey`:防重放的完整 key。

这些字段不一定都要公开给用户,但必须被验证器使用。若 schema 缺少 `targetDomain`,同一证明可能跨目标链重放;若缺少 `proofType`,非存在证明可能被错误塞进存在证明路径;若缺少 `stateContext`,同一 Merkle 路径可能在不同树规则下被解释;若缺少 `validatorContext`,签名阈值就变成中继器提供的上下文。

对于跨链路由器,schema 还提供风控输入。多签证明的风险指标是 signer 集中度和阈值;Merkle 证明的风险指标是 root finality 和 proof schema;轻客户端证明的风险指标是源链共识和验证成本;ZK proof 的风险指标是电路覆盖和公共输入绑定。没有 schema,路由器只能按桥品牌做粗粒度判断;有 schema,才能按路径做细粒度风险定价。

失败模式:桥验证错误如何变成资产损失

第一类失败是空 root 或默认 root 被接受。未初始化的 root、默认零值或空证明被验证器误认为有效状态,会让不存在的消息通过。检测信号包括大量证明使用相同短路径、同一 root 下消息异常扩散、初始化后首批消息金额异常。

第二类失败是叶子编码二义性。不同消息经过非规范编码后得到相同叶子,或内部节点哈希被当成叶子。检测信号包括可变长度字段、无域标签 hash、重复叶子、路径长度不固定。防御是 canonical encoding、长度前缀、叶子/节点域分离和固定树高度。

第三类失败是签名去重错误。重复签名、重复公钥、可塑性签名或不同编码的同一验证者被重复计权。检测信号包括 signer bitmap 异常、签名数量接近阈值但唯一 signer 少、同一公钥多种编码出现。防御是唯一验证者索引、低 `s` 规则、规范公钥编码和权重去重。

第四类失败是验证者集合时序错配。旧集合签新消息、新集合追认旧消息、或中继器选择有利 epoch。检测信号包括证明中的高度与 validatorSetId 不匹配、更新日志缺口、跨 epoch 大额释放。防御是单调 epoch、消息高度绑定和更新日志原子性。

第五类失败是消费状态不完整。消息验证通过后没有正确标记 consumed,或 consumed key 不包含源链、emitter、nonce、action。结果是同一证明可跨资产、跨目标链或跨业务动作重放。防御是完整 nonce 域、目标链绑定和执行前先写消费标记。

第六类失败是暂停机制粒度过粗。桥发现异常后只能全局暂停,导致所有路径和退款都被阻塞;或暂停粒度太细,无法阻断正在利用的证明类型。更好的设计是按 proofType、源链、目标链、资产和金额层级设置熔断器。这样在 Merkle proof 路径异常时,可以暂停高风险释放,同时保留已验证退款或低风险退出通道。

AllSwap 相关性:桥风险要进入路由和退款模型

AllSwap 这类跨链交换不应把所有桥路径视为同质基础设施。桥的信任根、验证者更新频率、Merkle 证明格式、签名阈值和历史事故处理方式,都会影响路由风险。一个简化评分可以写成:

`bridgeRisk = validatorSetRisk + proofFormatRisk + updateTimingRisk + replayRisk + incidentResponseRisk`

`validatorSetRisk` 来自多签集中度、唯一 signer 规则和阈值权重;`proofFormatRisk` 来自 Merkle 域分离、空证明处理和编码规范;`updateTimingRisk` 来自验证者更新窗口;`replayRisk` 来自 consumed key 设计;`incidentResponseRisk` 来自暂停、限额和回滚机制。价格更优的桥路径若验证语义更弱,最终并不一定是更优路由。

AllSwap 的路由系统可以把桥路径状态拆成 `normal`、`elevatedRisk`、`proofDegraded`、`paused`、`refundOnly`。当某条桥路径出现异常证明、验证者更新延迟或大额释放集中时,不一定要完全关闭所有功能;可以降低单笔限额、延长报价有效性检查、只允许退款路径、或切换到信任根更强但费用更高的路径。关键是让桥风险影响路线选择,而不是等事故发生后才人工处理。

产品状态也应更细。`proofObserved` 表示源链事件和证明已出现;`signatureVerified` 表示签名阈值通过;`proofVerified` 表示 Merkle 或状态证明通过;`releaseExecuted` 表示目标链资产释放;`challengeOrPause` 表示桥风险控制触发;`refundPending` 表示失败路径进入退款。用户不需要理解 Merkle 空证明,但需要知道资金为何等待、是否已释放、失败能否归因。

内链上,这篇文章应连接 AllSwap 跨链兑换页、费用页、资产页、跨链轻客户端和全链资产供应不变量文章。读者从产品进入,可以理解为什么不同跨链路径的风险不只来自价格;从技术文章进入,可以看到桥验证语义如何影响无托管交换的退款和路由。

未解决问题:桥安全需要可组合的验证标准

第一,跨链桥缺少统一证明描述语言。多签 VAA、Merkle inclusion、IBC commitment proof、ZK light client 都有不同语义,路由器很难用同一评分模型比较。

第二,验证者更新和消息执行的原子性仍复杂。升级、轮换、暂停和恢复都可能和大额消息释放交叠,形成人为风险窗口。

第三,链上签名库的实现差异仍然大。ECDSA、Ed25519、BLS、Schnorr 的编码和可塑性边界不同,移植到合约环境后更容易出现解析漏洞。

第四,Merkle 证明的“存在”和“非存在”语义常被混用。桥验证器需要显式区分 proof type,而不是让空值落入默认路径。

第五,跨链产品如何向用户解释桥验证风险仍不成熟。理想状态不是展示一堆密码学术语,而是把路径风险、等待原因、退款确定性和暂停机制表达清楚。

参考资料

[1] Wormhole documentation, Guardians, https://wormhole.com/docs/protocol/infrastructure/guardians/

[2] Wormhole generic message passing whitepaper, https://github.com/wormhole-foundation/wormhole/blob/main/whitepapers/0001_generic_message_passing.md

[3] Cosmos IBC, ICS-23 vector commitments, https://github.com/cosmos/ibc/tree/main/spec/core/ics-023-vector-commitments

[4] Cosmos ICS23 implementation and proofs, https://github.com/cosmos/ics23

[5] OpenZeppelin Contracts, MerkleProof, https://docs.openzeppelin.com/contracts/5.x/api/utils#MerkleProof

[6] OpenZeppelin Contracts, ECDSA, https://docs.openzeppelin.com/contracts/5.x/api/utils#ECDSA

[7] EIP-712: Typed structured data hashing and signing, https://eips.ethereum.org/EIPS/eip-712

[8] EIP-2: Homestead hard-fork changes and ECDSA malleability, https://eips.ethereum.org/EIPS/eip-2

[9] RFC 8032: Edwards-Curve Digital Signature Algorithm, https://datatracker.ietf.org/doc/html/rfc8032

[10] Chainlink, Cross-chain bridge vulnerabilities, https://blog.chain.link/cross-chain-bridge-vulnerabilities/

[11] SlowMist, Root cause analysis of Poly Network hack, https://slowmist.medium.com/the-root-cause-of-poly-network-being-hacked-ec2ee1b0c68f

[12] CertiK, Nomad bridge exploit incident analysis, https://www.certik.com/resources/blog/28fMavD63CpZJOKOjb9DX3-nomad-bridge-exploit-incident-analysis

常见问题

跨链桥签名伪造一定意味着私钥泄露吗?

不一定。很多风险来自验证语义错误,例如重复签名计权、签名域缺失、验证者集合版本错配或非规范编码被接受。攻击者不必破解私钥,也可能让错误证明被合约接受。

Merkle 空证明为什么危险?

如果验证器把空 sibling、默认零 root 或未初始化状态误当成有效存在证明,不存在的消息可能被目标链接受。防御需要固定树高度、域分离、canonical encoding 和明确 proof type。

验证者更新时序为什么会影响桥安全?

如果旧验证者集合能签署新区间消息,或新集合能追认旧消息,中继器就可能选择对自己有利的集合。安全设计必须把消息高度、validatorSetId 和更新日志原子绑定。

AllSwap 为什么需要关注桥验证细节?

跨链交换依赖桥路径释放和退款。若桥的签名阈值、Merkle 证明或消费状态设计较弱,价格更优的路径也可能带来更高释放失败、暂停或退款不确定性。

参考资料