簡單序列化
簡單序列化 (SSZ) 是信標鏈上使用的序列化方法。它在共識層的各個地方取代了執行層上使用的 RLP 序列化,但對等節點發現協定除外。若要了解有關 RLP 序列化的更多資訊,請參閱 遞迴長度前綴 (RLP)。SSZ 的設計具有確定性,並且能有效率地進行默克爾化 (Merkleize)。SSZ 可以被認為有兩個組成部分:一個序列化方案,以及一個設計用來與序列化資料結構有效運作的默克爾化方案。
SSZ 如何運作?
序列化
SSZ 是一種非自我描述的序列化方案,它依賴於必須事先知道的結構描述 (schema)。SSZ 序列化的目標是將任意複雜度的物件表示為位元組字串。對於「基本型別」來說,這是一個非常簡單的過程。元素只是簡單地轉換為十六進位位元組。基本型別包括:
- 無號整數
- 布林值
對於複雜的「複合」型別,序列化會更加複雜,因為複合型別包含多個可能具有不同型別或不同大小(或兩者兼具)的元素。當這些物件都具有固定長度時(即無論其實際值為何,元素的大小始終保持不變),序列化只是將複合型別中的每個元素依序轉換為小端序 (little-endian) 位元組字串。這些位元組字串會連接在一起。序列化物件具有固定長度元素的位元組列表表示形式,其順序與它們在反序列化物件中出現的順序相同。
對於具有可變長度的型別,實際資料會在序列化物件中該元素的位置被替換為「偏移量 (offset)」值。實際資料會被新增到序列化物件末端的堆積 (heap) 中。偏移量值是堆積中實際資料起點的索引,作為指向相關位元組的指標。
下面的範例說明了偏移量如何應用於同時具有固定長度和可變長度元素的容器:
struct Dummy {
number1: u64,
number2: u64,
vector: Vec<u8>,
number3: u64
}
dummy = Dummy{
number1: 37,
number2: 55,
vector: vec![1,2,3,4],
number3: 22,
}
serialized = ssz.serialize(dummy)
serialized 將具有以下結構(此處僅填充至 4 位元,實際上會填充至 32 位元,並保留 int 表示形式以求清晰):
[37, 0, 0, 0, 55, 0, 0, 0, 16, 0, 0, 0, 22, 0, 0, 0, 1, 2, 3, 4]
------------ ----------- ----------- ----------- ----------
| | | | |
數字 1 數字 2 向量的 數字 3 向量的
偏移量 值
為了清晰起見,分行顯示:
[
37, 0, 0, 0, # `number1` 的小端序編碼。
55, 0, 0, 0, # `number2` 的小端序編碼。
16, 0, 0, 0, # 指示 `vector` 的值從何處開始的「偏移量」(小端序 16)。
22, 0, 0, 0, # `number3` 的小端序編碼。
1, 2, 3, 4, # `vector` 中的實際值。
]
這仍然是一種簡化——上述示意圖中的整數和零實際上會儲存為位元組列表,如下所示:
[
10100101000000000000000000000000 # `number1` 的小端序編碼
10110111000000000000000000000000 # `number2` 的小端序編碼。
10010000000000000000000000000000 # 指示 `vector` 的值從何處開始的「偏移量」(小端序 16)。
10010110000000000000000000000000 # `number3` 的小端序編碼。
10000001100000101000001110000100 # `bytes` 欄位的實際值。
]
因此,可變長度型別的實際值會儲存在序列化物件末端的堆積中,而它們的偏移量則儲存在有序欄位列表中正確的位置。
還有一些需要特殊處理的特殊情況,例如 BitList 型別,它要求在序列化期間新增長度上限,並在反序列化期間將其移除。完整詳細資訊請參閱 SSZ 規範 (opens in a new tab)。
要反序列化這個物件,需要 結構描述。結構描述定義了序列化資料的精確佈局,以便每個特定元素都能從位元組資料塊反序列化為有意義的物件,並確保這些元素具有正確的型別、值、大小和位置。正是結構描述告訴反序列化器哪些值是實際值,哪些是偏移量。當物件被序列化時,所有欄位名稱都會消失,但在反序列化時會根據結構描述重新實例化。
默克爾化
然後可以對這個 SSZ 序列化物件進行默克爾化——也就是轉換為相同資料的默克爾樹表示形式。首先,確定序列化物件中 32 位元組區塊的數量。這些是樹的「葉節點」。葉節點的總數必須是 2 的次方,以便將葉節點一起進行雜湊運算最終產生單一的雜湊樹根 (hash-tree-root)。如果自然情況下並非如此,則會新增包含 32 位元組零的額外葉節點。圖解如下:
雜湊樹根
/ \
/ \
/ \
/ \
葉節點的雜湊 葉節點的雜湊
1 和 2 3 和 4
/ \ / \
/ \ / \
/ \ / \
葉節點1 葉節點2 葉節點3 葉節點4
也有一些情況,樹的葉節點不會像上述範例那樣自然均勻地分佈。例如,葉節點 4 可能是一個包含多個元素的容器,需要為默克爾樹新增額外的「深度」,從而建立一棵不均勻的樹。
我們可以為這些樹元素提供廣義索引 (generalized indices),而不是將它們稱為葉節點 X、節點 X 等,從根節點 = 1 開始,並沿著每一層從左到右計數。這就是上面解釋的廣義索引。序列化列表中的每個元素都有一個等於 2**depth + idx 的廣義索引,其中 idx 是其在序列化物件中從零開始的索引位置,而 depth 是默克爾樹中的層數,可以透過元素(葉節點)數量的以 2 為底的對數來確定。
廣義索引
廣義索引是一個整數,代表二元默克爾樹中的一個節點,其中每個節點都有一個廣義索引 2 ** depth + index in row。
1 --深度 = 0 2**0 + 0 = 1
2 3 --深度 = 1 2**1 + 0 = 2, 2**1+1 = 3
4 5 6 7 --深度 = 2 2**2 + 0 = 4, 2**2 + 1 = 5...
這種表示形式為默克爾樹中的每筆資料產生一個節點索引。
多重證明
提供代表特定元素的廣義索引列表,使我們能夠對照雜湊樹根來驗證它。這個根節點是我們所接受的現實版本。我們獲得的任何資料都可以透過將其插入默克爾樹中的正確位置(由其廣義索引決定)並觀察根節點是否保持不變,來對照該現實進行驗證。規範中這裡 (opens in a new tab)有一些函式,展示了如何計算驗證特定廣義索引集內容所需的最小節點集。
例如,要驗證下方樹中索引 9 的資料,我們需要索引 8、9、5、3、1 處資料的雜湊。 (8,9) 的雜湊應該等於雜湊 (4),它與 5 進行雜湊運算產生 2,再與 3 進行雜湊運算產生樹根 1。如果為 9 提供了不正確的資料,根節點就會改變——我們會偵測到這一點,並導致該分支驗證失敗。
* = 產生證明所需的資料
1*
2 3*
4 5* 6 7
8* 9* 10 11 12 13 14 15