本文深入探討了區塊鏈交易費用模型的重要性及其在確保網路安全和有效運行中的關鍵作用。 通過對乙太坊和 Solana 區塊鏈網路的交易費用模型進行比較分析,揭示了不安全的交易計費可能引發的網路安全風險。

作者:Certik

封面:Photo by Markus Spiske on Unsplash

導讀:本文深入探討了區塊鏈交易費用模型的重要性及其在確保網路安全和有效運行中的關鍵作用。 通過對乙太坊和 Solana 區塊鏈網路的交易費用模型進行比較分析,揭示了不安全的交易計費可能引發的網路安全風險。 特別關注了 CertiK 團隊發現並協助修復的 Solana 網路中大整數模冪運算的 CU 計算錯誤,這一錯誤可能導致潛在的 DOS 攻擊。 文章詳細分析了 Solana 的智慧合約計價模型、POH 共識機制、並行事務處理方式,並通過在 Solana 私有集群上的實驗,複現了遠端 DOS 攻擊的過程和成本。

 1.   摘要與背景

區塊鏈的計費模式是確保網路安全和有效運行的關鍵機制,通過收取使用者執行操作所需的費用(如 Gas 費用),防止惡意行為和資源濫用,保護使用者利益,並推動整個區塊鏈生態系統的發展和創新。 一個有效的計費系統不僅是財務基礎,也是促進技術進步和社區信任的重要因素。

本文將深入分析乙太坊(ETH)和 Solana 區塊鏈網路的交易費用模型,以及不安全的交易計費可能引發的網路安全風險。 重點討論 CertiK 團隊發現並修復的 Solana 網路中大整數模冪運算 CU(計算單元)計算錯誤所引發的潛在遠端 DOS 攻擊漏洞,並通過此案例探討區塊鏈計費模型中的安全隱患。

2.   交易費用模型的重要性

在 Web3.0 領域中,底層基礎設施運行在去中心化的區塊鏈網路上。 這些網路由全球驗證者共同維護和運營。 用戶通過交易和智慧合約進行互動,所有的交易都被記錄在分散式帳本上,並且這些記錄是永久不可篡改的。

驗證者們憑藉有限的資源共同維護著這龐大的區塊鏈網路,而交易費用在確保網路穩定性和安全性方面起到關鍵作用。 這些費用不僅激勵著網路參與者,還是推動區塊鏈成功發展的動力。 有效的交易費用模型能夠確保網路資源的合理分配,防止惡意行為和資源濫用,同時保護使用者利益,促進技術的持續創新和社區的健康發展。

交易費用模型的安全性對於區塊鏈的長期健康和穩定至關重要。 這種模型不僅僅是對網路資源的有效管理,還直接影響使用者的信任和參與度。 一個健全的交易費用模型能夠有效地防止網路遭受惡意行為,如拒絕服務攻擊(DDoS),通過設定適當的費用門檻使攻擊者難以濫用網路資源。

合理的費用結構能夠激勵驗證者和礦工投入足夠的資源來維護區塊鏈的安全性和穩定性,因為他們通過收取交易費用來獲取獎勵和補償。 此外,透明和公平的計費機制還可以保護使用者免受不當收費和資源耗費,增強他們對區塊鏈生態系統的信任感。 因此,一個經過良好設計的交易費用模型不僅是經濟上的基礎,更是確保區塊鏈網路安全和用戶權益的重要保障。

3. ETH 與 Solana 網路的交易費用模式設計

在 BTC 網路中,所有交易的複雜度相對一致,並採用單一的交易計費模型。 相比之下,ETH 和 Solana 網路採用圖靈完備的腳本語言,其交易計費模型設計更為複雜,涵蓋頻寬消耗、存儲消耗和計算消耗等多個方面。 智慧合約可以消耗任意數量的頻寬、存儲和計算資源,而 Gas 費用則是衡量合約執行所需計算工作量的單位。 通過限制 Gas 費用的消耗,可以有效控制智慧合約對資源的過度利用。

 3.1:ETH 網路的交易計費模型設計

  在乙太坊(ETH)網路中,交易計費模型設計如下:

 3.1.1:單位和概念解釋

Wei 和 Gwei 單位:Wei 是以太幣(ether)的最小單位,1 ether = 1 x 10^18 wei。 Gwei 是 Gas 的計量單位,1 gwei = 1 x 10^9 wei。

 3.1.2:Gas 費用計算系統

Gas Limit:用戶願意為確認交易或執行操作支付的最大 Gas 量。 在倫敦升級 [1]后,Gas Limit 可以根據網路需求在 15M 至 30M 之間動態調整。

Gas Used:實際消耗的 Gas 數量,不超過 Gas Limit。 對於未使用的 Gas 部分,將會自動退回到使用者的錢包餘額。

