早期拒絕對抗性 BAL

本文為機器翻譯
展示原文

早期拒絕對抗性 BAL

特別感謝AnsgarMariusFrancescoCarlMariaJochem的貢獻與合作。

EIP-7928 (塊級訪問列表,BAL)通過要求構建者聲明塊執行期間訪問的所有狀態位置及其狀態更改,實現了塊驗證的並行執行和批量 I/O。這使得客戶端能夠預取狀態並並行執行事務。

然而,當前的BAL規範可能會引入驗證不對稱性,這種不對稱性可能會被利用來強制執行不必要的塊驗證工作。這雖然不會破壞執行並行化,但會抵消批量 I/O 預取的優勢,同時還會顯著增加帶寬和執行成本。

簡而言之:無效區塊的失效速度可能不及有效區塊(最壞情況下的區塊)的驗證速度。這阻礙了我們的擴展能力。客戶端可以通過簡單的 gas 預算檢查來緩解這個問題。針對 EIP 的 PR 草稿可以在這裡找到。

為了理解這個問題及其解決方案,讓我們先來了解一下 BAL 的工作原理。

背景: BAL結構

BAL是按賬戶組織的:

[address,storage_changes, # slot → [(block_access_index, post_value)] storage_reads, # [slot] balance_changes,nonce_changes,code_changes]

BAL包含兩種不同類型的信息:

  1. 事務後狀態差異寫入操作會用block_access_index進行註釋,因此我們可以準確地知道哪些事務更改了狀態的特定部分。

  2. 區塊後狀態位置——讀取操作storage_reads和無*_changes地址)映射到事務索引。我們知道它們在The Block中的某個位置被訪問過,但不知道是由哪個事務訪問的。

為什麼讀取操作要省略事務索引?這樣可以節省帶寬。當一個事務同時讀取和寫入一個存儲槽時,`storage_changes` 表中的寫入條目會包含讀取條目,從而避免重複。由於並行化只需要知道訪問了哪些狀態位置(不需要知道何時訪問),因此事務索引是不必要的開銷。

這種不對稱性對於理解漏洞至關重要:寫入操作可以及早驗證,但讀取操作則不能。

使用 BAL 進行塊驗證

收到數據塊後,客戶端使用BAL來執行以下操作:

  • 並行執行事務,同時延遲完整的BAL驗證,直到事務執行完畢。
  • 通過批量 I/O預取所需狀態以減少磁盤延遲
  • 與執行並行計算後狀態根

在 BAL 出現之前,區塊驗證非常簡單:最壞的情況就是區塊消耗掉所有可用 gas。忽略有效性不會讓情況變得更糟——一旦 gas 達到上限,客戶端就會中止交易並將The Block標記為無效。但有了 BAL,情況就不同了:我們必須確保有效區塊的最壞情況執行時間不比無效區塊更糟。

哪些內容可以早期驗證

事務執行期間可以強制執行事務後狀態差異驗證。由於寫入操作映射到事務索引,因此可以根據預期狀態變化獨立驗證每個事務。

截圖日期:2026-01-30 08-41-01
屏幕截圖,拍攝於 2026-01-30 08-41-01,尺寸 941×427,大小 47.7 KB

哪些事情無法早期驗證

在單個交易執行期間,無法驗證區塊後狀態的位置

事務x讀取存儲槽可能被事務y寫入。在彙總最終BAL時,寫入操作會消耗讀取操作(該存儲槽僅出現在storage_changes中,而不出現storage_reads中)。存儲槽最終是否被聲明為讀取操作取決於所有事務的寫入操作總數。

因此,驗證已聲明的讀取操作需要先執行所有事務。

案例研究:蓋斯

例如, Geth使用多個工作 goroutine 並行執行事務並驗證其狀態差異。對於狀態位置(讀取)操作,驗證工作被延遲到ResultHandler goroutine 中,該 goroutine 會在所有事務完成後聚合結果。

截圖日期:2026-01-29 18-18-41
屏幕截圖,日期:2026年1月29日 18:18:41,分辨率:1014×682,大小:58.8 KB

實際操作中:

  • 工作節點獨立執行事務並驗證狀態差異。
  • 結果流式傳輸到ResultHandler收集已訪問的存儲槽。
  • 只有在所有事務完成後,處理程序才能驗證聲明的讀取操作與實際訪問操作是否一致。

這種延遲驗證會造成攻擊機會。

襲擊

現在讓我們來看看惡意區塊生成器可能會利用什麼漏洞。

設置

