03 ETH-状态树

发布时间 2023-05-02 14:41:41作者: YangYi215

03 ETH-状态树

地址到状态(balance、nonce、code、storage)的映射。

以太坊地址一般160bits,一般表示为40个16进制的数。


那么如何设计映射?像是 key:value pair?那么,能不能只用一个hash表来实现?(如果不考虑hash碰撞的话),那这样是不是太简单了?

用hash表的话,如果需要提供Merkle Proof,如何提供?

比如:一个人要签合同,需要别人证明一下,他有这么多钱,这个如何提供证明?

一个简单的方法,把hash表中的元素,组织成一颗Merkle tree,算出一个根hash值,根hash值需要保存在block header中,公布出去,根hash值只要是正确的,就能保证数据不被篡改。这个有什么问题吗?

不要一开始就看什么数据结构,而是一步步思考,想想数据结构是如何设计出来的。(多想想别人是为什么这样设计?)

新区块中包含有新的交易,执行交易必然会使hash表的内容发生变化,我们发布下一给区块的时候,难道要把hash表中的数据再重新组织成Merkle tree吗?那这个代价是不是太大了?

实际上,真正发生账户状态变化的只是一小部分。只有新区块中交易所关联的账户才会发生变化,大多数账户的状态是不变的。每次构建一次Merkle tree,代价是很大的。

比特币中不是每出一个区块,也要构建一颗Merkle tree吗,那个为什么没有这个问题?

比特币中的Merkle tree构建完之后,是不会再改了,下次发布新的区块,再重新构建Merkle tree。比特币中构建Merkle tree也仅仅是几百到几千比交易而已,以太坊中如果采用,会是什么结果?

是要把所有的以太坊账户一起构建成一个Merkle tree,这个数目比之前的比特币的交易数目高出好几个数量级。


除了验证账户的数据之外,Merkle tree还有更重要的特性,维护全节点之间数据的一致性。各个全节点之间要保持状态的一致才行。这也是比特币中为什么要把根hash值写在块头中的原因,对于当前区块的状态,所有全节点要有一个共识。

所以,如果每个全节点简单在本地维护一个hash表,然后构建出一个Merkle tree来,这种方法是不行的。(hash表本身查找、更新效率很好,但是构建Merkle tree,效率太低)


我们能不能不要hash表,然后直接构建一个账户的Merkle tree,修改数据直接在Merkle tree中修改(每次只需要改Merkle tree中的一小部分),该方法可行吗?

问题:Merkle tree没有提供一个快速查找、更新的方法。

如果我们把所有的账户放在Merkle tree中,这个Merkle tree要不要排序?Sorted Merkle tree

不排序没有办法证明non-membership。

如果不规定叶结点中的排列顺序,这样构建出来的Merkle tree不是唯一的。相应的,根hash值也是不一样的。

比特币当中不也是不排序吗?比特币中每个全节点收到的交易顺序也是不一样的,但是最后是获得记账权的节点说了算。

希望能够设计出自己的加密货币,设计出更好的数据结构。

如果以太坊中这样做,需要将全节点维护的Merkle tree中发布到区块中,但是包含的发布的是所有账户的状态,不是仅仅是交易的状态。数量级差距。

所以,不排序的Merkle tree是不行的。


如果用Sorted Merkle tree,是不是就没有问题了?

以太坊账户也是随机产生的,如果新产生的账户,刚好在树叶子结点的中间位置,后面的叶子节点数目也会发生改变。又变成了每次产生一个Merkle tree。这样代价也太大了了。

插入和删除代价都太大了。


以太坊采用MPT结构,

数据结构trie(来自retrieval)字典树、前缀树。

特点:

1 trie 中每个节点的分支数目取决于 key 值中每个元素的取值范围。

以太坊地址是40个16进制的树,分叉数目叫做 branding factor,0~f + 1(结束标识符),共17个。

2 trie的查找效率取决于 key 的长度,key的值越长,查找需要访问的内存次数就越多。

以太坊中的地址的键值都是一样长的。(比特币和以太坊的地址是不通用的,格式是不一样的)

3 如果用hash表来进行账户的存储,从理论上说,有可能存在hash碰撞,trie中是不会出现碰撞。

4 不论输入怎么打乱顺序,最后构成的 trie 是同一棵树。

5 更新的局部性(每次一笔交易,只有绝少数账户的信息会发生变化,所以,更新操作的局部性很重要)(注:上图中只画出来key,没有画出value)

缺点:

直观上可以看到,树结构深。

将单个节点进行合并,可以降低存储和查找的开销。


Patricia tree / Paticia trie

经过了路径压缩的前缀树。

访问内存的次数大大减少,效率提高。

但是,如果新插入一个单词,原来的路径可能需要拓展出来。

路径压缩在什么情况下效果比较明显?

键值分布比较稀疏的情况下,效果比较好。

以太坊中键值是地址,以太坊的地址是160位,为 $2^{160}$。

hash碰撞是可能存在的,地址的分布要足够长,分布足够稀疏,这样才不会产生碰撞,这是去中心化系统防止碰撞的唯一办法。

?


MPT(Merkle Patricia tree)

Merkle tree 和 binary tree 有什么区别?区块链和链表有什么关系?

hash指针代替常规指针。

将所有用户的账户信息组织成一个Patricia tree,用路径压缩提高效率,然后把普通指针换成hash指针,所以可以计算出一个根hash值。

比特币中区块头中只有一个根hash值,以太坊中有3个(状态树、交易树、收据树)。


MPT的根hash值有什么用?

  1. 防止篡改
  2. Merkle proof
  3. 可以证明MPT树中某个键值是不存在的(对应的账户不存在)。

以太坊中使用的是Modified MPT。

比如说:最下层3这个位置存放的是leaf node的hash值。


每次发布一个新区块的时候,状态数中一些节点的值会发生变化,改变不是在原地改的,而是新建一些分支,原来的状态其实是保留下来的。

树中大部分节点是共享的,只有发生改变的节点需要新建一个分支。(上述是一个合约账户,合约账户的存储也是用MPT的形式保存)

大的MPT包含小的MPT,每一个合约账户的存储都是一个小的MPT。

所以,系统中每个节点不是维护一个MPT,而是区块生成之后,都要新建一个MPT,只不过状态数中大部分节点是共享的。只有少数发生状态的节点需要新建分支。


问题:为什么要保留历史状态?为什么不原地直接修改?

以太坊出块时间很短,导致临时性的分叉很普遍,需要将交易进行回滚。

为了回滚而保存历史记录。

以太坊中有智能合约,智能合约中有时需要推算出之前的状态。所以为了支持回滚,必须保存历史状态。


以太坊中代码的数据结构:

区块头:

区块结构:

真正发布出去的区块:


状态树中保存的是 key:value pair,key是地址,value是怎么存储在状态树中的?需要经过序列化(RLP [Recursive Length Prefix]),protobuf一个很有名的做序列化的库。

RLP很简单,只支持一种类型,nested array of bytes。