JOVANA
Explore Library Glossary Getting Started Three Levels Fields How it works Mission
Join the mission
All guides

橋的信任模型——以及那些十億美元級的駭客事件

一座橋的安全程度,全看「是誰決定『那筆資金確實被鎖住了』」。我們將透過三起損失逾十億美元的駭客事件,認識從九取五多重簽章到 zk 輕用戶端的整條信任光譜。

一座橋就是一個六點二五億美元的蜜罐

2022 年 3 月 23 日,Ronin 橋——也就是讓資金進出 Axie Infinity 遊戲的那條通道——把 173,600 顆 ETH 與 2,550 萬枚 USDC 付給了一名攻擊者,約合六點二五億美元。沒有任何密碼學被攻破:沒有雜湊被逆推,也沒有憑空偽造出簽章。攻擊者只是湊齊了五把簽署金鑰,而那座橋便照著它被打造出來的樣子,分毫不差地執行了。整整六天沒有人發現。

回想上一篇談的鎖定—鑄造機制:跨鏈橋在 A 鏈鎖住抵押品,並在 B 鏈鑄造一張包裝過的借據。那枚包裝代幣有沒有價值,全看你是否信任「那筆抵押品確實被鎖住了」這個判斷的源頭。下這個判斷的角色,就是橋的信任假設——橋的生死所繫,正是它,而非數學。本篇要談的,就是「是誰、或是什麼」在下這個判斷,以及它三次釀成大禍的經過。

信任光譜:到底是誰在驗證那條訊息?

每一條跨鏈訊息都要面對同一個棘手的問題。B 鏈看不見 A 鏈的狀態,因此當 B 鏈上的橋接合約被告知「A 鏈鎖了 100 顆 ETH——鑄造 100 顆 wETH」時,必須有某個東西出來作證這是真的。是誰作證、以及你要怎麼抓到它說謊,正好定義了一座橋落在信任光譜的哪個位置。共有三大流派。

  1. 外部驗證(受信任型)。 一個外部團體——可能是多重簽章、MPC/門檻簽章委員會,或一組驗證者集合——監看 A 鏈並簽署一份證詞。B 鏈就只是信任他們的簽章。安全性等於這個團體的誠實程度(與金鑰保管的嚴謹程度)。正式環境中多數橋都屬此類——多數駭客事件也是。
  2. 樂觀式驗證。 訊息一被張貼便先假定有效,但會開啟一段挑戰視窗,其間任何誠實的監看者都能提交一份詐欺證明來推翻謊言。安全性等於至少一名誠實、且醒著的監看者,再加上一套真的能運作的挑戰機制。代價是延遲:你必須等過整段視窗。
  3. 原生/本地驗證(信任最小化)。 B 鏈親自驗證 A 鏈——在合約裡運行一個 A 鏈共識的輕用戶端,或檢查一份關於它的有效性證明。不引入任何新的受信任方;安全性收斂為「A 鏈自身的共識,加上正確的程式碼」。這是最難打造、也最罕見的一種。

幾乎人人都伸手去拿第一個選項,是有原因的。互通性三難困境(由 Arjun Bhuptani 命名)指出,一座橋很難同時兼具三者:信任最小化、通用(能承載任意資料,而不只是代幣轉移)、以及可擴展(能廉價地部署到任何新鏈上)。外部驗證買到了通用性與易於部署——代價是悄悄加進了一個受信任方。底下三起駭客事件,就是這個取捨引爆的三種不同方式。

Ronin:當五把鑰匙就是全部的安全模型

Ronin 是為 Axie Infinity 打造的一條以太坊側鏈。連接它與以太坊的那座橋,由一組共個節點的驗證者集合守護;要放行資金,需要這些節點簽出九取五多重簽章。Axie 背後的開發商 Sky Mavis,直接操作著這九個驗證者當中的四個。

為了吸收 2021 年 11 月的流量高峰,Axie DAO 把 Sky Mavis 列入白名單,授權它代表 DAO 簽署交易——這形同把第五個驗證者的控制權也交到了 Sky Mavis 手上。這個安排原本只是暫時的。那份白名單卻從未被撤銷。 於是四把鑰匙加上一把被授權的鑰匙,正好等於五——恰恰是那個門檻——而這五者全都能透過 Sky Mavis 自家的基礎設施觸及。紙面上「九個獨立各方」,悄悄變成了「一家公司的伺服器」。