讓:

  • G G = 阻塞氣體極限
  • G_{\mathrm{tx}} G t x = 最大單筆交易 gas 限制(≈ 2^{24} 2 24,根據EIP-7825
  • g_{\mathrm{sload}} = 2100 g s l o a d = 2100 = 冷態SLOAD的最低氣體成本

攻擊構造

對抗性提議者分兩步構建區塊:

步驟 1:聲明虛擬存儲讀取

storage_reads中聲明一個存儲槽集合S

|S| = \left\lfloor \frac{G - G_{\mathrm{tx}}}{g_{\mathrm{sload}}} \right\rfloor
| S | = G G t x g s l o a d

如果將所有剩餘的 gas(在預留一個最大 gas 交易後)用於SLOADs ,則該The Block可以容納的最大不同冷存儲讀取次數。

步驟 2:包含僅計算交易

包含一個或多個滿足以下條件的交易:

  • 氣體使用量 ≈ G_{\mathrm{tx}} G t x
  • S S中沒有任何插槽可供訪問
  • 純計算(例如,算術循環、哈希)

生成的BAL在語法上有效,並且符合所有 gas 限制,但是聲明的存儲讀取在執行期間從未被訪問。

為什麼這會造成問題

客戶端會在並行執行交易的同時開始預取數據。從任何單個交易的角度來看,它們無法在所有交易執行完畢之前使The Block失效。

結果:一個無效區塊完全停用了有用的 I/O 預取功能,使BAL膨脹到數百 KiB(在 60M 區塊 gas 限制下為 0.6 MiB),並加重了網絡負擔——而此時區塊的失效卻為時已晚。

截圖日期:2026-01-30 11-07-55
屏幕截圖,日期:2026年1月30日 11:07:55,分辨率:1228×421,大小:39.8 KB

這實際上意味著我們無法像最壞情況下的有效塊那樣擴展,而是受到無效塊的限制。

為什麼不直接過度聲明讀取操作呢?如果攻擊者聲明瞭\left\lfloor \frac{G}{g_{\mathrm{sload}}} \right\rfloor G g s l o a d如果使用全部gas 預算,客戶端可以在任何交易因未訪問存儲的操作而浪費 gas 時立即使The BlockThe Block。

不妨瞭解一下BALrog ,它是 Engine API 的一個簡單代理,可以將最壞情況下的 BAL 注入到代碼塊中,這對於測試非常有用。

解決方案:燃氣預算可行性檢查

此次攻擊利用了兩個事實:

  1. 各個帖子之間並不知道其他帖子發生了什麼。
  2. 只有在執行完所有交易後,狀態位置才能得到驗證。

但是,我們可以利用一個關鍵約束:首次訪問存儲槽至少需要花費冷SLOAD成本(2100 gas,或使用EIP-2930訪問列表時為 2000 gas,最終值取決於EIP-7981 )。

不變式

在執行過程中,客戶端會定期(例如,每 8 筆交易)進行驗證:

  • 已經使用了多少天然氣
  • 已訪問哪些已聲明的存儲槽
  • 還剩下多少已聲明的讀取次數

然後,令:

  • R_{\mathrm{declared}} R d e c l a r e d = BAL中已聲明狀態讀取的數量
  • R_{\mathrm{seen}} R s e e n = 已訪問次數
  • R_{\mathrm{剩餘}} = R_{\mathrm{聲明}} - R_{\mathrm{已見} } R r e m a i n i n g = R d e c la r e d R s e e n
  • G_{\mathrm{剩餘}} G r e m a i n i n g = 剩餘塊氣體

BAL有效性的必要條件是:

G_{\mathrm{剩餘}} \ge R_{\mathrm{剩餘}} \cdot 2100
GREMAING RMAING 2100

如果此不等式不成立,則可以立即拒絕The Block。

截圖日期:2026-01-30 07-33-54
屏幕截圖,拍攝於 2026-01-30 07-33-54,分辨率 1014×682,大小 65.1 KB

請點擊此處查看該規範的 PR 草案。

好處

gas 預算檢查只需一次最大 gas 交易(約 1600 萬 gas / 約 1 秒)即可儘早拒絕惡意區塊。BAL執行保持不變,保留了所有並行化優勢。BAL 格式無需更改,有效區塊的批量 I/O 功能也已完全恢復。實現複雜度極低——只需在現有結果處理程序中進行簡單的記賬——且週期性算術檢查帶來的性能開銷幾乎可以忽略不計。

考慮過的替代方案

另一種方法是使用首次訪問事務索引來註釋只讀訪問,這樣可以使 BAL 具有自描述性並簡化驗證邏輯。gas 預算方法也能實現類似的早期拒絕特性,但它是在執行期間進行額外的計費,而不是在BAL中添加額外的數據。將索引映射到讀取操作平均會向BAL中添加 4% 的額外(壓縮)數據。

另一種方法是按照狀態在The Block中被訪問的先後順序對BAL中的狀態位置進行排序。然而,這樣做會引入額外的複雜性,並且會使針對BAL進行驗證變得更加困難。

gas 預算可行性檢查提供了一種簡單有效的緩解措施:通過驗證剩餘的 gas 是否足以覆蓋剩餘的已聲明讀取,客戶端可以提前拒絕惡意塊,而無需更改BAL格式或犧牲並行化優勢。

示例結果處理程序邏輯

MIN_GAS_PER_READ = 2100 # cold SLOAD cost CHECK_EVERY_N_TXS = 8 def result_handler ( block, bal, tx_results_channel ): # Count expected storage reads from the BAL (once, upfront) expected_reads = sum ( len (acc.storage_reads) for acc in bal.accounts)accessed_slots = set ()total_gas_used = 0 for i, result in enumerate (tx_results_channel, start= 1 ): # Merge this transaction's accessed slots accessed_slots.update(result.accessed_slots)total_gas_used += result.gas_used # Periodic feasibility check if i % CHECK_EVERY_N_TXS == 0 :remaining_gas = block.gas_limit - total_gas_usedunaccessed_reads = expected_reads - len (accessed_slots)min_gas_needed = unaccessed_reads * MIN_GAS_PER_READ if min_gas_needed > remaining_gas: raise Exception( "BAL infeasible: " f" {unaccessed_reads} reads need {min_gas_needed} gas, " f"only {remaining_gas} left" )

資源


相关赛道:
來源
免責聲明:以上內容僅為作者觀點,不代表Followin的任何立場,不構成與Followin相關的任何投資建議。
喜歡
84
收藏
14
評論