昨天結束在一個數字上:在現行 MPT 上,30M gas 全部拿去讀不同合約 code 的單一 byte,區塊 witness 會膨脹到 330MB,所以 stateless 在 MPT 上不是「很難」,是不可能。EIP-7864 的二元樹把這件事修好了——code 進樹、chunk 化、arity 從 16 降到 2、分支從 2,880 bytes 降到 768。那麼修好之後,witness 到底多大?這個問題比想像中難回答,因為答案不由樹決定,由 gas 常數決定。而目前那組常數是為 Verkle 校準的,套進二元樹之後,最壞情況的 witness 在 60M gas limit 下是 8.86 MiB,在 200M 下是 29.54 MiB,而 p2p 網路對區塊尺寸的實務天花板是 10 MiB。更麻煩的是,製造這個最壞情況的方法不是隨機亂讀,而是完全按照設計意圖去利用局部性折扣

📖 學

一、先把 330MB 換成一個可比較的單位

「330MB」這個數字本身沒有可比性,因為它綁在 30M gas 上。把它除掉,得到一個真正有用的量綱:每消耗一單位 gas,攻擊者能往 witness 裡塞進幾個 byte

用 EIP-7864 自己的算式:

冷帳戶存取 2,400 gas → 30,000,000 / 2,400 = 12,500 次
每次成本 = 5 層 × 480 bytes + 24 KiB code = 2,400 + 24,576 = 26,976 bytes
總計 = 12,500 × 26,976 = 337,200,000 bytes = 321.58 MiB

337,200,000 ÷ 30,000,000 = 11.24 bytes/gas

11.24 bytes/gas。反過來說,MPT 上的 witness byte 只賣 0.089 gas。這就是為什麼 stateless 在 MPT 上死透了——不是因為 330MB 這個絕對值,是因為這個係數乘上任何一個 gas limit 都會爆炸。

有了這個單位,接下來所有東西都可以互相比較。先記住三個參照點:協議今天對 calldata byte 的地板價是 64 gas/byte(EIP-7976 把 TOTAL_COST_FLOOR_PER_TOKEN 從 EIP-7623 的 10 提到 16,而 floor 計算裡每個 byte 都算 4 個 token);EIP-7981 把 access list 的資料footprint 也拉到同樣的 64 gas/byte(每個地址 1,280、每個 storage key 2,048),理由白紙黑字寫在 Abstract 裡:「把最壞情況的區塊尺寸縮小約 21%」。這兩個 EIP 都在 Glamsterdam 的 Scheduled for Inclusion 名單上。

也就是說,Ethereum 目前正在花很大力氣,把「進區塊的 byte」的價格往上推到 64 gas/byte。60M gas limit 下,這讓純 calldata 的最壞情況區塊被壓在:

60,000,000 ÷ 64 = 937,500 bytes = 915.53 KiB

不到 1 MiB。記住這個數字,等一下要跟 witness 對照。

二、二元樹的最壞情況:8.86 MiB,而且是「照著設計走」得到的

witness 的定價寫在 EIP-4762: Statelessness gas cost changes(狀態:Draft,2022-02-03 提出,作者群與 EIP-7864 高度重疊)。它的五個常數是:

常數何時收費
WITNESS_BRANCH_COST1,900本交易內第一次碰到某個 stem
WITNESS_CHUNK_COST200已 warm 的 stem 裡第一次碰到某個 leaf
SUBTREE_EDIT_COST3,000第一次寫入某個 stem
CHUNK_EDIT_COST500第一次寫入某個 leaf
CHUNK_FILL_COST6,200寫入一個原本是 None 的 leaf

單一冷讀是 1,900 + 200 = 2,100,刻意對齊現行的 COLD_SLOAD_COST。這是整組常數的設計錨點,EIP 的 Rationale 講得很直白:讓單點存取的價格看起來跟今天一樣,把改變留在「同一個 stem 內的第二個以後」——那些只要 200。

Ignacio Hagopian(jsign,EIP-7864 與 EIP-4762 的共同作者)寫過一份專門的分析,把這組常數的後果推到底。推導只有三步:

第一步,把一個 stem 的 256 個值全部讀完,要付多少:

1,900 + 256 × 200 = 1,900 + 51,200 = 53,100 gas

第二步,這 53,100 gas 換到 state_diff 裡多少 bytes:

stem 前綴 31 bytes
+ 256 個值 × 32 bytes = 8,192 bytes
= 8,223 bytes

第三步,算係數:

8,223 ÷ 53,100 = 0.154859 bytes/gas
反過來 = 6.4575 gas/byte

於是任何 gas limit 都可以直接乘出來:

block gas limit完整 stem 組數witness leavesstate_diff 大小
30M565.0144,6334.43 MiB
36M678.0173,5595.32 MiB(jsign 原文的 ~5.3 MiB)
60M(今天)1,129.9289,2668.86 MiB
150M2,824.9723,16422.15 MiB
200M(Glamsterdam 目標)3,766.5964,21829.54 MiB

再算一個門檻。EIP-7782 的 Motivation 給了 p2p 的實務天花板:「不做大幅的網路層改動,把區塊尺寸推到 10MiB 以上是不切實際的。」代進去:

10 × 1024² ÷ 0.154859 = 67,711,767 gas ≈ 67.7M

67.7M gas。 今天的主網 gas limit 大約 60M。也就是說,如果二元樹連同 EIP-4762 的現行常數一起上線,witness 的最壞情況在比現在高不到 13% 的 gas limit 上,就會撞到 p2p 的實務上限。而 Glamsterdam 整套方案的公開目標是 200M。

跟 MPT 對照一下,二元樹確實改善了很多:

11.24 ÷ 0.154859 = 72.58 倍

72.6 倍。問題是 MPT 的起點太糟,72.6 倍之後仍然只能撐到 67.7M。

更正 1:二元樹不是「讓 witness 變小」,是「把 witness 從指數級的災難壓成線性的預算問題」

