以太坊中RLP编码详解一文全搞懂

定义

RLP(Recursive Length Prefix,递归长度前缀)是一种编码算法,用于编码任意的嵌套结构的二进制数据,它是以太坊中数据序列化/反序列化的主要方法,区块、交易等数据结构在持久化时会先经过RLP编码后再存储到数据库中。

RLP编码的定义只处理两类数据:一类是字符串(例如字节数组),一类是列表。字符串指的是一串二进制数据,列表是一个嵌套递归的结构,里面可以包含字符串和列表,例如["cat",["puppy","cow"],"horse",[[]],"pig",[""],"sheep"]就是一个复杂的列表。其他类型的数据需要转成以上的两类,转换的规则不是RLP编码定义的,可以根据自己的规则转换,例如struct可以转成列表,int可以转成二进制(属于字符串一类),以太坊中整数都以大端形式存储。

RLP编码规则

  1. 对于单个字节,如果它的值范围是[0x00, 0x7f],它的RLP编码就是它本身。
  2. 否则,如果一个字符串的长度是0-55字节,它的RLP编码包含一个单字节的前缀,后面跟着字符串本身,这个前缀的值是0x80加上字符串的长度。由于被编码的字符串最大长度是55=0x37,因此单字节前缀的最大值是0x80+0x37=0xb7,即编码的第一个字节的取值范围是[0x80, 0xb7]。
  3. 如果字符串的长度大于55个字节,它的RLP编码包含一个单字节的前缀,后面跟着字符串的长度,后面再跟着字符串本身。这个前缀的值是0xb7加上字符串长度的二进制形式的字节长度,说的有点绕,举个例子就明白了,例如一个字符串的长度是1024,它的二进制形式是10000000000,这个二进制形式的长度是2个字节,所以前缀应该是0xb7+2=0xb9,字符串长度1024=0x400,因此整个RLP编码应该是\xb9\x04\x00再跟上字符串本身。编码的第一个字节即前缀的取值范围是[0xb8, 0xbf],因为字符串长度二进制形式最少是1个字节,因此最小值是0xb7+1=0xb8,字符串长度二进制最大是8个字节,因此最大值是0xb7+8=0xbf。
  4. 如果一个列表的总长度(列表的总长度指的是它包含的项的数量加它包含的各项的长度之和)是0-55字节,它的RLP编码包含一个单字节的前缀,后面跟着列表中各元素项的RLP编码,这个前缀的值是0xc0加上列表的总长度。编码的第一个字节的取值范围是[0xc0, 0xf7]。
  5. 如果一个列表的总长度大于55字节,它的RLP编码包含一个单字节的前缀,后面跟着列表的长度,后面再跟着列表中各元素项的RLP编码,这个前缀的值是0xf7加上列表总长度的二进制形式的字节长度。编码的第一个字节的取值范围是[0xf8, 0xff]。

RLP编码例子

字符串 "dog" = [0x83, 'd', 'o', 'g' ] (规则二)

列表 ["cat","dog"] = [0xc8, 0x83, 'c', 'a', 't', 0x83, 'd', 'o', 'g' ] (规则四)

空字符串 "" = 0x80 (规则二)

空列表 [] = [0xc0] (规则四)

整数 15('\x0f') = 0x0f (规则一)

整数 1024('\x04\00') = [0x82, 0x04, 0x00] (规则二)

列表 [ [], [[]], [ [], [[]] ] ] = [0xc7, 0xc0, 0xc1, 0xc0, 0xc3, 0xc0, 0xc1, 0xc0] (规则四)

字符串 "Lorem ipsum dolor sit amet, consectetur adipisicing elit" = [0xb8, 0x38, 'L', 'o', 'r', 'e', 'm', ' ', ... , 'e', 'l', 'i', 't'] (规则三)

为什么又要造轮子

对象序列化方法有很多种,常见的像JSON编码,但是JSON有个明显的缺点:编码结果比较大。例如有如下的结构:

type Student struct{
    Name string `json:"name"`
    Sex string `json:"sex"`
}
s := Student{Name:"icattlecoder",Sex:"male"}
bs,_ := json.Marsal(&s)
print(string(bs))
// {"name":"icattlecoder","sex":"male"}

变量s序列化的结果是{"name":"icattlecoder","sex":"male"},字符串长度35,实际有效数据是icattlecoder 和male,共计16个字节,我们可以看到JSON的序列化时引入了太多的冗余信息。假设以太坊采用JSON来序列化,那么本来50GB的区块链可能现在就要100GB,当然实际没这么简单。

所以,以太坊需要设计一种结果更小的编码方法。

RLP编码定义

RLP实际只给以下两种类型数据编码:

1. byte数组

2. byte数组的数组,称之为列表

规则1:对于值在[0, 127]之间的单个字节,其编码是其本身。

例1:a的编码是97。

规则2:如果byte数组长度l <= 55,编码的结果是数组本身,再加上128+l作为前缀。

例2:空字符串编码是128,即128 = 128 + 0。

例3:abc编码结果是131 97 98 99,其中131=128+len("abc"),97 98 99依次是a b c。
  

规则3:如果数组长度大于55, 编码结果第一个是183加数组长度的编码的长度,然后是数组长度的本身的编码,最后是byte数组的编码。

请把上面的规则多读几篇,特别是数组长度的编码的长度。

例4:编码下面这段字符串:

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

这段字符串共86个字节,而86的编码只需要一个字节,那就是它自己,因此,编码的结果如下:

