以太坊解讀——Recursive Length Prefix協(xié)議圖解(下)

解碼過程

解碼是編碼的逆過程递雀,也就是將順序(flat sequence)字節(jié)轉(zhuǎn)化為嵌套樹形結(jié)構(gòu)(nested sequence)的過程形纺。在重建樹形結(jié)構(gòu)的過程中,首先庐扫,根據(jù)長度前綴(Length Prefix)瞳脓,也就是每個編碼段的第一個字節(jié)塑娇,來推斷數(shù)據(jù)的類型。然后篡殷,再讀取該編碼段的實(shí)際內(nèi)容钝吮,進(jìn)行解碼。另外板辽,由于RLP編碼中的列表是一種嵌套結(jié)構(gòu)奇瘦,如果類型是列表,則需要進(jìn)行遞歸解碼劲弦。

長度前綴的處理規(guī)則

根據(jù)長度前綴(Length Prefix)的數(shù)值范圍耳标,其對應(yīng)的類型及長度計(jì)算方法可以匯總為如下的表格。

長度前綴的范圍 對應(yīng)的類型 對應(yīng)的長度
[0x00-0x7f] 數(shù)組(字符串)類型邑跪,且為單字的數(shù)組 1字節(jié)
[0x80-0xb7] 數(shù)組(字符串)類型次坡,且為短數(shù)組(字符串) 數(shù)組長度=“長度前綴”-0x80
[0xb8-0xbf] 數(shù)組(字符串)類型,且為長數(shù)組(字符串) 獲取數(shù)組的長度画畅,需要兩步:1) 數(shù)組長度的字節(jié)數(shù) sz=Length Prefix-0xb8砸琅; 2) 取“長度前綴”后的sz的內(nèi)容,就是數(shù)組長度的值
[0xc0-0xf7] 列表轴踱,且為短列表 列表長度=“長度前綴”-0xc0
[0xf8-0xff] 列表症脂,且為長列表 獲取列表的長度,需要兩步:1)列表長度的字節(jié)數(shù) sz=Length Prefix-0xf8淫僻;2). 取“長度前綴”后的sz的內(nèi)容诱篷,就是列表長度的值

舉例

以字節(jié)碼 d0c88363617483646f6781b783646f6780為例,給出通過RLP解碼的過程如下圖所示雳灵。


圖4:解碼示例.png
  1. 首先棕所,整體的長度前綴(Length Prefix)為D0,通過查表悯辙,可以確定出類型為“短列表”琳省,列表長度=0xD0-0xC0=0x10迎吵,即為16個字節(jié);
  2. 第一個子項(xiàng)的前綴0xC8岛啸,通過查表钓觉,對應(yīng)的類型為“短列表”茴肥,列表長度=0xC8-0xC0=0x08坚踩;
    a. 嵌套的第一個子項(xiàng)的前綴為0x83,對應(yīng)的類型為數(shù)組(字符串)類型瓤狐,且為短數(shù)組(字符串)瞬铸,長度為0x83-0x80=3,內(nèi)容為后續(xù)的3個字節(jié)础锐,即為[0x61, 0x61, 0x74]=[c,a,t,]嗓节;
    b. 嵌套的第二個子項(xiàng)的前綴為0x83,對應(yīng)的類型為數(shù)組(字符串)類型皆警,且為短數(shù)組(字符串)拦宣,長度為0x83-0x80=3,內(nèi)容為后續(xù)的3個字節(jié)信姓,即為[0x64, 0x6f, 0x67]=[d,o,g]鸵隧;
  3. 第二子項(xiàng)的前綴為0x81,通過查表,對應(yīng)的類型為數(shù)組(字符串)類型意推,且為短數(shù)組(字符串)豆瘫,長度為0x81-0x80=1, 內(nèi)容為0xb7;
  4. 第三個子項(xiàng)的前綴為0x83菊值,通過查表外驱,對應(yīng)的類型為數(shù)組(字符串)類型,且為短數(shù)組(字符串)腻窒,長度為0x83-0x80=3昵宇,內(nèi)容為后續(xù)的3個字節(jié),即為[0x64, 0x6f, 0x67]=[d,o,g]儿子;
  5. 第四子項(xiàng)的前綴為0x80,通過查表瓦哎,對應(yīng)的類型為數(shù)組(字符串)類型,且為短數(shù)組(字符串)典徊,長度為0x80-0x80=0杭煎,也就是一個空數(shù)組;
  6. 完成對nest sequence的構(gòu)建卒落,結(jié)果為<<[cat], [dog]>, 0xb7, [dog], []>>羡铲。