這是關於 EIP-7864 最常見的第二個誤解(第一個是昨天講的「省磁碟」)。

MPT 的問題不是 witness 大,是 witness 的大小與存取的資料量脫鉤:讀合約的一個 byte,要付整份 24 KiB 的 code 進 witness。那是一個「以小博大」的放大器,放大倍率取決於受害合約有多大,協議無法定價。

二元樹做的事情是把放大器拆掉。code 被 chunk 化,你讀哪一個 31-byte chunk 就只付哪一個;所有東西都是 32-byte leaf;每個 leaf 都有一個明碼標價。改完之後,witness 大小變成 gas 的線性函數,而係數完全由 WITNESS_BRANCH_COSTWITNESS_CHUNK_COST 兩個常數決定。

這是巨大的進步,但它把問題從「密碼學/資料結構問題」轉成了「定價問題」。定價問題有一個特性:它永遠可以被重新開啟。二元樹上線不會終結 witness 的爭論,只會讓爭論搬到常數表上。

三、最反直覺的一點:最壞情況是「完全局部」,不是「完全隨機」

前面那個 8.86 MiB 是怎麼構造出來的?攻擊者不是隨機亂讀 storage,而是選定少數幾個 stem,把裡面 256 個值全部讀滿

jsign 的原文有一句話值得逐字記住:「最壞情況不是做隨機存取,實際上恰恰相反。」

理由很簡單。隨機存取的每一次都要付 1,900 + 200 = 2,100 gas 才換 32 bytes,係數是 32/2100 = 0.0152 bytes/gas——比完全局部存取的 0.1549 差了 10 倍。攻擊者要最大化 witness,就要最大化「每 gas 的 bytes」,而那正好是協議刻意提供的局部性折扣:第一個 leaf 付 2,100,後面 255 個各付 200

這件事的結構意義比數字本身重要。EIP-7864 把 256 個值放進同一個 stem,是為了資料局部性——常常一起被讀的東西共用一次 branch opening,proving 時少 hash 幾次,使用者少付幾次 gas。這是整個設計的賣點。而這個賣點就是攻擊面。折扣有多大,最壞情況就有多糟,兩者是同一個旋鈕的兩端。

昨天談 EIP-8037 時說過,它偷渡了 Ethereum 第一個雙維 gas 市場。這裡看到的是同一個病灶的另一面:一維的 gas 沒辦法同時表達「執行成本」和「頻寬成本」,而局部性折扣正是在執行維度上給折扣,卻在頻寬維度上完全沒有對應的節省——你讀 256 個值,就是 256 × 32 bytes 要送過網路,一個 byte 都省不掉。省下來的只有 proving 時的 hash 次數。

四、EIP-4762 的常數是為 Verkle 調的,而 Verkle 已經出局

上面那組常數會產生這個結果,不是設計失誤,是遷移殘留

EIP-4762 的 Rationale 自己把定價邏輯攤開來:

WITNESS_CHUNK_COST 設定為每 byte 收 6.25 gas,WITNESS_BRANCH_COST 平均每 byte 收約 13.2 gas(假設分支長度 144 bytes),在攻擊者刻意構造 key 以最大化證明長度的最壞情況下約 2.5 gas/byte。

驗算:

chunk : 200 ÷ 32  = 6.25 gas/byte
branch: 1,900 ÷ 144 = 13.19 gas/byte
branch 最壞: EIP 寫 ~2.5 gas/byte

那個 144 bytes 是關鍵。它是 Verkle 的分支尺寸——深度約 4 層,每層 32 bytes 的內部節點承諾,加上一個常數大小的 IPA/multiproof。Verkle 的核心優勢就是分支證明幾乎是常數大小,所以「多開幾個 branch」在證明尺寸上幾乎免費,昂貴的是 leaf 的原始值。定價自然就把重量壓在 chunk 上。

二元樹把這個關係整個倒過來。EIP-7864 自己的表格說,N = 2²⁴ 時 arity-2 的分支是 768 bytes。代進去:

1,900 ÷ 768 = 2.474 gas/byte

Verkle 的最壞情況(2.5 gas/byte),就是二元樹的平均情況。 而 EIP-4762 的常數是照 Verkle 的平均情況(13.2 gas/byte)訂的,誤差 5.3 倍——方向還是錯的,它高估了 branch 的相對便宜程度。

Dedaub 的 Neville Grech 在 2026 年 4 月 10 日那篇分析裡把這件事說得很清楚,並引用了 Hagopian 在 EIP-7864 的 Ethereum Magicians 討論串中的回應:「Gas remodeling 是 EIP-4762。它可能需要調整常數,但整體做法會是一樣的。」

翻譯成人話:二元樹的 witness 定價目前沒有人算過。 現有的那五個常數是 2022 年為一個已經被放棄的資料結構訂的,而 EIP-4762 到 2026 年 9 月仍然掛在 Draft,連 Account abstraction 那一節都還寫著 TODO : still waiting on a final decision between 7702 and 3074——而 EIP-7702 早就在 Pectra 上主網了。

更正 2(承接昨天的更正 4):EIP-7864 常數表的矛盾,答案是常數表過時,不是不變量錯

昨天我指出 EIP-7864 的 Abstract 說「前 64 個 storage slot 和前 128 個 code chunk」,但常數表寫 HEADER_STORAGE_OFFSET = 20CODE_OFFSET = 4CODE_CHUNKS_IN_HEADER = 16STORAGE_CHUNKS_IN_HEADER = 4,而且不變量 STEM_SUBTREE_WIDTH > CODE_OFFSET > HEADER_STORAGE_OFFSET 代進去是 256 > 4 > 20,後半段不成立。

今天在 EIP-4762 裡找到了交叉驗證,可以把這件事定案。EIP-4762 的 storage 定位函式寫著:

def get_storage_slot_tree_keys(storage_key: int) -> [int, int]:
    if storage_key < (CODE_OFFSET - HEADER_STORAGE_OFFSET):
        pos = HEADER_STORAGE_OFFSET + storage_key
    else:
        pos = MAIN_STORAGE_OFFSET + storage_key

