您正在查看: Ethereum-优秀转载 分类下的文章

以太坊源码分析:statedb

前言

就如以太坊黄皮书讲的,以太坊是状态机,区块的产生,实际是状态迁移的过程。那以太坊

  1. 是如何定义状态的?
  2. 是如何迁移状态的?
  3. 是怎么存储状态的?

这篇文章就介绍什么是状态,以及是怎么存储的。

状态基本知识

状态的定义

一个账户的信息,就是一个状态,而以太坊是所有状态的集合。比如,最开始的状态是:{A有10元,B有0元},后来A发起了交易,给B 2元,状态变成{A有8元,B有2元},这中间的过程就是状态转移。

以太坊实际最初的状态是创世块,每产生一个新区块就转移到一个新的状态。

状态表示

以太坊使用root表示状态。以太坊使用Trie组织状态,Trie可以理解为是字典树和默克尔树的结合,它有一个树根root,有这个root,你就可以访问所有的状态数据,即每个账户的信息,所以用root来表示一个状态。

获取状态

区块头中有一个字段Root,所以找到区块头,就能获取区块链的状态。

状态存在哪

状态不存在区块中。区块头中存放了root,这只是一个地址,从区块中并不能找到状态的数据。

状态只是临时的数据,可以再生成。创世块是最初的状态,把第一个区块中的交易都执行一遍,就得到了一个新的状态,把这个状态的root存到第一个区块头的Root中。如果有所有的区块,就可以把所有的交易都执行,然后生成最新区块中的状态。

状态存放在外部数据库。以太坊底层的数据库是LevelDB,区块存放在里面,状态也存放在里面。但状态是一个Trie,不能直接存在LevelDB里面。

StateDB

StateDB,从名字就能看出来,是用来存储状态的数据库。它把Trie和DB结合了起来,实现了对状态的存储、更新、回滚。我们先介绍它的设计思路,然后再介绍一些它的骨干实现。

StateDB的设计

以太坊使用LevelDB作为底层的存储数据库,虽然leveldb能够满足存取状态,但没有缓存功能、快速访问和修改状态等特性,以太坊实现了StateDB,来满足自身的需求。
我们就介绍下,它是如何设计来实现以上特性的。

底层存储设计

使用Trie实现快速访问。上文提到了,Trie是字典树和默克尔树的结合,可以实现快速查找,这里就看它是如何使用Trie的。
使用内存实现缓存。常用的数据,会被计算机留在内存中,同样,常用的状态也被留在内存中,并且使用StateDB把它们管理起来。
StateDB定义了2个接口:Trie和Database:Trie建立在Database之上,Trie的数据存放在Database中。

  • Trie被定义为带有缓存的KV数据库。你可以通过它快速存储、更新、删除数据。
  • Database被定义为一个打开Trie、拷贝Trie的数据库。它不直接对外访问,不能直接使用它存取数据。

在代码实现上,cachedTrie实现了Trie,cachingDB实现了Databse,他们定义在core/state/database.go

// 实现Database接口,缓存常用的trie
type cachingDB struct {
   //保存trie数据的db
   db *trie.Database
   mu sync.Mutex
   // 缓存过去的trie,队列类型
   pastTries     []*trie.SecureTrie
   codeSizeCache *lru.Cache
}

// 包含了trie和缓存db,trie实际是存在db中的
type cachedTrie struct {
   *trie.SecureTrie
   db *cachingDB
}

//从db中打开一个trie,如果不是最近使用过,则创建一个新的,存到db
func (db *cachingDB) OpenTrie(root common.Hash) (Trie, error) {
   db.mu.Lock()
   defer db.mu.Unlock()

   for i := len(db.pastTries) - 1; i >= 0; i-- {
      if db.pastTries[i].Hash() == root {
         return cachedTrie{db.pastTries[i].Copy(), db}, nil
      }
   }
   tr, err := trie.NewSecure(root, db.db, MaxTrieCacheGen)
   if err != nil {
      return nil, err
   }
   return cachedTrie{tr, db}, nil
}

StateDB的状态组织设计

StateDB使用Trie存放stateObject,是账户地址到账户信息的映射,每个stateObject都是一个账户的信息。
stateObject使用Trie存放数据,这些数据被称为storage,实现对某个账户的状态数据的存储和修改,key是数据的hash值,value是状态数据。
StateDB和stateObject都使用Database存放了自己的Trie,他们使用的是同一个DB。
但从逻辑层次上看,他们满足这种关系:

事务和回滚设计

stateDB这个KV数据库,实现了类似传统数据库的事务和回滚设计。每一个交易都是一个事务,每一个交易的执行,都是一次状态转移,在执行交易之前,先创建当前的快照,执行交易的过程中,会记录状态数据的每一次修改,如果交易执行失败,则进行回滚,交易执行完毕,会把所有修改的状态数据写入到Trie,然后更新Trie的根。
在生成1个区块的时候,会进行很多次Finalise,回滚是不能跨越交易的,也就是说,当前交易失败了,我不能回滚到上上一条交易。生成区块的时候,最后一次Finalize的Trie的Root,会保存到区块头的Header.Root。当区块要写入到区块链的时候,会执行一次Commit。

关于Finalise和Commit的主要调用关系如下图:

Finalise的主要调用场景是:

  1. 执行交易/合约,进行一次状态转移。
  2. 给矿工计算奖励后,进行一次状态转移。

Commit的主要调用场景是插入区块链,有2种情况:

  1. 自己挖到区块。
  2. 收到他人的区块。

StateDB的骨干实现

基于上面对StateDB设计的了解,我们再介绍一下StateDB一些主要的实现。这一小节主要覆盖以下内容:

  1. state所在的目录和文件划分。
  2. stateObject实现。
  3. stateDB的实现。

state目录和文件划分

state所在的目录是:core/state,它的文件和每个文件的主要功能如下:

core/state
├── database.go,底层的存储设计,`Trie`和`Database`定义在此文件。
├── dump.go,用来dumpstateDB数据。
├── iterator.go,用来遍历`Trie`。
├── journal.go,用来记录状态的每一步改变。
├── managed_state.go,给txpool使用,具体功能未研究。
├── state_object.go,每一个账户的状态。
├── statedb.go,以太坊整个的状态。
├── sync.go,用来和downloader结合起来同步state。

关于stateDB如何存储状态,主要关注这3个文件:

  1. database.go
  2. state_object.go
  3. statedb.go