Lazarus Group(FBI 將其歸因於北韓)以一份夾帶惡意 PDF 的假徵才信,對一名 Sky Mavis 工程師發動釣魚,再從公司系統一路橫向滲透進驗證者節點。攻擊者握著那五把簽署金鑰,偽造了兩筆提領,把 173,600 顆 ETH 與 2,550 萬枚 USDC 一掃而空。由於這座橋向來只檢查簽章,那些提領看起來完全合法。沒有任何警報響起——這起竊案在六天後才浮上水面,起因只是一位使用者提不出他的 5,000 顆 ETH。

// Ronin-style bridge withdrawal (simplified)
function withdrawERC20(WithdrawalReceipt receipt, Signature[] sigs) external {
    bytes32 h = hashReceipt(receipt);
    uint256 weight = 0;
    for (uint256 i = 0; i < sigs.length; i++) {
        address signer = ecrecover(h, sigs[i]);
        require(isValidator[signer], "not a validator");
        weight += validatorWeight[signer];
    }
    // The ENTIRE security of the bridge is this one line:
    require(weight >= threshold, "insufficient signatures"); // 5 of 9
    release(receipt.token, receipt.to, receipt.amount);
}
只要攻擊者掌握的驗證者金鑰權重達到 `threshold`,每一道檢查都會通過,資金便流出。不需要任何漏洞——這套模型分毫不差地照設計運作了。

Wormhole:驗證者沒問題,問題出在程式碼

五週之前,Wormhole——一座 Solana ↔ 以太坊的橋——在沒有任何一把金鑰外洩的情況下,損失了 120,000 顆 wETH(約 3.25 億美元)。Wormhole 由 19 個「守護者」節點看守;一條有效的跨鏈訊息(即 VAA,已驗證行動核可)需要守護者簽出 19 取 13 的簽章。驗證者集合既誠實又完好無缺。漏洞出在那段本該檢查他們簽章的程式碼裡。

在 Solana 上,簽章檢查是在同一筆交易中、較早的一個獨立指令裡執行的;橋接程式接著透過交易的指令清單(也就是「instructions sysvar」)來確認真正的 `secp256k1` 程式確實驗證過守護者的簽章。Wormhole 用了一個已被淘汰的輔助函式 `load_instruction_at`,而它並沒有去驗證自己讀取的那個 sysvar 帳戶是不是真貨

於是攻擊者遞上了一個偽造的「instructions」帳戶,謊稱 `secp256k1` 驗證已經成功——但其實根本沒跑過。程式信以為真,認定十三個守護者都簽了名,接受了一份偽造的 VAA,並在 Solana 上鑄造出 120,000 顆毫無抵押的 wETH。他們在抵押缺口被察覺之前,就把其中大部分搬到了以太坊。Wormhole 的支持者 Jump Crypto 在幾天內補進了那 120,000 顆 ETH,好讓 wETH 維持有抵押——這是紓困,而不是追回。

// VULNERABLE: never checks that `sysvar_account` is the real instructions sysvar
let current_ix = load_instruction_at(index, &sysvar_account.data);
verify_secp256k1_ran(current_ix);   // attacker forged sysvar_account -> check bypassed

// FIX: the *_checked variant validates the sysvar account's address first
let current_ix = load_instruction_at_checked(index, &sysvar_account)?;
verify_secp256k1_ran(current_ix);   // a spoofed account now reverts
如果鏈上程式碼可以被誘騙得整個跳過檢查,那麼「19 取 13 的守護者集合」就毫無意義。而修補的方法,幾乎就只是換一個函式名稱而已。

Nomad:一個歸零的值,打開了金庫

Nomad 試著走一條不一樣的路——它是一座樂觀式驗證的橋。Nomad 不是靠一個法定人數去簽每一條訊息,而是把待處理訊息的 Merkle 根提交上鏈,並開啟一段約 30 分鐘的視窗,讓監看者可以對造假的根提交詐欺證明。原則上,這是朝信任最小化那一端往下走的真實一步。

2022 年 8 月,一次例行升級把合約的受信任根重新初始化成了 `0x0000…0000`。證明邏輯會去檢查 `acceptableRoot[root]` 來判斷某條訊息是否已被確認——但由於那個新根被提交的方式,那個歸零的值被當成了一個可接受的根。於是每一條 Merkle 證明能對著「零」驗證通過的訊息——也就是實質上每一條訊息——現在都被視為已證明、可放行。

// Nomad-style message processing (simplified)
function process(bytes memory message) public {
    bytes32 root = computeRootFromProof(message);  // attacker fully controls `message`
    require(acceptableRoot[root], "not proven");    // BUG: acceptableRoot[0x00] == true
    // ...deliver the message, e.g. "release 100 WBTC to <attacker>"
    handle(message);
}
在那次失誤的升級之後,任何精心構造的訊息都能對著零根驗證通過,於是這道 require 對所有人都放行。沒有金鑰可偷——那道閘門根本就是被大敞著留在那裡。

