一位不重算就蓋章的稽核員
想像一位稽核員出席年終會議。她不逐行重算帳目,而是看一眼就在帳本上蓋下「核准」——但她宣布一條規則:在場任何人有一週時間,指著某一行、證明它是錯的;只要證成,整份報表作廢,當初簽核的人還要被罰款。 沒有人重做整場稽核,光是「可能在某一行被抓包」這個威脅,就足以逼出誠實的數字。這正是 樂觀 Rollup 押下的賭注。
上一篇你已看見每一種 Rollup 共通的骨架:在鏈下分批執行交易,再把交易的資料與一個新的狀態根(state root)張貼回 以太坊,由它擔任 結算層 與資料層。那一篇留下一個懸而未決的問題:L1 雖然存下了新的狀態根,卻從不重跑這些交易,那它怎麼知道這個根是對的?答案只有兩個。事前就證明它正確——那是用有效性證明的 ZK-Rollup,留待下一篇。或者先假定它是對的,事後再容許爭議——那就是樂觀路線,也是本篇要拆解的對象。
樂觀的賭注:先斷言,再容許挑戰
就機制而言,定序器會排序並執行一批 L2 交易,然後向 L1 上的一份合約送出兩樣東西:批次資料(壓縮後的交易,自 EIP-4844 起以 blob 形式張貼)與一則斷言——形如「執行這一批,會讓鏈從舊狀態根 A 轉移到新狀態根 B」的主張。關鍵在於:這則斷言不附帶任何正確性證明,它只是一個押了保證金的公開宣稱。
- 排序並執行。 定序器把湧入的 L2 交易排好順序、執行它們,並給使用者一個即時的軟確認——這份俐落的體驗正是 L2 之所以「感覺很快」的來源。
- 把資料張貼到 L1。 它將壓縮後的批次資料發布到以太坊,好讓任何人日後都能重建 L2 的狀態——這就是那個不可妥協的 資料可得性 步驟。
- 斷言新的狀態根。 它向 L1 上的 Rollup 合約張貼斷言(A → B)並附上保證金。這個新根在 L2 內部立即視為有效,但 L1 此時還不會為了提款而信任它。
- 開啟挑戰窗口。 計時器啟動——也就是 挑戰期,在 Arbitrum 與 OP Mainnet 上約為七天。在這段窗口內,任何人都可以對該斷言提出爭議。
- 確認或爭執。 若窗口關閉時沒有任何成功的挑戰,該根便成為正統,依附其上的 L1 提款隨之解鎖。若有人提出挑戰並獲勝,斷言遭駁回,作弊的斷言者賠掉保證金。
// SPDX-License-Identifier: MIT
pragma solidity ^0.8.20;
// A drastically simplified optimistic-rollup contract on Ethereum (L1).
// The sequencer ASSERTS a new state root; it becomes canonical only after
// the challenge window passes with no successful fraud proof.
contract RollupAssertions {
struct Assertion {
bytes32 prevStateRoot;
bytes32 newStateRoot; // claimed result of executing the batch
bytes32 batchHash; // commitment to the L2 tx data posted as a blob
uint256 proposedAt; // L1 timestamp when it was posted
address asserter;
uint256 bond;
bool confirmed;
}
uint256 public constant CHALLENGE_WINDOW = 7 days;
uint256 public constant BOND = 1 ether;
Assertion[] public assertions;
// The sequencer posts a result WITHOUT proving it is correct.
function assertState(bytes32 prevRoot, bytes32 newRoot, bytes32 batchHash)
external payable
{
require(msg.value == BOND, "post a bond");
assertions.push(Assertion(prevRoot, newRoot, batchHash,
block.timestamp, msg.sender, msg.value, false));
}
// After the window closes with no challenge, the root becomes final.
function confirm(uint256 id) external {
Assertion storage a = assertions[id];
require(!a.confirmed, "already final");
require(block.timestamp >= a.proposedAt + CHALLENGE_WINDOW, "still challengeable");
a.confirmed = true;
payable(a.asserter).transfer(a.bond); // honest asserter reclaims the bond
// newStateRoot is now trusted: L1 withdrawals against it unlock.
}
// Anyone may open a dispute during the window (the game is in the next section).
function challenge(uint256 id) external payable {
require(msg.value == BOND, "post a bond");
Assertion storage a = assertions[id];
require(!a.confirmed, "already final");
require(block.timestamp < a.proposedAt + CHALLENGE_WINDOW, "window closed");
// ... start the bisection game; the loser's bond is paid to the winner.
}
}整幅圖景的安全性,全押在挑戰窗口上。在它過完之前,L1 手上只有一個未經驗證的主張——這正是為什麼提款回到 L1 必須等待。而這一切只有在批次資料確實被張貼出來時才成立:挑戰者無法針對自己看不到的資料去證明造假,所以 結算層 上的 資料可得性,是整套方案賴以站立的基石。
詐欺證明:一個誠實的驗證者如何贏得這場爭論
這裡有個顯而易見的反駁:要檢查定序器的狀態根對不對,L1 難道不必把整批交易重跑一次嗎?若真要重跑,這個 Rollup 就毫無意義了——那份重新執行的成本,正是我們搬離鏈上的東西。跳出這個陷阱的辦法,就是 詐欺證明,它的精妙之處在於:從不要求 L1 去跑整批交易,而是把分歧收斂到單一一條機器指令,只跑那一條。
正式環境中的樂觀 Rollup 用一場互動式二分博弈(也稱爭議博弈)來做這件事。斷言者實質上已對整段執行軌跡做了承諾——也就是機器在每一步之後狀態的雜湊。挑戰者對最終狀態根有異議,這意味著他們必然在沿途某一步上意見相左。於是雙方對這段軌跡玩二分查找:「我們在跑到一半時的狀態上同意嗎?」每個答案都把搜尋範圍砍半,直到鎖定他們唯一有歧見的那一條指令。
# Interactive fraud proof = a bisection game over the execution trace.
# Asserter claims: running the batch takes state root A -> B in N machine steps,
# and commits to a hash of the machine state after every step.
# The challenger disagrees about the FINAL root, so they disagree about SOME step.
lo, hi = 0, N # the first disputed step lies in [lo, hi)
while hi - lo > 1: # ~log2(N) rounds, one cheap L1 tx each
mid = (lo + hi) // 2
h = asserter.stateHashAt(mid) # asserter commits to the state hash at step mid
if challenger.agreesWith(h): # they agree through mid -> the fault is LATER
lo = mid
else: # they diverge at/before mid -> the fault is EARLIER
hi = mid
# Now hi == lo + 1: they disagree on exactly ONE instruction.
# The L1 contract runs that single step on-chain -- the "one-step proof":
claimed = asserter.stateHashAt(hi)
actual = L1.executeOneStep(asserter.stateHashAt(lo), program[lo])
if actual == claimed:
slash(challenger) # asserter was right; the challenge fails
else:
slash(asserter) # FRAUD proven: reject the assertion, roll the root back把數字代進去。假設執行一批要花 N = 2^30 ≈ 10.7 億 個機器步驟。二分查找只需 log₂(N) = 30 回合就能鎖定爭議步驟,每回合不過是一筆廉價的 L1 交易。接著 L1 只執行一條指令——即單步證明(one-step proof)——由數學客觀裁定勝負。輸家的保證金遭罰沒,用以支付贏家並補貼 gas。兩大主流實作都採此法:Arbitrum Nitro 在一台 WebAssembly 風格的機器(WAVM)上二分,Optimism 的 Cannon 則在 MIPS 執行軌跡上二分。機器不同,思路如一。
為什麼一個誠實的驗證者就夠——以及它倚賴什麼
這套安全模型值得背下來:樂觀 Rollup 只需要一個誠實且警醒的驗證者——一個 N 之中只要 1 個(1-of-N)的假設。對照之下,基礎鏈倚賴的是誠實多數假設,安全性需要過半(工作量證明)或三分之二質押(BFT)守規矩。在這裡,不論多少騙子張貼了錯誤斷言,只要有一個持有資料、願意提出挑戰的誠實方,就永遠會贏得二分博弈,因為單步證明是由客觀數學裁定,而非投票。一個誠實的靈魂,足以擊敗任意數量的作弊者。
但「一個誠實的驗證者」倚賴兩個不起眼的前提。其一,資料可得性。 你無法針對從未收到的位元組去證明造假;若定序器扣住批次資料,沒人能重算軌跡,詐欺證明便形同虛設。這正是為何把資料張貼到 L1 是強制的,也是為何 DA 最終成了真正的擴容瓶頸——那是後續某篇的主題。其二,活性(liveness)。 必須真有人在運行驗證者、盯著每一則斷言,並願意花 gas 去挑戰。若人人都想著「自然會有別人抓」,壞根就可能溜過去——這就是經典的驗證者困境。各協定的解法是用保證金與獎勵,讓「抓出造假」變成有利可圖的事。
七天的等待,與居中的定序器
挑戰窗口同時也是價碼。要透過 Rollup 的 官方橋 把資產提回 L1,你必須等完整個挑戰期——約莫七天——因為在窗口關閉前,L1 拒絕信任那個狀態根。注意這份不對稱:存入 L2、以及在 L2 內部搬錢都很快;只有 L1 的提款要吃下這段延遲,因為那是唯一一個 L1 必須倚賴尚未確認之狀態根的時刻。
實務上使用者鮮少真等一週,這得歸功於快速提款的流動性提供者。某個第三方橋看著你那筆待處理的 L2 提款,判定它幾乎篤定有效,便收取一筆費用、立刻在 L1 上把資金交給你——再自行等過那七天去領回官方提款。他們賣給你的是耐心,並吸收掉那份(若斷言誠實則極小的)風險。這是個漂亮的市場解法,但要留意:它為了速度,重新引入了一個你需要信任的交易對手。
接著,是居中而坐的那台 定序器。如今它幾乎總是單一、中心化的營運者(Arbitrum 是 Offchain Labs,Optimism 是 OP Labs)。好處是真切的:即時軟確認與順暢體驗。壞處同樣真切——它能審查交易、它會當機(Arbitrum 定序器在 2023 年 12 月發生過一次顯著的故障,數小時內無法處理新交易),它也透過決定排序來攫取 MEV。兩道防線至關重要。一個強制納入的逃生艙,讓被審查的使用者把交易直接送進 L1 的收件匣,使定序器無法無限期地噤聲任何人。而更長遠的解方,是徹底去中心化排序——共享定序器,或把排序權交給 L1 出塊提議者的 based rollup。
樂觀 vs ZK:誠實面對取捨
樂觀 Rollup 以低廉的代價買到安全,但用時間與活性來償還。因為順利路徑上不附帶證明,產證成本幾近於零,要達到完整的 EVM 等價也相對容易——這正是為何樂觀 Rollup 較早成熟,並能忠實運行未經改動的 Solidity。帳單則以約七天的 挑戰期、外加「必須有人保持誠實且警醒」的活性假設,以及對 資料可得性 的硬性依賴,一併到期。
ZK-Rollup——也就是下一篇——把取捨翻轉過來。它不再等著去抓造假,而是由定序器附上一份簡潔的有效性證明,證明狀態轉移是正確算出的,L1 當場便能廉價地驗證。回報是近乎即時的最終性、快速提款(不必苦等一週),以及不需要活性假設——保證正確的是數學,而非一群警醒的旁觀者。代價則是沉重的證明生成,與通往完整 zkEVM 那條歷來更難走的路。請留意:兩種設計仍都把資料張貼到 L1,所以 資料可得性 始終是它們共享的瓶頸——也是再下一篇的主題。