接下来通过源码介绍这3个文件的功能和实现。

database.go

database.go的主要代码和设计,已经在底层存储设计的时候介绍过了,这里补充介绍另外一个重要的函数OpenStorageTrie它与OpenTrie的区别:

  1. 实现区别,OpenTrie会先从db中查找,如果每找到才创建一个,而OpenStorageTrie是直接创建一个。
  2. 功能区别,OpenTrie创建的stateDB的Trie,而OpenStorageTrie创建的是stateObject的Trie。

把1和2合并:cachingDB会缓存stateDB使用的Trie,而不会缓存stateObject使用的Trie。

// OpenStorageTrie opens the storage trie of an account.
// 创建一个账户的存储trie,但实际没有使用到addrHash
func (db *cachingDB) OpenStorageTrie(addrHash, root common.Hash) (Trie, error) {
   return trie.NewSecure(root, db.db, 0)
}

// OpenTrie opens the main account trie.
// 从db中打开一个trie,如果不是最近使用过,则创建一个新的,存到db
func (db *cachingDB) OpenTrie(root common.Hash) (Trie, error) {
   db.mu.Lock()
   defer db.mu.Unlock()

   for i := len(db.pastTries) - 1; i >= 0; i-- {
      if db.pastTries[i].Hash() == root {
         return cachedTrie{db.pastTries[i].Copy(), db}, nil
      }
   }
   tr, err := trie.NewSecure(root, db.db, MaxTrieCacheGen)
   if err != nil {
      return nil, err
   }
   return cachedTrie{tr, db}, nil
}

state_object.go

该文件主要实现最小状态的存储和修改。stateObject代表最小粒度的状态,它是一个账户的状态信息。我们先看下基础的数据结构定义,再看它实现的主要功能。

账户和stateObject

以太坊的账户分为普通账户和合约账户,在代码上,他们都是用Account来表示,它记录了账户的数据,有:Nonce,余额,状态树根Root和合约代码的哈希值CodeHash。

// Account is the Ethereum consensus representation of accounts.
// These objects are stored in the main account trie.
// Account是账户的数据,不包含账户地址
// 账户需要使用地址来表示,地址在stateObject中
type Account struct {
    // 每执行1次交易,Nonce+1
    Nonce uint64
    Balance *big.Int
    // 该账户的状态,即trie的根
    Root common.Hash // merkle root of the storage trie
    // 合约账户专属,合约代码编译后的Hash值
    CodeHash []byte
}

以上是账户的数据,那如何表示一个账户呢?
使用账户地址表示账户,它记录在stateObject中:

// stateObject represents an Ethereum account which is being modified.
//
// The usage pattern is as follows:
// First you need to obtain a state object.
// Account values can be accessed and modified through the object.
// Finally, call CommitTrie to write the modified storage trie into a database.
// 地址、账户、账户哈希、数据库
type stateObject struct {
    // 账户信息
    address common.Address
    addrHash common.Hash // hash of ethereum address of the account
    data Account

    code Code
    // 更多信息省略
}

所以 一个stateObject记录了一个完整的账户信息:Account + Address + Code。

再来看下stateObject的完整信息,它记录了:账户信息、EVM执行过程中的错误、保存数据的storage trie、合约代码、缓存的storage数据cachedStorage、修改过的storage数据dirtyStorage,剩下的信息先忽略。storage代表了该对象/账户中存储的KV数据。

type stateObject struct {
    // 账户信息
    address common.Address
    addrHash common.Hash // hash of ethereum address of the account
    data Account
    // 所属于的stateDB
    db *StateDB

    // DB error.
    // State objects are used by the consensus core and VM which are
    // unable to deal with database-level errors. Any error that occurs
    // during a database read is memoized http://lessisbetter.site/2018/06/22/ethereum-code-statedb/ and will eventually be returned
    // by StateDB.Commit.
    // VM不处理db层的错误,先记录下来,最后返回,只能保存1个错误,保存存的第一个错误
    dbErr error

    // Write caches.
    // 使用trie组织stateObj的数据
    trie Trie // storage trie, which becomes non-nil on first access
    // 合约代码
    code Code // contract bytecode, which gets set when code is loaded

    // 存缓存,避免重复从数据库读
    cachedStorage Storage // Storage entry cache to avoid duplicate reads
    // 需要写到磁盘的缓存
    dirtyStorage Storage // Storage entries that need to be flushed to disk

    // Cache flags.
    // When an object is marked suicided it will be delete from the trie
    // during the "update" phase of the state transition.
    dirtyCode bool // true if the code was updated
    // 标记suicided,代表这个对象要从trie删除,在update阶段
    suicided bool
    deleted bool
}

账户地址和账户信息是stateObject的核心数据,有他们2个就能建立一个stateObject:

// newObject creates a state object.
// 使用地址和账户创建stateObject
func newObject(db *StateDB, address common.Address, data Account) *stateObject {
    if data.Balance == nil {
        data.Balance = new(big.Int)
    }
    if data.CodeHash == nil {
        data.CodeHash = emptyCodeHash
    }
    return &stateObject{
        db: db,
        address: address,
        addrHash: crypto.Keccak256Hash(address[:]),
        data: data,
        cachedStorage: make(Storage),
        dirtyStorage: make(Storage),
    }
}

stateObject的重要函数

stateObject保存了2个重要信息:

  1. 账户的信息:Account、Address、Code。创建账户之后,这些数据就不变了。
  2. 账户的数据:trie。对于合约账户,trie用来存储数据,因此trie是经常变化的。比如,投票合约,有新的投票,就有新的数据产生和改变,trie也就发生改变。

掌握关于trie的函数,就掌握了stateObject的核心操作:

  1. func (c *stateObject) getTrie(db Database) Trie。获取当前账户的trie。
  2. func (self *stateObject) SetState(db Database, key, value common.Hash)。设置trie中的kv数据对,能够完成创建、更新、删除功能。
  3. func (self *stateObject) updateRoot(db Database)。更新trie的根。
  4. func (self *stateObject) updateTrie(db Database) Trie。更新trie,把账户中修改过的数据写入到trie。

剩余的函数都是stateObject的基本Get和Set函数。

// 获取当前账户的trie,如果没有,则创建一个空的
func (c *stateObject) getTrie(db Database) Trie {
    if c.trie == nil {
        var err error
        c.trie, err = db.OpenStorageTrie(c.addrHash, c.data.Root)
        if err != nil {
            c.trie, _ = db.OpenStorageTrie(c.addrHash, common.Hash{})
            c.setError(fmt.Errorf("can't create storage trie: %v", err))
        }
    }
    return c.trie
}

