《迅雷链精品课》第七课:以太坊数据存储分析

上一节课我们学习了比特币的区块链数据存储,接着前一篇的内容,我们继续了解以太坊的相关内容。业界一直把以太坊认为是区块链发展进程中2.0的代表,因为它在比特币的基础上增加了图灵完备的智能合约,扩展了区块链的使用场景,同时还时刻保存着一份账户状态,方便查询账户信息。下面我们将在正文分析以太坊是如何实现这些特性。

在学习课程的时候,你也可以领取BaaS平台为期一个月的试用机会,免费使用高性能区块链服务(点击链接即可免费领取https://blockchain.xunlei.com/baas/try.html)。课程学习结合实践操作,让你迅速成为区块链大牛!

数据结构

MPT全称Merkle Patricia Trie,是以太坊用来组织管理账户数据、生成交易集合哈希的重要数据结构, 它融合了Trie、Patricia Trie、Merkle Tree这3种数据结构的优点,如下图所示:
在这里插入图片描述
MPT数据结构演变过程

在介绍MPT之前,我们首先简要地介绍一下其它几种树的特点:

Trie

Trie树,又称前缀树或字典树,是一种用于快速检索的多叉树结构,其中的键通常是字符串,如英文字母的字典树是一个26叉树,数字的字典树是一个10叉树。与二叉查找树不同,键不是直接保存在节点中,而是由节点在树中的位置决定。一个节点的所有子孙都有相同的前缀,也就是这个节点对应的字符串,根节点对应空字符串。实际上trie每个节点是一个确定长度的数组,数组中每个节点的值是一个指向子节点的指针,最后有个标志域,标识这个位置为止是否是一个完整的字符串。

在这里插入图片描述
三个字符串在字母Trie中的存储情况

Trie树的优点是最大限度地减少无谓的字符串比较,查询拥有共同前缀key的数据时十分高效;缺点是内存消耗大,尤其是前缀不重复的情况下,其次直接查询效率没有哈希表高。

Patricia Trie

Patricia Trie称为压缩前缀树,是一种更节省空间的Trie。对于每个节点,如果该节点是唯一的儿子的话,就和父节点合并。

在这里插入图片描述
Patricia Trie展示的字符串存储情况

Merkle Tree

Merkle Tree在前面一篇文章有介绍,这里不再过多介绍。

Merkle Patricia Trie

Merkle Patricia Trie是一种经过改良、融合了默克尔树和前缀树两种树结构优点的数据结构。在以太坊中,为MPT树新增了几种不同类型的树节点(空节点、分支节点、叶子节点、扩展节点),以尽量压缩整体的树高、降低操作的复杂度:

空节点(Null Node):初始化的节点;

分支节点(Branch Node):用来表示拥有超过一个孩子节点以上的非叶子节点,从其名称上可以看出来,这个节点可以引出多个分支;

叶子节点(Leaf Node):用来表示唯一的一个kv数据;

扩展节点(Extension Node):和叶子节点结构相同,但用它来表示多个key中的重复部分,value指向下一个节点而不是真实数据。增加这个节点的目的主要是压缩节点,减少存储空间;

既然叶子节点和扩展节点的结构是相同的,那么怎么区分这两个节点呢?这就要先说一下MPT中对这两种节点用到的key值编码方式:Hex-Prefix编码(十六进制前缀编码)。Hex-Prefix编码的规则如下:

  1. 在key之前增加一个4bit的flag前缀,其中最低位用来编码原本key长度的奇偶信息,key长度为奇数,则该位为1;低2位中编码一个特殊的终止标记符,若该节点为叶子节点,则该位为1;

  2. 若原本key的长度为偶数,则在key之前再增加一个值为0x0的4bit前缀,因为在十六进制编码中一个字节用两个字符表示,所以总的字符长度必须是偶数,这一步属于补位操作;

在这里插入图片描述
扩展节点和叶子节点几种Prefix

经过改良和融合之后的Merkle Patricia Trie也形成了自身的特性:

a. 所有的key都经过十六进展编码转换之后,再存入MPT树中;

b. 每个节点按hash引用,该hash代表着另外一个节点在数据库中索引,当整棵树存储到leveldb中也能保持关联关系;

c. 根据节点hash读取节点数据,不用加载整棵树的数据,避免了不需要的IO开销;

下面我们用四个kv数据,使用一棵MPT树结构来展示数据从处理、组织到存储的整个流程,从这个过程中也就可以看出以太坊是如何组织交易,账户状态等数据的。

在这里插入图片描述
四个kv数据十六进制编码转换

在这里插入图片描述

上图所示就是刚才处理之后的四个kv数据使用MPT结构组织的示意图,从这个图中可以看到分支节点、扩展节点和叶子节点的结构。分支节点之所以十六个数组再加一个value字段,是因为在以太坊中所有的key值都是十六进制的,所以key值里面的数字全部在0-f范围内;叶子节点和扩展节点结构完全一样,根据prefix来进行区分,prefix的值是按照Hex-Prefix编码得得到的,叶子节点的value是真实要存储的数据,扩展节点的key存储的是子节点共有的key,value指向下一个节点。当数据在内存中时按照上图组织,但当这些数据要存储到磁盘时,节点之间的关联关系就需要变成hash引用。从内存结构演变成存储到磁盘的kv过程如下两图所示:

在这里插入图片描述

节点之间通过hash引用

在这里插入图片描述

MPT树中节点编码成kv数据

从上面两张图可以看到节点之间由hash建立关联关系,并把每个节点编码成kv数据存储格式的过程。根节点的hash就是整棵树的HashRoot,每个节点按照kv方式存储到磁盘,在需要的时候可以根据HashRoot把整棵树在内存中还原,也可以单独根据节点的hash访问单个节点。

区块和交易

以太坊区块结构和比特币相似,由Header和Data组成,且它们都是采用的POW共识算法,所以POW相关的字段都有,区别在于以太坊的Header中多了两个关键的State Root和Receipt Root字段,下图所示为以太坊区块示意图:

在这里插入图片描述

以太坊区块示意图

Transaction Root:区块中包含的所有交易的hash;

State Root:所有账户(包括合约账户)状态的hash;

Receipt Root:所有交易执行结果(包括合约创建和执行结果)的hash;

上述三种HashRoot分别对应着三个MPT结构,而这些MPT结构都与区块息息相关。MPT组织数据的过程,在前面我们已经举例说明了,这里不再详述,区别就是数据源不一样,key值不相同。图中还有一个storage Trie,这也对应着一个MPT结构,这个树属于每个账户,后面详细介绍。在区块中的数据Data字段,包含了区块打包的所有交易,交易的数量会受到Header中GasLimit的限制。

在这里插入图片描述

Transaction Root计算过程

上图所示就是Transaction Root的计算过程,把一个区块中所有的交易用MPT组织,根节点的hash就是需要的数据。这里可以看到写入MPT的kv数据,key值是单个Transaction在整个交易列表中序号的(0,1,2,…)rlp编码值,value是Transaction的rlp编码值。rlp编码是以太坊结构体序列化的主要编码方式,它的内部实现细节这里不详细介绍,可以查阅相关文档。

在这里插入图片描述

区块序列化成kv数据

在以太坊中所有数据都存储在leveldb数据库,即所有要存储的数据都以kv方式存储,这和比特币用文件存储区块数据不太一样。上图所示为一个区块序列化成kv数据的过程,单个区块序列化成两个kv,一个存储header信息,key为标识符小写字母h+区块号+区块Hash, value为Header数据的rlp编码;一个存储Data信息,key为标识符小写字母b+区块号+区块Hash, value为Data数据的rlp编码。

除了区块序列化之外,以太坊为了高效的查询建立了很多索引,这些索引信息也以kv方式存储在leveldb数据库,例如上图中的交易信息索引。交易信息索引信息key为小写字母l+交易hash,value为结构体(TxLookupEntry)的rlp编码值。再列举几个:

  1. ‘H’+Block Hash -> Block Number 区块hash到区块高度索引

  2. ‘h’+Block Number+’n’ -> Block hash 区块高度到区块hash索引

  3. ‘r’+Block Number+Block Hash-> Block Receipts 区块Receipts索引信息

当然以太坊中的索引信息不止上面列举的这些内容,正是由于这些索引信息的存在,在业务层使用过程中才会高效的查询到需要的数据信息,包括区块浏览器从各个维度查询。

账户状态

区块链中的账户和银行系统的账户类似,就是记录资产的一个地址。在以太坊中有两种账户——普通账户和合约账户:用于个人数字资产记录的账户称为普通账户,这类账户由私人密钥控制,拥有者可以查询余额,可以创建和签名一笔交易发起转账或者支付;区块链为成功部署于其上的合约代码和数据生成的帐号称为合约账户,它是代码(它的功能)和数据(它的状态)的集合。这两类账户的数据使用相同结构来记录,即Account:

在这里插入图片描述

Account结构图

在Account中有Nonce、Balance、Storage Root、Code Hash四个属性值。Nonce是一个递增的整数值,用来标记每个账户发起交易时的交易序号,成功发起一笔交易该值就加1;Balance记录账户的资产余额;Storage Root和Code Hash是针对合约账户的,Storage Root是合约代码在evm中执行完成后,所有成员变量以MPT组织之后的根节点的hash,而Code Hash就是合约账户对应的合约代码的hash。

在以太坊中,每个账户(Address)对应着一个StateObject,StateObject包含一个Account,每个block对应一个完整的包含所有address的状态信息即StateObject的集合,可以称为账户状态。和block对应的账户状态使用MPT格式存储,即前面提到的State Trie,block中记录State Trie根节点的hash。使用MPT来存储账户的好处是每个block对应的State Trie可以只存储和计算有变化的节点,未变化的节点可以直接使用hash引用前面一个区块存储的节点,节省资源空间,提高存储效率。

在这里插入图片描述

两个State Trie的变化实例

假设StateRoot A在区块1,StateRoot B在区块2,两个区块因为一个账户0x2565bb的余额发生了变化如图13所示,我们来分析一下两个State Trie的变化。由于一个账户的状态变化仅仅影响根节点的一个分支,其它分支是不受影响的,所以从0x2565bb账户数据所在的叶子节点向上遍历到根节点经过的节点都受到了影响,这些节点的hash值都需要重新计算,并且重新存储,而没有任何改变的节点不需要重新计算和存储。在上图中根节点序号为1的分支还是引用上个区块存储的分支节点hash,而序号为2的分支引用的hash值需要更新,从而会得到一个新的StateRoot B。此时State Trie B只需要单独存储图中虚线的叶子节点和根节点,需要存储的数据很少。

从上面的例子可以看出以太坊使用MPT存储数据的巧妙之处,既在每个区块关联了所有账户的状态,但又没有使用太多的存储空间,查询效率也非常高。

合约执行

合约代码其实就是用一种编程语言编写的一段代码,该代码编译成二进制数据之后可以在以太坊虚拟机evm中被加载和执行。一个合约账户只对应一份合约代码数据,但同一份合约代码可以对应多个合约账户。既然合约是代码,那么合约是如何被执行的,执行结果怎么存储的,我们一步步分析。

首先,当我们发起一笔交易,to地址为空,交易中数据字段带上编译之后的合约代码,在evm执行这笔交易时,就会认为是创建合约的交易。接下来evm就会自动创建一个合约地址、执行合约代码并初始化代码中的变量、新建一个Account与合约地址绑定、存储合约代码和数据。前面讲过,以太坊中所有数据都是以kv方式存储的,合约代码就以其hash为key,代码本身为value进行存储,同时合约代码hash会设置到Account中的Code Hash;合约代码初始化之后的成员变量也按照一定规则序列化成kv,使用MPT(即前面提过的Storage Trie)组织并存储,计算出根节点的hash设置到Account中的Storage Root;如果交易中带了资产,这些资产会设置到Account中的Balance字段,转移给合约账户。

其次,当新来一笔to地址为合约地址,调用合约中某个方法的合约交易时, evm就会找到合约地址对应的Account,然后根据Code Hash加载合约代码,并在执行代码过程中加载成员变量的值进行逻辑运算。在合约执行完成之后,修改过的变量会再进行存储,然后重新计算Storage Root修改账户状态信息。

当然,合约交易的执行并不仅仅是修改成员变量的值,还可以通过逻辑代码进行资产的自动化转移改变其他账户的状态,这是普通交易做不到的。

小 结

本文从区块结构、交易结构和存储方式介绍了比特币实现方式和存储的相关内容,展示了区块链在去中心化、数据防篡改等方面的优势。下一篇继续介绍以太坊区块链相关方面的内容。

*恭喜完成第七课的学习,第八课我们将继续了解迅雷链多链结构的课程,欢迎关注~

  • 1
    点赞
  • 0
    收藏
    觉得还不错? 一键收藏
  • 0
    评论

“相关推荐”对你有帮助么?

  • 非常没帮助
  • 没帮助
  • 一般
  • 有帮助
  • 非常有帮助
提交
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值