Gas Price:用戶願意為每單位 Gas 支付的價格。 Gas Price 隨著乙太坊網路上交易擁堵情況而變化,通常是動態調整的,當前查詢 [2]Gas Price 約為 10gwei 左右。 倫敦升級引入了改進的 EIP-1559[3]  新增兩個參數(基礎費用 BaseFee 和優先順序費用 PriorityFee),改進后的 Gas Price 計算為 BaseFee + PriorityFee。

 3.1.3:ETH 網路交易費用計算示例

例如,如果 Gas Price 為 35 gwei/Gas,交易手續費計算如下:交易手續費 = Gas Price(10 gwei)* Gas Limit(21000)/ 10^9 = 0.00021 ETH 如果以每 ETH 單價 3500 美元計算,這筆交易手續費大約為 0.735 美元。 在網路極度擁堵時,Gas Price 可能會上升至 100 gwei/Gas 以上,導致單筆交易費用超過 10 美元。

 3.1.4:ETH 網路的 TPS

在區塊鏈領域中,每秒事務數(TPS)是指網路每秒處理或執行的事務數量。 當前查詢 [4]顯示乙太坊網路的 TPS 約為 12.7。 這些特性和參數使得乙太坊網路能夠根據需求動態調整交易費用,並有效管理網路資源,從而支援其廣泛的智慧合約和分散式應用生態系統的運行和發展。

 3.2:Solana 網路的交易計費模型設計

 3.2.1:Gas 費計算系統

本地代幣 SOL 和 Lamport:Lamports 是 Solana 網路中的本地代幣,相當於 SOL 的最小單位。 一個 SOL 等於 1,000,000,000 Lamports。 MicroLamport 是 Lamports 的最小單位,等於 0.000001 Lamports,用於計算優先順序費用。

簽名費用:每筆交易必須包含一個簽名,每個簽名的基本費用固定為 5000 Lamports(0.000005 SOL)。

計算單元(CU):用於衡量 Solana 區塊鏈智慧合約執行資源消耗的最小單位。 在 Solana 1.9.2 中引入了類似 ETH 的 Gas Limit 的功能,每筆交易預設具有 200,000 CU 預算,並且可以設置最大的 1,400,000 CU。

 3.2.2:Solana 經濟學設計

基礎費用:在 2020 年推出後的前兩年,Solana 的交易基礎費用固定為 0.000005 SOL 每筆(每筆交易一個簽名)。

優先費用:  自 2022 年起,Solana 引入了額外的優先費用機制,允許在交易時支付額外費用以優先處理。 計算優先費用的方法是將請求的最大計算單元(CU)乘以每個計算單元 0.000001 Lamports 的價格,四捨五入到最接近的 Lamports。

租金費用:每個 Solana 帳戶在區塊鏈上存儲數據的費用稱為「租金」。。 驗證者根據帳戶在記憶體中維護的數據收取基於時間和空間的租金費用。 帳戶需要足夠的 Lamports 餘額以免除租金並保留在 Solana 區塊鏈上。 帳戶若無法支付租金,則可能會通過垃圾收集過程從網路中刪除。

例如:如果一個帳戶持有至少 2 年的租金,則該帳戶被視為免租。 每次帳戶餘額減少時都會檢查此項,將餘額 減少到最低金額以下的交易將會失敗,目前的免租費用為每 MB 6.96 SOL。 創建新帳戶時,費用會分配給該帳戶; 刪除帳戶時,可重新收取免租費用,假如一個大小為 15,000 位元組的可執行程式需要 105,290,880 個 lamports(=~ 0.105 SOL)的餘額才能免租。

 3.2.3:Solana 交易費用計算示例

交易總費用計算:  假設一筆交易請求 1,000,000 CU,並設置每個 CU 的優先費用為 10,000 MicroLamports。 則交易總費用為 5000 Lamports(簽名費用)+ 1,000,000 CU * 10,000 MicroLamports * 0.0000001 Lamports = 0.000015 SOL。 當前 Solana 平均費用查詢 [5]顯示,平均費用約為 0.000021 SOL,額外費用約為 0.000025 SOL。 以每 SOL 100 美元的價格計算,單筆交易費用大約為 0.0021 美元。

計算單元時間限制:  計算單元 CU 也可以取決於在 Solana SVM 中執行時間的定價,每 33 ns 的計算時間等價於 1 CU。 假設預設上限為 200,000 CU,那麼在 Solana SVM 中執行 200,000 CU 的智慧合約時間約為 6.6 毫秒。 最大 1,400,000 CU 的執行時間約為 46.2 毫秒。

交易費用分配: Solana 網路將所有交易費用的 50% 燒毀,其餘 50% 分配給處理交易的驗證者。

 3.2.4:Solana 網路的 TPS

根據 Solana 的白皮書,理論上 Solana 每秒可以處理多達 710,000 筆交易。 在實際場景中,Solana 已經表現出超過 5,000 TPS 的能力,並且在測試期間達到了 65,000 TPS。 當前查詢 [6]顯示 Solana 的 TPS 約為 3300。

這些設計和參數使得 Solana 網路能夠高效處理大量交易,並提供靈活的費用和資源管理機制,以支援其快速發展和廣泛應用的智慧合約和分散式應用生態系統。

