以太坊如何组织账户状态的数据结构
以太坊采用基于账户的模式,系统中显式记录每个账户的余额。我们要完成的是从账户地址到账户状态的映射,addr-->state。
在以太坊中,账户地址为160位,表示为40个16进制数;状态包含了余额(balance)、交易次数(nonce),合约账户中还包含了code(代码)、存储(stroge)。
需要记住的是,在BTC和以太坊中,交易保存在区块内部,一个区块可以包含多个交易。通过区块构成区块链,而非交易。
直观地来看,其本质上为Key-value键值对,直接可以查询每个账户的状态,所以直观想法便用哈希表实现。若不考虑哈希碰撞,查询直接为常数级别的查询效率。但采用哈希表,难以提供Merkle proof。
eg:跟一个人签合同,希望他证明他的账户余额有多少钱
- 一种方法是像BTC中,将哈希表的内容组织为Merkle Tree
- 将哈希表的元素组织成一个Merkle Tree,算出根哈希值,并保存在blockheader中,只要保证根哈希值不变就能保证内容不变。
- 但当新区块发布,哈希表内容会改变,再次将其组织为新的Merkle Tree,如果这样,每当产生新区块(ETH中新区块产生时间为10s左右),都要重新组织Merkle Tree,很明显这是不现实的,代价太大。
- 而在区块链中没有这个缺点,因为比特币系统中没有账户概念,交易由区块管理,每个Merkle Tree包含着一个区块的所有交易,每一个区块对应一个Merkle Tree,一旦发布就不会再改了,所以Merkle Tree不是无限增大的。而ETH中,将账户信息组织成Merkle Tree,很明显其会越来越庞大。
- 第二种方法是不要哈希表了,直接使用Merkle Tree组织账户信息,每次修改只需要修改其中一部分即可
- 实际中,Merkle Tree并未提供一个高效的查找和更新的方案。
- 此外,将所有账户构建为一个大的Merkle Tree,为了保证所有节点的一致性和查找速度,必须进行排序,如果不排序,得到的Merkle Tree可能不一样。
- 为什么区块链没有这个缺点。在区块链中每个人组织的块里的交易顺序是不一样的,但是有决定权的只有有记账权的那个人,所以顺序一不一样无所谓。
- 第三种方法,经过排序,使用Sorted Merkle Tree可以吗?
- 新增账户,由于其地址随机,插入Merkle Tree时候很大可能在Tree中间,发现其必须进行重构,所以Sorted Merkle Tree插入、删除(实际上可以不删除)的代价太大。
- 而且其实创建账户没必要让别人知道,只有状态改变才需要让别人知道
既然哈希表和 Merkle Tree都不可以,实际中以太坊采取的数据结构:MPT(Merkle Patricia trie)
trie(字典树、前缀树)
先了解一个简单的数据结构trie
如下为一个通过5个单词组成的trie数据结构(只画出key,未画出value)
特点:
- trie中每个节点的分支数目取决于Key值中每个元素的取值范围(图中最多26个英文字母分叉+一个结束标志位)。以太坊中分支数目是17(16进制加一个结束标志位)
- trie查找效率取决于key的长度。实际应用中(以太坊地址长度为40)
- 理论上哈希会出现碰撞,而trie上面不会发生碰撞。
- 给定输入,无论如何顺序插入,构造的trie都是一样的。
- 更新操作局部性较好
缺点:
- trie的存储浪费。很多节点只存储一个key,但其“儿子”只有一个,过于浪费。因此,为了解决这一问题,我们引入Patricia tree/trie
Patricia tree/trie
Patricia trie就是进行了路径压缩的trie。
压缩后好处是树的高度缩短,这样访问内存的次数大大减少,效率变高。
需要注意的是,如果新插入单词,原本压缩的路径可能需要扩展开来。那么,需要考虑什么情况下路径压缩效果较好?树中插入的键值分布较为稀疏的情况下,可见路径压缩效果较好。
在以太坊系统中,160位的地址存在2^160 种,该数实际上已经非常大了,和账户数目相比,可以认为地址这一键值非常稀疏。
因此,我们可以在以太坊账户管理种使用Patricia tree这一数据结构。
但实际上,在以太坊种使用的并非简单的PT(Patricia tree),而是MPT(Merkle Patricia tree)。
Merkle Tree 和 Binary Tree
区块链和链表的区别在于区块链使用哈希指针,链表使用普通指针。同样,Merkle Tree 相比 Binary Tree,也是普通指针换成了哈希指针。
所以,以太坊系统中可如此,将所有账户状态组织为一个Patricia tree,用路径压缩提高效率,将普通指针换成了哈希指针,就可以计算出一个根哈希值,这个根哈希值存储于block header中。
根哈希值的作用:
- 第一:防止篡改,只要根哈希值不变,所有账户的状态也不变。
- 第二:提供Merkle Proof,可以证明账户余额,轻节点进行验证
- 第三,证明某个发生了交易的账户是否存在,也就是证明某个键值是否存在。
MPT(Modified Patricia tree)
以太坊中针对原生版的MPT(Merkle Patricia tree)进行了修改,我们称其为MPT(Modified Patricia tree)
下图为以太坊中使用的MPT结构示意图。右上角表示四个账户(为了直观,账户地址显示较短)和其状态(只显示账户余额)。
树中的节点分为三种,第一个是Extension Node,当发生了路径压缩就会有;第二个节点是Leaf Node,表示叶子节点,是最后的;第三个是Branch Node,表示分支节点。还有一个根节点,取哈希得到根哈希值,写入块头。
需要注意这里的指针都是哈希指针,普通指针里面存的是下一个节点的地址,而这里的哈希指针存的是下面节点的哈希值。
每次发布新区块,状态树中部分节点状态会改变,但改变并非在原地修改,而是新建一些分支,原本状态是保留的。
如下图中,仅仅有新发生改变的节点才需要修改,其他未修改节点直接指向前一个区块中的对应节点。
所以,系统中全节点并非维护一棵MPT,而是每次发布新区块都要新建MPT,只不过大部分节点共享,只有少数发生变化的节点是要新建分支的。
为什么要保存原本状态?为何不直接修改?
为了便于回滚。如下1中产生分叉,而后上面节点胜出,变为2中状态。那么,下面节点中状态的修改便需要进行回滚。
以太坊中有智能合约,理论上能实现很复杂的功能,如果不保存历史状态,智能合约执行完后,想推算出以前的状态是不可能的,因此,需要维护这些历史记录。
以太坊中代码的数据结构
block header 中的数据结构
ParentHash是前一区块块头的哈希值。Coinbase挖出区块的矿工的地址。
三棵树:状态数、交易树、收据树
Difficulty也需要根据需要修改
Nouce挖矿中需要猜的随机数
区块结构
区块在网上真正发布时的信息
状态树中保存Key-value对,key就是地址,而value状态通过RLP (Recursive Length Prefix,一种进行序列化的方法,特点是很简单) 编码序列号之后再进行存储。