以太坊解读——Recursive Length Prefix协议图解(下)

解码过程

解码是编码的逆过程,也就是将顺序(flat sequence)字节转化为嵌套树形结构(nested sequence)的过程。在重建树形结构的过程中,首先,根据长度前缀(Length Prefix),也就是每个编码段的第一个字节,来推断数据的类型。然后,再读取该编码段的实际内容,进行解码。另外,由于RLP编码中的列表是一种嵌套结构,如果类型是列表,则需要进行递归解码。

长度前缀的处理规则

根据长度前缀(Length Prefix)的数值范围,其对应的类型及长度计算方法可以汇总为如下的表格。

长度前缀的范围 对应的类型 对应的长度
[0x00-0x7f] 数组(字符串)类型,且为单字的数组 1字节
[0x80-0xb7] 数组(字符串)类型,且为短数组(字符串) 数组长度=“长度前缀”-0x80
[0xb8-0xbf] 数组(字符串)类型,且为长数组(字符串) 获取数组的长度,需要两步:1) 数组长度的字节数 sz=Length Prefix-0xb8; 2) 取“长度前缀”后的sz的内容,就是数组长度的值
[0xc0-0xf7] 列表,且为短列表 列表长度=“长度前缀”-0xc0
[0xf8-0xff] 列表,且为长列表 获取列表的长度,需要两步:1)列表长度的字节数 sz=Length Prefix-0xf8;2). 取“长度前缀”后的sz的内容,就是列表长度的值

举例

以字节码 d0c88363617483646f6781b783646f6780为例,给出通过RLP解码的过程如下图所示。


图4:解码示例.png
  1. 首先,整体的长度前缀(Length Prefix)为D0,通过查表,可以确定出类型为“短列表”,列表长度=0xD0-0xC0=0x10,即为16个字节;
  2. 第一个子项的前缀0xC8,通过查表,对应的类型为“短列表”,列表长度=0xC8-0xC0=0x08;
    a. 嵌套的第一个子项的前缀为0x83,对应的类型为数组(字符串)类型,且为短数组(字符串),长度为0x83-0x80=3,内容为后续的3个字节,即为[0x61, 0x61, 0x74]=[c,a,t,];
    b. 嵌套的第二个子项的前缀为0x83,对应的类型为数组(字符串)类型,且为短数组(字符串),长度为0x83-0x80=3,内容为后续的3个字节,即为[0x64, 0x6f, 0x67]=[d,o,g];
  3. 第二子项的前缀为0x81,通过查表,对应的类型为数组(字符串)类型,且为短数组(字符串),长度为0x81-0x80=1, 内容为0xb7;
  4. 第三个子项的前缀为0x83,通过查表,对应的类型为数组(字符串)类型,且为短数组(字符串),长度为0x83-0x80=3,内容为后续的3个字节,即为[0x64, 0x6f, 0x67]=[d,o,g];
  5. 第四子项的前缀为0x80,通过查表,对应的类型为数组(字符串)类型,且为短数组(字符串),长度为0x80-0x80=0,也就是一个空数组;
  6. 完成对nest sequence的构建,结果为<<[cat], [dog]>, 0xb7, [dog], []>>。

代码实现分析

上面给出了RLP解码过程的图形化分析,下面简要地对比不同语言版本对于RLP解码实现。首先是Aleth,Aleth是以太坊的C++客户端,并包含了一系列的工具库。Aleth采用了“迭代器Iterator模式”来实现对RLP的解码。其优点是,通过迭代器可以对每个子项进行动态地、迭代地的部分解码,是一种比较高效的解码方法。对应的C++ class文件在libdevcore/RLP.cpp中,详见链接【3】。
第二种实现方式是ethereumj提供的RLP解码方式,ethereumj是以太坊协议的JAVA实现。ethereumj中采用了递归的方法对RLP编码实现全解码,也就是对于输入的flat sequence,进行一次性解码,生成对应的RLP对象。在ethereumj的实现中,首先定义了如下图所示的RLP树结构。


图5:RLP对象结构.png

在ethereumj,进行全解码的递归函数fullTraverse的关键代码如下,即先按照长度前缀规则(Length Prefix)确定RLP的类型,也就是表1中各个类型的分支。需要注意的是,其中对于List类型,通过对fullTraverse递归,实现对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序列化方法的讨论

关于为什么以太坊需要单独设计一种新的序列化协议,目前还没有找到官方的描述,但与其他序列化方法相比,可以观察出其直接的优点,
首先,对于编码效率上

  • RLP使用了灵活的长度前缀来表示数据的实际长度,单字节直接编码,短数组/短列表(short array/short list)采用短的长度前缀,长数组/短列表(long array/long list)采用长的长度前缀,可以保证高的编码效率,提高内存和存储效率。
  • 长度前缀(Length Prefix)同时包含了对应的"类型"和"长度信息",提高了编码中前缀字符的效率;
  • 使用递归的方式,可以一次性编码相当大的数据内容。

