「研」 以太坊区块结构
本文最后更新于:1 年前
此篇文章用以太坊为例,研究一下一个区块里面到底有什么,以及以太坊中的数据是怎么存储的。
区块结构
一个区块由两部分组成:
- 区块头(Header)
- 区块体(Body)
轻节点只会存储 Header。
区块中的三棵树:
状态树:状态树是全局的、不断更新的,它的 key 为 keccak256(ethereumAddress),value 为 rlp(ethereumAccount)。其中 ethereumAddress 表示以太坊账户地址;ethereumAccount 表示以太坊账户,包含四个字段:nonce,balance,storageRoot,codeHash。如果 storageRoot 和 codeHash 不为空,则为合约账户,其中 codeHash 对应合约代码的哈希,storageRoot 对应另一棵树的树根,这棵树我们称为存储树。存储树存储了合约的所有状态数据,每个合约有单独的存储树。
交易树:每个区块都有一棵独立的交易树,对应区块头里的交易根。交易树的 key(路径)为 rlp(transactionIndex),value 为交易序列化后的值。其中 transactionIndex 表示交易在该区块中的下标。
回执树:每个交易对应一个交易回执,交易回执记录了交易执行结果,包括执行状态、Gas 消耗、事件日志等。每个区块有自己的回执树,对应了区块头里的回执根。与交易树类似,key 为 rlp(transactionIndex),value 为交易回执序列化后的值。
数据结构
以太坊中的账户状态是用什么数据结构存储的呢?
有关这个问题肖臻老师的公开课有非常详细的解释。这里直接说结论:Merkle Patricia Trie(MPT)
右上角为简化的 key-value 定义。我们可以看到图中有 2 个拓展节点,2 个分支节点,4 个叶子节点。在最下面的两个叶子节点中,prefix 3 的右边有个格子,有个箭头从 7 指向这个格子,表示 3 和 7 两个 nibble 组成一个字节存储。当一个节点被另一个节点在内部指向时(比如上图中的分支节点内部指向了叶子节点),父节点会存储 H(rlp.encode(x))。
其中 H(y) = keccak256(y) if len(y) >= 32 else y,rlp.encode 为 RLP 编码函数。
由于 MPT 树是确定性的,所以如果两棵树存储了完全相同的数据,那么这两棵树的节点将完全相同,包括根节点。假设在某一时刻,当其中一个合约修改了某个变量的数据,使得它与另一个合约的数据不同时,会生成一个新的节点,并从新节点开始由下往上直到根节点,整个路径的节点值都会更新,新生成的节点会存储到硬盘,但旧的节点不会从硬盘删除。
StateDB
在以太坊中,StateDB 是一个用于管理账户状态数据的内存数据库,它的数据存储在节点的内存中,并且随着区块链的不断增长而不断变化。但是,为了确保数据的可靠性和持久性,StateDB 数据需要被定期持久化到磁盘中。
为了实现数据的持久化,以太坊节点使用了 LevelDB 这样的键值对存储引擎。当 StateDB 中的数据发生变化时,节点会将这些变化记录在 LevelDB 中,以便在下次启动时从磁盘中重新加载数据。
需要注意的是,由于 LevelDB 是一种磁盘存储引擎,相比于内存数据库,它的读写速度要慢很多。因此,在以太坊节点中,StateDB 通常被用作内存数据库,而 LevelDB 则用于实现数据的持久化。这种方式可以同时满足节点的高性能和数据的可靠性要求。
block.go 源码
研究那么多不如来看一看区块结构的源码:go-ethereum/core/types/block.go
1 |
|
可见一个区块携带的数据主要是:Header 中的信息 + 叔块们的头部信息 + 块中包含的所有交易信息。