代碼實(shí)現(xiàn)分析

上面給出了RLP解碼過程的圖形化分析,下面簡要地對比不同語言版本對于RLP解碼實(shí)現(xiàn)儡毕。首先是Aleth也切,Aleth是以太坊的C++客戶端扑媚,并包含了一系列的工具庫。Aleth采用了“迭代器Iterator模式”來實(shí)現(xiàn)對RLP的解碼雷恃。其優(yōu)點(diǎn)是疆股,通過迭代器可以對每個子項(xiàng)進(jìn)行動態(tài)地、迭代地的部分解碼倒槐,是一種比較高效的解碼方法旬痹。對應(yīng)的C++ class文件在libdevcore/RLP.cpp中,詳見鏈接【3】讨越。
第二種實(shí)現(xiàn)方式是ethereumj提供的RLP解碼方式两残,ethereumj是以太坊協(xié)議的JAVA實(shí)現(xiàn)。ethereumj中采用了遞歸的方法對RLP編碼實(shí)現(xiàn)全解碼把跨,也就是對于輸入的flat sequence人弓,進(jìn)行一次性解碼,生成對應(yīng)的RLP對象着逐。在ethereumj的實(shí)現(xiàn)中崔赌,首先定義了如下圖所示的RLP樹結(jié)構(gòu)。


圖5:RLP對象結(jié)構(gòu).png

在ethereumj耸别,進(jìn)行全解碼的遞歸函數(shù)fullTraverse的關(guān)鍵代碼如下健芭,即先按照長度前綴規(guī)則(Length Prefix)確定RLP的類型,也就是表1中各個類型的分支太雨。需要注意的是吟榴,其中對于List類型,通過對fullTraverse遞歸囊扳,實(shí)現(xiàn)對RLP的嵌套解碼吩翻。

static void fullTraverse(byte[] msgData, int level, int startPos,
                         int endPos, RLPList rlpList, int depth) {
    if (msgData == null || msgData.length == 0)
        return;
    int pos = startPos;
    
    while (pos < endPos) {
        // It's a list with a payload more than 55 bytes
        // data[0] - 0xF7 = how many next bytes allocated
        // for the length of the list
        if ((msgData[pos] & 0xFF) > OFFSET_LONG_LIST) {
            
            byte lengthOfLength = (byte) (msgData[pos] - OFFSET_LONG_LIST);
            int length = calcLength(lengthOfLength, msgData, pos);
            
            byte[] rlpData = new byte[lengthOfLength + length + 1];
            System.arraycopy(msgData, pos, rlpData, 0, lengthOfLength
                             + length + 1);
            
            if (level + 1 < depth) {
                RLPList newLevelList = new RLPList();
                newLevelList.setRLPData(rlpData);
                
                fullTraverse(msgData, level + 1, pos + lengthOfLength + 1,
                             pos + lengthOfLength + length + 1, newLevelList, depth);
                rlpList.add(newLevelList);
            } else {
                rlpList.add(new RLPItem(rlpData));
            }
            
            pos += lengthOfLength + length + 1;
            continue;
        }
        // It's a list with a payload less than 55 bytes
        if ((msgData[pos] & 0xFF) >= OFFSET_SHORT_LIST
            && (msgData[pos] & 0xFF) <= OFFSET_LONG_LIST) {
            
            byte length = (byte) ((msgData[pos] & 0xFF) - OFFSET_SHORT_LIST);
            
            byte[] rlpData = new byte[length + 1];
            System.arraycopy(msgData, pos, rlpData, 0, length + 1);
            
            if (level + 1 < depth) {
                RLPList newLevelList = new RLPList();
                newLevelList.setRLPData(rlpData);
                
                if (length > 0)
                    fullTraverse(msgData, level + 1, pos + 1, pos + length + 1, newLevelList, depth);
                rlpList.add(newLevelList);
            } else {
                rlpList.add(new RLPItem(rlpData));
            }
            
            pos += 1 + length;
            continue;
        }
        // It's an item with a payload more than 55 bytes
        // data[0] - 0xB7 = how much next bytes allocated for
        // the length of the string
        if ((msgData[pos] & 0xFF) > OFFSET_LONG_ITEM
            && (msgData[pos] & 0xFF) < OFFSET_SHORT_LIST) {
            
            byte lengthOfLength = (byte) (msgData[pos] - OFFSET_LONG_ITEM);
            int length = calcLength(lengthOfLength, msgData, pos);
            
            // now we can parse an item for data[1]..data[length]
            byte[] item = new byte[length];
            System.arraycopy(msgData, pos + lengthOfLength + 1, item,
                             0, length);
            
            RLPItem rlpItem = new RLPItem(item);
            rlpList.add(rlpItem);
            pos += lengthOfLength + length + 1;
            
            continue;
        }
        // It's an item less than 55 bytes long,
        // data[0] - 0x80 == length of the item
        if ((msgData[pos] & 0xFF) > OFFSET_SHORT_ITEM
            && (msgData[pos] & 0xFF) <= OFFSET_LONG_ITEM) {
            
            byte length = (byte) ((msgData[pos] & 0xFF) - OFFSET_SHORT_ITEM);
            
            byte[] item = new byte[length];
            System.arraycopy(msgData, pos + 1, item, 0, length);
            
            if (length == 1 && (item[0] & 0xFF) < OFFSET_SHORT_ITEM) {
                throw new RuntimeException("Single byte has been encoded as byte string");
            }
            
            RLPItem rlpItem = new RLPItem(item);
            rlpList.add(rlpItem);
            pos += 1 + length;
            
            continue;
        }
        // null item
        if ((msgData[pos] & 0xFF) == OFFSET_SHORT_ITEM) {
            byte[] item = ByteUtil.EMPTY_BYTE_ARRAY;
            RLPItem rlpItem = new RLPItem(item);
            rlpList.add(rlpItem);
            pos += 1;
            continue;
        }
        // single byte item
        if ((msgData[pos] & 0xFF) < OFFSET_SHORT_ITEM) {
            
            byte[] item = {(byte) (msgData[pos] & 0xFF)};
            
            RLPItem rlpItem = new RLPItem(item);
            rlpList.add(rlpItem);
            pos += 1;
        }
    }
}