接下來發生的事前所未見:史上第一場群眾外包式的駭客攻擊。最初那名攻擊者的交易,就攤在公開的記憶池裡供所有人觀看;數以百計的旁觀者複製它、換上自己的位址、再重送一次。在混亂的數小時內,約 1.9 億美元就在數以千計的模仿交易裡被一掃而空——一場沒有頭目、人人都能輕易加入的全民搶劫。

把信任降到最低的那一端:樂觀式、輕用戶端與 zk 橋

那麼,當信任最小化的那一端被正確地打造出來時,究竟長什麼樣子?有三種構造,嚴謹度逐級遞增。

樂觀式橋,做對的版本。 把每條訊息連同一筆保證金與一段挑戰視窗一起張貼;只要訊息說了謊,任何監看者都能用一份詐欺證明沒收那筆保證金。安全性建立在「N 個監看者裡有 1 個誠實」的假設上——這比「信任這個委員會」弱得多,因此也強得多。代價是實實在在的:延遲(數分鐘到數小時),以及「監看者真的醒著、且能把挑戰交易送上鏈」這個假設。Nomad 的構想正是如此;它的漏洞卻出在別處。

輕用戶端(原生驗證)橋。 目的鏈以智慧合約的形式運行一個來源鏈的輕用戶端:它吸收來源鏈的區塊標頭,驗證來源驗證者的共識簽章(例如他們證詞的 BLS 聚合簽章),再檢查一份Merkle 證明,確認該事件確實落在一個已驗證的區塊裡。信任收斂為「來源鏈自身的共識,加上正確的程式碼」——完全不需要額外的委員會。Cosmos 的 IBC 就是這樣運作(下一篇會深入談)。代價是:在鏈上驗證另一條鏈的共識既昂貴、又得針對每一對鏈各自重新實作,而沒有快速最終性的鏈更是難以證明。

zk 橋,前沿地帶。 在鏈上驗證一整套共識太貴,於是 zk 橋用一份簡潔的有效性證明來取代它——一份 zk-SNARK,證明「存在一個包含此事件的有效來源區塊,且來源驗證者確實簽了名」。目的鏈只需廉價地驗證一份極小的證明:沒有挑戰視窗,也沒有委員會。信任降為「來源鏈的安全性+證明系統的健全性+一個正確的電路」。zkBridge、Polyhedra、Succinct 等設計正朝這個方向推進;最難的部分是證明的成本,以及把電路寫對。

在過橋之前,如何讀懂一座橋的信任模型

在你讓一座橋保管你的錢之前,要像稽核員那樣去讀它的信任模型。密碼學很少是弱點;金鑰、程式碼與升級權限才是。對任何你所倚賴的,把這份清單逐項走過一遍。

  1. 清點法定人數。 是外部驗證嗎?找出那個 N 取 M,以及是誰握著那 M 把鑰匙。「九取五,而且全由一家公司運作」距離 Ronin 只差一封釣魚信。比起架在少數幾台伺服器上的熱多重簽章,更該偏好龐大、獨立、由 DKG 產生的門檻集合。
  2. 找出管理金鑰。 有沒有升級或暫停金鑰?它是一把單獨的鑰匙、一個多重簽章,還是擺在時間鎖後面?一把即時、毫無延遲的升級金鑰,是一道出於善意的後門——而且無論它底下的驗證機制多優雅,它都能把一切搬空。
  3. 辨明驗證機制。 是外部委員會、樂觀式加監看者,還是原生/輕用戶端/zk?每一種都把風險推往不同的地方——推給人、推給監看者的存活性,或推給程式碼——你該清楚自己究竟在賭哪一個。
  4. 拿風險中的價值去對照審查強度。 鎖著數億美元,就理應對應到多份深入的稽核、一筆認真的漏洞賞金,以及在正式環境中歷練過的實際時間。先去讀讀類似設計的事後檢討——同樣的錯誤一再重演。
  5. 問一句:如果中繼者消失了,會壞掉什麼? 在良好的設計裡,一個中繼者能讓你卡住(存活性問題),卻不能偷走你的錢(安全性問題)。如果單一個中繼者兩者都能做,這種不對稱就是一面紅旗。

每一座橋的強度,都只等於它最弱的那一環,而歷史對那一環的所在位置直言不諱。Ronin 敗於金鑰管理、Wormhole 敗於驗證器漏洞、Nomad 敗於初始化失誤——沒有一個敗於崩潰的數學。當你過橋時,你信任的並不是以太坊或比特幣;你信任的是這座特定的橋所決定、用來替它們發言的那個東西。在你把資金送出去之前,先把那個東西看個一清二楚。