eth_rlp
序列化 RLP
编码
对象序列化的方式有很多,如json编码,如
1 | type Student struct{ |
但编码的结果为{"name":"icattlecoder","sex":"male"}
,实际有效数据是icattlecoder和male,共计16个字节,可以看到json序列化时引入了太多的冗余信息,所以,以太坊设计了结果更小的编码方式—— RLP编码
RLP的全称为Recursive Length Prefix,中文翻译过来叫递归长度前缀编码,它是以太坊序列化所采用的编码方式。RLP主要用于以太坊中数据的网络传输和持久化存储。
RLP实际只给以下两种类型数据编码:
- byte数组
- 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数组的编码。
呃,大于55是第一个是183+ 数组长度编码的长度,第二位才是长度的编码
如编码下面这段字符串:
1 | The length of this sentence is more than 55 bytes, I know it because I pre-designed it |
这段字符串共86个字节,而86的编码只需要一个字节,那就是它自己,因此,编码的结果如下:
1 | 184 86 84 104 101 32 108 101 110 103 116 104 32 111 102 32 116 104 105 115 32 115 101 110 116101 110 99 101 32 105 115 32 109 111 114 101 32 116 104 97 110 32 53 53 32 98 121 116 101 11544 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 100101 115 105 103 110 101 100 32 105 116 |
其中前三个字节的计算方式如下:
184 = 183 + 1
,因为数组长度86
编码后仅占用一个字节。86
即数组长度86
84
是T
的编码
编码一个重复1024次”a”的字符串,其结果为:185 4 0 97 97 97 97 97 97 ...
。
1024按 big endian编码为0 0 4 0
,省略掉前面的零,长度为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:
1 | ["The length of this sentence is more than 55 bytes, ", "I know it because I pre-designed it"] |
的编码结果是:
1 | 248 88 179 84 104 101 32 108 101 110 103 116 104 32 111 102 32 116 104 105 115 32 115 101 110116 101 110 99 101 32 105 115 32 109 111 114 101 32 116 104 97 110 32 53 53 32 98 121 116 101115 44 32 163 73 32 107 110 111 119 32 105 116 32 98 101 99 97 117 115 101 32 73 32 112 114101 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:最后我们再来看个稍复杂点的例子以加深理解递归长度前缀,
1 | ["abc",["The length of this sentence is more than 55 bytes, ", "I know it because I pre-designed it"]] |
编码结果是:
1 | 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 105115 32 115 101 110 116 101 110 99 101 32 105 115 32 109 111 114 101 32 116 104 97 110 32 53 5332 98 121 116 101 115 44 32 163 73 32 107 110 111 119 32 105 116 32 98 101 99 97 117 115 10132 73 32 112 114 101 45 100 101 115 105 103 110 101 100 32 105 116 |
列表第一项字符串abc
根据规则2,编码结果为131 97 98 99
,长度为4。
列表第二项也是一个列表项:
1 | ["The length of this sentence is more than 55 bytes, ", "I know it because I pre-designed it"] |
根据规则5,结果为
1 | 248 88 179 84 104 101 32 108 101 110 103 116 104 32 111 102 32 116 104 105 115 32 115 101 110116 101 110 99 101 32 105 115 32 109 111 114 101 32 116 104 97 110 32 53 53 32 98 121 116 101115 44 32 163 73 32 107 110 111 119 32 105 116 32 98 101 99 97 117 115 101 32 73 32 112 114101 45 100 101 115 105 103 110 101 100 32 105 116 |
长度为90,因此,整个列表的编码结果第二位是90 + 4 = 94
, 占用1个字节,第一位247 + 1 = 248
以上5条就是RPL的全部编码规则。
解码
解码时,首先根据编码结果第一个字节f
的大小,执行以下的规则判断:
- 如果f∈ [0,128), 那么它是一个字节本身。
- 如果f∈[128,184),那么它是一个长度不超过55的byte数组,数组的长度为
l=f-128
- 如果f∈[184,192),那么它是一个长度超过55的数组,长度本身的编码长度
ll=f-183
,然后从第二个字节开始读取长度为ll的bytes,按照BigEndian编码成整数l,l即为数组的长度。 - 如果f∈(192,247],那么它是一个编码后总长度不超过55的列表,列表长度为
l=f-192
。递归使用规则1~4进行解码。 - 如果f∈(247,256],那么它是编码后长度大于55的列表,其长度本身的编码长度
ll=f-247
,然后从第二个字节读取长度为ll的bytes,按BigEndian编码成整数l,l即为子列表长度。然后递归根据解码规则进行解码。
代码实现
语言在具体实现RLP编码时,首先需要将对像映射成byte数组或列表两种形式。以go语言编码struct为例,会将其映射为列表,例如Student
这个对象处理成列表["icattlecoder","male"]
1 | type Student struct{ |
如果编码map类型,可以采用以下列表形式:
1 | [["",""],["",""],["",""]] |
代码学习收获
bytes.Buffer
在需要字符拼接频繁时,可使用Buffer
buffer是一个变长的 bytes,具有 Read 和Write 方法。 Buffer 的 零值 是一个 空的 buffer,但是可以使用。
Buffer 就像一个集装箱容器,可以存东西,取东西(存取数据)1
2
3b1 := new(bytes.Buffer)
b1.Write([]byte("asd"))
b1.Bytes() --> asd在字符串倒来倒去的时候,或者是变长时候,Go中可以使用“+”合并字符串,但是这种合并方式效率非常低,每合并一次,都是创建一个新的字符串,就必须遍历复制一次字符串,在Java中会选择使用StringBuilder, 在go中,类似的方法就是bytes.Buffer(线程不安全)。
sync.Pool
对象池。创建的时候可以指定一个New函数,获取对象的时候如何在池里面找不到缓存的对象将会使用指定的new函数创建一个返回
它的用途仅仅是增加对象重用的几率,减少gc的负担,而开销方面也不是很便宜的
接口函数
在oo中,实现方法的多态,需要多个子类。在go中,使用接口函数即可实现类似功能,而且非常简便
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17type print func()
type A struct {
print
}
func printString() {
fmt.Println("I am a string...")
}
func printInt() {
fmt.Println("123456")
}
func TestA_SetPrint(t *testing.T) {
a := &A{}
a.SetPrint(printString)
a.print()
//I am a string...
}
//还是很像多态,只是将方法print作为父,其余printString、printInt都是子,调用的时候调用指向子的引用。