RLP序列化方法的討論

關(guān)于為什么以太坊需要單獨(dú)設(shè)計(jì)一種新的序列化協(xié)議,目前還沒有找到官方的描述锥咸,但與其他序列化方法相比狭瞎,可以觀察出其直接的優(yōu)點(diǎn),
首先搏予,對于編碼效率上

  • RLP使用了靈活的長度前綴來表示數(shù)據(jù)的實(shí)際長度熊锭,單字節(jié)直接編碼,短數(shù)組/短列表(short array/short list)采用短的長度前綴雪侥,長數(shù)組/短列表(long array/long list)采用長的長度前綴碗殷,可以保證高的編碼效率,提高內(nèi)存和存儲效率速缨。
  • 長度前綴(Length Prefix)同時包含了對應(yīng)的"類型"和"長度信息"锌妻,提高了編碼中前綴字符的效率;
  • 使用遞歸的方式旬牲,可以一次性編碼相當(dāng)大的數(shù)據(jù)內(nèi)容。

其次,對于以太坊數(shù)據(jù)的特殊性上蝴光,

  • 由于以太坊對于所有節(jié)點(diǎn)的“數(shù)據(jù)共識Consensus”的要求,為了防止出現(xiàn)數(shù)據(jù)的不一致堕仔,在以太坊中,并不支持浮點(diǎn)數(shù)類型晌区,其他的序列化方法摩骨,并不能直接使用;
  • 以太坊中采用的貨幣單位非常小契讲,比如Wei仿吞,并且1 ETH = 10^18 Wei滑频,所以在編碼中捡偏,需要考慮對很大的整數(shù)類型的序列化,在RLP中采用去除前導(dǎo)零(leading zero)的大端big-endian方式峡迷,可以有效處理大整數(shù)银伟。

參考鏈接

【1】Ethereum Wiki: https://en.wikipedia.org/wiki/Ethereum
【2】Ethereum Yellow Paper: https://ethereum.github.io/yellowpaper/paper.pdf
【3】ALETH:https://github.com/ethereum/aleth
【4】Ethereumj: https://github.com/ethereum/ethereumj