4. ETH 和 Solana 網路交易費用對比

 4.1:Gas 費用對比:

根據平均費用查詢,Solana 的單筆交易費用為 0.0021 美元,遠低於 ETH 的 0.7 美元交易費。 ETH 的交易費用隨著網路擁堵而上升,而 Solana 相對於 ETH 網路,其單筆交易費用非常低,大約 500 筆交易的總費用才相當於 1 美元。

 4.2:TPS 對比:

目前,Solana 的 TPS 約為 ETH 網路的 250 倍至 500 倍(ETH 的 tps 為 3000 至 5000 的 12 分之一)。

 4.3:總結:

總體而言,Solana 相對於 ETH 網路在極低的 Gas 費用下能夠處理大量交易。 然而,Solana 網路面臨垃圾交易的風險高於 ETH 網路,這對其穩定性和冗餘性構成了相當大的考驗。

5. ETH 和 Solana 網路交易計費設計缺陷帶來的風險

前面瞭解並對比了 ETH 網路和 Solana 網路計費系統設計的細節和差異性,計費系統設計有缺陷會帶來嚴重的網路安全風險,拒絕服務攻擊是圖靈完備的區塊鏈網路備受困擾的網路安全風險之一,後續將會詳細討論 ETH 網路和 Solana 網路面臨計費系統缺陷帶來的網路安全風險。

 5.1:ETH 網路歷史上的計費系統風險

ETH 網路在歷史上遭遇了兩次拒絕服務攻擊,並導致網路性能下降或者崩潰,解決方案分別是 2016 年 10 月 18 日的橘子哨(Tangerine Whistle)分叉 [7]和 2016 年 11 月 22 日的偽龍(Spurious Dragon)分叉 [8]

5.1.1:EXTCODESIZE 操作碼攻擊的原因: 

造成這次拒絕服務攻擊的罪魁禍首是 EXTCODESIZE 操作碼,由於 EXTCODESIZE 操作碼的 Gas Price 相當低,並且需要節點從磁碟讀取狀態資訊,只要這些操作的 Gas 加起來不超過區塊的 Gas Limit,每個區塊的攻擊交易可調用此操作碼大約 50,000 次。 這一個區塊內的交易所佔用的計算時間就被大大延長,從而導致了整個乙太坊網路的癱瘓。

修復方案 EIP-150[9]:通過增加 EXTCODESIZE 操作碼的 Gas 成本,從 20 增加到 700,以此防止類似的攻擊再次發生。

5.1.2:SELFDESTRUCT 操作碼攻擊的原因:

攻擊者利用 SELFDESTRUCT 操作碼生成了大量空帳戶。 每次 SELFDESTRUCT 操作會花費 90 Gas 生成一個空帳戶,這些帳戶需要存儲在乙太坊的狀態樹中。 攻擊者總共創建了約 1900 萬個空帳戶,導致狀態樹的存儲空間大幅膨脹,超過了實際創建帳戶所需的存儲空間,最終導致節點存儲壓力爆炸。

修復方案 EIP-161[10]:EIP-161 通過清除空帳戶和優化狀態樹的存儲機制,減少了存儲空間的浪費。 現在創建新帳戶時需要額外支付 25,000 Gas,空帳戶功能上等同於不存在的帳戶,可以通過交易與空帳戶交互來刪除它們。

 5.1.3:總結:

ETH 網路這兩次拒絕服務攻擊都利用了某些操作碼的低 Gas 成本,從而放大了攻擊的效果。 攻擊者可以用很少的資源對網路造成極大的負擔,暴露了區塊鏈網路在低 Gas 機制下的潛在脆弱性。

 5.2:Solana 網路面臨過的計費系統風險

ETH 網路在 2016 年經歷了重大的拒絕服務攻擊,成為當時的重要挑戰之一。 而彼時 Solana 還處於創始階段,被視為潛在的「乙太坊殺手」。。 Solana 在 2022 年經歷了嚴峻的網路中斷考驗(總計中斷 5 次),但在 2023 年僅有一次中斷。 作為超高性能的區塊鏈網路,Solana 引入了 8 項關鍵創新 [11],雖然帶來了技術進步,也引發了許多未知的風險。

在數次中斷事件中,2022 年 1 月和 2022 年 4 月,Solana 網路分別因大量垃圾交易遭遇拒絕服務攻擊。

5.2.1: 2022 年 1 月 – 嚴重的網络擁塞 [12]

由於重複交易過多導致緩存耗盡,網路性能嚴重下降,部分節點的處理能力受限,持續了較長時間。

5.2.2:2022 年 4 月和 5 月 – 短暫的中斷 [13]

2022 年 4 月,Solana 網路中斷了 2 小時 42 分鐘,主要原因是 NFT 機器人大量發送垃圾郵件,導致網路過載。 同年 5 月,Solana 再度因垃圾郵件攻擊中斷了 5 小時 31 分鐘,每秒 600 萬次的垃圾郵件交易使驗證器記憶體不足,網路流量超出 100 Gbps,最終導致主網 Beta 集群的共識停滯。 投票不足以清理舊區塊,進一步加劇了記憶體使用問題,導致驗證器崩潰。

