以太坊 数据结构

admin
admin 2019年08月22日
  • 在其它设备中阅读本文章

一、区块(Block)

区块 (Block)是 以太坊 (Ethereum)的核心数据结构之一。所有账户的相关活动,以 交易 (Transaction) 的格式存储,每个区块有一个交易对象的列表;每个交易的执行结果,由一个 收据 (Receipt)对象与其包含的一组 日志 (Log)对象记录;所有交易执行完后生成的收据列表,存储在区块中(经过压缩加密)。不同区块之间,通过前向指针 父哈希 (ParentHash)一个一个串联起来成为一个链表, 区块链 (BlockChain)结构体管理着这个链表。
区块链.jpg

数据定义

type Block struct {
    header       *Header       //区块头
    uncles       []*Header     //叔块头集合
    transactions Transactions  //交易集合
    hash         atomic.Value  //区块哈希
    size         atomic.Value  //区块大小
    td           *big.Int      //累计的区块难度
    ReceivedAt   time.Time     //区块接收时间
    ReceivedFrom interface{}   //区块来源
}

数据项注解:td(total difficulty)为当前链的总难度,即到当前区块截止,累积的所有区块难度之和
核心方法

  1. func (b *Block) Size() common.StorageSize // 区块 rlp 编码后的大小
  2. func (b *Block) Hash() common.Hash // 区块 keccak256 哈希,实际是 header 的哈希

1、区块头

区块头(Header)是 Block 的核心,注意到它的成员变量全都是公共的,这使得它可以很方便的向调用者提供关于 Block 属性的操作。
区块头.jpg

type Header struct {
    ParentHash  common.Hash     //父区块哈希。除了创世块外,每个区块有且只有一个父区块
    UncleHash   common.Hash     //所属区块uncles成员的哈希值。uncles是一个Header数组
    Coinbase    common.Address  //打包该区块的矿工的地址,用于接收矿工费
    Root        common.Hash     //状态树的根哈希值
    TxHash      common.Hash     //交易树的根哈希值
    ReceiptHash common.Hash     //收据树的根哈希值
    Bloom       Bloom           //交易收据日志组成的Bloom过滤器
    Difficulty  *big.Int        //本区块的难度
    Number      *big.Int        //区块的序号。等于其父区块Number+1
    GasLimit    uint64          //区块内所有Gas消耗的上限,该数值在区块创建时设置,用于限定区块大小。
    GasUsed     uint64          //区块内所有Transaction执行时所实际消耗的Gas总和
    Time        uint64          //区块产生的时间戳,一般是打包区块的时间,这个字段不是出块的时间戳
    Extra       []byte          //区块的附加数据
    MixDigest   common.Hash     //哈希值,与Nonce的组合用于工作量计算
    Nonce       BlockNonce      //一个64bit的哈希数,它被应用在区块的"挖掘"阶段,并且在使用中会被修改
}

GasLimit 是用于限制区块中的交易个数用的。具体由每个矿工自己设置,有些矿工觉得本区块的 GasLimit 太大,可以在挖下一个区块时间 Gas Limit 下调 1 /1024, 觉得 GasLimit 太小的时候,就会上调 1 /1024。整个区块链中区块的大小,就是所有矿工微调的平均值。以太坊中正是通过 GasLimit 来限制区块的大小。
区块头中比较重要的三个字段是 Root、TxHash 和 ReceiptHash 三个哈希值。收据树必须在区块的所有交易执行完成才能生成;交易树理论上只需交易数组即可,不过依然被限制在所有交易执行完后才生成;最有趣的是状态树,由于它存储了所有账户的信息,比如余额,发起交易次数,虚拟机指令数组等等,所以随着每次交易的执行,状态其实一直在变化,这就使得 Root 值也在变化中。于是 StateDB 定义了一个函数 IntermediateRoot(),用来生成那一时刻的 Root 值:

2、交易

