摘要:跨鏈橋的風險常常不在“簽名算法被攻破”,而在驗證語義寫錯
跨鏈橋最危險的漏洞通常不是密碼學原語本身失效,而是驗證合約把“看起來像證明”的字節串誤當成了“綁定到正確鏈、正確消息、正確驗證者集合、正確狀態根的證明”。簽名僞造、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 證明或消費狀態設計較弱,價格更優的路徑也可能帶來更高釋放失敗、暫停或退款不確定性。
參考資料
- Wormhole documentation: Guardians
- Wormhole generic message passing whitepaper
- Cosmos IBC: ICS-23 vector commitments
- Cosmos ICS23 implementation and proofs
- OpenZeppelin MerkleProof
- OpenZeppelin ECDSA
- EIP-712 typed structured data signing
- EIP-2 ECDSA malleability
- RFC 8032 EdDSA
- Chainlink cross-chain bridge vulnerabilities
- SlowMist Poly Network root cause analysis
- CertiK Nomad bridge exploit incident analysis