與 2016 年 ETH 網路拒絕服務攻擊類似,Solana 在幾次中斷後也通過提高 Gas 費用來緩解攻擊問題。 特別是引入了優先順序費用,允許使用者為交易支付額外費用,以確保其交易優先處理。 這種機制有效增加了攻擊成本,降低了垃圾郵件攻擊的可行性。

 5.2.3:總結

無論是 ETH 還是 Solana 網路,正確設計計費系統對於防範拒絕服務攻擊至關重要。 攻擊者往往會利用 Gas 費用機制中的漏洞發起攻擊。 雖然 Solana 引入了優先順序費用來提高攻擊成本,但其交易成本仍然非常低,這可能意味著未來仍存在未知的攻擊面。

6. Solana 智慧合約的計價模型

 6.1:Solana 合約精確計算模型

Solana 虛擬機(SVM)在 Solana 區塊鏈上運行合約至關重要,因其高效的計算單元(CU)模型和快速執行速度。 SVM 通過精確的計費結構和優化的執行引擎,支援高輸送量和低延遲的應用程式需求,同時保障了合約的安全性和穩定性。

Solana 的合約計費模式採用精確計算單元(CU)模型。 每 33 納秒的計算時間對應消耗 1 CU,CU 是衡量 Solana 虛擬機中資源消耗的標準單位。 合約的執行成本是基於 SVM 中運行的位元組碼所消耗的 CU 來計算的。

SVM 的 CU 計算模型對於 Solana 生態系統至關重要。 根據 CertiK 團隊於 5 月 24 日發布的一篇關於 SVM 的 CU 計算漏洞的研究文章,SVM 的 CU 計算指令在某些情況下可能會出現錯誤翻譯,導致智慧合約在 SVM 中出現無限計算消耗,嚴重時可能會導致整個 Solana 區塊鏈網路的崩潰。

6.2:Solana 引入 syscall 功能採用預估計算模型

隨著 Solana 的發展,可能會引入新的功能或補丁來改變集群的行為和程式的運行方式。 這些更改可以通過增加或減少 syscall 功能來實現。

Solana 支援一種稱為運行時功能的機制,類似於熱補丁,用於在集群發生行為更改時調整程序的行為。 這些更改通過 syscall 功能門實現,預設情況下這些功能是禁用的,除非開啟了相應的 syscall 功能。

syscall 的計費通過為每個特定的系統調用功能分配固定的 CU 值來實現,這些值被定義在計算預算中。

在 Solana 的 CU 計費研究中,syscall 功能的執行並不完全依賴於 SVM 標準模型,而是採用預估計算模型,某些情況下可能涉及第三方庫的支援。 syscall 的這一特性使其在功能引入時更靈活,但同時也帶來了一定的複雜性。

7. syscall 功能預估模型的 CU 計算錯誤

Solana 的 Gas 成本限制通過 CU 資源限制來實現的,CU 計算的錯誤會導致合約在 Solana 鏈上執行消耗的資源(cpu、time)嚴重超出了限制,進一步導致潛在的 Solana 區塊鏈上遠端 DOS 攻擊。

在對 Solana 的 syscall 功能研究中,結合 ETH 網路和 Solana 歷史漏洞同時,CertiK 團隊針對 syscall 功能的 Gas 成本做了深入研究,研究發現 big_mod_exp syscall 功能在 CU 成本計算存在漏洞,bits 和 bytes 的混用,導致 CU 計算出問題,嚴重偏移了資源限制消耗,最終會導致 Solana 區塊鏈遠端 DOS 攻擊。

7.1:syscall 功能 big_mod_exp 模組的作用與其 CU 計算模型

7.1.1:引入的 pull 提案

Solana 的 syscall 中引入了大整數模冪運算功能,詳細見提案:add big_mod_exp syscall #28503[14]。 該功能類似於 EIP-198[15]中的實現。 big_mod_exp 的 CU 計算模型如下:

[1u8; len] for the base and exponent Use a prime or an RSA modulus for each bit-sizes.

 7.1.2:CU 計算與消耗時間預期測試結果

由於該系統調用適用於使用 RSA 演算法的程式,輸入數據最大支援 2048 位(支援的位數包括 32、64、128、256、512、1024、2048)。 CU 的計算公式如下:

计算时间(ns)= bits^2CU = 计算时间 / 33 ns

以 2048 位為例,預期輸入的執行時間為 4,194,304 納秒,其 CU 計算結果為:

CU = 4,194,304 / 33 ≈ 127,100

注:Solana 這裡最高引入了 4096 位的計算,對於 4096 位的輸入,計算時間為 52 毫秒,對應的 CU 為:

CU = 52,000,000 / 33 ≈ 1,575,757