「住在 header stem 裡的 storage slot 數量」= CODE_OFFSET − HEADER_STORAGE_OFFSET。要讓這個值等於 Abstract 說的 64,就必須是 128 − 64。而 EIP-4762 的 code 存取事件更直接:

(address, (chunk_id + 128) // 256, (chunk_id + 128) % 256)

那個硬寫在式子裡的 128 就是 CODE_OFFSET

所以正確的值是 HEADER_STORAGE_OFFSET = 64CODE_OFFSET = 128,不變量 256 > 128 > 64 完全成立。昨天的結論要修正:不是「EIP 自己聲明的不變量不成立」,而是常數表裡那四個值是 Verkle 時代的舊值,沒跟著改。Dedaub 的分析文與 EIP-7864 的官方示意圖(green 的 account header stem 標著 basic_datacode_hash、64 個 storage slot、128 個 code chunk)採用的都是 64/128。

順帶一提,128 × 31 = 3,968 bytes ≈ 4 kB,這就是每個合約「便宜區」的實際大小:前 4 kB 的 bytecode 加前 64 個 storage slot。

五、真實合約的局部性有多差:26% 與 96%

上面講的都是最壞情況。平均情況呢?

Ethereum Foundation 在 2021 年委託 Dedaub 做過一次實測,用兩條獨立路徑:一是基於 gigahorse-toolchain 的路徑敏感靜態分析,對前兩週內有交易的所有主網合約的每個 public function 算 code chunking 的成本上界;二是改造過的 Erigon,用新的 gas 語意重跑歷史交易。結論:

  • 對重放的 internal transaction,平均 gas 成本增加 約 26%
  • 約 96% 的 internal transaction 變貴
  • 分布近似常態,沒有孤立的懸崖,是全面性的位移

Dedaub 在 2026 年 3 月重新抽樣主網區塊確認這個結論還成立,並給了幾個更尖銳的局部性數字:

  • 約 85% 的合約呼叫至少碰到一個編號在前 64 之外的 storage slot
  • 只有約 56% 的呼叫碰到過前 64 個 slot
  • 100% 的執行當然從前 128 個 chunk 開始(進入點在那裡),但 約 60% 的執行會延伸到 128 個 chunk 之外

為什麼這麼差?因為今天的 Solidity 慣例剛好在燒掉那個熱區:

  • mapping 佔一個 slot 卻什麼都不存。 mapping(address => uint256) balances 的 slot p 是空的,真正的值在 keccak256(h(k) . p),均勻散佈在整個位址空間。兩個相鄰使用者的餘額落在完全不相關的 stem。
  • 動態陣列一律出線。 slot p 只放長度,元素從 keccak256(p) 開始。bytesstring 有 ≤31 bytes 的內嵌模式,泛型 T[] 沒有——一個只有一個元素的 uint256[] 也要付完整的間接成本。
  • 可升級合約把熱區送給 __gap OpenZeppelin 的 storage gap 慣例會宣告 uint256[45] private __gap,把 slot 5–49 永久留空。加上 slot 0–1 通常是 mapping,一個典型的 upgradeable ERC-20 的前 50 個 slot 幾乎沒有一個是熱路徑會讀的。
  • ERC-1967 是其中最糟的。 implementation 指標放在 0x360894a1...、admin 放在 0xb5312768...,兩個都是為了「不可能碰撞」而刻意選的 hash 值。而 implementation slot 不是偶爾讀,是每一次 proxy 呼叫都必讀——在二元樹下,那是每次呼叫都保證發生一次跨 stem 的冷讀,而且發生在實作合約的任何一個 byte 被碰到之前。

Dedaub 的判斷是:這 26% 是一次性的重新定價上界,不是穩態。壞掉的模式同時也是最好修的模式,大部分可以靠編譯器與標準函式庫機械化地補回來。他們提的具體方案包括:把 selector dispatch 從展開的二分搜尋改成一個真正的迴圈(讀一次 selector,用 CODECOPY 走一張排序好的 in-bytecode 表,只碰約 log₂(N) 個 chunk,而且整個迴圈體住在同一個 chunk);把常出現的短指令序列做成 superinstruction helper 放進前 128 個 chunk(呼叫一次約 20 gas,省下載入一個 chunk 的 200 gas);把所有以 revert 結尾的錯誤處理分支趕出熱區;以及在標準函式庫層提供 InlineArray<T, N> 這種「前 N 個元素留在熱區、尾巴才 spill 到 hash slot」的原語。

更正 3:二元樹對開發者不是好消息,至少頭幾年不是

中文圈談 Verkle/二元樹時的敘事幾乎都是「更便宜、更快、更輕」。實測是反過來的:對今天已經部署的合約,平均貴 26%,96% 變貴。

而且 Dedaub 指出一個今天完全不存在的成本類別:純執行 code 也要付錢。在 EIP-2929 之下,只要 CALL 的費用付過了,執行合約 code 本身是免費的,無論它有多長、跳到哪裡。在 EIP-4762 之下,每進入一個沒碰過的 31-byte chunk 就要付 200 gas(如果那個 chunk 所在的 stem 也是新的,再加 1,900)。一個 Uniswap V2 router 的 WETH() getter——它幾乎什麼都不做,只回傳一個 immutable——在新語意下,歸因於 code chunking 的 gas 比實際執行的 gas 高一個數量級,因為 solc 的共用指令序列最佳化讓控制流在至少四個不相鄰的 bytecode 區域之間彈跳。

這裡有一個對編譯器工程師來說很重要的反轉:「展開一定比迴圈快」是從有分支預測器和 i-cache 的 CPU 繼承來的直覺。在一個逐 chunk 計費的 VM 上,展開是錯的。

六、要修常數,代價是把局部性折扣砍掉

假設決定把最壞情況壓在 10 MiB 以內,而且要撐到 200M gas limit。要把 WITNESS_CHUNK_COST 調到多少?

目標:8,223 bytes / group_gas × 200,000,000 ≤ 10 × 1024² bytes
→ group_gas ≥ 8,223 × 200,000,000 ÷ 10,485,760 = 156,841 gas
→ 1,900 + 256 × C ≥ 156,841
→ C ≥ 605.2

WITNESS_CHUNK_COST 要從 200 提到約 605,3 倍。

但這樣一來單一冷讀就變成 1,900 + 605 = 2,505,比現行 COLD_SLOAD_COST 貴 19%,破壞了整組常數的設計錨點。jsign 的文件提到的補救方式是同時把 branch 調降:

BRANCH = 2,100 − 605 = 1,495
驗算:group_gas = 1,495 + 256 × 605 = 156,375
      8,223 ÷ 156,375 × 200,000,000 = 10.03 MiB
      單一冷讀 = 1,495 + 605 = 2,100 ✓

單點存取的價格分毫不動,最壞情況剛好壓在 10 MiB。看起來是個乾淨的解。

代價在哪?在 stem 內的第二個到第 256 個值,價格從 200 變成 605。 那正是 EIP-7864 拿來說服所有人的那個折扣。Dedaub 花了一整篇文章論證編譯器應該重新設計以擠進熱區、標準函式庫應該提供 InlineArray、開發者應該把熱資料塞進前 64 個 slot——所有這些工作的報酬率,都取決於「熱區內第二個 leaf 只要 200」這件事。把它提到 605,報酬率掉到三分之一。

這就是我認為今天最值得記下來的結構:局部性折扣的大小,和最壞情況 witness 的大小,是同一個數字。 你沒辦法同時要「局部存取很便宜」和「witness 有上界」。EIP-7864 的整個賣點(資料局部性)與 stateless 的整個前提(witness 要小到能在一個 slot 內傳播完)在數學上直接對立。

要繞開這個對立,只有兩條路:一是把 witness 的頻寬單獨計價,也就是 EIP-8037 那種雙維 gas 的做法再開一個維度(EIP-8011「Multidimensional Gas Metering」提過,但在 Glamsterdam 被 Declined);二是改變傳播方式,讓 witness 不必在 12 秒的 slot 裡完整 fan-out。第二條路 Ethereum 已經在走了,只是掛在別的名字底下。

七、BAL:已經在出貨的那半個 witness

jsign 對 execution witness 的定義是:

class ExecutionWitness(container):
    state_diff: StateDiff
    verkle_proof: VerkleProof

而他對 state_diff 的說明是:「這是描述區塊執行中被存取/改變的清單。這與 block-access list 提供的東西類似,只是格式不同、按 stem 分組。」

Block-Level Access List 就是 EIP-7928,狀態 Review,Glamsterdam 兩個執行層 headliner 之一,已經在 devnet 上跑完。也就是說,Ethereum 現在正在出貨的,就是 execution witness 的一半——只是把它包裝成「平行執行」而不是「無狀態驗證」。

這不是聯想,是規格上的事實。EIP-7928 的 Motivation 明列四個用途,第三個是「不執行交易就重建狀態」(State reconstruction without executing transactions)——那就是 stateless 的核心動作,只差一個證明。而配套的兩個網路層 EIP 說得更明白:

  • EIP-8159: eth/71 — Block Access List Exchange,讓 BAL 走自己的 p2p 交換路徑
  • EIP-8189: snap/2 — BAL-Based State Healing(Review,2026-03-06 提出,作者 Toni Wahrstätter 與 Gary Rong),直接移除 GetTrieNodes/TrieNodes,改成 GetBlockAccessLists/BlockAccessLists,讓同步中的節點下載 BAL 並依序套用 state diff,不再逐個抓 trie node。回應的建議軟上限是 2 MiB

BAL 在架構上也已經是 witness 的形狀:它不在 block body 裡,而是 ExecutionPayload 的一個獨立欄位,經 engine API 傳遞,由 EL 分開儲存,只有 block_access_list_hash 進 header。這正是一個「與區塊分離傳播、可獨立驗證」的 witness 通道。

實測尺寸(EIP-7928 的 Rationale,60M gas limit 版本):

成分壓縮後大小佔比
storage writes~29.2 KiB40.3%
storage reads~18.7 KiB25.8%
balance diffs~6.7 KiB9.2%
帶 diff 的帳戶地址~7.7 KiB10.7%
只被碰到的地址~3.5 KiB4.8%
code diffs~1.2 KiB1.6%
nonce diffs~1.1 KiB1.5%
RLP 編碼開銷~4.4 KiB6.1%
合計~72.4 KiB

換算頻寬:

72.4 KiB × 7,200 blocks/day = 521,280 KiB = 509.06 MiB/day

而 EIP-7928 要求 EL 至少保留 BAL 一個 weak subjectivity period,規格寫的是 = 3533 epochs:

3,533 × 32 × 12 = 1,356,672 秒 = 15.70 天
509.06 MiB/day × 15.70 = 7.81 GiB

15.7 天、7.81 GiB。跟昨天算的 EIP-4444 歷史窗口 33,024 epochs = 146.77 天放在一起看,Ethereum 現在同時維護著兩個完全不同尺度的資料保留義務。

更正 4:BAL 的尺寸約束建立在一個二元樹會摧毀的假設上

EIP-7928 對 BAL 大小的約束不是一個固定上限,而是:

bal_items <= block_gas_limit // ITEM_COST
其中 bal_items = storage_keys + addresses,ITEM_COST = 2000

60M 之下就是 30,000 個 item。EIP 對 ITEM_COST = 2000 的理由寫得很清楚:

在 EIP-7981 之下,往 BAL 加一個 item 最便宜的方式是一次冷 SLOAD,成本 COLD_SLOAD_COST(2,100)。ITEM_COST 被刻意設在這個最低值之下,以留出約 block_gas_limit / 42000 個額外 item 的緩衝⋯⋯

整個約束的地基是**「加一個 item 至少要 2,100 gas」**。而二元樹的局部性折扣做的事情,正好就是把這個地基拆掉:在 warm stem 裡加一個 leaf 只要 200 gas。

把兩邊放在一起:

BAL 在 60M 下的 item 上限   : 60,000,000 ÷ 2,000 = 30,000
EIP-4762 在 60M 下的 leaf 上限: 60,000,000 ÷ 200  = 300,000
比值 = 10.0

同一個區塊、同一個 gas limit,兩份規格對「有多少個狀態位置可以被記錄下來並送過網路」的答案差 10 倍。而它們描述的幾乎是同一個物件。

同樣的假設也出現在 EIP-7928 的 Security Considerations,那個防止 proposer 宣告幽靈讀取的檢查:

G_remaining >= R_remaining * 2000

一樣是 2,000 gas 一個 item。二元樹上線那天,這個不等式會直接失效——攻擊者可以用 200 gas 宣告一個讀取,這個 early rejection 機制就不再是 early 的了。

我沒有在任何一份公開文件裡看到有人把 ITEM_COST = 2000WITNESS_CHUNK_COST = 200 放在一起討論過,這個對照是我自己拉出來的。需要標注兩個保留:第一,BAL 的 item 是 (address, slot) 對,而 witness 的 leaf 還包含 code chunk 和帳戶 header,兩者的計數基底不完全相同;第二,BAL 與二元樹之間隔著至少一個硬分叉,到時候兩份規格都可能已經改過。但這個 10 倍的落差是實打實的,而且它指向一個結構性的結論:Glamsterdam 在替一個「加一項至少 2,100 gas」的世界訂定區塊尺寸紀律,而二元樹的整個賣點就是讓那個數字變成 200——已標存疑,但值得盯著。

八、兩套 byte 的價格:64 vs 6.25

把前面所有東西收攏成一張表,這是 2026 年 9 月 Ethereum 對「一個 byte 進入區塊傳播」的定價:

途徑每 byte 價格出處狀態
calldata(floor)64 gasEIP-7976,TOTAL_COST_FLOOR_PER_TOKEN = 16 × 4 tokens/byteGlamsterdam SFI
access list 資料64 gasEIP-7981,地址 1,280 / storage key 2,048Glamsterdam SFI
BAL item~2,000 gas / item(約 30 gas/byte,item 約 64–70 bytes)EIP-7928 ITEM_COSTGlamsterdam SFI
witness leaf(warm stem)6.25 gasEIP-4762 WITNESS_CHUNK_COST = 200 ÷ 32Draft
witness branch(二元樹實際)2.47 gas1,900 ÷ 768Draft

64 對 6.25,10.24 倍。這兩個數字在描述同一件事:一個 byte 要從 builder 的機器複製到全世界每一個驗證者的機器上。

具體到 60M gas limit 的區塊:

純 calldata 最壞情況:60,000,000 ÷ 64 = 915.53 KiB
witness 最壞情況    :60,000,000 × 0.154859 ÷ 1024² = 8.86 MiB

9.9 倍。 Ethereum 花了三個 EIP(7623 → 7976 → 7981)把區塊體的最壞情況壓在 1 MiB 以內,而同一個區塊的 witness 可以是它的十倍。

我要標注一個誠實的保留:這兩個東西不是完全對等的。calldata 是必須放進區塊體、被所有人永久保存的資料;witness 是可以與區塊分離傳播、可以在驗證後丟棄的資料,而且在部分無狀態的設計下,並不是每個節點都需要完整的 witness。所以 witness 的 byte 理應比 calldata 的 byte 便宜。問題不在於「便宜」,在於便宜 10 倍是不是對的數量級——而目前沒有任何一份文件回答過這個問題,因為那個係數是 2022 年為 Verkle 訂的,從來沒有為二元樹重算過。已標存疑。

九、VOPS:不做完整 statelessness 的理由

上面整篇都預設「每個區塊帶一份完整的 witness」。2026 年 Ethereum 實際在推的不是這個。

完整 statelessness 的死結在 mempool。如果驗證區塊只需要 witness,那 witness 必須跟著交易走;而交易在 mempool 裡等待期間,前面的交易會改動狀態,witness 就失效了。同時交易尺寸暴增,p2p 傳播成本承受不住。這不是可以靠工程優化解決的問題。

VOPS(Validity-Only Partial Statelessness) 的做法是繞過去,不是解開。它由 Thomas Thiery(soispoke)在 ethresear.ch 提出(2025-04-29),致謝名單包含 Julian Ma、Ignacio Hagopian、Carlos Perez、Guillaume Ballet、Justin Drake、Francesco D’Amato、Caspar Schwarz-Schilling。核心設計是:

  • VOPS 節點對每個 EOA 只存四個欄位:address、nonce、balance、code flag
  • 不存合約 code,不存 storage tree
  • 用多重證明或 zkVM 產生的 SNARK 驗證狀態轉移,透過 state diff 更新本地的帳戶表,不重新執行區塊裡的交易
  • 公開的估計是節點儲存需求可以降到約 1/25

關鍵在於它保住了什麼:一個只存 EOA 表的節點,仍然可以判斷 mempool 裡一筆交易的有效性(簽章對不對、nonce 對不對、餘額夠不夠付 gas)。這就足以維持一個健康的公開 mempool,而 mempool 是抗審查的前提——這也是為什麼 EF 把 VOPS 和 FOCIL 放在同一條「Harden the L1」的 track 底下,由 Thiery 同時負責。

昨天提過 EIP-7864 的 storage type 前綴(HEADER_SUBTREE = 0CODE_SUBTREE = 1STORAGE_SUBTREE = 255)是為 separate syncability 設計的。EIP 的 Rationale 對這個理由的原文是:「可以只同步一棵子樹而不同步其他的。這在近期對於同步 VOPS 特別有價值。

現在可以看懂那句話的完整意思了:VOPS 節點需要的正好就是 header subtree,而三種狀態各自佔據樹的不同區域,讓「我只同步 header,不同步 code 和 storage」變成一個乾淨的操作,而且因為每種類型在自己的子樹裡,更新時要維護的 sister node 不會跨類型糾纏。EIP-7864 的樹形設計裡,已經內建了一個為部分無狀態準備的介面。

而 VOPS 的存在也解釋了為什麼前面那個 8.86 MiB 沒有讓整個路線停擺:如果不是每個節點都需要完整 witness,那麼 witness 的傳播就不必是全網 fan-out,最壞情況的預算可以放寬。代價是節點角色的分化——維護 mempool 的、includer、attester 各自需要存哪些狀態,變成一組必須明確規定的協議規則,而不是「跑一個全節點就好」。

十、2026 年 9 月 6 日的座標

主題EIP狀態(2026-09-06)
統一二元狀態樹EIP-7864Draft,hash function 未定,常數表過時(見更正 2)
Statelessness gas 定價EIP-4762Draft,常數為 Verkle 校準,AA 那節仍是 TODO
Block-Level Access ListsEIP-7928Review,Glamsterdam SFI,執行層 headliner
增加合約大小上限 24→64 KiBEIP-7954Review,Glamsterdam SFI
狀態創建定價 CPSBEIP-8037Glamsterdam SFI
提高 calldata floor(10→16/token)EIP-7976Glamsterdam SFI
access list 資料計價EIP-7981Review,Glamsterdam SFI
區塊 gas 記帳不含退費EIP-7778Glamsterdam SFI
狀態存取定價EIP-8038Considered for Inclusion,尚未排定
降低 intrinsic gasEIP-2780Considered for Inclusion,尚未排定
eth/71 BAL 交換EIP-8159Networking
snap/2 BAL 狀態修復EIP-8189Review,Networking
6 秒 slotEIP-7782Declined for Inclusion
chunk-based code merkelizationEIP-2926Declined for Inclusion
多維 gas 計價EIP-8011Declined for Inclusion
FOCILEIP-7805Declined(Glamsterdam),Hegotá 共識層 headliner

Glamsterdam 的 headliner 是 EIP-7732(ePBS)EIP-7928(BAL),Scheduled for Inclusion 共 10 個 EIP:7708、7732、7778、7843、7928、7954、7976、7981、8024、8037。

更正 5(承接昨天的表格):有兩項我昨天寫成「排定納入」,實際上不是

回去核對 EIP-7773(Hardfork Meta - Glamsterdam,狀態 Draft)的 master 版本:

  • EIP-8038(狀態存取定價)Considered for Inclusion,不在 Scheduled for Inclusion。
  • EIP-2780(降低 intrinsic gas) 同樣在 Considered for Inclusion

昨天那張表把兩者都寫成「Glamsterdam 排定納入」,是錯的。CFI 和 SFI 在 EIP-7723 的定義裡是兩個明確不同的階段,CFI 隨時可能被砍。

另外一個要澄清的是 EIP-7954:規格原文明確寫著 24,576 → 65,536 bytes(24 KiB → 64 KiB),initcode 上限 49,152 → 131,072(48 KiB → 128 KiB)。搜尋結果裡有二手來源寫成「提到 32 KiB」,那是錯的。昨天寫的 64 KiB 是對的。

時程的存疑處:Glamsterdam 的 Sepolia 分叉日期,不同來源給出 2026-08-03、2026-09-21、2026-09-28 三個版本,主網日期則有 2026-09-16、2026-11-04、以及籠統的「Q4 2026」。而 EIP-7773 的 Activation 表格裡,Sepolia、Holešky、Mainnet 三格全部是空的,底下註明「rows in the table above will be filled as activation times are decided by client teams」。在客戶端團隊把數字填進那張表之前,所有日期都是二手轉述——已標存疑

更正 6:EIP-7782(6 秒 slot)不是「還在討論」,是被否決了

昨天引用 EIP-7782 的 10MiB 上限時,我沒有查它的納入狀態。查清楚了:它在 EIP-7773 的 Declined for Inclusion 名單上。

被拒的理由本身跟今天的主題直接相關:縮短 slot 會壓縮 real-time ZK proving 的時間預算,而 proving 目前已經「逼近 12 秒的目標」,再砍一半會讓剛剛追上的那條線再度落後。換句話說,昨天談的那個「hash-friendly SNARK 追上標準 hash」的勝利,是險勝,勝出的餘裕不足以支撐一個把時間預算砍半的改動。

同一份名單上另一個值得注意的是 EIP-2926: Chunk-based code merkelization 也被 Declined。code chunk 化曾經被當成一個可以獨立於換樹先做的準備動作,現在它退回到 EIP-7864 內部,只會跟整棵樹一起上線。這意味著昨天算的那個 330MB 的 code witness 問題,在二元樹上主網之前沒有任何緩解措施

EIP-7782 被否決還有一個後果:那個「把 witness 分散到更多、更小的 slot 裡傳播」的減壓閥沒有了。8.86 MiB 仍然要在 12 秒的 slot 內傳播完,而現行 12 秒 slot 的區塊傳播預算是前三分之一,約 4 秒(EIP-7782 原本要把 6 秒 slot 的 attestation deadline 訂在 3,000 ms,正是因為傳播是整個 slot 裡最耗時的一段)。

🧠 記

  • 把 witness 換成 bytes/gas 這個單位,所有東西才能互相比較。MPT:337,200,000 ÷ 30,000,000 = 11.24 bytes/gas。二元樹 + EIP-4762:8,223 ÷ 53,100 = 0.1549 bytes/gas。改善 72.6 倍
  • 二元樹的最壞情況是完全局部存取,不是隨機存取。 讀滿一個 stem:1,900 + 256 × 200 = 53,100 gas,換 31 + 256 × 32 = 8,223 bytes。隨機讀每 2,100 gas 只換 32 bytes,差 10 倍。折扣就是攻擊面。
  • 三個必記的 witness 尺寸:36M → 5.32 MiB、60M → 8.86 MiB、200M → 29.54 MiB。撞上 p2p 實務天花板 10 MiB 的 gas limit 是 67.7M,比今天的 60M 只高 13%。
  • EIP-4762 的常數是為 Verkle 訂的。 1,900 ÷ 144 = 13.19 gas/byte 是 Verkle 分支的算法;二元樹的分支是 768 bytes,1,900 ÷ 768 = 2.47 gas/byte——Verkle 的最壞情況是二元樹的平均情況。這組常數從來沒有為二元樹重算過。
  • 要把 200M 下的最壞情況壓進 10 MiB,WITNESS_CHUNK_COST 要從 200 提到約 605(3 倍);想保住 2,100 的單點冷讀,WITNESS_BRANCH_COST 要降到 1,495。代價是熱區內第二個以後的 leaf 從 200 變 605——把二元樹的整個賣點砍掉三分之二。
  • 局部性折扣的大小 = 最壞情況 witness 的大小。 同一個旋鈕。你不能同時要「局部存取便宜」和「witness 有上界」,除非把頻寬單獨開一個 gas 維度(EIP-8011,被 Declined),或改變傳播模型(VOPS)。
  • EIP-7864 常數表的 64/128 之謎解開了:EIP-4762 的 storage_key < (CODE_OFFSET − HEADER_STORAGE_OFFSET)(chunk_id + 128) 交叉驗證,坐實 CODE_OFFSET = 128HEADER_STORAGE_OFFSET = 64,不變量 256 > 128 > 64 成立。常數表過時,不是不變量錯。熱區 = 前 64 個 slot + 前 128 × 31 = 3,968 bytes 的 code。
  • 真實合約的局部性很差:85% 的呼叫碰到前 64 之外的 slot,只有 56% 碰到過前 64 個;60% 的執行超出前 128 個 chunk。mapping 的 slot 是空的、動態陣列一律出線、__gap 佔掉 45 個 slot、ERC-1967 的 implementation 指標是每次呼叫必讀的隨機冷 slot。
  • 對今天的合約,二元樹平均貴 26%,96% 變貴,主因是 code chunking(今天純執行 code 完全免費)。這是一次性的重新定價上界,不是穩態——但「展開比迴圈快」這個從 CPU 繼承的編譯器直覺,在逐 chunk 計費的 VM 上是錯的。
  • EIP-7928(BAL)就是 execution witness 的 state_diff,已經在出貨,只是掛著「平行執行」的名義。60M 下平均 72.4 KiB 壓縮後,509 MiB/day,保留期 3,533 epochs = 15.70 天(約 7.81 GiB)。EIP-8159 給它 p2p 通道,EIP-8189 用它取代 snap sync 的 trie node healing。
  • BAL 的 ITEM_COST = 2000 與 EIP-4762 的 WITNESS_CHUNK_COST = 200 差 10 倍,而 EIP-7928 的 rationale 明說它假設「加一項最少要 2,100 gas」——那正是二元樹的局部性折扣要摧毀的假設(自行對照,已標存疑)。
  • 協議對「進區塊的 byte」有兩套價:calldata 與 access list 是 64 gas/byte(EIP-7976 / EIP-7981,都在 SFI),witness leaf 是 6.25 gas/byte。60M 下 calldata 最壞 915.53 KiB,witness 最壞 8.86 MiB,差 9.9 倍。
  • VOPS 才是 2026 年實際在推的路線:EOA 只存 address / nonce / balance / codeFlag 四欄,不存 code 與 storage tree,儲存需求降到約 1/25,靠 state diff 更新而不重執行。EIP-7864 的 storage type 前綴就是為它預留的同步介面。
  • EIP-7782(6 秒 slot)被 Declined,理由是會壓縮已經逼近 12 秒的 real-time ZK proving 預算。EIP-2926(code merkelization)也被 Declined,退回 EIP-7864 內部。

✍️ 實踐

1. 算出自己合約的 witness footprint。 對主要的使用者路徑,數出三個數字:碰到幾個不同的 stem、每個 stem 裡碰到幾個 leaf、以及執行過程進入了幾個 code chunk。前兩個可以從 storage layout 推:slot 編號 < 64 的算在 header stem,其餘按 slot // 256 分組。code chunk 數用 ceil(runtime_bytecode_length / 31) 估上界,實際值要看控制流。乘上 1,900 × stem 數 + 200 × leaf 數 + 200 × chunk 數,和今天的 2,100 × 冷存取次數 對比。差額就是二元樹上線那天你的使用者要多付的錢。

2. 把每次呼叫必讀的東西搬進前 64 個 slot。 這是 EIP-7864 語意下最高報酬率的單一改動,而且今天做完全無害。優先順序:每次都讀的設定值(暫停旗標、費率、管理員)> 熱路徑的計數器 > 冷路徑的設定。特別檢查有沒有 uint256[45] __gap 這種東西擋在前面——如果協議還在早期、layout 還能改,把 gap 移到 slot 100 之後。

3. ERC-1967 proxy 的 implementation slot 值得單獨想一想。 它現在住在 0x360894a1...,一個為了「不可能碰撞」而選的 hash 位址。在二元樹下,每一次 proxy 呼叫都會為它付一次跨 stem 的冷讀(1,900 + 200 = 2,100 gas),而且發生在實作合約的任何一個 byte 被載入之前。這是一個 ERC 級別的問題,不是單一專案能解的,但如果你在設計新的 proxy 標準或新的 upgradeable 框架,現在就該把「implementation 指標放在 header stem 內」列進需求。

4. 用 BAL 當作 witness 的先行指標。 Glamsterdam 上主網之後,block_access_list_hash 進 header,BAL 本體透過 engine API 傳遞並被 EL 分開儲存。這是第一次可以在主網上實測「一個區塊到底碰了多少個狀態位置」——而那個數字乘上 32 bytes 加上證明,就是未來 witness 的規模。如果你在做基礎設施,現在就該把 BAL 的解析與統計加進 pipeline;如果你在做協議,現在就該量自己的合約在 BAL 裡佔多少 bytes。

5. 檢查有沒有依賴「執行 code 是免費的」的設計。 典型模式:把大量邏輯放進單一巨型合約以省下 DELEGATECALL;用展開的長跳轉表做 dispatch;把不常用的錯誤處理和事件定義混在熱路徑中間。這些在 EIP-2929 下是零成本或負成本的最佳化,在 EIP-4762 下每一個都要按 chunk 付錢。EIP-7954 把合約上限提到 64 KiB 之後,這個誘因會更強——而更大的合約在二元樹下更貴,因為只有前 3,968 bytes 在便宜區。

6. 追蹤 EIP-4762 的常數,不要追蹤 EIP-7864 的樹。 樹的形狀基本定了(arity 2、單一樹、stem/subindex、256 分組),還在變的是 hash function 和這五個 gas 常數。而對開發者來說,真正決定成本的是後者。EIP-4762 到今天還是 Draft,Rationale 裡的「假設分支長度 144 bytes」是 Verkle 的遺留,一旦有人為二元樹重算,WITNESS_CHUNK_COST 很可能是 200 以外的數字。訂閱那份 EIP 的 commit 歷史,比讀任何二手摘要有用。

🔗 延伸學習

  • EIP-4762: Statelessness gas cost changes — 決定 witness 成本的那五個常數的出處。Rationale 裡「6.25 gas/byte、假設分支長度 144 bytes」那段是理解整組定價為什麼結構性錯位的關鍵;Security Considerations 對客戶端資料庫佈局(把一個 stem 的 256 個 leaf 存成一個 8kB BLOB)的要求也很值得看。
  • EIP-4762 execution witness worst-case(Ignacio Hagopian) — 本篇核心數字的一手來源。三步推導出 5.3 MiB 的最壞情況,並明確指出「最壞情況不是隨機存取,恰恰相反」。文末對調整常數的討論是目前公開文獻裡對這個問題最誠實的表述。
  • You Pay For What You Touch: Locality as Ethereum’s Next Cost Model(Dedaub, 2026-04-10) — 唯一一份對真實合約做過實測的局部性分析。26% / 96% 的數字、Uniswap V2 getter 與 Curve gauge 的具體 trace、以及對編譯器與標準函式庫的具體提案,全部在這裡。
  • EIP-7928: Block-Level Access Lists — Glamsterdam 執行層 headliner。除了規格本身,Rationale 的「BAL Size Considerations (60m block gas limit)」給出 72.4 KiB 的成分拆解,Security Considerations 的 G_remaining >= R_remaining * 2000 則是本篇更正 4 的另一半。
  • A pragmatic path towards Validity-Only Partial Statelessness (VOPS) — Thomas Thiery 的原始提案與整串討論。要理解為什麼 Ethereum 不做完整 statelessness,以及 EIP-7864 的 storage type 前綴到底是為誰預留的,這是唯一的一手來源。

💬 問 AI

我在研究 Ethereum stateless 路線的 witness 尺寸問題。請以一手規格(EIP 原文、EF 部落格、ethresear.ch)為依據回答,每個數字標出處與章節,查不到就明說「查不到」,不要用記憶補。
 
1. 請完整重算二元樹 witness 的最壞情況。列出 EIP-4762 的五個常數,推導「讀滿一個 stem 的 gas 成本」與「換到 state_diff 裡的 bytes」,算出 bytes/gas,然後給出 36M / 60M / 150M / 200M / 300M 五個 gas limit 下的 state_diff 大小。再算出撞到 10 MiB 所需的 gas limit。
 
2. 解釋為什麼最壞情況是「完全局部存取」而不是「隨機存取」。把兩種存取模式的 bytes/gas 都算出來對比。然後回答一個設計問題:EIP-7864 引入 256 值分組是為了局部性,而局部性折扣正是這個攻擊的來源——請列出至少三種可能的緩解方向,並各自說明代價。
 
3. EIP-4762 的 Rationale 說 WITNESS_BRANCH_COST 平均是 13.2 gas/byte,假設分支長度 144 bytes。請說明這個 144 是哪一種樹的分支尺寸,並用 EIP-7864 自己給的 arity-2 分支長度重算 gas/byte。這個差異對常數重新校準意味著什麼?
 
4. 請把 EIP-7928 的 ITEM_COST=2000 與 EIP-4762 的 WITNESS_CHUNK_COST=200 放在一起分析。EIP-7928 的 BAL 尺寸約束公式建立在什麼假設上?二元樹上線後這個假設還成立嗎?如果不成立,BAL 的尺寸約束與 Security Considerations 裡的 early rejection 檢查各自要怎麼改?
 
5. 對比協議對「進區塊的 byte」的各種定價:EIP-7623 / EIP-7976 的 calldata floor、EIP-7981 的 access list 資料成本、EIP-7928 的 ITEM_COST、EIP-4762 的 WITNESS_CHUNK_COST。全部換算成 gas/byte 列表。然後回答:witness 的 byte 應不應該比 calldata 的 byte 便宜?便宜多少倍是合理的?請給出論證而不只是立場。
 
6. 解釋 VOPS 的設計。VOPS 節點存哪些欄位、不存哪些?它如何在不重新執行交易的情況下維持 mempool 的有效性檢查?它與完整 statelessness 的差別在哪?EIP-7864 的 storage type 前綴如何為它服務?最後說明 VOPS 目前有沒有對應的 EIP 編號,還是仍停留在研究階段。
 
7. 請核對 EIP-7773 的 master 版本,列出 Glamsterdam 的 Scheduled for Inclusion、Considered for Inclusion、Declined for Inclusion 三份名單,並說明 EIP-7782 與 EIP-2926 被 Declined 的理由。Activation 表格目前填了嗎?
 
8. 最後,請指出上面我的問題裡有沒有錯誤的前提、或我因為問法而漏掉的重要面向。