「SCRY技術分享」Scale編解碼演算法介紹

「SCRY技術分享」Scale編解碼演算法介紹

在上次

介紹substrate儲存鍵構成的文章

中,主要分享了在鏈儲存KV資料庫中值的鍵是怎樣來生成的,使用生成的鍵可以透過呼叫rpc提供的方法直接將對應的值查詢出來。

使用這種方式來實現儲存值查詢是一種偏底層的方式,但也是更通用的方式。

區塊鏈儲存系統主要透過KV方式來實現區塊資料的儲存。

透過這種底層方法將key對應的值查詢出來,得到編碼後的結果。

在substrate中使用編解碼演算法為簡單串聯聚合小端編解碼(SCALE Codec-Simple Concatenated Aggregate Little-Endian)。這篇文章的目的是對這個編解碼演算法進行分享,這也是cashbox在實現交易簽名構造交易資料過程中比較重要的環節。

序列化與反序列化

我們知道,資料在儲存、網路傳輸過程中最終都是透過二進位制位元組陣列來實現的。為了能夠實現將程式中例項資料轉換為二進位制資料,並且在接收方能夠將二進位制資料轉換為正確的物件例項,實現資料的跨平臺互動。針對資料的跨平臺互動,存在不同的解決方案,比如:

在Java中,物件透過實現java。io。Serializable來實現將物件例項資料轉換為二進位制資料以及將二進位制資料轉換為Java例項;

在Grpc具有資料的跨平臺,跨語言互動的能力,其底層是利用Protocol Buffers來實現資料的序列化與反序列化;

透過這些序列化工具使得應用資料在跨平臺之間的互動變得更加容易。

SCALE演算法

由於Substrate的節點可以透過Wasm方式和Native這兩種方式執行,為了使這些節點能夠正常進行資料編解碼,parity團隊重新設計了一種編碼方案,而不是直接使用已經廣泛使用的rust編碼庫。它旨在用於高效能,無複製的編碼和對資源受限的執行上下文中資料的編解碼,如substrate中的runtime。該演算法編碼過程沒有型別的描述資訊,而且解碼上下文也不知道已編碼資料包含的資料型別;

為了能夠是正確的解碼編碼後的資料,還原為對應的結構體例項,所需對存在的資料型別編碼方式進行約定,下面簡要的對該演算法編碼規則進行介紹。

整形

整形是在程式碼中使用的比較多的一種變數,根據所佔的位數以及是否存在符號位,分別對應著不同的型別。為了在資料傳輸過程中更高效的利用記憶體空間、網路頻寬,整形的編碼分為固定長度的編碼和變長編碼,規則如下:

固定長度

直接使用使用固定長度的小端格式,比如

有符號8位整數 69:0x45;

無符號16位整數 42:0x2a00;

無符號32位整數 1677215:0xffff;

變長編碼

使用固定長度編碼的方式,每個編碼後的結果都為固定長度,當數字較小位數定義較大的變數,會造成利用率不高的問題。使用緊湊整數編碼方案能夠完全解決大整數的編碼而且針對大多數值來說,比固定長度版本更有效率;這種編碼模式使用最低兩位bit來表示所使用的模式,不同的模式分別用於編碼值大小範圍

0b00: 單位元組模式,實際表示的數用剩下的6bit來表示,因此單位元組允許表示值的範圍為:0~63。比如 無符號數0,表示為0x00;

0b01: 雙位元組模式,使用剩下的6bit和另外一個位元組來表示真實的數,因此雙位元組允許表示數的範圍為:64~2**14-1。比如 無符號數1, 0x04;

0b10: 四位元組模式,使用剩下的6bit和另外三個位元組來表示真實的數,因此雙位元組允許表示數的範圍為:2**14~2**30-1。比如 無符號數42, 0xa8;

