以太坊RLP(递归长度前缀)编码

RLP(Recursive Length Prefix)即 递归长度前缀编码
,RLP主要用于以太坊数据的网络传输和持久化存储。

说明:十六进制表示数字前面会加‘0x’, 十进制直接用数字表示,如 0x80
128
是一个数字的不同表示,只占用一个字节。

为什么需要RLP编码

比较常见的序列化方法有 JSON
ProtoBuf
,但是这些序列化方法在以太坊这样的场景下都有一些问题。 比如像 Json
编码,编码后的体积比较大。

type Persion struct {
    Name string
    Age uint 
}

p := &Persion{Name: "Tom", Age: 22}
data, _ := json.Marshal(p)

fmt.Println(string(data))
//  {"Name":"Tom","Age":22}

从编码后的结果可以看到 {"Name":"Tom","Age":22}
,其实我们需要的数据是 Name
, Tom
, Age
, 22
,其余的括号和引号都是描述这种格式的,也就是冗余数据。

当然,会有人说可以用 protoBuf
这样的二进制格式,但是例如 JavaScript
这样的弱类型语言,是没有 int
类型的,所有数字都是用 Number
类型,底层用浮点数来实现,这样就会导致因为语言的不同编码后的数据有一定差异,最后得到的 hash
值也不同。 针对这些问题,以太坊设计了 RLP
编码,同时也大幅简化了编码规则。

RLP
编码定义

RLP(Recursive Length Prefix)即 递归长度前缀编码
, 不定义任何指定的数据类型, 仅以嵌套数组的形式存储结构。

RLP
简化了编码的类型,只定义了两种类型编码:

列表

RLP
编码基于上面两种数据类型提出了5条编码规则;

规则一:

对于值在[0, 127](十六进制[0x00, 0x7f])之间的单个字节,其编码是其本身;无需前缀。

a的编码是97

规则二:

如果字符串的长度为0-55个字节之间,编码的结果是数组本身,再加上 0x80 + 字符串长度
作为前缀, 前缀范围是:[0x80, 0xb7] 即十进制[128, 183]。

空字符串的编码是128,即 128=128+0
abc的编码是131 97 98 99,其实131=128+len("abc"), 97 98 99依次是`a b c`

规则三:

如果字符串(数组)长度大于55字节,编码结果第一个值是183(128+55)+ 数组长度的编码的字节长度
,然后是数组长度本身的编码,最后是byte数组的编码(共三个部分)。前缀范围是:[0xb8, 0xbf] 即十进制[184, 191]。

编码一个重复1024次"a"的字符串,其结果是`185 4 0 97 97 97 ...

1024(2的10次方)按照大端编码是 0000 0000 001
转换为十六进制是 0 0 4 0
,省略前面的 0
,长度为 2
, 因此 185 = 183 + 2

大端编码(BigEndian): 低字节在高内存地址 ; 小端编码(LittleEndian): 低字节在低内存地址

规则四

如果列表总内容RLP编码后字节长度小于55,编码结果第一位是 192
+ 列表内容编码的长度
,然后依次连接各个子列表的编码,前缀范围是:[0xc0, 0xf7] 即十进制[192,247]。

空列表[]编码结果为:192
["abc", "def"]的编码结果是 200 131 97 98 99 131 100 101 102

其中 abc
的编码是 131 97 98 99
, 131 = 128 + len("abc")
, abc的编码长度是4,同样 def
的编码是 131 100 101 102
,编码长度是4,两个子字符串的编码后总长度为8,编码结果的第一位 200 = 192 + 8

规则五

如果总内容RLP编码后字节长度超过55,编码结果第一位是 0xf7
+ 列表长度的编码长度
,然后是列表长度本身的编码,最后依次连接子列表的编码,前缀范围是:[0x80, 0xb7] 即十进制[247,256]。

["The length of this sentence is more than 55 bytes, ", "I know it because I pre-designed it"]

其中前两个字节的计算方式如下:

1. "The length of this sentence is more than 55 bytes, "的长度为51(0x33),根据规则二得出前缀179 (0xb3 = 0x80 + 0x33 )
2. "I know it because I pre-designed it"的长度为35(0x23),根据规则2得出前缀163 (0xa3 = 0x80 + 0x33)
3. 列表长度本身的编码为:51 + 35 + 2(个子串的前缀占用) = 88 (0x58)
4. 最后根据规则5,0x58只占用一个字节,即 247(0xf7) + 1 = 248 , 前缀为 248。

的编码结果表示是:
其中规则三,规则四,规则5是递归定义的(允许嵌套)。

编码实现

def rlp_decode(input):
    if len(input) == 0:
        return
    output = ''
    (offset, dataLen, type) = decode_length(input)
    if type is str:
        output = instantiate_str(substr(input, offset, dataLen))
    elif type is list:
        output = instantiate_list(substr(input, offset, dataLen))
    output = output + rlp_decode(substr(input, offset + dataLen))
    return output

def decode_length(input):
    length = len(input)
    if length == 0:
        raise Exception("input is null")
    prefix = ord(input[0])
    if prefix <= 0x7f:
        return (0, 1, str)
    elif prefix  prefix - 0x80:
        strLen = prefix - 0x80
        return (1, strLen, str)
    elif prefix  prefix - 0xb7 and length > prefix - 0xb7 + to_integer(substr(input, 1, prefix - 0xb7)):
        lenOfStrLen = prefix - 0xb7
        strLen = to_integer(substr(input, 1, lenOfStrLen))
        return (1 + lenOfStrLen, strLen, str)
    elif prefix  prefix - 0xc0:
        listLen = prefix - 0xc0;
        return (1, listLen, list)
    elif prefix  prefix - 0xf7 and length > prefix - 0xf7 + to_integer(substr(input, 1, prefix - 0xf7)):
        lenOfListLen = prefix - 0xf7
        listLen = to_integer(substr(input, 1, lenOfListLen))
        return (1 + lenOfListLen, listLen, list)
    else:
        raise Exception("input don't conform RLP encoding form")

def to_integer(b):
    length = len(b)
    if length == 0:
        raise Exception("input is null")
    elif length == 1:
        return ord(b[0])
    else:
        return ord(substr(b, -1)) + to_integer(substr(b, 0, -1)) * 256

参考文章

本文作者是深入浅出区块链共建者清源,欢迎关注清源的博客,不定期分享一些区块链底层技术文章。
备注:编者在原文上略有修改。

深入浅出区块链 – 系统学习区块链,学区块链的都在这里,打造最好的区块链技术博客。