由此可以看出,big_mod_exp 的 CU 消耗基於輸入的位長度進行計算,位數越高,CU 的消耗也越大。

7.2:syscall 功能 big_mod_exp 代碼深入分析

Solana 引入 #28503 時,big_mod_exp 功能 4096 bits 的 CU 預算是 1,575,757,而測試發現 CU 預算只有 8043,big_mod_exp 功能的 CU 計算出現了問題。 big_mod_exp 的關鍵代碼位於 SyscallBigModExp  中,三個主要輸入參數為 base、exponent 和 modulus,分別對應的長度為 params.base_lenparams.exponent_len 和 params.modulus_len。 需要注意的是,這些參數的單位是 bytes:

let params = &translate_slice::<BigModExpParams>(
            memory_mapping,
            params,
            1,
            invoke_context.get_check_aligned(),
            invoke_context.get_check_size(),
        )?
        .get(0)
        .ok_or(SyscallError::InvalidLength)?;
        if params.base_len > 512 || params.exponent_len > 512 ||
params.modulus_len > 512 {
            return Err(Box::new(SyscallError::InvalidLength));
        }
let input_len: u64 = std::cmp::max(params.base_len, params.exponent_len);
let input_len: u64 = std::cmp::max(input_len, params.modulus_len);

 big_mod_exp 功能關鍵 CU 計算:

let budget = invoke_context.get_compute_budget();
        consume_compute_meter(
            invoke_context,
            budget.syscall_base_cost.saturating_add(
                input_len
                    .saturating_mul(input_len)
                    .checked_div(budget.big_modular_exponentiation_cost)
                    .unwrap_or(u64::MAX),
            ),
        )?;

  從代碼中可以推匯出 CU 的計算公式為:

CU = bytes^2 / big_modular_exponentiation_cost (33)

對比 Solana 在引入 #28503[16]時的原始計算公式:

CU = bits^2 / 33

這揭示了漏洞的核心問題:輸入的單位為 bytes,但應轉換為 bits。 正確的 CU 計算方式應為:

CU = (bytes * 8) ^2 / big_modular_exponentiation_cost

以 4096 位輸入為例,正確的 CU 計算為:

CU = 4096 ^ 2 / 33 ~= 508,400

在當前存在漏洞的情況下,CU 預算為 200,000 時,調用 4096 位的 big_mod_exp 功能合約的執行時間為:

200_000 / 8043 ≈ 2424 * 37 ms ≈ 890 ms

而在最大 CU 上限為 1,400,000 的情況下,單個合約的執行時間約為 6.23 秒。 Solana 的平均出塊時間約為 400ms~600ms,具體出塊時間可在 Solana Explorer[17]中查看,目前約為 400ms。 考慮到 Solana 通過 Proof of History(PoH)實現的共識機制及其並行事務處理能力,單個最高 6.23s 的合約能造成 Solana 集群的遠端 DOS 攻擊嗎? 或者並行執行多個合約呢? 後續將會對 Solana 共識機制和事務處理展開討論,結合有問題的 big_mod_exp 合約實現對本地 Solana 集群的遠端 DOS 攻擊。

8.   深入分析 Solana 區塊鏈與智能合約交互

 8.1:POH 共識

Slot 和 Epoch 是 Solana 網路中的時間單位。 Slot 是最小的單位,432,000 個 Slot 組成一個 Epoch。 PoH(Proof of History)通過可驗證延遲函數(VDF)生成一系列不可預測的時間序列,用於給每個 Slot 中生成的區塊打上時間戳。

Solana 每筆交易都有時間作為交易生命週期 [18]:每筆交易都包含一個 “最近的區塊哈希”,用作 PoH 時鐘時間戳,並在該區塊哈希不再足夠 “最新” 時過期,在 150 個 Slot 週期內。

Solana 驗證器會查找他們希望在區塊中處理的每筆交易的區塊鏈相應時隙編號。 如果驗證器找不到區塊哈希的插槽號,或者查找到的插槽號比正在處理的區塊的插槽號高 150 個以上,交易就會被拒絕:

Solana 區塊鏈的工作原理涉及以下 6 個基本步驟:

 (1)領導者選舉與時隙分配

Solana 網路通過基於 PoS 的選舉機制輪流選出一個領導者(Leader)。 每個領導者被分配四個連續的時間槽(Slot)來處理數據,總共持續約 1.6 秒(4 個區塊,每個區塊 400 毫秒)。 領導者的選舉是隨機的,概率與驗證者的權益權重成正比,選舉週期約為 2-3 天(即 432,000 個時隙)。

(2)領導者消息排序與數據流最大化 

領導者使用 Proof of History(PoH)生成一條可驗證的時間序列,確保了全網的讀一致性和時間的可驗證性。 領導者對使用者提交的交易進行排序,確保驗證者能夠以一致的順序處理交易,從而最大化數據流的效率並保持網路高效運行。

 (3)領導者執行交易 

