Jenner's Blog

不变秃,也要变强!

0%

账户模型

优势

  1. 节省大量空间
  2. 可替代性更高
  3. 实现简单
  4. 轻客户端

账户基本概念

外部账户

EOAs-外部账户(external owned accouts)是由人们通过私钥创建的账户。 是真实世界的金融账户的映射,拥有该账户私钥的任何人都可以控制该账户。 如同银行卡,到ATM机取款时只需要密码输入正确即可交易。 这也是人类与以太坊账本沟通的唯一媒介,因为以太坊中的交易需要签名, 而只能使用拥有私有外部账户签名。
外部账户特点总结:

  • 拥有以太余额。
  • 能发送交易,包括转账和执行合约代码。
  • 被私钥控制。
  • 没有相关的可执行代码。

合约账户

含有合约代码的账户。被外部账户或者合约创建,合约在创建时被自动分配到一个账户地址,用于存储合约代码以及合约部署或执行过程中产生的存储数据。 合约账户地址是通过SHA3哈希算法产生,而非私钥。因无私钥,因此无人可以拿合约账户当做外部账户使用。只能通过外部账户来驱动合约执行合约代码。
合约账户特点总结:

  • 拥有以太余额。
  • 有相关的可执行代码(合约代码)。
  • 合约代码能够被交易或者其他合约消息调用。
  • 合约代码被执行时可再调用其他合约代码。
  • 合约代码被执行时可执行复杂运算,可永久地改变合约内部的数据存储。

数据结构

1
2
3
4
5
6
type Account struct {
Nonce uint64
Balance *big.Int
Root common.Hash
CodeHash []byte
}

外部账户的 Root/StateRootHash/StorageRootHash 和 CodeHash 是一个空默认值。

存储

MPT存储所有账户状态。

账户状态值存储

账户状态数据经过RLP(Recursive Length Prefix)序列化后存储,RLP序列化方式相比protobuf较为简单,只支持字符嵌套数组(nested array of bytes),实现起来比较容易。

RLP编码

RLP编码定义

RLP编码只处理两类数据:一类是字符串(字节数组),一类是列表。

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

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

参考

以太坊技术与实现

点击下方打赏按钮,获得支付宝二维码