184 86 84 104 101 32 108 101 110 103 116 104 32 111 102 32 116 104 105 115 32 115 101 110 116 101 110 99 101 32 105 115 32 109 111 114 101 32 116 104 97 110 32 53 53 32 98 121 116 101 115 44 32 73 32 107 110 111 119 32 105 116 32 98 101 99 97 117 115 101 32 73 32 112 114 101 45 100 101 115 105 103 110 101 100 32 105 116

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

184 = 183 + 1,因为数组长度86编码后仅占用一个字节。

86即数组长度86

84是T的编码

例5:编码一个重复1024次"a"的字符串,其结果为:185 4 0 97 97 97 97 97 97 ...。

1024按 big endian编码为0 0 4 0,省略掉前面的零,长度为2(我的理解:1024(10000000000)需要用到2个字节),因此185 = 183 + 2。

规则1~3定义了byte数组的编码方案,下面介绍列表的编码规则。在此之前,我们先定义列表长度是指子列表编码后的长度之和。

规则4:如果列表长度小于55,编码结果第一位是192加列表长度的编码的长度,然后依次连接各子列表的编码。

注意规则4本身是递归定义的。

例6:["abc", "def"]的编码结果是200 131 97 98 99 131 100 101 102。
其中abc的编码为131 97 98 99,def的编码为131 100 101 102。两个子字符串的编码后总长度是8,因此编码结果第一位计算得出:192 + 8 = 200。

规则5:如果列表长度超过55,编码结果第一位是247加列表长度的编码长度,然后是列表长度本身的编码,最后依次连接各子列表的编码。

规则5本身也是递归定义的,和规则3相似。

例7:

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

的编码结果是:

248 88 179 84 104 101 32 108 101 110 103 116 104 32 111 102 32 116 104 105 115 32 115 101 110 116 101 110 99 101 32 105 115 32 109 111 114 101 32 116 104 97 110 32 53 53 32 98 121 116 101 115 44 32 163 73 32 107 110 111 119 32 105 116 32 98 101 99 97 117 115 101 32 73 32 112 114 101 45 100 101 115 105 103 110 101 100 32 105 116

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

248 = 247 +1

88 = 86 + 2

在规则3的示例中,长度为86,而在此例中,由于有两个子字符串,每个子字符串本身的长度的编码各占1字节,因此总共占2字节。
第3个字节179依据规则2得出179 = 128 + 51
第55个字节163同样依据规则2得出163 = 128 + 35

例8:最后我们再来看个稍复杂点的例子以加深理解递归长度前缀,

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

编码结果是:

248 94 131 97 98 99 248 88 179 84 104 101 32 108 101 110 103 116 104 32 111 102 32 116 104 105 115 32 115 101 110 116 101 110 99 101 32 105 115 32 109 111 114 101 32 116 104 97 110 32 53 53 32 98 121 116 101 115 44 32 163 73 32 107 110 111 119 32 105 116 32 98 101 99 97 117 115 101 32 73 32 112 114 101 45 100 101 115 105 103 110 101 100 32 105 116

列表第一项字符串abc根据规则2,编码结果为131 97 98 99,长度为4。
列表第二项也是一个列表项:

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

根据规则5,结果为

248 88 179 84 104 101 32 108 101 110 103 116 104 32 111 102 32 116 104 105 115 32 115 101 110 116 101 110 99 101 32 105 115 32 109 111 114 101 32 116 104 97 110 32 53 53 32 98 121 116 101 115 44 32 163 73 32 107 110 111 119 32 105 116 32 98 101 99 97 117 115 101 32 73 32 112 114 101 45 100 101 115 105 103 110 101 100 32 105 116

长度为90,因此,整个列表的编码结果第二位是90 + 4 = 94, 占用1个字节,第一位247 + 1 = 248

以上5条就是RPL的全部编码规则。

语言实现

各语言在具体实现RLP编码时,首先需要将对像映射成byte数组或列表两种形式。以go语言编码struct为例,会将其映射为列表,例如Student这个对象处理成列表["icattlecoder","male"]

type Student struct{
    Name string
    Sex string
}
s := Student{Name:"icattlecoder",Sex:"male"}
buff := bytes.Buffer{}
rpl.Encode(&buff, &s)
print(buff.Bytes())
// [210 140 105 99 97 116 116 108 101 99 111 100 101 114 132 109 97 108 101]

如果编码map类型,可以采用以下列表形式:

[["",""],["",""],["",""]]

RLP解码

解码时,首先根据编码结果第一个字节f的大小,执行以下的规则判断:

1. 如果f∈ [0,128), 那么它是一个字节本身。

2. 如果f∈[128,184),那么它是一个长度不超过55的byte数组,数组的长度为 l=f-128

3. 如果f∈[184,192),那么它是一个长度超过55的数组,长度本身的编码长度ll=f-183,然后从第二个字节开始读取长度为ll的bytes,按照BigEndian编码成整数l,l即为数组的长度。

4. 如果f∈(192,247],那么它是一个编码后总长度不超过55的列表,列表长度为l=f-192。递归使用规则1~4进行解码。

5. 如果f∈(247,256],那么它是编码后长度大于55的列表,其长度本身的编码长度ll=f-247,然后从第二个字节读取长度为ll的bytes,按BigEndian编码成整数l,l即为子列表长度。然后递归根据解码规则进行解码。

以上解释了什么叫递归长度前缀编码,这个名字本身很好的解释了编码规则。

猜你喜欢

转载自blog.51cto.com/634435/2476161