区块的成员 transactions 其实是 Transaction 数组,交易 (Transaction) 是指由一个外部账户转移一定资产给某个账户, 或者发出一个消息指令到某个智能合约。在以太坊网络中,交易执行属于一个事务。具有原子性、一致性、隔离性、持久性特点。为了安全性,交易需要发送者签名才有效,别人不能伪造签名发起交易而盗取以太币。
需要注意的是,TxByNonceh 是按 nonce 排序的 Transaction 数组,TxByPrice 是按 gas 价格排序的 Transaction 数组
数据定义

type Transaction struct {
    data txdata         //交易详情
    // caches
    hash atomic.Value   //交易哈希
    size atomic.Value   //交易大小,data成员rlp编码大小
    from atomic.Value   //交易来源
}

type txdata struct {
    AccountNonce uint64           //账户nonce
    Price        *big.Int         //gas价格
    GasLimit     uint64           //gas上限
    Recipient    *common.Address  //交易接受者
    Amount       *big.Int         //以太币数量
    Payload      []byte           //交易附加数据,智能合约所需

    // Signature values
    V *big.Int                    //交易签名r、s、v,发送者签名
    R *big.Int                    //
    S *big.Int                    //

    // This is only used when marshaling to JSON.
    Hash *common.Hash             //交易哈希
}

注解

  1. 账户 nonce 用于撤销交易、防止双花等
  2. 交易的哈希、大小、来源等需要经常读写,所以可以把它们缓存起来
  3. 特别注意的是交易的来源,它是从交易的签名 R、S、V 中提取出来
  4. 另外从签名 V 中可以提取出 chainId

核心方法

  1. func (tx Transaction) ChainId() big.Int
  2. func (tx *Transaction) MarshalJSON() ([]byte, error)
  3. func (tx *Transaction) UnmarshalJSON(input []byte) error
  4. func (tx *Transaction) WithSignature(signer Signer, sig []byte)
  5. func (tx *Transaction) AsMessage(s Signer) (Message, error)

二、交易池

交易池是一个节点存储交易的容器,无论是本节点创建的交易还是其他节点广播过来的交易,都会缓存在交易池中,当需要生成区块时,就从交易池中选择合适的交易打包成块,经由共识最终确认生成新的区块。另外交易池配置限定了交易的 gas 价格下限、每个账户可保留的交易数、交易最大排队时间等等,一定程度控制了交易池的大小。
数据定义

type TxPool struct {
    config      TxPoolConfig             //交易池配置
    chainconfig *params.ChainConfig      //区块链配置
    chain       blockChain               //区块链接口
    gasPrice    *big.Int                 //交易池gas价格下限
    txFeed      event.Feed               //交易订阅器
    scope       event.SubscriptionScope  //订阅范围,可以批量关闭订阅
    signer      types.Signer             //交易签名者接口,用于从交易解析发送者
    mu          sync.RWMutex             //读写锁

    currentState  *state.StateDB         //当前区块链的世界状态(包括普通账户和合约账户),
    pendingNonces *txNoncer              //Pending状态交易的nonce,如果没有就从currentState取
    currentMaxGas uint64                 //当前最大交易gasLimit

    locals  *accountSet                  //本地账户集合,可以避免被踢出交易池
    journal *txJournal                   //备份到磁盘的本地交易记录

    pending map[common.Address]*txList   //所有当前可处理的交易
    queue   map[common.Address]*txList   //所有当前等待处理的交易
    beats   map[common.Address]time.Time //每个已知帐户的最后一次心跳
    all     *txLookup                    //所有交易集合,按哈希索引并加锁
    priced  *txPricedList                //按价格排序的所有交易

    chainHeadCh     chan ChainHeadEvent      //新区块事件通道
    chainHeadSub    event.Subscription       //新区块事件订阅器
    reqResetCh      chan *txpoolResetRequest //重置交易池请求通道
    reqPromoteCh    chan *accountSet         //账户提升请求通道,升级为本地账户
    queueTxEventCh  chan *types.Transaction  //新交易事件,放入成员queue中,等待处理
    reorgDoneCh     chan chan struct{}       //重新排序完成
    reorgShutdownCh chan struct{}            //重新排序终止
    wg              sync.WaitGroup           //等待一组操作完成
}