// SetState updates a value in account storage.
// 设置一个新的kv:保存过去的kv,然后设置新的。
func (self *stateObject) SetState(db Database, key, value common.Hash) {
    self.db.journal.append(storageChange{
        account: &self.address,
        key: key,
        prevalue: self.GetState(db, key),
    })
    self.setState(key, value)
}

// 先加入缓存和dirty
func (self *stateObject) setState(key, value common.Hash) {
    self.cachedStorage[key] = value
    self.dirtyStorage[key] = value
}

// updateTrie writes cached storage modifications into the object's storage trie.
// 把标记为dirty的kv写入、删除、更新到存储trie、
func (self *stateObject) updateTrie(db Database) Trie {
    tr := self.getTrie(db)
    for key, value := range self.dirtyStorage {
        delete(self.dirtyStorage, key)
        // 空value代表删除kv
        if (value == common.Hash{}) {
            self.setError(tr.TryDelete(key[:]))
            continue
        }
        // Encoding []byte cannot fail, ok to ignore the error.
        v, _ := rlp.EncodeToBytes(bytes.TrimLeft(value[:], "\x00"))
        self.setError(tr.TryUpdate(key[:], v))
    }
    return tr
}

// UpdateRoot sets the trie root to the current root hash of
// 更新root:更新trie,然后获取新的root。Finalize使用
func (self *stateObject) updateRoot(db Database) {
    self.updateTrie(db)
    self.data.Root = self.trie.Hash()
}

statedb.go

该文件主要实现stateDB的功能:

  • 存储所有的账户信息(stateObject)。
  • 提供增删、修改账户的状态数据(stateObject)的接口。
  • Finalise和提交修改的账户信息(stateObject)。
  • 对每个状态数据改变记录日志,创建快照,实现回滚。

接下来对这4个功能依次介绍。

存储账户信息

关于对stateObject的存储,之前是设计已经讲过其存储思路。现从StateDB的定义讲存储和管理stateObject:

  1. 使用trie来组织它所有的stateObject。
  2. 使用db存储trie。
  3. 使用stateObjects存储最近使用过的stateObject。
  4. 使用stateObjectsDirty存储被修改过的stateObject。
// StateDBs within the ethereum protocol are used to store anything
// within the merkle trie. StateDBs take care of caching and storing
// nested states. It's the general query interface to retrieve:
// * Contracts
// * Accounts
// 在merkle树种保存任何数据,形式是kv
type StateDB struct {
   // 存储本Trie的数据库
   db Database
   // 存储所有的stateObject
   trie Trie

   // This map holds 'live' objects, which will get modified while processing a state transition.
   // 最近使用过的数据对象,他们的账户地址为key
   stateObjects map[common.Address]*stateObject
   // 修改过的账户对象
   stateObjectsDirty map[common.Address]struct{}

   // DB error.
   // State objects are used by the consensus core and VM which are
   // unable to deal with database-level errors. Any error that occurs
   // during a database read is memoized http://lessisbetter.site/2018/06/22/ethereum-code-statedb/ and will eventually be returned
   // by StateDB.Commit.
   dbErr error

   // The refund counter, also used by state transitioning.
   refund uint64

   thash, bhash common.Hash
   txIndex      int
   logs         map[common.Hash][]*types.Log
   logSize      uint

   preimages map[common.Hash][]byte

   // Journal of state modifications. This is the backbone of
   // Snapshot and RevertToSnapshot.
   // 快照和回滚的主要参数
   // 存放每一步修改了啥
   journal *journal
   // 快照id和journal的长度组成revision,可以回滚
   validRevisions []revision
   // 下一个可用的快照id
   nextRevisionId int

   lock sync.Mutex
}

创建StateDB很简单,传入已知的root和使用的db即可。调用cachingDB.OpenTrie打开一个trie,该trie就用来存放所有的stateObject。

func New(root common.Hash, db Database) (*StateDB, error) {
   tr, err := db.OpenTrie(root)
   if err != nil {
      return nil, err
   }
   return &StateDB{
      db:                db,
      trie:              tr,
      stateObjects:      make(map[common.Address]*stateObject),
      stateObjectsDirty: make(map[common.Address]struct{}),
      logs:              make(map[common.Hash][]*types.Log),
      preimages:         make(map[common.Hash][]byte),
      journal:           newJournal(),
   }, nil
}

增删改和查询账户信息(状态数据)

创建账户。账户使用地址来标记,所以创建账户的时候要传入地址。如果当前的地址已经代表了一个账户,再执行创建账户,会创建1个新的空账户,然后把旧账户的余额,设置到新的账户,其他账户信息比如Nonce、Code等都设置为初始值了。

// CreateAccount explicitly creates a state object. If a state object with the address
// already exists the balance is carried over to the new account.
//
// CreateAccount is called during the EVM CREATE operation. The situation might arise that
// a contract does the following:
//
//   1. sends funds to sha(account ++ (nonce + 1))
//   2. tx_create(sha(account ++ nonce)) (note that this gets the address of 1)
//
// Carrying over the balance ensures that Ether doesn't disappear.
// 创建一个新的空账户,如果存在该地址的旧账户,则把旧地址中的余额,放到新账户中
func (self *StateDB) CreateAccount(addr common.Address) {
   new, prev := self.createObject(addr)
   if prev != nil {
      new.setBalance(prev.data.Balance)
   }
}

// createObject creates a new state object. If there is an existing account with
// the given address, it is overwritten and returned as the second return value.
// 创建一个stateObject,对账户数据进行初始化,然后记录日志
func (self *StateDB) createObject(addr common.Address) (newobj, prev *stateObject) {
   prev = self.getStateObject(addr)
   newobj = newObject(self, addr, Account{})
   newobj.setNonce(0) // sets the object to dirty
   if prev == nil {
      self.journal.append(createObjectChange{account: &addr})
   } else {
      self.journal.append(resetObjectChange{prev: prev})
   }
   self.setStateObject(newobj)
   return newobj, prev
}

查询账户。getStateObject入参是账户地址,先查询缓存中是否存在账户,没有的话,再从trie中读取。有一点需要注意:trie中实际保存的stateObject中的Account数据,从trie中获取到Account信息后,然后再合成stateObject,它通常被查询账户数据的函数所使用。
GetOrNewStateObject是先查询一下stateObject,如果不存在则创建一个新的。通常是被Set系列函数在更新状态数据的时候使用。