其次,对于以太坊数据的特殊性上,

  • 由于以太坊对于所有节点的“数据共识Consensus”的要求,为了防止出现数据的不一致,在以太坊中,并不支持浮点数类型,其他的序列化方法,并不能直接使用;
  • 以太坊中采用的货币单位非常小,比如Wei,并且1 ETH = 10^18 Wei,所以在编码中,需要考虑对很大的整数类型的序列化,在RLP中采用去除前导零(leading zero)的大端big-endian方式,可以有效处理大整数。

参考链接

【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

最后编辑于
©著作权归作者所有,转载或内容合作请联系作者
  • 序言:七十年代末,一起剥皮案震惊了整个滨河市,随后出现的几起案子,更是在滨河造成了极大的恐慌,老刑警刘岩,带你破解...
    沈念sama阅读 216,496评论 6 501
  • 序言:滨河连续发生了三起死亡事件,死亡现场离奇诡异,居然都是意外死亡,警方通过查阅死者的电脑和手机,发现死者居然都...
    沈念sama阅读 92,407评论 3 392
  • 文/潘晓璐 我一进店门,熙熙楼的掌柜王于贵愁眉苦脸地迎上来,“玉大人,你说我怎么就摊上这事。” “怎么了?”我有些...
    开封第一讲书人阅读 162,632评论 0 353
  • 文/不坏的土叔 我叫张陵,是天一观的道长。 经常有香客问我,道长,这世上最难降的妖魔是什么? 我笑而不...
    开封第一讲书人阅读 58,180评论 1 292
  • 正文 为了忘掉前任,我火速办了婚礼,结果婚礼上,老公的妹妹穿的比我还像新娘。我一直安慰自己,他们只是感情好,可当我...
    茶点故事阅读 67,198评论 6 388
  • 文/花漫 我一把揭开白布。 她就那样静静地躺着,像睡着了一般。 火红的嫁衣衬着肌肤如雪。 梳的纹丝不乱的头发上,一...
    开封第一讲书人阅读 51,165评论 1 299
  • 那天,我揣着相机与录音,去河边找鬼。 笑死,一个胖子当着我的面吹牛,可吹牛的内容都是我干的。 我是一名探鬼主播,决...
    沈念sama阅读 40,052评论 3 418
  • 文/苍兰香墨 我猛地睁开眼,长吁一口气:“原来是场噩梦啊……” “哼!你这毒妇竟也来了?” 一声冷哼从身侧响起,我...
    开封第一讲书人阅读 38,910评论 0 274
  • 序言:老挝万荣一对情侣失踪,失踪者是张志新(化名)和其女友刘颖,没想到半个月后,有当地人在树林里发现了一具尸体,经...
    沈念sama阅读 45,324评论 1 310
  • 正文 独居荒郊野岭守林人离奇死亡,尸身上长有42处带血的脓包…… 初始之章·张勋 以下内容为张勋视角 年9月15日...
    茶点故事阅读 37,542评论 2 332
  • 正文 我和宋清朗相恋三年,在试婚纱的时候发现自己被绿了。 大学时的朋友给我发了我未婚夫和他白月光在一起吃饭的照片。...
    茶点故事阅读 39,711评论 1 348
  • 序言:一个原本活蹦乱跳的男人离奇死亡,死状恐怖,灵堂内的尸体忽然破棺而出,到底是诈尸还是另有隐情,我是刑警宁泽,带...
    沈念sama阅读 35,424评论 5 343
  • 正文 年R本政府宣布,位于F岛的核电站,受9级特大地震影响,放射性物质发生泄漏。R本人自食恶果不足惜,却给世界环境...
    茶点故事阅读 41,017评论 3 326
  • 文/蒙蒙 一、第九天 我趴在偏房一处隐蔽的房顶上张望。 院中可真热闹,春花似锦、人声如沸。这庄子的主人今日做“春日...
    开封第一讲书人阅读 31,668评论 0 22
  • 文/苍兰香墨 我抬头看了看天上的太阳。三九已至,却和暖如春,着一层夹袄步出监牢的瞬间,已是汗流浃背。 一阵脚步声响...
    开封第一讲书人阅读 32,823评论 1 269
  • 我被黑心中介骗来泰国打工, 没想到刚下飞机就差点儿被人妖公主榨干…… 1. 我叫王不留,地道东北人。 一个月前我还...
    沈念sama阅读 47,722评论 2 368
  • 正文 我出身青楼,却偏偏与公主长得像,于是被迫代替她去往敌国和亲。 传闻我的和亲对象是个残疾皇子,可洞房花烛夜当晚...
    茶点故事阅读 44,611评论 2 353

推荐阅读更多精彩内容