数据结构关系图

     以太坊采用账号系统,因而相比比特币,它除了区块数据外还有账号数据。同时它有图灵完备的智能合约虚拟机,因而又多了一个状态数据,同时为了保留执行记录,又多了一个receipt数据


Block:
      由header和body构成,header里有三个trie的rootHash
  • <hash, receipt>数据构造的receipt trie, header.receiptHash=这个trie的rootHash
  • <hash, transaction>数据构造的transaction trie, header.txHahs=这个trie的rootHash
  • <address, StateObject>数据构造的state trie, header.root=这个trie的rootHash
StateObject:
        是一个账号(地址)的状态信息
  •     对于普通账号,这个对象保存了balance, nonce等信息
  •     对于智能合约账号,还额外保留了智能合约的状态,这个状态就是智能合约里的定义的各种变量的值。以太坊虚拟机的变量是以<k, v>存储的,所以这个状态就是大量<k, v>对象。
            比如一般的ICO智能合约里,都会定义一个balance的变量,用来保存各个地址的代币持有量
            
  mapping(address => uint256) balances;
         那这个值就会以<k, v>的形式存储在leveldb中,运行时会以trie的数据结构加载到StateObject对象里
LevelDb:
       以太坊所有的数据都是以<k, v>的方式存储,方便检索,最终都是通过LevelDB写入持久化存储的数据库文件。
       除了上面提到的<k, v>数据对,还有<trie.nodehash, trie.noderawrpl>, <header.hash, header>等,核心<k ,v>数据对格式详情如下
keyvalue
'h' + num + hashheader's RLP raw data
'h' + num + hash + 't'td
'h' + num + 'n'hash
'H' + hashnum
'b' + num + hashbody's RLP raw data
'r' + num + hashreceipts RLP
'l' + hashtx/receipt lookup metadata

数据结构详细

Header
   
type   Header   struct   {
    ParentHash common.Hash   `json:"parentHash" gencodec:"required"`
    UncleHash common.Hash   `json:"sha3Uncles" gencodec:"required"`
    Coinbase common.Address   `json:"miner" gencodec:"required"`
    Root common.Hash   `json:"stateRoot" gencodec:"required"`
    TxHash common.Hash   `json:"transactionsRoot" gencodec:"required"`
    ReceiptHash common.Hash   `json:"receiptsRoot" gencodec:"required"`
    Bloom Bloom   `json:"logsBloom" gencodec:"required"`
    Difficulty   * big.Int   `json:"difficulty" gencodec:"required"`
    Number   * big.Int   `json:"number" gencodec:"required"`
    GasLimit   uint64   `json:"gasLimit" gencodec:"required"`
    GasUsed   uint64   `json:"gasUsed" gencodec:"required"`
    Time   * big.Int   `json:"timestamp" gencodec:"required"`
    Extra [] byte   `json:"extraData" gencodec:"required"`
    MixDigest common.Hash   `json:"mixHash" gencodec:"required"`
    Nonce BlockNonce   `json:"nonce" gencodec:"required"`
}

  Header是Block的核心,它的成员变量都很重要,需要仔细分析
  • ParentHash:父区块(parentBlock)的hash, 通过这个hash可进一步找到parentBlock。除了创世块(Genesis Block)外,每个区块有且只有一个父区块
  • UncleHash:Block结构体的成员uncles的RLP哈希值。uncles是一个Header数组。以太坊引入叔块还是很合理的,相比比特币10分钟出一个区块,以太坊9s出一个区块,因而区块分叉的概率高很多,从而会出现大量新块不被采纳的情况,为了增加矿工的积极性,系统会将部分无用的区块打包进区块并给这些无用块(叔块)创造者一小份收益。
  • Coinbase:区块的作者的地址。在每次执行交易时系统会给与作者一定的奖励Ether
  • Root:“state Trie”的根节点的RLP哈希值
  • TxHash: “tx Trie”的根节点的RLP哈希值
  • ReceiptHash:"Receipt Trie”的根节点的RLP哈希值。Block的所有Transaction执行完后会生成一个Receipt数组,这个数组中的所有Receipt被逐个插入一个MPT结构中,最后形成"Receipt Trie"
  • Bloom:Bloom过滤器(Filter),用来快速判断一个参数Log对象是否存在于一组已知的Log集合中。
  • Difficulty:区块的难度。Block的Difficulty由共识算法基于parentBlock的Time和Difficulty计算得出,并会动态调整
  • Number:区块的高度(即index)。Block的Number等于其父区块Number +1。
  • Time:区块“应该”被创建的时间。由共识算法确定,其一般等于parentBlock.Time + 9s,也可能等于当前系统时间
  • GasLimit:区块内所有Gas消耗的上限。该数值在区块创建时赋值,与父区块的数据相关。具体来说,根据父区块的GasUsed同创世块的GasLimit * 2/3的大小关系来计算得出。
  • GasUsed:执行区块内所有Transaction实际消耗的Gas总和
  • Nonce:一个64bit的哈希数,它被用于POW等挖块算法,暴力碰撞得出。
  • mixDigest: 区块头除去Nonce, mixDigest数据的hash+nonce的RLP的hash值