// Retrieve a state object given by the address. Returns nil if not found.
// stateDB中使用trie保存addr到stateObject的映射,stateObject中保存key到value的映射
// 先从stateObjects中读取,否则从Trie读取Account,然后创建stateObject,存到stateObjects
func (self *StateDB) getStateObject(addr common.Address) (stateObject *stateObject) {
   // Prefer 'live' objects.
   if obj := self.stateObjects[addr]; obj != nil {
      if obj.deleted {
         return nil
      }
      return obj
   }

   // Load the object from the database.
   enc, err := self.trie.TryGet(addr[:])
   if len(enc) == 0 {
      self.setError(err)
      return nil
   }
   // trie中实际实际保存的是Account
   var data Account
   if err := rlp.DecodeBytes(enc, &data); err != nil {
      log.Error("Failed to decode state object", "addr", addr, "err", err)
      return nil
   }
   // Insert into the live set.
   obj := newObject(self, addr, data)
   self.setStateObject(obj)
   return obj
}

// Retrieve a state object or create a new state object if nil.
// 获取stateObject,不存在则创建
func (self *StateDB) GetOrNewStateObject(addr common.Address) *stateObject {
   stateObject := self.getStateObject(addr)
   if stateObject == nil || stateObject.deleted {
      stateObject, _ = self.createObject(addr)
   }
   return stateObject
}

更新状态数据。stateObject的修改,修改后都暂存在stateDB.stateObjects中,当执行updateStateObject的时候,是把stateOject进行RLP编码,然后存到stateDB.trie中。
tire中实际保存的是stateObject的Account的RLP编码。因为stateObject实现了EncodeRLP函数,在RLP执行编码的时候,会调用该函数对stateObject进行编码,该函数实际只对state.data进行了编码。

// updateStateObject writes the given object to the trie.
// 把对象RLP编码,然后写到trie
func (self *StateDB) updateStateObject(stateObject *stateObject) {
   addr := stateObject.Address()
   data, err := rlp.EncodeToBytes(stateObject)
   if err != nil {
      panic(fmt.Errorf("can't encode object at %x: %v", addr[:], err))
   }
   self.setError(self.trie.TryUpdate(addr[:], data))
}

// EncodeRLP implements rlp.Encoder.
func (c *stateObject) EncodeRLP(w io.Writer) error {
   return rlp.Encode(w, c.data)
}

更新状态数据,就是一些列的Set函数了,这里就不讲了。

Finalise和Commit

Finalise和Commit是和存储过程紧密关联的2个函数,Finalise代表修改过的状态已经进入“终态”,Commit代表所有的状态都写入到数据库。我们使用下面这个图介绍一下。

  • Finalise会把stateObjects写入到trie,并且计算trie的树根,但trie本身的所有节点,还在trie(trie暂时保存在内存)中,没有写入到trie数据库中。
  • Commit要比Finalise深一步,它会把trie的所有节点写入到trie的数据库中,然后还会使用传入的回调函数处理trie的叶子节点。

我们再结合代码,看Finalise和Commit实现上的差异。Finalise处理的journal中标记为dirty的对象,不处理stateObjectsDirty中的对象,对于自杀的对象和空的对象,要把它们删除对象,降低trie的存储。然后,每向trie里写入1个对象,就会更新一次trie的根,然后才把对象加入到stateObjectsDirty,最后清空journal,因为这些journal已经过时了。

Commit会把journal中所有标记的对象加入到stateObjectsDirty,然后清空自杀和空的对象,把修改的对象写入到trie,把对象trie写入到数据库,最后把自己的trie写入到数据库。

// Finalise finalises the state by removing the self destructed objects
// and clears the journal as well as the refunds.
// 最终化数据库,遍历的日志中标记为dirty的账户,删除部分自杀、或空的数据,然后把数据写入存储trie,然后更新root,但每个对象都没有commit
func (s *StateDB) Finalise(deleteEmptyObjects bool) {
   // 只处理journal中标记为dirty的对象,不处理stateObjectsDirty中的对象
   for addr := range s.journal.dirties {
      stateObject, exist := s.stateObjects[addr]
      if !exist {
         // ripeMD is 'touched' at block 1714175, in tx 0x1237f737031e40bcde4a8b7e717b2d15e3ecadfe49bb1bbc71ee9deb09c6fcf2
         // That tx goes out of gas, and although the notion of 'touched' does not exist there, the
         // touch-event will still be recorded in the journal. Since ripeMD is a special snowflake,
         // it will persist in the journal even though the journal is reverted. In this special circumstance,
         // it may exist in `s.journal.dirties` but not in `s.stateObjects`.
         // Thus, we can safely ignore it http://lessisbetter.site/2018/06/22/ethereum-code-statedb/
         continue
      }

      if stateObject.suicided || (deleteEmptyObjects && stateObject.empty()) {
         s.deleteStateObject(stateObject)
      } else {
         // 把对象数据写入到storage trie,并获取新的root
         stateObject.updateRoot(s.db)
         s.updateStateObject(stateObject)
      }
      // 加入到stateObjectsDirty
      s.stateObjectsDirty[addr] = struct{}{}
   }
   // Invalidate journal because reverting across transactions is not allowed.
   // 清空journal,没法再回滚了
   s.clearJournalAndRefund()
}

// 清空journal,revision,不能再回滚
func (s *StateDB) clearJournalAndRefund() {
   s.journal = newJournal()
   s.validRevisions = s.validRevisions[:0]
   s.refund = 0
}