type txList struct {
    strict bool         //nonce值是否严格连续
    txs    *txSortedMap //已排序的交易集合

    costcap *big.Int //最高的交易成本价格(gasprice * gaslimit)
    gascap  uint64   //最高的交易GasLimit 
}

type txLookup struct {
    all  map[common.Hash]*types.Transaction
    lock sync.RWMutex
}

type txPricedList struct {
    all    *txLookup
    items  *priceHeap //按价格排序的交易堆
    stales int        //重构堆阀值
}

注解

  1. 交易池用于缓存交易,过滤无效交易
  2. 新的交易会尝试放入 pending,否则放入 queued,如果 queued 也不行就舍弃
  3. 低于设定的 gasPrice(节点自行设置)交易会被舍弃
  4. 交易来源有本地的和远程的,本地的有特权, 本地交易会缓存到磁盘中
  5. 交易池存在内存中,不可能无限大,等超过一定阈值就需要对交易池里面的交易进行清理
  6. 收到新区块会重构交易池

核心方法

  1. func (pool TxPool) validateTx(tx types.Transaction, local bool) error
  2. func (pool TxPool) journalTx(from common.Address, tx types.Transaction)
  3. func (pool TxPool) AddLocal(tx types.Transaction) error
  4. func (pool TxPool) AddRemote(tx types.Transaction) error
  5. func (pool *TxPool) Status(hashes []common.Hash) []TxStatus
  6. func (pool TxPool) Get(hash common.Hash) types.Transaction
  7. func (pool *TxPool) removeTx(hash common.Hash, outofbound bool)

三、收据

交易通过 evm 虚拟机执行完成之后会产生一些 收据 (Receipt),一个交易一个收据并包括多个 日志 (Log),收据记载的是 交易执行的结果。在以太坊虚拟机里,日志代表对事件的存储,主要记录触发的事件及其数据(比如一个代币转账事件,Topics 是转账事件的一些签名,Data 记录的是谁给谁转多少代币)。

type Receipt struct {
    // 这些字段由黄皮书定义
    PostState         []byte    //执行交易完成后状态树的根
    Status            uint64    //交易执行结果,成功为1,失败为0
    CumulativeGasUsed uint64    //一个区块里执行到此交易(包括)的gas花费
    Bloom             Bloom     //布隆过滤器
    Logs              []*Log    //虚拟机智能合约执行日志

    // 这些字段在处理交易时由geth节点添加。它们存储在区块链数据库中
    TxHash          common.Hash    //交易哈希
    ContractAddress common.Address //合约地址
    GasUsed         uint64         //gas消耗

    //这些字段提供与此收据对应的交易记录的信息
    BlockHash        common.Hash //所属区块的哈希
    BlockNumber      *big.Int    //所属区块的高度
    TransactionIndex uint        //所在交易列表的序号
}

type Log struct {
    Address common.Address  //生成日志的合约地址
    Topics []common.Hash    //日志提供的主题列表
    Data []byte             //日志提供的数据,通常为abi编码

    BlockNumber uint64      //区块高度
    TxHash common.Hash      //交易哈希
    TxIndex uint            //交易在区块里的索引
    BlockHash common.Hash   //区块哈希
    Index uint              //在区块中的索引

    Removed bool            //是否已删除
}

注解

  1. 收据的日志只有执行智能合约的事件才会产生,实质是由虚拟机 log 指令(LOG0,LOG1,LOG2,LOG3,LOG4)生成的。
  2. 如果由于链重组日志不需要了,则 Removed 字段设为 true。
  3. 日志里面 topics 数组里的第一个元素就是事件的 sha3 签名,其余的为 indexed 的参数签名,参考合约 ABI 文件

核心方法

  1. func NewReceipt(root []byte, failed bool, cumulativeGasUsed uint64) *Receipt
  2. func (r *Receipt) setStatus(postStateOrStatus []byte) error
  3. func (r *Receipt) Size() common.StorageSize