以太坊系列 - 数据持久化(2) 源码分析 -- 数据类型与数据存储

Ethereum 选用的是LevelDB, 属于非关系型数据库,存储单元是[k,v]键值对
有关LevelDB的介绍,请看我的另一篇文章-- Geth持久化(1) 采用的是单机数据库–LevelDB

Geth的文件目录

  1. chaindata,lightchaindata,nodes目录
    进入chaindata,区块链最后的本地存储都是以ldb文件(db sst table 持久化文件,新版本的后缀是.ldb,老版本后缀为.sst)
  2. 区块和交易等数据最终都是存储在leveldb数据库中的,数据库的存储位置在datadir/geth/chaindata中,在core/database_util.go (v1.8.0)中封装了所有与区块存储和读取相关的代码

Geth源码相关

  1. core/types文件夹 数据类型

  2. rlp文件夹 RLP编码 实现对字符串及列表对象进行序列化与反序列化

  3. trie文件夹 mpt(Merkle Patricia Tree)结构

    • 每个父节点的哈希值来源于所有子节点哈希值的组合,一个顶点的哈希值能够代表一整个树形结构.可以方便的判断一个交易是否在区块中,hash可以还原出节点上的数据
    • 用来存储用户账户的状态及其变更、交易信息、交易的收据信息
    • 相当于在leveldb之上的一层缓存,使用mpt结构以组织和管理[k,v]型数据
      Root,TxHash和ReceiptHash,分别取自三个MPT类型对象:stateTrie, txTrie, 和receiptTrie的根节点 哈希值。用一个32byte的哈希值,来代表一个有若干节点的树形结构(或若干元素的数组),这是为了加密。比如在Block的同步过程中,通过比对收到的TxHash,可以确认数组成员transactions是否同步完整。
  4. ethdb文件夹 数据存储

    • database.go(leveldb)和memory_database.go(内存) 实现了interface.go接口
    • 内存数据库与物理数据库操作接口完全相同

LevelDB key中的前缀可以用来区分数据的类型

core/database_util.go (v1.8.0) core/rawdb/schema.go (v1.8.11)中定义了各种前缀,Block结构体的所有重要成员,都被存储进了底层数据库

headHeaderKey = []byte("LastHeader") //当前最新Header
headBlockKey  = []byte("LastBlock")  //当前最新Block
headFastKey   = []byte("LastFast")   //快速同步时最新的Header

headerPrefix        = []byte("h")   //headerPrefix + num (uint64 big endian) + hash -> header
tdSuffix            = []byte("t")   //headerPrefix + num (uint64 big endian) + hash + tdSuffix -> td
numSuffix           = []byte("n")   //headerPrefix + num (uint64 big endian) + numSuffix -> hash
blockHashPrefix     = []byte("H")   //blockHashPrefix + hash -> num (uint64 big endian)
bodyPrefix          = []byte("b")   //bodyPrefix + num (uint64 big endian) + hash -> block body
blockReceiptsPrefix = []byte("r") 	// blockReceiptsPrefix + num (uint64 big endian) + hash -> block receipts
lookupPrefix        = []byte("l") 	// lookupPrefix + hash -> transaction/receipt lookup metadata
bloomBitsPrefix     = []byte("B") 	// bloomBitsPrefix + bit (uint16 big endian) + section (uint64 big endian) + hash -> bloom bits

value值多用rlp编码序列化数据对象

写新数据

为了减少leveldb的交互,写数据的时候一般会以Batch进行,就是先往batch里写一堆数据,然后再统一把这个Batch写到leveldb。

以太坊的core/blockchain.go中写block的时候就是新建Batch,然后把Batch写入leveldb

func (bc *BlockChain) WriteBlockWithState(block *types.Block, receipts []*types.Receipt, state *state.StateDB) (status WriteStatus, err error) {
...
batch := bc.db.NewBatch()
if err := WriteBlock(batch, block); err != nil {
	return NonStatTy, err
}
...

StateDB的存储 (MPT树)

为了能够快速检索和更新账户状态,StateDB采用了两级缓存机制

  • 第一级缓存以map的形式存储stateObject
  • 第二级缓存以MPT的形式存储(interface Trie)
  • 第三级就是LevelDB上的持久化存储(interface Database)

StateDB里的Trie以账户地址为key,以MPT结构存储经过RLP编码后的stateObject。

stateObject有一个成员变量data,类型是Accunt结构体,里面存有账户Ether余额,合约发起次数,最新发起合约指令集的哈希值,以及一个MPT结构的顶点哈希值
stateObject里的Trie也被称为Storage trie,存储的是智能合约执行后修改的变量值(也用了两级缓存机制)

Transactions的存储 (Merkle树)

交易在LeveDB中并不是单独存储的,而是存储在区块的Body中 key以b开头

交易的索引信息会另存一份 ,称为TxLookupEntry key以l开头

Receipts的存储 (Merkle树)

交易回执是单独存储到LevelDB中的,以r为前缀

TxLookupEntry可用于快速定位 Transactions和Receipts

需要注意的是

  • 在不同的节点之间并不会发送状态(StateDB)和收据(Receipts),两者都是在本地通过交易计算得到
  • 当其他用户收到块,根据块里的交易可以计算出收据和状态,计算三个根哈希后和区块头的三个字段进行验证,判断这是否为合法的块。

单元测试

  • ethdb/database_test.go //测试经geth封装的leveldb的操作
  • (v1.8.0)core/database_util_test.go //测试区块存储和读取相关的代码 ,用的是内存数据库
  • (v1.8.11)core/rawdb/accessors_chain_test.go //测试区块存储和读取相关的代码 ,用的是内存数据库

你可能感兴趣的:(源码分析,以太坊系列)