// Commit writes the state to the underlying in-memory trie database.
// 把数据写入trie数据库,与Finalize不同,这里处理的是Dirty的对象
func (s *StateDB) Commit(deleteEmptyObjects bool) (root common.Hash, err error) {
   // 清空journal无法再回滚
   defer s.clearJournalAndRefund()

   // 把journal中dirties的对象,加入到stateObjectsDirty
   for addr := range s.journal.dirties {
      s.stateObjectsDirty[addr] = struct{}{}
   }
   // Commit objects to the trie.
   // 遍历所有活动/修改过的对象
   for addr, stateObject := range s.stateObjects {
      _, isDirty := s.stateObjectsDirty[addr]
      switch {
      case stateObject.suicided || (isDirty && deleteEmptyObjects && stateObject.empty()):
         // If the object has been removed, don't bother syncing it
         // and just mark it for deletion in the trie.
         s.deleteStateObject(stateObject)
      case isDirty:
         // Write any contract code associated with the state object
         // 把修改过的合约代码写到数据库,这个用法高级,直接把数据库拿过来,插进去
         // 注意:这里写入的DB是stateDB的数据库,因为stateObject的Trie只保存Account信息
         if stateObject.code != nil && stateObject.dirtyCode {
            s.db.TrieDB().Insert(common.BytesToHash(stateObject.CodeHash()), stateObject.code)
            stateObject.dirtyCode = false
         }
         // Write any storage changes in the state object to its storage trie.
         // 对象提交:把任何改变的存储数据写到数据库
         if err := stateObject.CommitTrie(s.db); err != nil {
            return common.Hash{}, err
         }
         // Update the object in the main account trie.
         // 把修改后的对象,编码后写入到stateDB的trie中
         s.updateStateObject(stateObject)
      }
      delete(s.stateObjectsDirty, addr)
   }
   // Write trie changes.
   // stateDB的提交
   root, err = s.trie.Commit(func(leaf []byte, parent common.Hash) error {
      var account Account
      if err := rlp.DecodeBytes(leaf, &account); err != nil {
         return nil
      }
      // 如果叶子节点的trie不空,则trie关联到父节点
      if account.Root != emptyState {
         // reference的功能还没搞懂
         s.db.TrieDB().Reference(account.Root, parent)
      }
      // 如果叶子节点的code不空(合约账户),则把code关联到父节点
      code := common.BytesToHash(account.CodeHash)
      if code != emptyCode {
         s.db.TrieDB().Reference(code, parent)
      }
      return nil
   })
   log.Debug("Trie cache stats after commit", "misses", trie.CacheMisses(), "unloads", trie.CacheUnloads())
   return root, err
}

关于Commit保存对象信息的时候,还有1个重点关注:stateObject.Code并没有保存在stateObject.trie中,而是保存在stateDB.trie中。所以调用stateObject.Code获取合约代码的时候,实际传入的是stateDB.db,cachingDB.ContractCode实际也不使用合约的地址,因为(CodeHash, Code)本身就是作为KV存放在Trie中。

// Code returns the contract code associated with this object, if any.
// 从db读取合约代码,db实际是stateDB.db
func (self *stateObject) Code(db Database) []byte {
   if self.code != nil {
      return self.code
   }
   if bytes.Equal(self.CodeHash(), emptyCodeHash) {
      return nil
   }
   code, err := db.ContractCode(self.addrHash, common.BytesToHash(self.CodeHash()))
   if err != nil {
      self.setError(fmt.Errorf("can't load code hash %x: %v", self.CodeHash(), err))
   }
   self.code = code
   return code
}

// ContractCode retrieves a particular contract's code.
// 合约账户的code
func (db *cachingDB) ContractCode(addrHash, codeHash common.Hash) ([]byte, error) {
   //addrHash无用
   code, err := db.db.Node(codeHash)
   if err == nil {
      db.codeSizeCache.Add(codeHash, len(code))
   }
   return code, err
}

日志和回滚

以太坊使用记录每一步状态的变化来支持回滚,每一步变化就是日志。假如从状态A转移到状态B,需要经过8步,在第1不的时候创建了snapshot,执行到第6步的时候出现了错误,回滚操作就是:把操作2,3,4,5步之前的数据,以5,4,3,2的顺序设置回去。

// Snapshot returns an identifier for the current revision of the state.
// 快照只是一个id,把id和日志的长度关联起来,存到Revisions中
// EVM在执行在运行一个交易时,在修改state之前,创建快照,出现错误,则回滚
func (self *StateDB) Snapshot() int {
   id := self.nextRevisionId
   self.nextRevisionId++
   self.validRevisions = append(self.validRevisions, revision{id, self.journal.length()})
   return id
}

// RevertToSnapshot reverts all state changes made since the given revision.
// 回滚到指定vision/快照
func (self *StateDB) RevertToSnapshot(revid int) {
   // Find the snapshot in the stack of valid snapshots.
   idx := sort.Search(len(self.validRevisions), func(i int) bool {
      return self.validRevisions[i].id >= revid
   })
   if idx == len(self.validRevisions) || self.validRevisions[idx].id != revid {
      panic(fmt.Errorf("revision id %v cannot be reverted", revid))
   }
   snapshot := self.validRevisions[idx].journalIndex

   // Replay the journal to undo changes and remove invalidated snapshots
   // 反操作后续的操作,达到回滚的目的
   self.journal.revert(self, snapshot)
   self.validRevisions = self.validRevisions[:idx]
}

在journal.go中有更多的日志操作,以及每种类型操作需要记录的数据。

转载自:https://lessisbetter.site/2018/06/22/ethereum-code-statedb/

以太坊源码分析:共识(3)PoW

前言

Ethash实现了PoW,PoW的精妙在于通过一个随机数确定,矿工确实做了大量的工作,并且是没有办法作弊的。接下来将介绍:

  1. Ethash的挖矿本质。
  2. Ethash是如何挖矿的。
  3. 如何验证Ethash的随机数。

Ethash的挖矿本质

挖矿的本质是找到一个随机数,证明自己做了很多工作(计算)。在Ethash中,该随机数称为Nonce,它需要满足一个公式:

Rand(hash, nonce) ≤ MaxValue / Difficulty

参数解释

  • hash:去除区块头中Nonce、MixDigest生成的哈希值,见HashNoNonce()
  • nonce:待寻找的符合条件的随机数。
  • MaxValue:固定值2^256,生成的哈希值的最大取值。
  • Difficulty:挖矿难度。
  • Rand():使用hash和nonce生成一个哈希值,这其中包含了很多哈希运算。

以上参数中,在得到区块头的hash之后,只有nonce是未知的。公式的含义是,使用hash和nonce生成的哈希值必须落在合法的区间
利用下图介绍一下,Rand()函数结果取值范围是[0, MaxValue],但只有计算出的哈希值在[0, MaxValue / Difficulty]内,才是符合条件的哈希值,进而该Nonce才是符合条件的,否则只能再去寻找下一个Nonce。

以太坊可以通过调整Difficulty来调节当前挖矿的难度,Difficulty越大,挖矿的难度越大。当Difficulty越大时, MaxValue / Difficulty越小,合法的哈希值范围越小,造成挖矿难度增加。