Block
    
type   Block   struct   {
    header *Header
    uncles [] * Header
    transactions Transactions

     // caches
    hash atomic.Value
    size atomic.Value

     // Td is used by package core to store the total difficulty
     // of the chain up to and including the block.
    td   * big.Int

     // These fields are used by package eth to track
     // inter-peer block relay.
    ReceivedAt time.Time
    ReceivedFrom   interface {}
}
    Block的大部分功能都由header代劳,只是多一些transaction数据
func   (b   * Block)   Number ()   * big.Int  {   return   new ( big.Int ). Set (b.header.Number) }
func   (b   * Block)   GasLimit ()   uint64   {   return   b.header.GasLimit }
func   (b   * Block)   GasUsed ()   uint64   {   return   b.header.GasUsed }
func   (b   * Block)   Difficulty ()   * big.Int  {   return   new ( big.Int ). Set (b.header.Difficulty) }
func   (b   * Block)   Time ()   * big.Int  {   return   new ( big.Int ). Set (b.header.Time) }

func   (b   * Block)   NumberU64 ()   uint64   {   return   b.header.Number. Uint64 () }
func   (b   * Block)   MixDigest () common.Hash {   return   b.header.MixDigest }
func   (b   * Block)   Nonce ()   uint64   {   return   binary.BigEndian. Uint64 (b.header.Nonce[:]) }
func   (b   * Block)   Bloom () Bloom {   return   b.header.Bloom }
func   (b   * Block)   Coinbase () common.Address {   return   b.header.Coinbase }
func   (b   * Block)   Root () common.Hash {   return   b.header.Root }
func   (b   * Block)   ParentHash () common.Hash {   return   b.header.ParentHash }
func   (b   * Block)   TxHash () common.Hash {   return   b.header.TxHash }
func   (b   * Block)   ReceiptHash () common.Hash {   return   b.header.ReceiptHash }
func   (b   * Block)   UncleHash () common.Hash {   return   b.header.UncleHash }
func   (b   * Block)   Extra () [] byte   {   return   common. CopyBytes (b.header.Extra) }

     那为啥还要设计两个对象,主要是header是轻量级数据,而block数据可能很大,因而对于网络传输的时候,先传输header数据验证重复性及合法性可以节省大量带宽。同时对于SPV的支持也是很有用的。
   Block的hash和header的hash是一样,都是header中除nonce和mixDigest数据外的rlp的hash,这样相同的交易内容的相同该区块hash是一样,哪怕是由不同的节点创建出来的。
    一个block的还原就是根据H+hash=>header, B+hash=>body,也就是一个block的数据时分为两个<k, v>储存的,尽管hash相同,但是前缀不一样,导致key不一样

StateDB

    一个block通过root字段可以构造加载一个StateDB对象,也就是每个block有一个StateDB实例