最后編輯于
?著作權(quán)歸作者所有,轉(zhuǎn)載或內(nèi)容合作請聯(lián)系作者
  • 序言:七十年代末,一起剝皮案震驚了整個濱河市绘搞,隨后出現(xiàn)的幾起案子彤避,更是在濱河造成了極大的恐慌,老刑警劉巖夯辖,帶你破解...
    沈念sama閱讀 216,544評論 6 501
  • 序言:濱河連續(xù)發(fā)生了三起死亡事件琉预,死亡現(xiàn)場離奇詭異,居然都是意外死亡蒿褂,警方通過查閱死者的電腦和手機(jī)圆米,發(fā)現(xiàn)死者居然都...
    沈念sama閱讀 92,430評論 3 392
  • 文/潘曉璐 我一進(jìn)店門,熙熙樓的掌柜王于貴愁眉苦臉地迎上來啄栓,“玉大人娄帖,你說我怎么就攤上這事£汲” “怎么了近速?”我有些...
    開封第一講書人閱讀 162,764評論 0 353
  • 文/不壞的土叔 我叫張陵,是天一觀的道長堪旧。 經(jīng)常有香客問我削葱,道長,這世上最難降的妖魔是什么淳梦? 我笑而不...
    開封第一講書人閱讀 58,193評論 1 292
  • 正文 為了忘掉前任析砸,我火速辦了婚禮,結(jié)果婚禮上谭跨,老公的妹妹穿的比我還像新娘干厚。我一直安慰自己李滴,他們只是感情好,可當(dāng)我...
    茶點(diǎn)故事閱讀 67,216評論 6 388
  • 文/花漫 我一把揭開白布蛮瞄。 她就那樣靜靜地躺著所坯,像睡著了一般。 火紅的嫁衣襯著肌膚如雪挂捅。 梳的紋絲不亂的頭發(fā)上芹助,一...
    開封第一講書人閱讀 51,182評論 1 299
  • 那天,我揣著相機(jī)與錄音闲先,去河邊找鬼状土。 笑死,一個胖子當(dāng)著我的面吹牛伺糠,可吹牛的內(nèi)容都是我干的蒙谓。 我是一名探鬼主播,決...
    沈念sama閱讀 40,063評論 3 418
  • 文/蒼蘭香墨 我猛地睜開眼训桶,長吁一口氣:“原來是場噩夢啊……” “哼累驮!你這毒婦竟也來了?” 一聲冷哼從身側(cè)響起舵揭,我...
    開封第一講書人閱讀 38,917評論 0 274
  • 序言:老撾萬榮一對情侶失蹤谤专,失蹤者是張志新(化名)和其女友劉穎,沒想到半個月后午绳,有當(dāng)?shù)厝嗽跇淞掷锇l(fā)現(xiàn)了一具尸體置侍,經(jīng)...
    沈念sama閱讀 45,329評論 1 310
  • 正文 獨(dú)居荒郊野嶺守林人離奇死亡,尸身上長有42處帶血的膿包…… 初始之章·張勛 以下內(nèi)容為張勛視角 年9月15日...
    茶點(diǎn)故事閱讀 37,543評論 2 332
  • 正文 我和宋清朗相戀三年拦焚,在試婚紗的時候發(fā)現(xiàn)自己被綠了蜡坊。 大學(xué)時的朋友給我發(fā)了我未婚夫和他白月光在一起吃飯的照片。...
    茶點(diǎn)故事閱讀 39,722評論 1 348
  • 序言:一個原本活蹦亂跳的男人離奇死亡耕漱,死狀恐怖算色,靈堂內(nèi)的尸體忽然破棺而出,到底是詐尸還是另有隱情螟够,我是刑警寧澤灾梦,帶...
    沈念sama閱讀 35,425評論 5 343
  • 正文 年R本政府宣布,位于F島的核電站妓笙,受9級特大地震影響若河,放射性物質(zhì)發(fā)生泄漏。R本人自食惡果不足惜寞宫,卻給世界環(huán)境...
    茶點(diǎn)故事閱讀 41,019評論 3 326
  • 文/蒙蒙 一萧福、第九天 我趴在偏房一處隱蔽的房頂上張望。 院中可真熱鬧辈赋,春花似錦鲫忍、人聲如沸膏燕。這莊子的主人今日做“春日...
    開封第一講書人閱讀 31,671評論 0 22
  • 文/蒼蘭香墨 我抬頭看了看天上的太陽坝辫。三九已至,卻和暖如春射亏,著一層夾襖步出監(jiān)牢的瞬間近忙,已是汗流浹背。 一陣腳步聲響...
    開封第一講書人閱讀 32,825評論 1 269
  • 我被黑心中介騙來泰國打工智润, 沒想到剛下飛機(jī)就差點(diǎn)兒被人妖公主榨干…… 1. 我叫王不留及舍,地道東北人。 一個月前我還...
    沈念sama閱讀 47,729評論 2 368
  • 正文 我出身青樓窟绷,卻偏偏與公主長得像锯玛,于是被迫代替她去往敵國和親。 傳聞我的和親對象是個殘疾皇子钾麸,可洞房花燭夜當(dāng)晚...
    茶點(diǎn)故事閱讀 44,614評論 2 353

推薦閱讀更多精彩內(nèi)容