哈希值满足条件的概率是 p = (MaxValue / Difficulty) / MaxValue = 1 / Difficulty,矿工需要进行1 / p = Difficulty次的判断,才有可能找到一个符合条件的Nonce,当前以太坊难度为3241847139727150

为什么PoW需要做那么多的运算,而不是通过公式反推,计算出满足条件的结果(Nonce)?

PoW可以表示为许多数学公式的合集,每次运算的入参:前一个区块头的哈希,当前高度的DataSet,目标值Nonce,这些数学公式都是哈希函数,哈希函数的特性就是不可逆性,不能通过摘要获得输入数据。虽然,前一个区块头的哈希和当前高度的DataSet是固定的,但由于哈希函数的不可逆性,依然无法倒推出Nonce,只能随机的产生Nonce,或累加Nonce,并不断的重试,直到找到符合条件的Nonce。

如何挖矿

Ethash挖矿的主要思想是,开启多个线程去寻找符合条件的Nonce,给每个线程分配一个随机数,作为本线程的Nonce的初始值,然后每个线程判断当前的Nonce是否符合上面的公式,如果不符合,则把Nonce加1,再次进行判断,这样不定的迭代下去,直到找到一个符合条件的Nonce,或者挖矿被叫停。

接下来介绍挖矿的几个主要函数的实现,它们是:

  1. 挖矿的入口Seal函数。
  2. 挖矿函数mine函数。
  3. 挖矿需要的数据cache和dataset。
  4. Rand()函数的实现hashimotoFull和hashimoto。

挖矿入口Seal()

Seal是引擎的挖矿入口函数,它是管理岗位,负责管理挖矿的线程。它发起多个线程执行Ethash.mine进行并行挖矿,当要更新或者停止的时候,重新启动或停止这些线程。

挖矿函数mine()

mine函数负责挖矿。Seal在启动每一个mine的时候,给它分配了一个seed,mine会把它作为Nonce的初始值,然后生成本高度使用的dataset,然后把dataset, hash, nonce传递给hashimotoFull函数,这个函数可以认为是原理介绍中的Rand随机函数,他会生成哈希值Result,当Result <= Target的时候,说明哈希值落在符合条件的区间了,mine找到了符合条件的Nonce,使用Digest和nonce组成新的区块后,发送给Seal,否则验证下一个Nonce是否是符合条件的

挖矿需要的数据cache和dataset

dataset用来生成Result,而cache用来生成dataset。至于如何使用dataset生成Result在hashimoto()中讲述,本节介绍如何生成dataset。

dataset和cache中存放的都是伪随机数,每个epoch的区块使用相同的cache和dataset,并且dataset需要暂用大量的内存。刚开始时cache是16MB,dataset是1GB,但每个epoch它们就会增大一次,它们的大小分别定义在datasetSizes和cacheSizes,dataset每次增长8MB,最大能达到16GB,所以挖矿的节点必须有足够大的内存。

使用cache生成dataset。使用cache的部分数据,进行哈希和异或运算,就能生成一组dataset的item,比如下图中的cache中黄色块,能生成dataset中的黄色块,最后把这些Item拼起来就生成了完整的Dataset,完成该功能的函数是generateDataset。

dataset.generate()是dataset的生成函数,该函数只执行一次,先使用generateCache()生成cache,再将cache作为generateDataset()的入参生成dataset,其中需要重点关注的是generateDatasetItem(),该函数是根据部分cache,生成一组dataset item,验证PoW的nonce的时候,也需要使用该函数。

Rand()的实现hashimotoFull()和hashimoto()

hashimotoFull功能是使用dataset、hash和nonce生成Digest和Result。它创建一个获取dataset部分数据的lookup函数,该函数能够返回连续的64字节dataset中的数据,然后把lookup函数、hash和nonce传递给hashimoto。

hashimoto的功能是根据hash和nonce,以及lookup函数生成Digest和Result,lookup函数能够返回64字节的数据就行。它把hash和nonce合成种子,然后根据种子生成混合的数据mix,然后进入一个循环,使用mix和seed获得dataset的行号,使用lookup获取指定行的数据,然后把数据混合到mix中,混合的方式是使用哈希和异或运算,循环结束后再使用哈希和异或函数把mix压缩为64字节,把mix转为小端模式就得到了Digest,把seed和mix进行hash运算得到Result。

如何验证

PoW的验证是证明出块人确实进行了大量的哈希计算。Ethash验证区块头中的Nonce和MixDigest是否合法,如果验证通过,则认为出块人确实进行了大量的哈希运算。验证方式是确定区块头中的Nonce是否符合公式,并且区块头中的MixDigest是否与使用此Nonce计算出的是否相同。

验证与挖矿相比,简直是毫不费力,因为:

  • 时间节省。验证只进行1次hashimoto运算,而挖矿进行大约Difficulty次。
  • 空间节省。验证只需要cache,不需要dataset,也就不需要计算庞大的dataset,因此不挖矿的验证节点,不需要很高的配置。

接下来介绍验证函数VerifySeal(),以及根据cache生成Digest和Result的hashimotoLight()。

验证函数VerifySeal

Ethash.VerifySeal实现PoW验证功能。首先先判断区块中的Difficulty是否匹配,然后生成(获取)当前区块高度的cache,把cache和nonce传递给hashimotoLight,该函数能根据cache, hash, nonce生成Digest和Result,然后校验Digest是否匹配以及Result是否符合条件。

hashimotoLight函数

hashimotoLight使用cache, hash, nonce生成Digest和Result。生成Digest和Result只需要部分的dataset数据,而这些部分dataset数据时可以通过cache生成,因此也就不需要完整的dataset。它把generateDatasetItem函数封装成了获取部分dataset数据的lookup函数,然后传递给hashimoto计算出Digest和Result。

FAQ

  • Q:每30000个块使用同一个dataset,那可以提前挖出一些合法的Nonce?
    A:不行。提前挖去Nonce,意味着还不知道区块头的hash,因此无法生成合法的Nonce。
  • Q:能否根据符合条件的哈希值,反推出Nonce呢?
    A:不行。因为哈希运算具有不可逆性,不能根据摘要反推出明文,同理根据哈希值也无法推出Nonce。

转载自:https://lessisbetter.site/2018/06/22/ethereum-code-consensus-3/

以太坊源码分析:共识(2)接口

前言