領導者將使用者提交的交易存儲在 RAM 中,並在當前狀態上執行這些交易,處理諸如代幣轉移、智慧合約執行等操作。 交易數據在 RAM 中的臨時存儲提高了處理速度和輸送量。

 (4)領導者發佈交易結果 

領導者在執行完交易並更新狀態后,對交易集的哈希和最終狀態進行簽名,然後將這些資訊發佈給驗證者(也稱為複製節點),確保交易結果的不可篡改性和可驗證性。

 (5)驗證者驗證交易與最終狀態 

驗證者接收到領導者發佈的交易和狀態簽名后,在其本地狀態副本上重複執行相同的交易,以確認最終狀態的正確性。 驗證者通過應用分叉選擇規則(Fork Choice Rule)評估領導者提出的區塊,並確保其與網路的整體一致性。

 (6)共識確認 

驗證者通過 Gossip 網路發佈其狀態簽名,作為共識演算法的一部分,確保網路達成一致。 這些簽名作為投票,用於確認區塊的有效性,最終形成全網共識。

 8.2:並行事務處理

Solana 驗證器中通過多線程來執行多佇列的,單個線程執行單筆佇列,其中參數 NUM_THREADS 和 MIN_TOTAL_THREADS 用於控制線程數量:

 pub const NUM_THREADS: u32 = 6;
    const NUM_VOTE_PROCESSING_THREADS: u32 = 2;
    const MIN_THREADS_BANKING: u32 = 1;
    const MIN_TOTAL_THREADS: u32 = NUM_VOTE_PROCESSING_THREADS +
MIN_THREADS_BANKING;
    ...
    pub fn num_threads() -> u32 {
            cmp::max(
                env::var("Solana_BANKING_THREADS")
                    .map(|x| x.parse().unwrap_or(NUM_THREADS))
                    .unwrap_or(NUM_THREADS),
                MIN_TOTAL_THREADS,
            )
         }

 8.3:深入事務處理代碼

Solana 事務處理是在一個多線程中進行的,通過調用 process_loop[19]函數來迴圈處理交易事務。

Builder::new()            .name(format!("solBanknStgTx{id:02}"))            .spawn(move || {                Self::process_loop(                    &mut packet_receiver,                    &decision_maker,                    &mut forwarder,                    &consumer,                    id,                    unprocessed_transaction_storage,                )            })            .unwrap()

在 process_loop 裡面 [20]通過 unprocessed_transaction_storage: UnprocessedTransactionStorage 迴圈獲取待處理的交易 hash,函數 process_buffered_packets 將帶出來的交易 hash 傳遞到下層函數,最終到 Solana 的 SVM 虛擬機進行處理,receive_and_buffer_packets 則迴圈接收待處理交易 hash。

 loop {
            if !unprocessed_transaction_storage.is_empty()
                || last_metrics_update.elapsed() >= SLOT_BOUNDARY_CHECK_PERIOD
            {
                let (_, process_buffered_packets_time) = measure!(
                    Self::process_buffered_packets(
                        decision_maker,
                        forwarder,
                        consumer,
                        &mut unprocessed_transaction_storage,
                        &banking_stage_stats,
                        &mut slot_metrics_tracker,
                        &mut tracer_packet_stats,
                    ),
                    "process_buffered_packets",
                );
                slot_metrics_tracker
                    .increment_process_buffered_packets_us(process_buffered_packets_time.as_us());
                last_metrics_update = Instant::now();
            }

            tracer_packet_stats.report(1000);

            match packet_receiver.receive_and_buffer_packets(
                &mut unprocessed_transaction_storage,
                &mut banking_stage_stats,
                &mut tracer_packet_stats,
                &mut slot_metrics_tracker,
            ) {
                Ok(()) | Err(RecvTimeoutError::Timeout) => (),
                Err(RecvTimeoutError::Disconnected) => break,
            }
            banking_stage_stats.report(1000);
        }

Solana 處理交易的關鍵在 execute_and_commit_transactions_locked[21],通過在 SVM 虛擬機執行指令,並檢查錯誤後,將完整的交易 commit 提交,返回被正確記錄的交易到上層 process,調用了 record_transactions 記錄交易,committer.commit_ transactions 提交完整的事務:

let (load_and_execute_transactions_output, load_execute_us) = measure_us!(bank
            .load_and_execute_transactions(
                batch,
                MAX_PROCESSING_AGE,
                &mut execute_and_commit_timings.execute_timings,
                TransactionProcessingConfig {
                    account_overrides: None,
                    log_messages_bytes_limit: self.log_messages_bytes_limit,
                    limit_to_load_programs: true,
                    recording_config: ExecutionRecordingConfig::new_single_setting(
                        transaction_status_sender_enabled
                    ),
                }
            ));
        execute_and_commit_timings.load_execute_us = load_execute_us;
        .......
        .......
        let (record_transactions_summary, record_us) = measure_us!(self
            .transaction_recorder
            .record_transactions(bank.slot(), executed_transactions));
        execute_and_commit_timings.record_us = record_us;
        .......
        ......
        let (commit_time_us, commit_transaction_statuses) = if executed_transactions_count != 0 {
            self.committer.commit_transactions(
                batch,
                &mut loaded_transactions,
                execution_results,
                last_blockhash,
                lamports_per_signature,
                starting_transaction_index,
                bank,
                &mut pre_balance_info,
                &mut execute_and_commit_timings,
                signature_count,
                executed_transactions_count,
                executed_non_vote_transactions_count,
                executed_with_successful_result_count,
            )
        } else {
            (
                0,

                vec![CommitTransactionDetails::NotCommitted; execution_results.len()],
            )
        };

