以太坊RLP编码原理

36次阅读

共计 2980 个字符,预计需要花费 8 分钟才能阅读完成。

RLP 编码是什么
  RLP(Recursive Length Prefix,递归的长度前缀)是一种编码规则,主要用来序列化 / 反序列化数据,可用于编码任意嵌套的二进制数组数据。RLP 编码是以太坊数据序列化的主要编码方式,具有较好的数据处理效率,尤其是将长度和类型统一作为前缀,实际上 RLP 是基于 ASCII 编码的一种结构化扩充,既能表示长度还能表示类型,是一种非常紧凑的结构化编码方案。区块、交易等数据结构在持久化时会先经过 RLP 编码后再存储到数据库中。RLP 的唯一目标就是解决结构体的编码问题;对原子数据类型(比如,字符串,整数型,浮点型)的编码则交给更高层的协议;以太坊中要求数字必须是一个大端字节序的、没有零占位的存储的格式(也就是说,一个整数 0 和一个空数组是等同的)。对于在 RLP 格式中对一个字典数据的编码问题,有两种建议的方式,一种是通过二维数组表达键值对,比如[[k1,v1],[k2,v2]…],并且对键进行字典序排序;另一种方式是通过以太坊文档中提到的高级的基数树编码来实现。
RLP 编码的优势
对象序列化方式又很多,比如常见的 JSON 格式,但是这些编码方式显示更直观,结构却不够紧凑,不是合适用来存储,例如 json
type Student struct{
Name string `json:”name”`
Sex string `json:”sex”`
}
s := Student{Name:”cateria”,Sex:”male”}
bs,_ := json.Marsal(&s)
print(string(bs))
// {“name”:”icattlecoder”,”sex”:”male”}
变量 s 序列化的结果是{“name”:”icattlecoder”,”sex”:”male”},字符串长度 35,实际有效数据是 cateria 和 male,共计 16 个字节,我们可以看到 JSON 的序列化时引入了太多的冗余信息,这就会导致存储时占用空间太大。所以,以太坊需要设计一种结果更小的编码方法。
RLP 编码定义
RLP 编码只处理两类数据:一类是字符串(字节数组),一类是列表。

字符串,指的是一串二进制数据,
列表,是一个嵌套递归的结构,里面可以包含字符串和列表,例如 [“cat”,[“puppy”,”cow”],”horse”,[[]],”pig”,[“”],”sheep”] 就是一个复杂的列表。

其他类型的数据需要转成以上的两类,转换的规则不是 RLP 编码定义的,可以根据自己的规则转换。例如 struct 可以转成列表,int 可以转成二进制(属于字符串一类),以太坊中整数都以大端形式存储。
RLP 编码方式的特点:

递归,被编码的数据是递归的结构,编码算法也是递归进行处理的;
长度前缀,也就是 RLP 编码都带有一个前缀,这个前缀是跟被编码数据的长度相关的,从下面的编码规则中可以看出这一点。

RLP 编码规则

规则一:对于单个字节,如果它的值范围是[0x00, 0x7f],它的 RLP 编码就是它本身。这里面需要注意的是 0x7f 这个边界,因为 ASCII 编码最大值就是 0x7f,也就是说在 0x7f 以内完全当做 ASCII 编码使用。
规则二:如果一个字符串的长度是 0 -55 字节,它的 RLP 编码包含一个单字节的前缀,后面跟着字符串本身,这个前缀的值是 0x80 加上字符串的长度。由于被编码的字符串最大长度是 55=0x37, 因此单字节前缀的最大值是 0x80+0x37=0xb7,即编码的第一个字节的取值范围是[0x80, 0xb7]。
规则三:如果字符串的长度大于 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。
规则四:如果一个列表的总长度(列表的总长度指的是它包含的项的数量加它包含的各项的长度之和)是 0 -55 字节,它的 RLP 编码包含一个单字节的前缀,后面跟着列表中各元素项的 RLP 编码,这个前缀的值是 0xc0 加上列表的总长度。编码的第一个字节的取值范围是[0xc0, 0xf7]。
规则五:如果一个列表的总长度大于 55 字节,它的 RLP 编码包含一个单字节的前缀,后面跟着列表的长度,后面再跟着列表中各元素项的 RLP 编码,这个前缀的值是 0xf7 加上列表总长度的二进制形式的字节长度。编码的第一个字节的取值范围是[0xf8, 0xff]。

例子分析:

15(‘\x0f’) = 0x0f(规则一)
空字符串 “” = 0x80(规则二)
字符串 “dog” = [0x83, ‘d’, ‘o’, ‘g’](规则二)

1024(‘\x04\00’) = [0x82, 0x04, 0x00](规则二)
字符串 “Lorem ipsum dolor sit amet, consectetur adipisicing elit” = [0xb8, 0x38, ‘L’, ‘o’, ‘r’, ‘e’, ‘m’, ‘ ‘, … , ‘e’, ‘l’, ‘i’, ‘t’](规则三)
空列表 [] = [0xc0](规则四)
列表 [“cat”,”dog”] = [0xc8, 0x83, ‘c’, ‘a’, ‘t’, 0x83, ‘d’, ‘o’, ‘g’](规则四)

总结
  通过上述的案例分析我们可以看出 RLP 的设计思想,就是通过首字节快速判断一串编码的类型,充分利用了一个字节的存储空间,将 0x7f 以后的值赋予了新的含义,以往我们见到的编码方式主要是对指定长度字节进行编码,比如 Unicode 等,在处理这些编码时一般按照指定长度进行拆分解码,最大的弊端是传统编码无法表现一个结构,就是本文说的列表,RLP 最大的优点是在充分利用字节的情况下,同时支持列表结构,也就是说可以很轻易的利用 RLP 存储一个树状结构。程序处理 RLP 编码时也非常容易,根据首字节就可以判断出这段编码的类型,同时调用不同的方法进行解码,如果您熟悉 jason 这种结构,会发现 RLP 很类似,支持嵌套的结构,通过递归调用可以将整个 RLP 快速还原成一颗树,或者转译成一个 jason 结构,便于其他程序使用。RLP 使用首字节存储长度的位数,再用后续的字节表明整体字符串的长度,根据规则二计算,RLP 可以支持的单个最大字符串长度为 2 的 64 次方,这无疑是个天文数字,再加上嵌套规则,所以理论上 RLP 可以编码任何数据。

欢迎订阅「K 叔区块链」– 专注于区块链技术学习 博客地址:http://www.jouypub.com 简书主页:https://www.jianshu.com/u/756c9c8ae984segmentfault 主页:https://segmentfault.com/blog/jouypub 腾讯云主页:https://cloud.tencent.com/developer/column/72548

正文完
 0