engine是以太坊封定义的一个接口,它的功能可以分为3类:

  1. 验证区块类,主要用在将区块加入到区块链前,对区块进行共识验证。
  2. 产生区块类,主要用在挖矿时。
  3. 辅助类。

接下来我们看一下engine具体定义了哪些功能,还有各功能的使用场景。

engine定义的具体功能

engine有3类功能,验证区块类、产生区块类、辅助类。因为产生区块在前,验证区块在后,接下来采用产生区块类、验证区块类、辅助类,分别介绍它们的功能和使用场景。

验证区块类

  1. Prepare:初始化区块头信息,不同的共识算法初始化不同。使用场景是,worker创建work的时候调用。
  2. Finalize:根据数据生成“基本定型”的区块,但区块头中还缺少部分数据。使用场景是,1)模拟区块链的时候,被GenerateChain调用,用来生成区块链。2)交易状态管理时,被StateProcessor.Process调用用来执行交易。3)worker创建work的时候调用。
  3. Seal:根据传入的块,进行的是挖矿工作,使用挖矿的结果,修改区块头,然后生成新的区块。使用场景是,被agent.mine调用。

验证区块类

  1. VerifyHeader:验证区块头。使用在fetcher中,当fetcher要插入区块的时候,需要先对区块头进行校验。
  2. VerifyHeaders:验证一批区块头。有2种使用场景,1)区块链中,insertChain当把一批区块插入到区块链这个链条的时候,需要进行检查;2)LightChain中,把一批区块头插入到本地链。
  3. VerifyUncles:验证区块中的叔块。insertChain当区块插入区块链的时候,需要对叔块进行验证,调用在VerifyHeaders之后。
  4. VerifySeal:针对Seal函数做的功能进行验证。验证Seal()所修改的区块头中的数据。对外的使用场景是,把Work发送给远端Agent的时候调用。对内的使用场景是,验证区块头的时候会被调用。

辅助类

  1. APIs:生成以太坊共识相关的API。在以太坊启动RPC服务时,生成API。
  2. Author:读取区块头中的coinbase。被ethstats使用,ethstats是以太坊状态管理服务,当报告数据的时候,需要获取区块的Author信息。

最后关注一下蓝色的线条,它们代表insertChain所调用的范围,先关的有VerifyHeaders、VerifyUncles、Finalize,涉及到块头的验证、叔块的验证,以及执行区块中的交易,一个区块加入到区块链中,不仅要验证,还要执行各种交易,改变各种状态,所有节点执行确定性的行为之后,达成一致性。

Faq

  • Q:谁实现engine
    A:以太坊中的Ethash和Clique实现了engine,Ethash是基于PoW的共识,Clique是基于PoA的共识。
  • Q:为什么insertChain没有调用VerifySeal?
    A:因为Seal()修改的是header中的部分数据,在验证区块头的时候,会被调用。只是调用流程在Ethash和Clique中的实现略有不同,后续讲解。

转载自:https://lessisbetter.site/2018/06/22/ethereum-code-consensus-2/

以太坊源码分析:共识(1)矿工

前言

矿工在PoW中负责了产生区块的工作,把一大堆交易交给它,它生成一个证明自己工作了很多区块,然后将区块加入到本地区块链并且广播给其他节点。

接下来我们将从以下角度介绍矿工:

  1. 角色。矿工不是一个人,而是一类人,可以把这一类人分成若干角色。
  2. 一个区块产生的主要流程。
  3. 矿工的主要函数介绍,掌握矿工的主要挖矿机制。

介绍矿工由哪些部分组成,会和哪些其他模块进行交互,这些部分是如何协作产生区块的。

角色

有3种角色:miner、worker、agent。

  • miner:是矿长,负责管理整个矿场的运作,比如:启动、停止挖矿,处理外部请求,设置挖矿获得的奖励的钱包地址等等。
  • worker:副矿长,负责具体挖矿工作的安排,把挖矿任务(Work)安排给agent。
  • agent:真实的矿工,他们负责挖矿,把自己的劳动成果(Result)交给worker,agent默认只有1个,可以通过API创建多个。

一个区块产生的主要流程

实际的挖矿过程基本不涉及miner,只涉及worker、agent和engine,engine是共识引擎模块,我们利用下图介绍生成一个区块的主要流程。
挖矿过程中只涉及engine的3个接口:

  1. Prepare()挖矿前的准备工作,
  2. Finalize()形成一个基本定型的区块,
  3. Seal()形成最终的区块。
  • worker把区块头、交易、交易执行的收据等传递给engine.Finalize。
  • engine.Finalize返回一个block,该block的header中缺少Nonce和MixDigest,这两个值是挖矿获取的。
  • worker把block封装到work,把work发送给所有的agent。
  • agent.update把work传递给agent.mine。
  • agent.mine把work传递给engine.Seal,调用engine.Seal挖矿。
  • engine.Seal把Nonce和MixDigest填到区块头,生成一个new block交给agent.mine.
  • agent.mine把new block封装成Result,发送给worker。

矿工的主要函数

介绍miner、worker和agent的主要函数,他们是矿工的具体运作机制。

miner的主要函数

主要关注2个函数:

  1. New():负责创建miner。还创建1个worker和1个agent,但agent还可以通过API创建,然后启动update函数。
  2. update():负责关注downloader的3个事件:StartEvent、DoneEvent、FailedEvent。StartEvent是开始同步区块,必须停止挖矿,DoneEvent和FailedEvent是同步成功或者失败,是同步的结束,已经可以挖矿了。表明:挖矿和同步区块不可同时进行,尽量降低了区块冲突的可能。

worker的主要函数

主要是3个函数:

  1. commitNewWork():负责生成work,分配agent。这个阶段做了很多工作,调用Engine.Prepare进行准备工作,创建Header,执行交易,获取Uncle,使用Engine.Finalize形成“基本定型”的临时区块,创建Work,最后把work传递给agent。另外commitNewWork存在多处调用,并且worker有wait和update另外2个协程,他们都会调用commitNewWork,所以存在临界区需要谨慎加锁。
  2. update():负责处理外部事件。它是死循环,主要处理3种事件:1)ChainHeadEvent,有了新区块头,所以得切换到挖下一个高度的区块,2)ChainSideEvent,收到了uncle区块,缓存起来,3)TxPreEvent,预处理交易,如果在挖矿执行commitNewWork,如果未挖矿,则交易设置为未决状态。
  3. wait():负责处理agent挖矿的结果。它是死循环,一直等待接收agent发回的result,然后把区块加入到本地数据库,如果没有问题,就发布NewMinedBlockEvent事件,通告其他节点挖到了一个新块。

