以太坊数据库

以太坊中处理过的的区块链相关的数据最终都会持久化到数据库中,以太坊采用的底层数据库是 LevelDB,LevelDB 是由 Google 开发的基于 key-value 的非关系型数据库存储系统,特别适用于写多读少的场景。
在 Geth 启动流程这一节的介绍中提到过,创建 Ethereum 实例时,创建一个数据库用于存储链相关的数据,该数据库实例为 LDBDatabase,对 levelDB 的基本操作进行了一层封装,提供数据库的操作接口,数据库的名字为“chaindata”。Ethereum 中的组件也引用了该实例作为数据库,如图 3-25 所示。

3.9.1 rawdb

rawdb实际上是Geth提供的一个Golang包,提供了直接通过LDBDatabase读写区块链相关数据的接口。按照存储的数据类型不同,接口可以分为3类,具体见表3-3至表3-5。

image.png

 

image.png

 

image.png

3.9.2 stateDB

除了存储3.9.1节的链相关的数据,更重要的是存储每个账户的状态数据,因为以太坊支持合约账户,每个合约账户会存储大量的状态数据,以太坊中使用 MPT 树(Merkle Patricia trie)来组织所有状态数据,再将组织后的数据存储到底层数据库 LevelDB。MPT 树的机制在其他章节中有介绍,这里不再赘述,本节通过一个实例来展示以太坊对 MPT 树的实现。
首先来看一下以太坊MPT树键值路径的表示方法。我们知道MPT树是在Patriciatrie树的基础上结合Merkle提供数据真实性快速验证。一般情况下使用trie树,数据的路径path为26个字母组成的字符串,例如“coin”。可以使用长度为26的数组来索引字母组成的路径。而在以太坊中存在大量的Hash值,形如“0x8c4c3dfe045770a8bc...,”如果将其作为路径,则不适合用字母表来索引,需要寻找另一种方式来索引Hash路径,以太坊使用十六进制值来做索引,索引数组长度也变为16(0~0xf)。以太坊将Hash值转换为十六进制字符串“8c4c3dfe045770a8bc...”,为了统一算法,也将一般字符串转换为字节数组,例如“coin”为[64,6f,67,65],再转换为十六进制字符串“646f6765”,然后取出每一个元素,其数值范围就是0~15,作为索引,这样就统一了Hash和字符串索引:[8,c,4,c...],[6,4,6,f...]。
通过拆分字节的方式统一了键值路径的表示方法是一种可行的思路,但是这种方式在代码实现时会浪费存储空间:之前一个字节(8c),要使用2个字节的空间来分开存储:[8,c]。以太坊为了节约空间,使用了一种压缩字节的表示方法,将2个字节拼成一个字节存储,只是计算的时候分别取出来使用,但这样又引入了一个新的问题,由于Patricia是压缩trie树,压缩后的索引长度存在奇数的情况,无法组成完整的字节,例如[64,6f,67,65],压缩后可能是[6,46f,6765],这样一个字节就无法放下 46f 这样的索引了。为了解决这个问题,以太坊对索引增加一个字节的前缀,该前缀前 4 位指示当前索引是奇数长度还是偶数长度,实际上也是指示扩展节点还是叶子节点,具体表示方式如表3-6所示。

image.png

如果是奇数长度,那么该前缀的后 4 位用来存储多出来的索引值,例如压缩后的索引[6,46f,6765] 添加前缀之后,可能表示为 [16,146f,206765] ,其中 16 表示奇数长度扩展节点索引,146f 表示奇数长度扩展节点索引,206765 表示偶数长度叶子节点索引。
了解了以太坊键值索引的设计之后,下面来看一个实际的例子,为了方便展示,定义 4 组 key值为字符串的键值对:
image.png

接下来看一下生成 MPT 树后的结果,如下:
image.png