正常合約完整執行過程為(SVM 虛擬機執行-> 記錄交易-> 提交交易):

8.4:存在 CU 漏洞的合約會跨 Slot 導致事務處理混亂

分析完 Solana 的 POH 共識機制和並行事務處理方式,這裡引入帶有 CU 計算漏洞的 big_mod_exp 合約,結合漏洞探究其對 Solana 鏈上執行合約造成的影響和原理。 當執行一次帶有 4096 位 big_mod_exp 功能的合約,在預設 200,000CU 上限中,執行時間 890ms 的可以跨越 1-2 個 Slot,當跨越 Slot 將會導致 record[22]失敗,是因為交易後傳入的原始 Slot 比當前 slot 慢 1-2 個 Slot,返回 PohRecorderError:: MaxHeightReached 錯誤:

if bank_slot != working_bank.bank.slot() {
                return Err(PohRecorderError::MaxHeightReached);
            }

上文我們分析了 Solana 的交易是存在生命週期 [23]的,而其生命週期的控制是通過參數 MAX_PROCESSING_AGE,MAX_PROCESSING_AGE 的值為 150 也就是對比是否小於 150 Slot,帶有 big_mod_exp 漏洞的合約因為跨了 Slot 導致事務失敗,失敗後 Solana 會比較當前 Slot 和 MAX_PROCESSING_AGE,只要小於 150 Slot,Solana 會設置 retryable_transaction_indexes 為 0,並且返 回到 process_packets 填充 retryable_packets,重新 retry 當前交易:

fn process_packets<F>(
        &mut self,
        bank: &Bank,
        banking_stage_stats: &BankingStageStats,
        slot_metrics_tracker: &mut LeaderSlotMetricsTracker,
        mut processing_function: F,
    ) -> bool
    where
        F: FnMut(
            &Vec<Arc<ImmutableDeserializedPacket>>,
            &mut ConsumeScannerPayload,
        ) -> Option<Vec<usize>>,
    {
        let mut retryable_packets = self.take_priority_queue();
        let original_capacity = retryable_packets.capacity();
        let mut new_retryable_packets = MinMaxHeap::with_capacity(original_capacity);
        let all_packets_to_process = retryable_packets.drain_desc().collect_vec();
        ..............
                ..........
                while let Some((packets_to_process, payload)) = scanner.iterate() {
            let packets_to_process = packets_to_process
                .iter()
                .map(|p| (*p).clone())
                .collect_vec();
            let retryable_packets = if let Some(retryable_transaction_indexes) =
                processing_function(&packets_to_process, payload)
            {
                Self::collect_retained_packets(
                    payload.message_hash_to_transaction,
                    &packets_to_process,
                    &retryable_transaction_indexes,
                )
            } else {
                packets_to_process
            };

            new_retryable_packets.extend(retryable_packets);
        }
        .......
      }

所以最後帶有 CU 計算漏洞的 bid_mod_exp 合約將會重複執行 150 次后,最後導致交易過期失敗,而 MAX_PROCESSING_AGE 的檢查在 chek_transaction_age[24]調用 4096 位的 big_mod_exp 功能合約:200_000 / 8043 ~= 24, time : 24 * 37 ms ~= 890 ms,而 retry 150 次後,整個交易將會在 133,500ms,約 130s 後結束,長期佔用資源將會導致佇列堵塞。 將會是以下過程:

9.   在搭建的 Solana 私有集群上複現遠端 DOS 攻擊

本地測試準備了 10-20 個獨立帳戶,每個獨立帳戶調用一次帶有 4096 位的 big_mod_exp 功能的合約,都是預設 200,000CU 上限,同時運行多個獨立帳戶的正常合約進行對比:

  私有集群測試

私有集群總計 4 個節點,包括 Leader Node、User RPC Node、Attacker RPC Node、Node4 , User 界面通過 10 個獨立帳戶調用正常合約執行,正常合約調用從 rpc 用戶端請求到返回結束平均花費 740ms 左右,Attacker 是模擬攻擊者的介面,通過 20 個獨立帳戶,每個帳戶調用 20 次帶有 4096 位的 big_mod_ exp 功能合約,以下截圖是模擬攻擊攻擊發送了 20 次惡意合約的場景,可以觀察到 Leader Node 的 cpu 已經是滿負荷運行, 並且 User 介面已沒有正常合約調用的 rpc 返回:

