Skip to main content
已提交狀態必須滿足兩項相互拉扯的需求。執行需要快速拿到某個鍵的當前值,每秒數百萬次。驗證需要一份證明,說明在某個特定版本上該值確實就是網路所聲稱的那個。用同一種結構同時服務兩邊,只會兩件事都做不好。 所以儲存把問題拆到彼此獨立的儲存區裡,每一個都按自己的存取模式來塑形。

三個儲存區

已提交區塊
帳本儲存區交易 · 輸出 · 事件 · 寫入集 · 累加器 · 區塊中繼資料
重放與稽核
狀態 KV 儲存區當前值與歷史值,分成十六個分片,高頻狀態另有一層
查詢與執行
狀態 Merkle 儲存區一棵帶版本的稀疏 Merkle 樹,外加追蹤被取代節點的索引
驗證 — 證明
只要取值的讀取,不必為樹走訪付出代價;而一份證明,也不必從一個專為點查詢最佳化的儲存區裡重建出來。
帳本儲存區保存鏈的歷史:交易、交易的輸出與輔助資料、事件、寫入集、區塊中繼資料,以及累加器。這是你用來重放的東西。 狀態 KV 儲存區保存狀態值,按鍵和版本編址。這是執行與查詢讀取的東西。它分成十六個分片,因此一個區塊的寫入會散到十六個相互獨立的 RocksDB 實例上,不必爭搶同一個。高頻存取的狀態另外保存在自己的一層裡,這樣活躍市場的工作集,就不必從鏈上有史以來存過的所有東西裡去撈。 狀態 Merkle 儲存區保存認證結構:用來證明某個值的樹節點,以及追蹤哪些節點已被取代的索引。 拆開的好處是:只要取值的讀取,不必為樹走訪付出代價;而一份證明,也不必從一個專為點查詢最佳化的儲存區裡重建出來。

認證

兩種 Merkle 結構做的是不同的工作。
帶版本的稀疏 Merkle 樹
累加器
你的鍵及其值
兄弟節點雜湊
兄弟節點雜湊
狀態根,由共識提交
你的交易,或一個事件
交易累加器 · 事件累加器各自承諾到目前為止按順序包含的全部內容
帳本根,由共識提交
樹證明某個值曾是什麼;累加器證明發生了什麼、以何種順序發生。兩者合起來,「這筆交易在鏈上的這個位置」不必持有整條鏈就能證明。
一棵帶版本的稀疏 Merkle 樹為狀態提供認證。每個鍵的位置由自己的雜湊決定,而樹的每個版本都共享那些沒有變化的節點:所以在一個區塊裡寫入一個鍵,增加的是一條路徑,不是一棵樹。某個鍵在某個版本上的證明,就是從那個鍵的葉子,一路走到網路提交的那個根。 累加器為順序提供認證。一個累加交易,一個累加事件,各自產生一個根,承諾了到目前為止按順序包含的全部內容。有了它,「這筆交易在鏈上的這個位置」不必持有整條鏈就能證明。 兩者之間的分工:狀態樹證明某個值曾是什麼,累加器證明發生了什麼以及以何種順序發生。

推測狀態

一個區塊的結果,在提交之前就已經存在。與其先寫進持久化的樹、區塊沒提交再撤回來,未提交的狀態改放在一層記憶體內稀疏 Merkle 覆蓋層裡,疊在最近一個已提交版本之上。 執行透過這層覆蓋讀取,看到的是一致的視圖。區塊提交了,覆蓋層就物化下來;沒有提交,覆蓋層直接丟掉,持久化儲存從頭到尾沒被碰過。正是這一點,讓推測執行不會在儲存裡留下殘渣。

快取

樹節點在兩個層次上快取:上層是一個感知版本的快取,保持近期版本可定址;底下再一層最近最少使用(LRU)快取。一條交易鏈的存取模式——一小組每個區塊都會碰到的熱點鍵,加上一條極少碰到的長尾——正是這類快取針對的形態。

修剪

永久保留每一個版本是一種選擇,不是一項要求。三個相互獨立的修剪器分別針對這三個儲存區運行,各有自己的保留策略:一個針對帳本,一個針對狀態值,一個針對 Merkle 節點。 Merkle 修剪器和狀態值修剪器靠過期索引驅動,而這些索引是和資料同時寫下去的。某個版本一旦取代了某個節點或某個值,被取代的條目就會在該版本上記為過期。於是修剪變成對一個索引做範圍掃描,不必到處搜尋垃圾:寫入方早就講明了什麼會變成可回收,以及在什麼時候。
保留策略是維運上的決定,而且有實際後果。修剪得積極的節點,當前狀態提供得很有效率,卻答不了歷史查詢,也沒辦法替一個從更早處起步的節點做狀態同步。歸檔節點保留一切,並為此付出代價。參見運行節點。

備份與還原

這些儲存區可以獨立於運行中的節點備份和還原。正因如此,才有辦法從快照架起一個節點,不必從創世開始重放;也才有辦法拿已提交的根去驗證還原後的節點狀態,而不是只能選擇相信那份備份。

後續閱讀

狀態同步

節點怎麼不重放全部歷史就追上這條鏈。

狀態模型

儲存的是什麼,以及哪一種表示是權威。

索引器

從已提交紀錄中重建歷史。

運行節點

節點角色,以及如何諮詢節點營運事宜。