0b11: 大整數模式,使用剩下的6bit表示後面跟隨的位元組數,最少為4位元組。最高位必須為非零值;合法的值為2**30)-(2**536-1 。比如 無符號數69,編碼為0x1501;

Boolean

使用單個位元組的最低bit來表示相應的值,比如false編碼為0x00, true編碼

0x01

Options

針對存在的兩種情況值,其中需要注意的是若Option處理的型別是boolean,還是使用一個位元組來編碼,規則如下:

None 編碼為 0x00;

當結果為Some(true)時,編碼為 0x01;

當值為Some(false)時,編碼的值為 0x02;

若Option處理的值為另外的型別則直接將對應值的編碼跟隨在後面即可;

Results

Results一般是用於表示操作的成功與否,編碼規則如下:

當操作成功時,使用0x00表示,後面再跟具體型別的返回值;

當操作失敗時使用0x01來表示,後面跟再跟具體的錯誤資訊;

Vectors

針對List,Sets這類集合型別,在編碼的時候將集合中包含的成員數量使用緊湊編碼的結果作為字首,後面再依次新增針對集合型別中值的編碼。以一個包含16位無符號整數的集合為:[4, 8, 15, 16, 23, 42],包含元素的個數為6,對應的緊湊編碼結果為0x18,所以最後的編碼結果為:

0x18040008000f00100017002a00

Strings

由於String是一個包含元素為UTF-8字元的集合型別,因此編碼規則同上述的Vectots相同;

Tuples

是由一系列固定大小型別值,每個型別可能不用但位置是固定的型別,針對這種型別的編碼直接將個型別的值連結起來即可;

比如(3,false)編碼結果為0x0c00

資料結構

根據實際需要,使用上述的各種型別組合而成的,每個struct的定義都是有名字和對應的型別組成,在編碼的時候名字會被忽略掉,只會編碼對應的值,根據其對應的型別使用相應的編碼方案;將各個型別編碼的值連結起來就是最後結構體的編碼結果。其中需要注意的是若存在集合型別。最終解碼後的元素順序取決於集合的性質,存在編碼前後元素間順序不一致的可能性。

需要注意的是在解碼位元組陣列的時候,可能會存在陣列編碼順序和解碼結果順序不一致的問題;

列舉型別

列舉型別是一個包含有幾種可能取值的型別,每個列舉變數只能表示該列舉型別中的一種型別變數。針對列舉型別使用所在型別定義序號以及對應的值組成,其中序號使用第一個位元組來表示,型別具體的值緊隨其後。根據這個編碼規則,當前支援的列舉型別包含的型別數不能超過256。

enum IntOrBool{ Int(u8), Bool(bool),}

比如:Int(42)0x002a

Bool(true):0x0101

賬戶資訊解碼

在介紹了各種型別資料的編碼規則後,我們來看一下在實際場景中的應用。這裡使用cashbox查詢eee鏈(基於substrate開發)上的賬戶資訊為例,透過原始碼查詢得到主要部分程式碼:

trait Store for Module as System { /// The full account information for a particular account ID。 pub Account get(fn account): map hasher(blake2_128_concat) T::AccountId => AccountInfo; }

這裡我們繼續使用Alice這個賬戶為例,動態庫根據cashbox flutter模組傳遞進來的引數得到該賬戶對應的key為:

0x26aa394eea5630e07c48ae0c9558cef7b99d880ec681799c0cf30e8886371da9de1e86a9a8c739864cf3cc5ec2bea59fd43593c715fdd31c61141abd04a99fd6822c8558854ccde39a5684e7a56da27d

透過向鏈節點發送JsonRpc請求

{“id”:1,“jsonrpc”:“2。0”,“method”:“state_getStorage”,“params”:[“0x26aa394eea5630e07c48ae0c9558cef7b99d880ec681799c0cf30e8886371da9de1e86a9a8c739864cf3cc5ec2bea59fd43593c715fdd31c61141abd04a99fd6822c8558854ccde39a5684e7a56da27d”]}

鏈服

務端將返回該Account ID對應的賬戶資訊,結果如下所示:

0x25000000000000000000629d72210bb20a13000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000

要想正確的解碼出這部分的資料,我們需要知道構成這些資料對應的型別,透過原始碼能夠知道AccountInfo定義如下:

/// Index of a transaction in the chain。pub type Index=u32;/// Balance of an account。pub type Balance=u128;/// Type used to encode the number of references an account has。pub type RefCount=u32;typeAccountData=pallet_balances::AccountData;/// Information of an account。#[derive(Clone, Eq, PartialEq, Default, RuntimeDebug, Encode, Decode)]pubstructAccountInfo{ /// The number of transactions this account has sent。 pub nonce:Index, /// The number of other modules that currently depend on this account‘s existence。 The account /// cannot be reaped until this is zero。 pub refcount:RefCount, /// The additional data that belongs to this account。 Used to store the balance(s) in a lot of /// chains。 pub data:AccountData,}/// All balance information for an account。#[derive(Encode, Decode, Clone, PartialEq, Eq, Default, RuntimeDebug)]pub struct AccountData{ /// Non-reserved part of the balance。 There may still be restrictions on this, but it is the /// total pool what may in principle be transferred, reserved and used for tipping。 /// /// This is the only balance that matters in terms of most operations on tokens。 It /// alone is used to determine the balance when in the contract execution environment。 pub free:Balance, /// Balance which is reserved and may not be used at all。 /// /// This can still get slashed, but gets slashed last of all。 /// /// This balance is a ’reserve‘ balance that other subsystems use in order to set aside tokens /// that are still ’owned‘ by the account holder, but which are suspendable。 pub reserved:Balance, /// The amount that `free` may not drop below when withdrawing for *anything except transaction /// fee payment*。 pub misc_frozen:Balance, /// The amount that `free` may not drop below when withdrawing specifically for transaction /// fee payment。 pub fee_frozen:Balance,}

根據結構體編碼規則,該變數在編碼時是將各變數值分別編碼後依次連線起來的,對查詢出來的結果值按照組成型別所佔位數進行分割,得到如下結果:

{“nonce”:“0x25000000”,“refcount”:“0x00000000”,“data”:{“free”:“0x0000629d72210bb20a13000000000000”,“reserved”:“0x00000000000000000000000000000000”,“miscFrozen”:“0x00000000000000000000000000000000”,“feeFrozen”:“0x00000000000000000000000000000000”}}

由於為小端編碼,轉換為十進位制值為得到最終的結果為

{“nonce”:37,“refcount”:0,“data”:{“free”:89922260000000000000000,“reserved”:0,“miscFrozen”:0,“feeFrozen”:0}}

總結

在實際程式碼編寫過程中不需要按照變數所佔位數來進行編解碼操作,ParitySCALE Codec已經在多種語言中實現了對應的庫,能夠直接對各種變數型別進行編解碼操作。透過對資料進行拆分,能夠清楚的知道查詢結果是怎麼構成的。針對在應用實現過程中,若在某些場景下需要構造對應的型別變數很複雜,則可以直接透過這種特性提取我們需要的資料。該編解碼演算法在substrate中廣泛的應用,瞭解該演算法可以對區塊資料組織結構能夠有更深入的瞭解。如同這個編解碼演算法名字所描述一樣,編碼規則簡單、高效。當我們在做跨平臺應用時,該編解碼演算法是一個可考慮的方案。