作者:Vitalik Buterin
編輯:Karen,Foreisght News
在以太坊中,資源直到最近還是有限的,並透過一種稱為“Gas”的單一資源來定價。 Gas 是衡量處理特定交易或區塊所需「計算量」(computational effort)的度量單位。 Gas 將多種類型的「計算量」融合在一起,其中最重要的包括:
1、原始計算(Raw computation,如ADD、MULTIPLY);
2、讀寫和寫入以太坊儲存(例如SSTORE、SLOAD、ETH 轉帳);
3、數據頻寬;
4、產生區塊的ZK-SNARK 證明的成本。
例如,我發送的這筆交易總共消耗了47,085 Gas。其中包括:(i)基本成本為21000 Gas,(ii)作為交易一部分包含的calldata 位元組消耗了1556 Gas,(iii)讀寫儲存消耗了16500 Gas,(iv)產生日誌(log)消耗了2149 Gas,其餘用於EVM 執行。用戶必須支付的交易費用與交易消耗的Gas 成正比。一個區塊最多可以包含3000 萬Gas,並且Gas 價格透過EIP-1559 targeting 機制不斷調整,確保每個區塊平均包含1500 萬Gas。
這種方法有一個主要的優點:因為所有內容都合併為一個虛擬資源,所以市場設計非常簡單。優化交易以最小化成本很容易,優化區塊以收取盡可能高的費用相對容易(不包括MEV),並且沒有奇怪的激勵機制鼓勵一些交易與其他交易捆綁以節省費用。
然而,這種方法也存在低效性:它將不同的資源視為可以相互轉換,而實際的底層限制並不一樣。要理解這個問題,你可以先看下面這張圖表:
Gas 限制加了一個約束條件:
實際的底層安全約束通常更接近:
這種差異導致Gas 限制要么無端地排除了實際安全的區塊,要么接受了實際上不安全的區塊,或者兩者兼而有之。
如果有n 種資源有不同的安全限制,那麼一維Gas 可能會使吞吐量最多降低n 倍。因此,長期以來,人們一直對多維Gas 的概念感興趣,而透過EIP-4844,我們現在實際上已經在以太坊上實現了多維Gas。本文探討了這種方法的優點,以及進一步增強的前景。
Blob:Dencun 中的多維Gas
今年年初,平均區塊大小為150 kB。其中很大一部分是Rollup 資料:Layer2 協定在鏈上儲存資料。這些數據非常昂貴:儘管Rollup 上的交易成本僅為以太坊L1 上相應交易的5-10 倍,但即使這樣的成本對於許多用例來說也太高了。
那為什麼不降低calldata 的Gas 成本(目前非零位元組為16 Gas,零位元組為4 Gas),以讓Rollup 更便宜呢?我們之前這樣做過,現在也可以再做一次。但這裡的答案是:區塊的最大大小是30,000,000/16=1,875,000 非零字節,而網路勉強能或幾乎不能處理這樣大小的區塊了。再將成本降低4 倍會使最大值提高到7.5 MB,將對安全性帶來巨大風險。
這個問題最終透過在每個區塊中引入一個獨立的、對Rollup 友善的資料空間(稱為blob)來解決。
這兩種資源有不同的價格和限制:在Dencun 硬分叉之後,一個以太坊區塊最多可以包含(i)3000 萬Gas 和(ii)6 個blob,每個blob 可以包含約125 kB 的calldata 。這兩種資源都有單獨的價格,並透過單獨的類似EIP-1559 的定價機制進行調整,目標是每區塊平均使用1500 萬Gas 和3 個blob。
結果是,Rollup 的成本降低了100 倍,Rollup 上的交易量增加了3 倍以上,而理論上的最大區塊大小僅略有增加:從約1.9 MB 增加到約2.6 MB。
註:Rollup 交易費用,由Growthepie.xyz 提供。 Dencun 分叉發生於2024 年3 月13 日,引入了多維定價blob。
多維Gas 和無狀態客戶端
在不久的將來,無狀態客戶端(stateless clients)的儲存證明也會出現類似的問題。無狀態用戶端是一種新型用戶端,將能夠驗證鏈而無需在本地儲存大量或任何資料。無狀態客戶端透過接受該區塊中交易需要存取的以太坊狀態的特定部分的證明來實現這一點。
上圖展示了一個無狀態客戶端接收一個區塊,以及證明該區塊執行所觸及的狀態特定部分(例如,帳戶餘額、程式碼、儲存)當前值的證明,這使得節點能夠在沒有任何儲存的情況下驗證一個區塊。
一次儲存讀取需要花費2100-2600 Gas,取決於讀取類型,而儲存寫入成本更高。平均而言,一個區塊會執行大約1000 次儲存讀寫操作(包括ETH 餘額檢查、 SSTORE 和SLOAD 呼叫、合約程式碼讀取和其他操作)。然而,理論上的最大值是30,000,000/2,100=14,285 次讀取。無狀態客戶端的頻寬負載與該數字成正比。
目前的計劃是透過將以太坊的State tree 設計從Merkle Patricia trees 轉變為Verkle trees 來支援無狀態客戶端。然而,Verkle trees 不具備抗量子性,並且對於較新的STARK 證明系統來說並不是最佳選擇。因此,許多人有興趣透過二進位Merkle trees 和STARKs 來支援無狀態客戶端,要么完全跳過Verkle,要么在Verkle 過渡幾年後,一旦STARK 變得更加成熟,就進行升級。
基於二進位雜湊樹分支的STARK 證明具有許多優點,但其關鍵弱點在於生成證明的時間很長:Verkle 樹可以每秒證明十萬個以上的值,基於哈希的STARKs 通常只能每秒證明幾千個哈希,而證明每個值都需要包含許多哈希的“分支”(branch)。
考慮到今天從Binius 和Plonky3 等超優化證明系統以及Vision-Mark-32 等專用哈希中預測的數字,我們似乎將在一段時間內處於一個實用範圍內,即每秒證明1000 個值是可行的,但證明14,285 個值則不可行。平均區塊會沒問題,但潛在最壞情況下的區塊(由攻擊者發布)會破壞網路。
我們處理此類情況的default 方法是重新定價:提高儲存讀取的成本,以減少每個區塊的最大值到更安全的水平。但是,我們已經這樣做了很多次,如果再次這樣做,會讓太多應用變得太昂貴。一個更好的方法是多維Gas:分別對存儲訪問進行限制和收費,將平均使用量保持在每個區塊1,000 次存儲訪問,但設置每個區塊的上限進行設置,例如2000 次。
多維Gas 的普遍性
另一個值得考慮的資源是狀態大小的成長:即增加以太坊狀態大小的操作,這些操作之後需要全節點來保存。狀態大小成長的獨特之處在於,限制它的理由完全來自於長期持續的使用,而不是高峰。
因此,為增加狀態大小的操作(例如,zero-to-nonzero SSTORE、合約創建)添加一個單獨的Gas 維度可能是有價值的,但目標不同:我們可以設定一個浮動價格來針對特定的平均使用量,但完全不設定每個區塊的限制。
這顯示了多維Gas 的一個強大屬性:它讓我們能夠分別針對每個資源,詢問(i)理想平均使用量是多少? (ii)每個區塊的安全最大使用量是多少?與基於每個區塊的最大值來設定Gas 價格,並讓平均使用量跟隨其後不同,我們有2n 自由度來設定2n 參數,根據對網路安全的考慮來調整每一個參數。
更複雜的情況,例如當兩種資源的安全性考慮部分相加時,可以透過使一個操作碼或資源消耗多種類型的Gas 的某種數量來處理(例如,一個zero-to-nonzero SSTORE 可能消耗5000 個無狀態客戶端證明Gas 和20000 個儲存擴充Gas)。
每筆交易Max(選取資料或計算消耗量較大的那種)
令𝑥1 為資料的Gas 成本, 𝑥2 為計算Gas 成本,因此在一維Gas 系統中我們可以寫出一筆交易的Gas 成本:
在這個方案中,我們將交易的Gas 成本定義為:
也就是說,交易不是根據資料加計算來收費,而是根據它消耗的兩種資源中哪一種資源比較來收費。這可以輕鬆擴展以覆蓋更多維度(例如𝑚𝑎𝑥(...,𝑥3∗𝑠𝑡𝑜𝑟𝑎𝑔𝑒_𝑎𝑐𝑠𝑒𝑠) )。
應該很容易看出這如何在保證安全性的同時提高吞吐量。理論上一個區塊中的最大資料量仍然是GasLIMIT/𝑥1,與一維Gas 方案中完全相同。類似地,理論上最大的計算量是GasLIMIT/𝑥2 ,同樣與一維Gas 方案中完全相同。然而,任何消耗數據和計算的交易的Gas 成本都會降低。
這大概是提議的EIP-7623 中採用的方案,以減少最大區塊大小,同時進一步增加blob 計數。 EIP-7623 中的精確機制稍微複雜一些:它保持當前的calldata 價格為每字節16 Gas,但增加了每字節48 Gas 的floor price;交易支付( 16 * bytes + execution_Gas ) 和( 48 * bytes ) 中的較高者。因此,EIP-7623 將區塊中理論最大交易調用資料從約1.9 MB 減少到約0.6 MB,同時保持大多數應用程式的成本不變。這種方法的好處是它與目前的一維Gas 方案相比變化非常小,因此非常容易實現。
不過這種方法有兩個缺點:
1、即使區塊中的所有其他交易只使用很少的該資源,但大量佔用一種資源的交易仍然會不必要地收取大量費用;
2、它激勵數據密集型和計算密集型交易合併到一個捆綁包中以節省成本。
我認為,EIP-7623 這樣的規則,無論是對於交易calldata 還是其他資源,都可以帶來足夠大的好處,即使存在這些缺點,也是值得的。
然而,如果我們願意投入(顯著更高的)開發努力,就會出現一種更理想的方法。
多維EIP-1559:更困難但理想的策略
讓我們先回顧一下常規EIP-1559 的工作原理。我們將重點放在EIP-4844 中針對blob 引入的版本,因為它在數學上更加優雅。
我們追蹤一個參數excess_blobs 。在每個區塊期間,我們設定:
excess_blobs <-- max(excess_blobs + len(block.blobs) - TARGET, 0)
其中TARGET = 3 。也就是說,如果某個區塊的blob 數量多於目標,則excess_blobs 會增加,如果某個區塊的blob 數量少於目標,則excess_blobs 會減少。然後我們設定blob_basefee = exp(excess_blobs / 25.47) ,其中exp 是指數函數𝑒𝑥𝑝(𝑥)=2.71828^𝑥 的近似值。
也就是說,每當excess_blobs 增加約25 時,blob 基本費用就會增加約2.7 倍。如果blob 變得太貴,平均使用量就會下降,並且excess_blobs 開始減少,從而自動再次降低價格。 Blob 的價格不斷調整,以確保平均而言,區塊是半滿的,也就是說,每個區塊平均包含3 個Blob。
如果使用量出現短期峰值,則會出現限制:每個區塊最多只能包含6 個blob,在這種情況下,交易可以透過提高優先費來相互競爭。然而,在正常情況下,每個blob 只需支付blob_basefee 加上少量的額外優先費用作為被納入的誘因。
這種Gas 定價在以太坊中已經存在多年:早在2020 年,EIP-1559 就引入了非常相似的機制。透過EIP-4844,我們為Gas 和Blobs 設定了兩個獨立的浮動價格。
註:2024 年5 月8 日一小時內的Gas 基本費用,單位為gwei。來源:ultrasound.money
原則上,我們可以為儲存讀取和其他類型的操作添加更多獨立浮動的費用,不過我在下一節中將詳細闡述一個需要注意的問題。
對於用戶來說,這種體驗與今天非常相似:你不再支付一筆基本費用(basefee),而是支付兩項基本費用,但你的錢包可以將其從你的手中抽像出來,只向你顯示可以預期支付的預期費用和最高費用。
對於區塊建構者來說,大多數時候最佳策略與今天相同:包括任何有效的內容。大多數區塊都未滿——無論是Gas 還是Blob。一個具有挑戰性的情況是,當有足夠的Gas 或足夠的Blob 超過區塊限制時,建構者需要潛在地解決多維knapsack 問題以最大化其利潤。然而,即使存在相當好的近似演算法,在這種情況下,透過制定專有演算法來優化利潤所獲得的收益也比使用MEV 進行相同操作所獲得的收益要小得多。
對於開發人員來說,主要的挑戰是需要重新設計EVM 及其相關基礎設施的功能,這些功能目前是基於單一價格和單一限制設計的,現在需要將其改造成能夠適應多個價格和多個限制的設計。
應用程式開發人員面臨的一個問題是優化變得稍微困難:在某些情況下,您不能再明確地說A 比B 更有效率,因為如果A 使用更多的calldata 而B 使用更多的執行,那麼當calldata 為便宜,當calldata 昂貴時則較昂貴。
應用程式開發者面臨的一個問題是優化會變得稍微困難一些:在某些情況下,你無法明確地說A 比B 更有效率,因為如果A 使用了更多的calldata,而B 使用了更多的執行,那麼當calldata 便宜時A 可能更便宜,而當calldata 昂貴時A 可能更貴。
然而,開發者仍然可以透過基於長期歷史平均價格進行最佳化,來獲得相當不錯的結果。
多維定價、EVM 和sub-calls
有一個問題在blobs 中沒有出現,在EIP-7623 或甚至是針對calldata 的完整多維定價實作中也不會出現,但如果我們試圖對狀態存取或其他任何資源進行單獨定價,那麼這個問題就會出現:即子呼叫(sub-calls)中的Gas 限制。
EVM 中的Gas 限制存在於兩個地方。首先,每筆交易都會設定一個Gas 限制(Gas Limit),限制了該交易中可以使用的Gas 總量。其次,當一個合約呼叫另一個合約時,該呼叫可以設定自己的Gas 限制。這允許合約調用他們不信任的其他合約,並且仍然保證他們在調用後仍有剩餘的Gas 來執行其他計算。
註:帳戶抽象交易的蹤跡,其中一個帳戶呼叫另一個帳戶,並且僅向被呼叫者提供有限數量的Gas,以確保即使被呼叫者消耗了分配給它的全部Gas,外部呼叫也可以繼續運作。
挑戰在於:讓不同類型的執行之間實現多維Gas 似乎需要子調用為每種類型的Gas 提供多個限制,這將需要對EVM 進行非常深入的更改,並且與現有應用程式不相容。
這就是為什麼多維Gas 提案通常停留在兩個維度的原因之一:資料和執行。資料(無論是交易calldata 還是blob)僅在EVM 外部分配,因此EVM 內部無需更改任何內容即可使calldata 或blob 單獨定價。
我們可以想出一個「EIP-7623 式的解」來解決這個問題。這是一種簡單的實作:在執行期間,對儲存作業收取4 倍的費用;為了簡化分析,假設每個儲存作業有10000 氣體。交易結束時,退款min(7500 * storage_operations, execution_Gas) 。結果是,在扣除退款後,用戶需要支付以下費用:
execution_Gas + 10000 * storage_operations - min(7500 * storage_operations, execution_Gas)
這等於:
max(execution_Gas + 2500 * storage_operations, 10000 * storage_operations)
這反映了EIP-7623 的結構。另一種方法是即時追蹤storage_operations 和execution_Gas ,並根據當時max(execution_Gas + 2500 * storage_operations, 10000 * storage_operations) 上漲多少收取2500 或10000。操作碼被呼叫。這避免了交易需要過度分配Gas,而這些Gas 主要透過退款來收回。
我們沒有獲得子呼叫的細粒度許可:子呼叫可能會消耗交易的所有allowance 以進行廉價的儲存操作。
但我們確實得到了一些足夠好的東西,即進行子調用的合約可以設定一個限制,並確保一旦子調用執行完畢,主調用仍有足夠的Gas 進行所需的後處理(post-processing)。
我能想到的最簡單的「完整的多維定價解決方案」是:我們將子呼叫Gas 限制視為成比例的。也就是說,假設有𝑘 種不同的執行類型,並且每個交易設定了多維限制𝐿1...𝐿𝑘 。假設在目前執行點,剩餘Gas 為𝑔1...𝑔𝑘 。假設呼叫CALL 操作碼,並使用子呼叫Gas 限制𝑆 。令𝑠1=𝑆 ,然後𝑠2=𝑠1/𝑔1∗𝑔2 , 𝑠3=𝑠1/𝑔1∗𝑔3 ,以此類推。
也就是說,我們將第一種類型的Gas(實際上是VM 執行)視為一種特殊的「帳戶單位」,然後分配其他類型的Gas,以便子呼叫在每種類型的Gas 中都獲得相同百分比的可用Gas。這種方法有點難看(ugly),最大限度地保證了向後相容性。
如果我們想在不犧牲向後相容性的情況下,使該方案在不同類型的Gas 之間更加“中立”,我們可以簡單地將子調用的Gas 限制參數表示為當前context 中剩餘Gas 的一部分(例如,[1...63]/64)。
然而,無論在哪種情況下,都值得強調的是,一旦開始引入多維執行Gas,固有的複雜性(ugliness)就會增加,這似乎很難避免。
因此,我們的任務是做出一個複雜的權衡:我們是否接受在EVM 層面上的某種程度的複雜性(ugliness)增加,以安全地解鎖顯著的L1 可擴展性增益,如果是的話,哪種具體提案對協議經濟和應用程式開發者最有效?很有可能,我上面提到的兩個方案都不是最好的,但仍有空間可以提出更優雅、更好的方案。
特別感謝Ansgar Dietrichs、Barnabe Monnot 和Davide Crapis 的回饋和審查。