区块链学习笔记15——ETH状态树
学习视频:北京大学肖臻老师《区块链技术与应用》
笔记参考:北京大学肖臻老师《区块链技术与应用》公开课系列笔记——目录导航页
在以太坊中,有三棵树的说法,分别是状态树、收据树和交易树。了解了这三棵树,就弄清楚了以太坊的基础数据结构设计。
引入
要实现的功能:地址到状态的映射
ETH的账户地址是160位的,一般表示成40个十六进制数
状态就是外部账户和合约账户的状态,包括余额、交易次数,合约账户还有代码和存储。
数据结构的组织形式
-
直观上地址到状态的映射用哈希表存储比较简单,为什么不用哈希表存储呢?
用哈希表如何提供Merkle Proof?将哈希表内容组织为一颗Merkle tree像比特币一样提供Merkle Proof,但是当新区块发布时,哈希表内容会改变,我们需要再次将其组织为新的Merkle Tree,这样的话,每当产生新区块(ETH中新区块产生时间为10s左右),都要重新组织Merkle Tree,这样的代价是很大的,实际上发生变化的账户只有新交易所关联的那一部分。
需要注意的是,比特币系统中没有账户概念,交易由区块管理,而区块包含上限为4000个交易左右,每次发布一个区块对应一棵新的Merkle tree,一旦发布是不会改变的,所以Merkle Tree不是无限增大的。而ETH中,Merkle Tree用来组织账户信息,是要把所有的以太坊账户一起构建一个Merkle tree,这个数目比BTC中的Merkle tree会大好几个数量级。
实际中,发生变化的仅仅为很少一部分数据,我们每次重新构建Merkle Tree代价很大 -
能不能不用哈希表,直接用Merkle tree,改的时候直接在Merkle tree里改?
Merkle tree没有提供一个高效的查找和更新的方法。为了保证所有节点的一致性和查找速度,必须进行排序。如果以太坊不排序的话,每个全节点维护的账户顺序不一样那么计算的Merkle tree的根哈希也是不一样的,比特币系统中虽然没有进行排序,但是交易的顺序是由发布区块的结点确定的,顺序是唯一的。如果以太坊要想实现的话必须把所有账户的状态也发布到区块上,数量级很大是不可行的。 -
如果用排序的Merkle tree会有什么问题?
插入账户的话代价很大
综上所述,以上两种简单的数据结构均是不可行的,实际中以太坊采取的数据结构是MPT。
简单的数据结构——trie
trie:字典树,边代表字母,从根结点到树上某一结点的路径就代表了一个字符串
特点:
- 每个结点的分支数目取决于Key值中每个元素的取值范围
图例中的分支数目为27(最多26个小写的英文字母+一个结束标志位);以太坊中的地址为40个十六进制数,所以分支数目为17(16个十六进制数+一个结束标志符) - 查找效率取决于key值的长度,以太坊中所有的键值是相同长度,都是40位16进制数
Ps:比特币和以太坊的地址是不通用的,格式长度均不同,以太坊的地址是公钥取哈希从前面截取一段(160bit)得来的 - 哈希表存储理论上是会出现哈希碰撞的,trie是不会出现碰撞的
- 给定一组输入构成的trie是相同的,与顺序无关
- 更新的局部性比较好,只需要更改元素所属的分支即可
缺点:
- 存储浪费,很多中间节点都只有一个子节点,如果能将其合并,可以减少存储的开销,同时提高了查找的效率。因此,为了解决这一问题,我们引入Patricia tree/Patricia trie。
Patricia tree
经过了路径压缩的前缀树
需要注意的是,如果新插入单词,原本压缩的路径可能需要扩展开来。
路径压缩在什么情况下效果比较好:键值分布比较稀疏
Trie:
Patricia tree:
以太坊的地址是160bit的,共有2160种,是非常稀疏的,这样可以避免产生碰撞,这是去中心化系统防止产生碰撞的唯一办法。
MPT——Merkle Patricia tree
Merkle tree与binary tree的区别:把普通指针转为哈希指针
Merkle Patricia tree:所有账户组织成一棵Patricia tree,用路径压缩提高效率,然后把普通指针换成哈希指针,这样就可以计算出一个根哈希值。(比特币的block header中只有一个根哈希值,就是区块中包含的交易组成的Merkle tree的根哈希值;以太坊中有三个)
这个根哈希值的作用:
- 防篡改
- Merkle proof,证明账户余额
- 证明账户不存在,如果存在的话应该存在哪个分支上,将这个分支作为Merkle proof发送,可证明其不存在
实际用到的MPT——Modified MPT
每次发布新区块,状态树中部分节点状态会改变。**但改变并非在原地修改,而是新建一些分支,保留原本状态。**如下图中,仅仅有新发生改变的节点才需要修改,其他未修改节点直接指向前一个区块中的对应节点。
合约账户的存储也是MPT形式保存,也是用一棵MPT,以太坊的结构是一棵大的MPT包含很多小的MPT,每一个合约账户的存储都是一个小的MPT
所以,系统中全节点并非维护一棵MPT,而是每次发布新区块都要新建MPT。只不过大部分节点共享。
为什么要保留历史状态?
系统中有时会出现分叉,出块时间降到十几秒,临时性的分叉是很普遍的。
保留历史状态为了便于回滚。
如下图1中产生分叉,而后上面节点胜出,变为2中状态。那么下面节点中状态的修改便需要进行回滚,将这个结点的状态取消掉,退回到上一个区块,然后沿着上一个链向后推进。
因此,需要维护这些历史记录。
比特币中的交易可以通过简单的反向交易退回到之前的状态,但是以太坊中有智能合约的存在,一旦执行完后就无法推算出之前的状态,所以要想回滚必须保存历史状态。
以太坊中代码的数据结构
区块头的结构:
区块的结构:
区块在网上真正发布时的信息:
value的存储
状态树中保存的是(key,value)
key就是地址,value是经过RLP(Recursive Length Prefix)编码做序列化之后再存储
RLP的特点:简单,只支持一种类型——nested array of bytes