agent的主要函数

主要2个函数:

  1. update():负责接收worker发来的任务(work)。它是死循环,把work交给mine去挖矿。
  2. mine():负责挖矿。它拥有挖矿的能力,调用Engine.Seal挖矿,如果挖矿成功则生成result,发送给worker。

转载自:https://lessisbetter.site/2018/06/22/ethereum-code-consensus-1/

以太坊源码分析:交易缓冲池txpool

区块链就是何交易打交道,我们今天就介绍下,交易处理过程中的一个重要组成部分:txpool。这篇文章主要从功能角度介绍,通过这篇文章会了解:

  1. txpool的在交易中的位置和作用。
  2. txpool的功能,核心组成部分queued和pending。
  3. txpool如何实现它的功能。
  4. txpool源码的重要关注点。

以太坊内部有个重要的内部功能是txpool,从字面意思就能看出来,交易池就是存放交易的池子。它在以太坊中的位置如下图,只要有新交易,无论是本节点创建的,还是其他peer节点广播来的,都会先加入到交易池里,在打包区块的时候,就从这个池子里提取,区块产生之后,共识区块,交易上链。

txpool有4个功能:

  1. 作为存放交易的缓冲区,大量交易到来时,先存起来
  2. 为打包区块服务,合适交易会被打包到区块
  3. 清理交易
  4. 当交易的数量多于缓冲区大小时,过滤/惩罚发送大量交易的账户(攻击者)

我们来一张稍微详细点的模块交互图,看txpool怎么实现上面4个功能的。

缓存功能的设计

txpool中的交易分为queued和pending 2种,其中queued存放未来的、当前无法执行的交易。以太坊使用nonce值决定某个账户的交易顺序,多条交易值nonce值必须连续,如果和过去的交易不连续,则无法执行,我们不妨使用nonce值,标记交易的号码,nonce为10的交易,称为第10号交易。举个例子,当前账户的nonce是10,txpool中有该账户的第100号交易,但txpool中没有第11~99号交易,这些交易的缺失,造成第100号交易无法执行,所以第100号交易就是未来的交易、不可执行的交易,存放在queue中。

pending存放可执行的交易。比如我们把上面的11 ~ 99号交易补全了,那么100号交易都可以进入到pending,因为这些交易都是连续的,都可以打包进区块。

当节点收到交易(本地节点发起的或peer广播来的)时,会先存放到queued,txpool在某些情况下,把queued中可执行的交易,转移到pending中。

为区块打包服务

这是txpool最核心的功能,worker在打包区块的时候,会获取所有的pending交易,但这些交易还存在txpool中,worker只是读取出来,至于txpool何时删除交易,稍后从txpool清理交易的角度单独在看。

清理交易

txpool清理交易有以下几种条件,符合任意以下1条的,都是无效交易,会被从pending或者queued中移除:

  1. 交易的nonce值已经低于账户在当前高度上的nonce值,代表交易已过期,交易已经上链就属于这种情况
  2. 交易的GasLimit大于区块的GasLimit,区块容不下交易
  3. 账户的余额已不足以支持该交易要消耗的费用
  4. 交易的数量超过了queued和pending的缓冲区大小,需要进行清理

交易清理主要有3个场景:

  1. txpool订阅了ChainHeadEvent事件,该事件代表主链上有新区块产生,txpool会根据最新的区块,检查每个账号的交易,有些无效的会被删除,有些由于区块回滚会从pending移动到queued,然后把queued中可执行的交易移动到pending,为下一轮区块打包组号准备。
  2. queued交易移动到pending被称为“提升”(promote),这个过程中,同样会检查交易,当交易不符合以上条件时,就会被直接从queued中删除。
  3. 删除停留在queued中超过3小时的交易,3小时这个超时时间是可以通过geth的启动参数调整的。txpool记录了某个账户交易进入pending的时间,如果这个时间超过了3小时,代表该账号的交易迟迟不能被主链打包,既然无法被主链接受,就删除掉在queued中本来就无法执行的交易

惩罚恶意账号

这也是txpool很重要的一个属性,可以防止恶意账户以发起大量垃圾交易。防止恶意用户造成:

  1. 占用txpool空间
  2. 浪费节点大量内存和CPU
  3. 降低打包性能

只有当交易的总数量超过缓冲区大小时,txpool才会认为有恶意账户发起大量交易。
pending和queued缓冲区大小不同,但处理策略类似:

  1. pending的缓冲区容量是4096,当pending的交易数量多于此时,就会运行检查,每个账号的交易数量是否多于16,把这些账号搜集出来,进行循环依次清理,什么意思呢?就是每轮只删除(移动到queued)这些账号的每个账号1条交易,然后看数量是否降下来了,不满足再进行下一轮,直到满足。
  2. queued的缓冲区容量是1024,超过之后清理策略和pending差不多,但这里可是真删除了。

该部分功能未抽象成单独的函数,而是在promoteExecutables()中,就是在每次把queued交易转移到pending后执行的。

本地交易的特权

txpool虽然对交易有诸多限制,但如果交易是本节点的账号发起的,以上数量限制等都对他无效。所以,如果你用本节点账号不停的发送交易,并不会被认为是攻击者,你用txpool.status命令,可以查看到交易的数量,肯定可以大于4096,我曾达到过60000+。

重点关注的源码

txpool的主要设计上面就讲完了,如果你想把txpool的代码阅读一番,我建议你重点关注一下这些函数和变量,按图索骥能就完全掌握txpool的实现。

  • TxPoolConfig:txpool的配置参数
  • chainHeadCh:txpool订阅了新区块事件
  • pending:pending的交易,每个账号都有一个交易列表
  • queue:queued的交易,每个账号都有一个交易列表
  • loop:txpool的事件处理函数
  • addTx:添加1条交易的源头,你能找到类似的函数
  • promoteExecutables:queued交易移动到pending
  • reset:根据当前区块的最新高度,重置txpool中的交易

仔细阅读一遍,你会发现txpool会涉及多个锁(TxPool.mu, TxPool.all, TxPool.priced.all),所以当txpool中的交易很多时,它的性能是很低的,这也会影响到区块的打包。

转载自:https://lessisbetter.site/2018/12/11/ethereum-design-of-txpool/