这就是一棵完整的按照Patricia树组织的压缩前缀树,数据以Hash为Key存储在levelDB上,Hash正如merkle树一样,自下而上生成,最终得到一个rootHash,任何数据的篡改,都能通过比对根Hash来发现。
如果要查询“whois”这个key对应的值是多少,则从rootHash开始,取出数据为[<17,76>,hashA],匹配索引为776,因为是奇数并且是扩展节点,所以加前缀1,找到HashA,接下来的索引是8,在HashA对应的值中找到第8个值,为HashB,继续从数据库中取出HashB的数据,发现还是个扩展节点,匹配索引6f,取出HashD的数据,接下来取出索引6,发现是HashE,从数据库中取出HashE的数据,发现还是个扩展节点,但是索引正好是973,表明path路径已搜索完毕,value值存放在HashF的数据的最后一个成员中,即“potato”。
相信通过上面的例子,读者应该对以太坊 MPT 的组织和存储有了清晰的认识。接下来继续看 stateDB 结构的组织。
为了方便上层服务操作状态数据,这里通过提供一个 stateDB 对象,为每个账户分配一个stateObject实例,简化了上层服务对状态数据 MPT 树的操作,如图 3-26 所示。

 

image.png


stateDB存储与MPT树相关的所有数据,StateDB为区块中每一个需要修改状态的账户建立一个stateObject对象,trie.Database是一个存储已读取和待写入底层数据库trie数据的缓存,state.cachingDB实现了state.Database接口,是关于trie和合约代码的操作接口,屏蔽了trie树的操作细节,定义如下:

type Database interface {
    // 打开账户trie树
    OpenTrie(root common.Hash) (Trie, error)
    // 打开一个账户的存储trie树
    OpenStorageTrie(addrHash, root common.Hash) (Trie, error)
    //复制给定的trie树
    CopyTrie(Trie) Trie
    // 获取合约字节码
    ContractCode(addrHash, codeHash common.Hash) ([]byte, error)
    // 获取合约大小
    ContractCodeSize(addrHash, codeHash common.Hash) (int, error)
    // 获取底层存储trie数据的数据库
    TrieDB() *trie.Database

}


由此可见,为了管理和操作状态数据,stateDB 只维护了需要修改的账户和状态数据集合,通过分级“缓存”的方式抽象各个操作,使得每一层次都“可插拔”,操作过程总体如下。
1)数据的更新在封装区块和验证区块时,生成一个StateDB实例。
2)如果有状态被更新,即执行SetState(),那么stateObjectDirty会记录下该账户的更新数据,所有的更新数据均存储在stateOBject的ditryStorage中。
3)当执行IntermediateRoot()时,所有ditryStorage均被更新到trie中。
4)在执行CommitTo()时,trie中的数据被持久化到底层数据库levelDB中。
StateDB结构维护的MPT树,并不像levelDB一样作为单个数据结构实例存储在数据库中,而是只需要在使用时根据状态根Hash从数据库中提取相关的数据组成该结构即可,处理完成后,再将更新的树节点数据存储到数据库中。这样做的好处显而易见,可以大大节省存储空间,例如,当前块只包含一笔对其中某个账户的转账交易,那么,根据当前块的交易对父块状态进行状态转换时,其中只修改了历史区块状态树中一个账户下的一个状态变量,修改后重新计算当前块的状态根Hash,保存到当前块的区块头中,再对这个账户状态修改相关的分支节点、叶子节点并存储到数据库中。在子块进行状态转换需要提取当前块的状态树时,当前块的状态根Hash提取到所有相关的树节点数据,其中包含当前块更新过的树节点数据,以及未更新过的父块树节点数据。总之,这是一种类似Github代码提交的增量修改的机制,每次状态转换只存储修改的部分,然后与未修改部分生成新的状态树根Hash。
状态转换需要支持回滚操作,因为在智能合约的执行过程中可能存在各种原因的中断执行,因此需要对已经修改的状态进行回滚。stateDB 通过 journal、revision 来管理状态修改历史和回滚。journal 结构如下:
type journal struct {

entries []journalEntry         //修改日志
dirties map[common.Address]int // 被修改的账户和修改次数
 
 

}
其中 journalEntry 是一个接口定义,定义了revert和dirtied,记录修改数据和修改的地址。由于数据类型较多,因此不同的类型对该接口的实现也不同,具体包括如下修改内容。
□stateObject修改
□智能合约suicide操作
□账户余额修改
□Nonce修改
□合约字节码更新
□状态变量修改
□退款
□添加日志
□添加preimage
revision,顾名思义,用来描述一个“版本”,作为回退的依据。每次有数据进行修改时,系统都记录一个版本,如图3-27所示。

 

image.png

需要进行回退操作时,只需要指定revision版本号,取出对应的journalIndex,将对应的所有历史修改都恢复到历史值即可。

 

引用自:https://developer.aliyun.com/article/723599

posted @ 2025-06-26 16:39  若-飞  阅读(21)  评论(0)    收藏  举报