type   StateDB   struct   {
    //leveldb操作接口
    db Database
    // <address, stateObject>生成的trie
    trie Trie

     // This map holds 'live' objects, which will get modified while processing a state transition.
    // <address, stateObject>数据cache
    stateObjects   map [common.Address] * stateObject
    stateObjectsDirty   map [common.Address] struct {}
    ….
}

      StateDB保存的是所有的账号信息即stateObject, 因而<address, stateObject>是一个巨量的<k, v>数据集,通过map[common.Address]*stateObject来加载所有的账号信息是不可能的,因而需要“分级”缓存机制:
  • 第一缓存stateObjects,这里保留了近期活跃的账号信息
  • 第二级缓存trie,以trie的方式维护<address, stateObject>信息,也能快速访问
  • 第三级存储leveldb,从leveldb数据库中根据address获取对应的stateObject
    我们知道设计缓存的目的是解决速度和容量的平衡。比如上面的应该第一级缓存空间小,速度快,后面的缓存应该空间大,速度小。这几级缓存设计咋一看很合理也没什么疑惑。但是仔细看了代码觉得不对啊。第一级缓存map保存某一block交易涉及到stateObject(数量有限的)。但是map从二级缓存trie读取数据却从来没删除过,这样map和trie的空间是一样的,这样看来trie这级缓存并没有卵用。然而,以太坊发展了这么久,如果这个设计有这么明显的错误肯定应该早就发现了。因此还得从自身的理解找问题,经过仔细的代码阅读和深入的思考,我个人认为trie这一模块不应该被称作为缓存,但是trie是要有的,且大有用处。
  • <address, stateObject>数据有分片验证的需求,即需要一个merkle tree, 即检验某一个address的stateObject数据是否真的存在某个block
  • 需要实现<address, stateObject>私有,我们知道底层的leveldb的<k, v>是全局共享的,即对一个address,只会保存一个stateObject, 而每个block都需要记录当前区块的所有address的stateObject信息,因而不同区块肯定会存在一个address对应不同stateObject情况。因而需要一种新的编码方式,将<k, v>转成<encode(k+v), v>存储到leveldb中,这样对于相同的address,如果其在不同的block里的stateObject不一样,其在leveldb中对应的key也不一样。
      这两个功能的结合就是MPT树,更详细的信息可以查看MPT这篇博文
      StateDB的map和trie随着交易的相关操作肯定会不断扩展变大,那这么多的数据何时回收呢?这个由go的自动gc实现。StateDB一般都是在进行block的相关过程中临时创建的,因而很快就被gc释放掉了,对应的map,trie自然也自动回收了
      每个block的stateDB都是在parentBlock的stateDB的基础上执行交易更新而来的

stateObject
    
type   stateObject   struct   {
    address common.Address
    addrHash common.Hash   // hash of ethereum address of the account
    //普通账号的信息
    data Account
    //leveldb操作接口
    db   * StateDB

     // Write caches.
    // <k, v>状态数据生成的MPT
    trie Trie   // storage trie, which becomes non-nil on first access
    code Code   // contract bytecode, which gets set when code is loaded

    //最近使用的<k, v>缓存
    cachedStorage Storage   // Storage entry cache to avoid duplicate reads
    dirtyStorage Storage   // Storage entries that need to be flushed to disk
}

type   Storage   map [common.Hash]common.Hash


Receipt

type   Receipt   struct   {
     // Consensus fields
    PostState [] byte   `json:"root"`
    Status   uint   `json:"status"`
    CumulativeGasUsed   uint64   `json:"cumulativeGasUsed" gencodec:"required"`
    Bloom Bloom   `json:"logsBloom" gencodec:"required"`
    Logs []*Log `json:"logs" gencodec:"required"`

     // Implementation fields (don't reorder!)
    TxHash common.Hash   `json:"transactionHash" gencodec:"required"`
    ContractAddress common.Address   `json:"contractAddress"`
    GasUsed   uint64   `json:"gasUsed" gencodec:"required"`
}
    

    Receipt里的核心数据时Logs,智能合约允许开发人员通过event定义一些事件并广播到全网,这些event就是记录在Logs里面的

原文链接:https://blog.csdn.net/itleaks/article/details/80094294

Logo

华为开发者空间,是为全球开发者打造的专属开发空间,汇聚了华为优质开发资源及工具,致力于让每一位开发者拥有一台云主机,基于华为根生态开发、创新。

更多推荐