Solana 虛擬機(SVM)正在成為多個 Layer-2(L2)解決方案的執行層。 然而,SVM 原始設計中的一個關鍵限制是其全局狀態根的模糊性。這對彙總系統構成了重大挑戰,因為全局狀態根對於確保完整性、啟用欺詐證明以及支持跨層操作至關重要。
在彙總系統中,提議者定期向 Layer-1(L1)提交 L2 狀態根(Merkle 根),為 L2 鏈建立檢查點。這些檢查點支持對任何賬戶狀態的包含證明,從一個檢查點到另一個檢查點實現無狀態執行。欺詐證明依賴於這一機制,參與者可以提供包含證明來驗證爭議中的有效輸入。此外,Merkle 樹通過允許用戶為提現交易生成包含證明,增強了規範化橋接的安全性,從而確保了 L2 和 L1 之間的無信任交互。
為了解決這些挑戰,SOON Network 在 SVM 執行層中引入了 Merkle 樹,使得客戶端可以提供包含證明。SOON 通過使用獨特的條目將狀態根直接嵌入基於 SVM 的區塊鏈,結合歷史證明(Proof-of-History)。這一創新使得 SOON 技術棧能夠支持新的 SVM 彙總系統,提升了安全性、可擴展性和實用性。
Solana 一直以高吞吐量為核心目標進行設計,在早期開發中,必須進行設計上的權衡以實現其創新的性能。在這些權衡中,其中一個最具影響力的決定是 Solana 在何時、如何將其狀態進行 Merklization。
最終,這一決定給客戶端在證明全局狀態、交易包含性和簡單支付驗證(SPV)方面帶來了重大挑戰。由於缺乏一個持續哈希的狀態根來代表 Merklized 的 SVM 狀態,這為像輕客戶端和彙總系統這樣的項目帶來了相當大的困難。
接下來,我們將探討其他區塊鏈如何進行 Merklization,並進一步分析 Solana 協議架構帶來的挑戰。
在比特幣網絡中,交易通過 Merkle 樹存儲在區塊中,Merkle 樹的根存儲在區塊頭中。比特幣協議會將交易的輸入和輸出(以及其他一些元數據)進行哈希運算,生成交易 ID(TxID)。為了在比特幣上證明狀態,用戶只需計算 Merkle 證明,將 TxID 與區塊的 Merkle 根進行驗證。
這一驗證過程也驗證了狀態,因為 TxID 是唯一對應某一組輸入和輸出的,且這兩者都反映了地址狀態的變化。需要注意的是,比特幣交易也可以包含 Taproot 腳本,這些腳本會生成交易輸出,在驗證時可以通過重新執行腳本,使用交易的輸入和腳本的見證數據,並與輸出進行驗證。
類似於比特幣,以太坊使用一種自定義的數據結構(源自 Merkle 樹),稱為 Merkle Patricia Trie(MPT)來存儲交易。該數據結構旨在支持快速更新並優化大規模數據集。自然地,這是因為以太坊需要管理的輸入和輸出遠比比特幣多。
以太坊虛擬機(EVM)充當全局狀態機。EVM 本質上是一個巨大的分佈式計算環境,支持可執行的智能合約,每個合約在全局內存中保留自己的地址空間。因此,想要驗證以太坊上的狀態的客戶端,不僅需要考慮交易的結果(如日誌、返回代碼等),還需要考慮交易所引發的全局狀態變化。
幸運的是,EVM 巧妙地利用了三種重要的 Trie 結構,它們的根存儲在每個區塊頭中:
給定一筆交易,客戶端可以通過評估交易 Trie 的根(類似於比特幣)來證明其包含在區塊中,通過評估收據 Trie 來驗證交易結果,並通過評估狀態 Trie 來確認全局狀態的變化。
Solana 高吞吐量的一個原因是,它沒有像以太坊那樣的多層樹結構。雖然 Solana 的領導節點在創建區塊時確實計算 Merkle 樹,但它們的結構與 EVM 中的 Merkle 樹不同。不幸的是,這正是 SVM 基礎彙總系統的問題所在。
Solana 將交易 Merklize 為所謂的條目(entries),每個插槽中可以有多個條目,因此每個區塊中可以有多個交易根。此外,Solana 僅在每個時代(大約 2.5 天)計算一次賬戶狀態的 Merkle 根,而該根並不會存儲在賬本中。
事實上,Solana 區塊頭根本不包含任何 Merkle 根。相反,它們包含前一個和當前區塊的區塊哈希,這些哈希是通過 Solana 的歷史證明(Proof of History,PoH)算法計算得出的。PoH 要求驗證者通過遞歸哈希區塊條目(這些條目可以為空或包含一批交易)來不斷註冊“刻度”(ticks)。PoH 算法的最終刻度(哈希)就是該區塊的區塊哈希。
歷史證明的問題在於,它使得從區塊哈希中證明狀態變得非常困難。Solana 被設計為流式傳輸 PoH 哈希,以維持其時間流逝的概念。交易根僅在 PoH 刻度中包含了一個帶有交易的條目時才可用,而不是整個區塊,而且沒有狀態根存儲在任何條目中。
然而,存在另一種哈希,客戶端可以使用:銀行哈希(bank hash)。有時被稱為插槽哈希(slot hash),銀行哈希存儲在 SlotHashes 系統變量賬戶中,客戶端可以查詢。銀行哈希是從一組輸入創建的,具體包括:
如上所示,銀行哈希包含多個哈希輸入,這增加了客戶端在嘗試證明交易或狀態信息時的複雜性。此外,只有執行了“時代賬戶哈希”(每個時代一次計算所有賬戶的哈希)的銀行哈希,才會在其中包含該特定根。另外,SlotHashes 系統變量賬戶只保留最新的 512 個銀行哈希。
SOON 網絡是一個基於以太坊的 SVM L2。在將 Merklization 集成到 SOON 時,我們優先考慮使用經過驗證的、成熟的解決方案,以保證穩定性,而不是重新發明輪子。在決定使用哪種數據結構或哈希算法時,我們考慮了它們與 Optimism Stack 的 L1 合約的兼容性、與 Solana 架構的無縫集成能力,以及它們是否能夠在 Solana 的賬戶模型下實現高性能。
我們發現,隨著賬戶數量的增加,基於 LSM-Tree 的 Merkle Patricia Trie(MPT)存儲模型會導致更多的磁盤讀寫放大,從而導致性能下降。因此,我們決定通過基於 rETH 團隊在 Rust 上做出的出色工作,並增加對 Solana 賬戶模型的支持,來集成 Erigon MPT。
如前所述,SOON 的狀態 Trie 是一個支持 Solana 賬戶的 MPT。因此,我們定義了一種兼容 SVM 的賬戶類型,用於作為每個葉子節點的數據:
struct TrieSolanaAccount {
lamports: u64,
data: Vec<u8>,
executable: bool,
rent_epoch: u64,
owner: Pubkey,
}
為了使 MPT 模塊能夠實時訂閱 SVM 賬戶的最新狀態,我們引入了一個賬戶通知器(account notifier)。在銀行階段(Banking Stage),賬戶通知器會通知 MPT 模塊賬戶狀態的變化,MPT 模塊則會在 Trie 結構中增量更新這些變化。
需要注意的是,MPT 模塊僅在每 50 個插槽(slot)時更新其子樹,而不會在每個插槽結束時計算狀態根。採取這種方式有兩個原因:首先,計算每個插槽的狀態根會顯著影響性能;其次,狀態根只有在提議者向 L1 提交 outputRoot 時才需要。因此,它只需要週期性計算,基於提議者的提交頻率。
outputRoot = keccak256(version, state_root, withdraw_root, l2_block_hash)
SOON 的 MPT 模塊同時維護兩種類型的 Trie 結構:一個用於全局狀態的狀態 Trie,另一個用於提現交易的提現 Trie。提議者會定期生成由狀態根和提現根組成的 outputRoot,並將其提交給 L1。
目前,SOON 每 450 個插槽計算一次狀態根和提現根,並將其附加到 L2 區塊鏈中。這樣可以確保網絡中其他節點的 MPT 數據的一致性。然而,Solana 的區塊結構不包括區塊頭,這意味著沒有地方存儲狀態根。讓我們先來看一下 Solana 區塊鏈的基本結構,然後探討 SOON 如何引入 UniqueEntry 來存儲狀態根。
Solana 區塊鏈由插槽(slots)組成,插槽由 PoH 模塊生成。一個插槽包含多個條目(entries),每個條目包括刻度(ticks)和交易。在網絡層和存儲層,插槽使用 shreds 作為最小單元進行存儲和傳輸。shreds 可以轉換為條目,也可以從條目中轉換回來。
#[derive(Serialize, Deserialize, Debug, Default, PartialEq, Eq, Clone)]
pub struct Entry {
/// 前一個條目 ID 以來的哈希數。
pub num_hashes: u64,
/// 在前一個條目 ID 之後的 SHA-256 哈希。
pub hash: Hash,
/// 在生成條目 ID 之前觀察到的未排序交易列表。它們可能在之前的條目 ID 之前被觀察到,但為了確保賬本的確定性解釋,它們被推回到這個列表中。
pub transactions: Vec<VersionedTransaction>,
}
我們遵循了 PoH 生成的區塊鏈結構,並保留了 shred 結構,這樣我們可以重用 Solana 現有的存儲層、網絡層和 RPC 框架。為了在 L2 區塊鏈上存儲額外的數據,我們引入了 UniqueEntry。這個特性允許我們自定義條目負載,併為數據定義獨立的驗證規則。
pub const UNIQUE_ENTRY_NUM_HASHES_FLAG: u64 = 0x8000_0000_0000_0000;
/// Unique entry 是一種特殊的條目類型。當我們需要將一些數據存儲在區塊存儲中,但不想驗證它時,它非常有用。
///
/// `num_hashes` 的佈局是:
/// |...1 bit...|...63 bit...|
/// \ \_____ _____/
/// \ \
/// flag custom field
pub trait UniqueEntry: Sized {
fn encode_to_entries(&self) -> Vec<Entry>;
fn decode_from_entries(
entries: impl IntoIterator<Item = Entry>,
) -> Result<Self, UniqueEntryError>;
}
pub fn unique_entry(custom_field: u64, hash: Hash) -> Entry {
Entry {
num_hashes: num_hashes(custom_field),
hash,
transactions: vec![],
}
}
pub fn num_hashes(custom_field: u64) -> u64 {
assert!(custom_field < (1 << 63));
UNIQUE_ENTRY_NUM_HASHES_FLAG | custom_field
}
pub fn is_unique(entry: &Entry) -> bool {
entry.num_hashes & UNIQUE_ENTRY_NUM_HASHES_FLAG != 0
}
在 UniqueEntry 中,num_hashes
被用作位佈局,其中第一個位標誌用於區分條目(Entry)和 Unique Entry,後面 63 位用於定義負載類型。hash
字段作為負載,包含所需的自定義數據。
讓我們看一個示例,使用三個 UniqueEntry 來存儲插槽哈希、狀態根和提現根。
/// MPT 根的 UniqueEntry。
#[derive(Default, Debug, Clone, PartialEq, Eq)]
pub struct MptRoot {
pub slot: Slot,
pub state_root: B256,
pub withdrawal_root: B256,
}
impl UniqueEntry for MptRoot {
fn encode_to_entries(&self) -> Vec<Entry> {
let slot_entry = unique_entry(0, slot_to_hash(self.slot));
let state_root_entry = unique_entry(1, self.state_root.0.into());
let withdrawal_root_entry = unique_entry(2, self.withdrawal_root.0.into());
vec![slot_entry, state_root_entry, withdrawal_root_entry]
}
fn decode_from_entries(
entries: impl IntoIterator<Item = Entry>,
) -> Result<Self, UniqueEntryError> {
let mut entries = entries.into_iter();
let entry = entries.next().ok_or(UniqueEntryError::NoMoreEntries)?;
let slot = hash_to_slot(entry.hash);
let entry = entries.next().ok_or(UniqueEntryError::NoMoreEntries)?;
let state_root = B256::from(entry.hash.to_bytes());
let entry = entries.next().ok_or(UniqueEntryError::NoMoreEntries)?;
let withdrawal_root = B256::from(entry.hash.to_bytes());
Ok(MptRoot {
slot,
state_root,
withdrawal_root,
})
}
}
由於 UniqueEntry 重新定義了 num_hashes
的語義,它不能在 PoH 驗證階段處理。因此,在驗證過程的開始,我們首先篩選出 unique entries,並根據其負載類型將它們引導到自定義的驗證流程中。其餘的常規條目則繼續通過原始的 PoH 驗證過程。
本地橋接是 Layer 2 解決方案中的關鍵基礎設施,負責 L1 與 L2 之間的信息傳輸。從 L1 到 L2 的消息稱為存款交易(deposit transactions),而從 L2 到 L1 的消息稱為提現交易(withdrawal transactions)。
存款交易通常由派生管道處理,用戶只需要在 L1 上發送一次交易即可。提現交易則更為複雜,用戶需要發送三次交易才能完成整個流程:
outputRoot
被提議者提交到 L1,用戶需要向 L1 提交該提現交易的包含證明(inclusion proof)。驗證成功後,質疑期開始。提現交易的包含證明確保了提現確實發生在 L2 上,從而大大增強了規範橋接的安全性。
在 OP Stack 中,用戶的提現交易的哈希存儲在與 OptimismPortal 合約對應的狀態變量中。然後,eth_getProof
RPC 接口被用來提供用戶某個特定提現交易的包含證明。
與 EVM 不同,SVM 合約數據存儲在賬戶的 data 字段中,所有類型的賬戶都處於同一層級。
SOON 引入了本地橋接程序:Bridge1111111111111111111111111111111111111
。每當用戶發起提現交易時,橋接程序會為每個提現交易生成一個全局唯一的索引,並使用該索引作為種子,創建一個新的程序派生賬戶(Program Derived Account,PDA)來存儲對應的提現交易。
#[derive(Clone, Copy, Debug, PartialEq)]
pub struct WithdrawalTransaction {
/// 提現計數器
pub nonce: U256,
/// 申請提現的用戶
pub sender: Pubkey,
/// 用戶在 L1 上的地址
pub target: Address,
/// 提現金額,單位為 lamports
pub value: U256,
/// L1 上的 gas 限制
pub gas_limit: U256,
/// L1 上的提現 calldata
pub data: L1WithdrawalCalldata,
}
我們定義了 WithdrawalTransaction
結構體,用於在 PDA 的數據字段中存儲提現交易。
這一設計與 OP Stack 類似。一旦包含提現交易的 outputRoot
被提交到 L1,用戶可以向 L1 提交提現交易的包含證明,啟動質疑期。
當提議者提交 outputRoot
到 L1 後,意味著 L2 的狀態已經結算。如果質疑者發現提議者提交了錯誤的狀態,他們可以發起質疑,以保護橋接中的資金。
構建故障證明時最關鍵的方面之一是確保區塊鏈能夠以無狀態的方式從狀態 S1 轉換到狀態 S2。這確保了 L1 上的仲裁合約能夠無狀態地重放程序指令,從而執行仲裁。
然而,在此過程的開始階段,質疑者必須證明狀態 S1 中的所有初始輸入是有效的。這些初始輸入包括參與賬戶的狀態(例如,lamports、數據、所有者等)。與 EVM 不同,SVM 自然將賬戶狀態與計算分離。然而,Anza 的 SVM API 允許通過 TransactionProcessingCallback
特徵將賬戶傳遞到 SVM,如下所示:
pub trait TransactionProcessingCallback {
fn account_matches_owners(&self, account: &Pubkey, owners: &[Pubkey]) -> Option<usize>;
fn get_account_shared_data(&self, pubkey: &Pubkey) -> Option<AccountSharedData>;
fn add_builtin_account(&self, _name: &str, _program_id: &Pubkey) {}
}
因此,我們只需要使用狀態 S1 和輸入賬戶的包含證明來驗證質疑程序輸入的有效性。
SOON 在 SVM 生態系統的發展中標誌著一個關鍵的里程碑。通過整合 Merklization,SOON 解決了 Solana 缺乏全局狀態根的問題,從而實現了故障證明的包含證明、安全提現和無狀態執行等關鍵功能。
此外,SOON 在 SVM 內部的 Merklization 設計可以支持基於 SVM 的鏈上的輕客戶端,儘管 Solana 本身目前不支持輕客戶端。甚至可以設想,這些設計中的一些可能有助於將輕客戶端引入主鏈。
通過使用 Merkle Patricia Tries(MPT)進行狀態管理,SOON 與以太坊的基礎設施對齊,增強了互操作性,並推動了基於 SVM 的 L2 解決方案。這一創新通過提高安全性、可擴展性和兼容性,加強了 SVM 生態系統,同時促進了去中心化應用程序的發展。
Solana 虛擬機(SVM)正在成為多個 Layer-2(L2)解決方案的執行層。 然而,SVM 原始設計中的一個關鍵限制是其全局狀態根的模糊性。這對彙總系統構成了重大挑戰,因為全局狀態根對於確保完整性、啟用欺詐證明以及支持跨層操作至關重要。
在彙總系統中,提議者定期向 Layer-1(L1)提交 L2 狀態根(Merkle 根),為 L2 鏈建立檢查點。這些檢查點支持對任何賬戶狀態的包含證明,從一個檢查點到另一個檢查點實現無狀態執行。欺詐證明依賴於這一機制,參與者可以提供包含證明來驗證爭議中的有效輸入。此外,Merkle 樹通過允許用戶為提現交易生成包含證明,增強了規範化橋接的安全性,從而確保了 L2 和 L1 之間的無信任交互。
為了解決這些挑戰,SOON Network 在 SVM 執行層中引入了 Merkle 樹,使得客戶端可以提供包含證明。SOON 通過使用獨特的條目將狀態根直接嵌入基於 SVM 的區塊鏈,結合歷史證明(Proof-of-History)。這一創新使得 SOON 技術棧能夠支持新的 SVM 彙總系統,提升了安全性、可擴展性和實用性。
Solana 一直以高吞吐量為核心目標進行設計,在早期開發中,必須進行設計上的權衡以實現其創新的性能。在這些權衡中,其中一個最具影響力的決定是 Solana 在何時、如何將其狀態進行 Merklization。
最終,這一決定給客戶端在證明全局狀態、交易包含性和簡單支付驗證(SPV)方面帶來了重大挑戰。由於缺乏一個持續哈希的狀態根來代表 Merklized 的 SVM 狀態,這為像輕客戶端和彙總系統這樣的項目帶來了相當大的困難。
接下來,我們將探討其他區塊鏈如何進行 Merklization,並進一步分析 Solana 協議架構帶來的挑戰。
在比特幣網絡中,交易通過 Merkle 樹存儲在區塊中,Merkle 樹的根存儲在區塊頭中。比特幣協議會將交易的輸入和輸出(以及其他一些元數據)進行哈希運算,生成交易 ID(TxID)。為了在比特幣上證明狀態,用戶只需計算 Merkle 證明,將 TxID 與區塊的 Merkle 根進行驗證。
這一驗證過程也驗證了狀態,因為 TxID 是唯一對應某一組輸入和輸出的,且這兩者都反映了地址狀態的變化。需要注意的是,比特幣交易也可以包含 Taproot 腳本,這些腳本會生成交易輸出,在驗證時可以通過重新執行腳本,使用交易的輸入和腳本的見證數據,並與輸出進行驗證。
類似於比特幣,以太坊使用一種自定義的數據結構(源自 Merkle 樹),稱為 Merkle Patricia Trie(MPT)來存儲交易。該數據結構旨在支持快速更新並優化大規模數據集。自然地,這是因為以太坊需要管理的輸入和輸出遠比比特幣多。
以太坊虛擬機(EVM)充當全局狀態機。EVM 本質上是一個巨大的分佈式計算環境,支持可執行的智能合約,每個合約在全局內存中保留自己的地址空間。因此,想要驗證以太坊上的狀態的客戶端,不僅需要考慮交易的結果(如日誌、返回代碼等),還需要考慮交易所引發的全局狀態變化。
幸運的是,EVM 巧妙地利用了三種重要的 Trie 結構,它們的根存儲在每個區塊頭中:
給定一筆交易,客戶端可以通過評估交易 Trie 的根(類似於比特幣)來證明其包含在區塊中,通過評估收據 Trie 來驗證交易結果,並通過評估狀態 Trie 來確認全局狀態的變化。
Solana 高吞吐量的一個原因是,它沒有像以太坊那樣的多層樹結構。雖然 Solana 的領導節點在創建區塊時確實計算 Merkle 樹,但它們的結構與 EVM 中的 Merkle 樹不同。不幸的是,這正是 SVM 基礎彙總系統的問題所在。
Solana 將交易 Merklize 為所謂的條目(entries),每個插槽中可以有多個條目,因此每個區塊中可以有多個交易根。此外,Solana 僅在每個時代(大約 2.5 天)計算一次賬戶狀態的 Merkle 根,而該根並不會存儲在賬本中。
事實上,Solana 區塊頭根本不包含任何 Merkle 根。相反,它們包含前一個和當前區塊的區塊哈希,這些哈希是通過 Solana 的歷史證明(Proof of History,PoH)算法計算得出的。PoH 要求驗證者通過遞歸哈希區塊條目(這些條目可以為空或包含一批交易)來不斷註冊“刻度”(ticks)。PoH 算法的最終刻度(哈希)就是該區塊的區塊哈希。
歷史證明的問題在於,它使得從區塊哈希中證明狀態變得非常困難。Solana 被設計為流式傳輸 PoH 哈希,以維持其時間流逝的概念。交易根僅在 PoH 刻度中包含了一個帶有交易的條目時才可用,而不是整個區塊,而且沒有狀態根存儲在任何條目中。
然而,存在另一種哈希,客戶端可以使用:銀行哈希(bank hash)。有時被稱為插槽哈希(slot hash),銀行哈希存儲在 SlotHashes 系統變量賬戶中,客戶端可以查詢。銀行哈希是從一組輸入創建的,具體包括:
如上所示,銀行哈希包含多個哈希輸入,這增加了客戶端在嘗試證明交易或狀態信息時的複雜性。此外,只有執行了“時代賬戶哈希”(每個時代一次計算所有賬戶的哈希)的銀行哈希,才會在其中包含該特定根。另外,SlotHashes 系統變量賬戶只保留最新的 512 個銀行哈希。
SOON 網絡是一個基於以太坊的 SVM L2。在將 Merklization 集成到 SOON 時,我們優先考慮使用經過驗證的、成熟的解決方案,以保證穩定性,而不是重新發明輪子。在決定使用哪種數據結構或哈希算法時,我們考慮了它們與 Optimism Stack 的 L1 合約的兼容性、與 Solana 架構的無縫集成能力,以及它們是否能夠在 Solana 的賬戶模型下實現高性能。
我們發現,隨著賬戶數量的增加,基於 LSM-Tree 的 Merkle Patricia Trie(MPT)存儲模型會導致更多的磁盤讀寫放大,從而導致性能下降。因此,我們決定通過基於 rETH 團隊在 Rust 上做出的出色工作,並增加對 Solana 賬戶模型的支持,來集成 Erigon MPT。
如前所述,SOON 的狀態 Trie 是一個支持 Solana 賬戶的 MPT。因此,我們定義了一種兼容 SVM 的賬戶類型,用於作為每個葉子節點的數據:
struct TrieSolanaAccount {
lamports: u64,
data: Vec<u8>,
executable: bool,
rent_epoch: u64,
owner: Pubkey,
}
為了使 MPT 模塊能夠實時訂閱 SVM 賬戶的最新狀態,我們引入了一個賬戶通知器(account notifier)。在銀行階段(Banking Stage),賬戶通知器會通知 MPT 模塊賬戶狀態的變化,MPT 模塊則會在 Trie 結構中增量更新這些變化。
需要注意的是,MPT 模塊僅在每 50 個插槽(slot)時更新其子樹,而不會在每個插槽結束時計算狀態根。採取這種方式有兩個原因:首先,計算每個插槽的狀態根會顯著影響性能;其次,狀態根只有在提議者向 L1 提交 outputRoot 時才需要。因此,它只需要週期性計算,基於提議者的提交頻率。
outputRoot = keccak256(version, state_root, withdraw_root, l2_block_hash)
SOON 的 MPT 模塊同時維護兩種類型的 Trie 結構:一個用於全局狀態的狀態 Trie,另一個用於提現交易的提現 Trie。提議者會定期生成由狀態根和提現根組成的 outputRoot,並將其提交給 L1。
目前,SOON 每 450 個插槽計算一次狀態根和提現根,並將其附加到 L2 區塊鏈中。這樣可以確保網絡中其他節點的 MPT 數據的一致性。然而,Solana 的區塊結構不包括區塊頭,這意味著沒有地方存儲狀態根。讓我們先來看一下 Solana 區塊鏈的基本結構,然後探討 SOON 如何引入 UniqueEntry 來存儲狀態根。
Solana 區塊鏈由插槽(slots)組成,插槽由 PoH 模塊生成。一個插槽包含多個條目(entries),每個條目包括刻度(ticks)和交易。在網絡層和存儲層,插槽使用 shreds 作為最小單元進行存儲和傳輸。shreds 可以轉換為條目,也可以從條目中轉換回來。
#[derive(Serialize, Deserialize, Debug, Default, PartialEq, Eq, Clone)]
pub struct Entry {
/// 前一個條目 ID 以來的哈希數。
pub num_hashes: u64,
/// 在前一個條目 ID 之後的 SHA-256 哈希。
pub hash: Hash,
/// 在生成條目 ID 之前觀察到的未排序交易列表。它們可能在之前的條目 ID 之前被觀察到,但為了確保賬本的確定性解釋,它們被推回到這個列表中。
pub transactions: Vec<VersionedTransaction>,
}
我們遵循了 PoH 生成的區塊鏈結構,並保留了 shred 結構,這樣我們可以重用 Solana 現有的存儲層、網絡層和 RPC 框架。為了在 L2 區塊鏈上存儲額外的數據,我們引入了 UniqueEntry。這個特性允許我們自定義條目負載,併為數據定義獨立的驗證規則。
pub const UNIQUE_ENTRY_NUM_HASHES_FLAG: u64 = 0x8000_0000_0000_0000;
/// Unique entry 是一種特殊的條目類型。當我們需要將一些數據存儲在區塊存儲中,但不想驗證它時,它非常有用。
///
/// `num_hashes` 的佈局是:
/// |...1 bit...|...63 bit...|
/// \ \_____ _____/
/// \ \
/// flag custom field
pub trait UniqueEntry: Sized {
fn encode_to_entries(&self) -> Vec<Entry>;
fn decode_from_entries(
entries: impl IntoIterator<Item = Entry>,
) -> Result<Self, UniqueEntryError>;
}
pub fn unique_entry(custom_field: u64, hash: Hash) -> Entry {
Entry {
num_hashes: num_hashes(custom_field),
hash,
transactions: vec![],
}
}
pub fn num_hashes(custom_field: u64) -> u64 {
assert!(custom_field < (1 << 63));
UNIQUE_ENTRY_NUM_HASHES_FLAG | custom_field
}
pub fn is_unique(entry: &Entry) -> bool {
entry.num_hashes & UNIQUE_ENTRY_NUM_HASHES_FLAG != 0
}
在 UniqueEntry 中,num_hashes
被用作位佈局,其中第一個位標誌用於區分條目(Entry)和 Unique Entry,後面 63 位用於定義負載類型。hash
字段作為負載,包含所需的自定義數據。
讓我們看一個示例,使用三個 UniqueEntry 來存儲插槽哈希、狀態根和提現根。
/// MPT 根的 UniqueEntry。
#[derive(Default, Debug, Clone, PartialEq, Eq)]
pub struct MptRoot {
pub slot: Slot,
pub state_root: B256,
pub withdrawal_root: B256,
}
impl UniqueEntry for MptRoot {
fn encode_to_entries(&self) -> Vec<Entry> {
let slot_entry = unique_entry(0, slot_to_hash(self.slot));
let state_root_entry = unique_entry(1, self.state_root.0.into());
let withdrawal_root_entry = unique_entry(2, self.withdrawal_root.0.into());
vec![slot_entry, state_root_entry, withdrawal_root_entry]
}
fn decode_from_entries(
entries: impl IntoIterator<Item = Entry>,
) -> Result<Self, UniqueEntryError> {
let mut entries = entries.into_iter();
let entry = entries.next().ok_or(UniqueEntryError::NoMoreEntries)?;
let slot = hash_to_slot(entry.hash);
let entry = entries.next().ok_or(UniqueEntryError::NoMoreEntries)?;
let state_root = B256::from(entry.hash.to_bytes());
let entry = entries.next().ok_or(UniqueEntryError::NoMoreEntries)?;
let withdrawal_root = B256::from(entry.hash.to_bytes());
Ok(MptRoot {
slot,
state_root,
withdrawal_root,
})
}
}
由於 UniqueEntry 重新定義了 num_hashes
的語義,它不能在 PoH 驗證階段處理。因此,在驗證過程的開始,我們首先篩選出 unique entries,並根據其負載類型將它們引導到自定義的驗證流程中。其餘的常規條目則繼續通過原始的 PoH 驗證過程。
本地橋接是 Layer 2 解決方案中的關鍵基礎設施,負責 L1 與 L2 之間的信息傳輸。從 L1 到 L2 的消息稱為存款交易(deposit transactions),而從 L2 到 L1 的消息稱為提現交易(withdrawal transactions)。
存款交易通常由派生管道處理,用戶只需要在 L1 上發送一次交易即可。提現交易則更為複雜,用戶需要發送三次交易才能完成整個流程:
outputRoot
被提議者提交到 L1,用戶需要向 L1 提交該提現交易的包含證明(inclusion proof)。驗證成功後,質疑期開始。提現交易的包含證明確保了提現確實發生在 L2 上,從而大大增強了規範橋接的安全性。
在 OP Stack 中,用戶的提現交易的哈希存儲在與 OptimismPortal 合約對應的狀態變量中。然後,eth_getProof
RPC 接口被用來提供用戶某個特定提現交易的包含證明。
與 EVM 不同,SVM 合約數據存儲在賬戶的 data 字段中,所有類型的賬戶都處於同一層級。
SOON 引入了本地橋接程序:Bridge1111111111111111111111111111111111111
。每當用戶發起提現交易時,橋接程序會為每個提現交易生成一個全局唯一的索引,並使用該索引作為種子,創建一個新的程序派生賬戶(Program Derived Account,PDA)來存儲對應的提現交易。
#[derive(Clone, Copy, Debug, PartialEq)]
pub struct WithdrawalTransaction {
/// 提現計數器
pub nonce: U256,
/// 申請提現的用戶
pub sender: Pubkey,
/// 用戶在 L1 上的地址
pub target: Address,
/// 提現金額,單位為 lamports
pub value: U256,
/// L1 上的 gas 限制
pub gas_limit: U256,
/// L1 上的提現 calldata
pub data: L1WithdrawalCalldata,
}
我們定義了 WithdrawalTransaction
結構體,用於在 PDA 的數據字段中存儲提現交易。
這一設計與 OP Stack 類似。一旦包含提現交易的 outputRoot
被提交到 L1,用戶可以向 L1 提交提現交易的包含證明,啟動質疑期。
當提議者提交 outputRoot
到 L1 後,意味著 L2 的狀態已經結算。如果質疑者發現提議者提交了錯誤的狀態,他們可以發起質疑,以保護橋接中的資金。
構建故障證明時最關鍵的方面之一是確保區塊鏈能夠以無狀態的方式從狀態 S1 轉換到狀態 S2。這確保了 L1 上的仲裁合約能夠無狀態地重放程序指令,從而執行仲裁。
然而,在此過程的開始階段,質疑者必須證明狀態 S1 中的所有初始輸入是有效的。這些初始輸入包括參與賬戶的狀態(例如,lamports、數據、所有者等)。與 EVM 不同,SVM 自然將賬戶狀態與計算分離。然而,Anza 的 SVM API 允許通過 TransactionProcessingCallback
特徵將賬戶傳遞到 SVM,如下所示:
pub trait TransactionProcessingCallback {
fn account_matches_owners(&self, account: &Pubkey, owners: &[Pubkey]) -> Option<usize>;
fn get_account_shared_data(&self, pubkey: &Pubkey) -> Option<AccountSharedData>;
fn add_builtin_account(&self, _name: &str, _program_id: &Pubkey) {}
}
因此,我們只需要使用狀態 S1 和輸入賬戶的包含證明來驗證質疑程序輸入的有效性。
SOON 在 SVM 生態系統的發展中標誌著一個關鍵的里程碑。通過整合 Merklization,SOON 解決了 Solana 缺乏全局狀態根的問題,從而實現了故障證明的包含證明、安全提現和無狀態執行等關鍵功能。
此外,SOON 在 SVM 內部的 Merklization 設計可以支持基於 SVM 的鏈上的輕客戶端,儘管 Solana 本身目前不支持輕客戶端。甚至可以設想,這些設計中的一些可能有助於將輕客戶端引入主鏈。
通過使用 Merkle Patricia Tries(MPT)進行狀態管理,SOON 與以太坊的基礎設施對齊,增強了互操作性,並推動了基於 SVM 的 L2 解決方案。這一創新通過提高安全性、可擴展性和兼容性,加強了 SVM 生態系統,同時促進了去中心化應用程序的發展。