以太坊数据结构

数据结构关系图

    以太坊采用账号系统,因而相比比特币,它除了区块数据外还有账号数据。同时它有图灵完备的智能合约虚拟机,因而又多了一个状态数据,同时为了保留执行记录,又多了一个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>数据对格式详情如下
key value
'h' + num + hash header's RLP raw data
'h' + num + hash + 't' td
'h' + num + 'n' hash
'H' + hash num
'b' + num + hash body's RLP raw data
'r' + num + hash receipts RLP
'l' + hash tx/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