測試結果顯示私有集群和本地集群在一次性處理多個(20)獨立帳戶帶有 4096 位的 big_mod_exp 功能的合約時,總計調用 20 次,出現了長時間高達(133s)的堵塞,反覆攻擊將會造成嚴重的 DOS 攻擊!

10.   遠端 DOS 攻擊成本

一次攻擊成本預估:帶有 4096 位的 big_mod_exp 功能的合約在本地集群運行時,由於交易過期被 drop 掉,Gas 費後續都為 0,Solana 的 CU 計算成本類似 ETH 的 Gas 計算,通過限制合約消耗的網路資源來防止遠端 DOS 攻擊,Solana 引入的預估費用計算模型存在的缺陷帶來了 CU 計算漏洞,導致了嚴重的資源消耗偏移, 最終導致潛在的遠端 DOS 攻擊。 幸運的是 Solana 在引入 syscall 新功能前會經過大量測試和驗證並且包括了和外部安全研究員漏洞賞金合作的模式,確保了上線後的穩定性,這次提前發現了 Solana 的 syscall 的 big_mod_exp 功能存在嚴重的漏洞,維護了 Solana 網路的穩定性和安全性!

11.   漏洞確認與修復

Solana 確認了 CertiK 團隊提交的大整數模冪運算(big_mod_exp)中 CU 計算錯誤將會導致潛在的遠端 DOS 攻擊,並分類為 DOS 攻擊漏洞,Solana 開發者修復方案 [25]重新計算了 big_mod_exp 的 CU 成本,通過重新基準測試模冪運算的性能,調整計算單位(CU)為 N^2/2 + 190, 雖然修復不是把 bytes 置換成 bits,但是重新計算了大整數模冪運算的 CU 成本最終修復了安全漏洞,根據安全公告未來可能還會優化演算法以提高性能。

  參考:

[1]  倫敦升級: https://ethereum.org/zh/history/#london

[2]  當前查詢: https://cn.etherscan.com/Gastracker

[3] EIP-1559: https://eips.ethereum.org/EIPS/eip-1559

[4]  當前查詢: https://etherscan.io/

[5]  查詢: https://beta-analysis.solscan.io/public/dashboard/06d689e1-dcd7-4175-a16a-efc074ad5ce2

[6]  查詢: https://solscan.io/analytics

[7]  橘子哨(Tangerine Whistle)分叉: https://ethereum.org/zh/history/#tangerine-whistle

[8]  偽龍(Spurious Dragon)分叉: https://ethereum.org/zh/history/#spurious-dragon

[9] EIP-150: https://eips.ethereum.org/EIPS/eip-150

[10] EIP-161: https://eips.ethereum.org/EIPS/eip-161

[11] 8 項關鍵創新: https://medium.com/Solana-labs/proof-of-history-a-clock-for-blockchain-cf47a61a9274

[12] 2022 年 1 月 – 嚴重的網络擁塞: https://twitter.com/SolanaStatus/status/1484947431796219906?s=20&t=x6Itu5Yn_8-HtapAyLBrfA

[13] 2022 年 4 月和 5 月 – 短暫的中斷: https://solana.com/news/04-30-22-Solana-mainnet-beta-outage-report-mitigation

[14]  添加 big_mod_exp 系統調用 #28503:https://github.com/Solana-labs/Solana/pull/28503

[15] EIP-198:https://github.com/ethereum/EIPs/blob/master/EIPS/eip-198.md

[16] #28503:https://github.com/Solana-labs/Solana/pull/28503

[17] Solana Explorer: https://explorer.solana.com/

[18]  交易生命週期: https://docs.solana.com/developing/transaction_confirmation

[19] process_loop:https://github.com/anza-xyz/agave/blob/5263c9d61f3af060ac995956120bef11c1bbf182/core/src/banking_stage.rs#L644C8-L656C22

[20] process_loop  裡面: https://github.com/anza-xyz/agave/blob/5263c9d61f3af060ac995956120bef11c1bbf182/core/src/banking_stage.rs#L742

[21] execute_and_commit_transactions_lockedhttps://github.com/anza-xyz/agave/blob/5263c9d61f3af060ac995956120bef11c1bbf182/core/src/banking_stage/consumer.rs#L569

[22] record: https://github.com/anza-xyz/agave/blob/master/poh/src/poh_recorder.rs#L942

[23]  生命週期: https://solana.com/docs/advanced/confirmation

[24] chek_transaction_age: https://github.com/anza-xyz/agave/blob/5263c9d61f3af060ac995956120bef11c1bbf182/runtime/src/bank.rs#L3513

[25]  修復:https://github.com/anza-xyz/agave/commit/eb37b21d4d5ed29d1bf40c9ca7c64509681a2a09

免責聲明:作為區塊鏈資訊平臺,本站所發佈文章僅代表作者及嘉賓個人觀點,與 Web3Caff 立場無關。 文章內的資訊僅供參考,均不構成任何投資建議及要約,並請您遵守所在國家或地區的相關法律法規。