资讯专栏INFORMATION COLUMN

Ethereum DPOS源码分析

neu / 2429人阅读

摘要:的主要功能就是成为候选人投票对方获得投票,以及系统定期自动执行的选举。它是更大范围上的存在,不直接操作的五棵树,而是通过它聚合的对五棵树进行增删改查。在共识中,返回的是。

1 导语

区块链的主要工作就是出块,出块的制度、方式叫做共识;
块里的内容是不可篡改的信息记录,块连接成链就是区块链。

出块又叫挖矿,有各种挖矿的方式,比如POW、DPOS,本文主要分析DPOS共识源码。

以太坊存在多种共识:

PoW (etash)在主网使用

PoA(clique) 在测试网使用

FakePow 在单元测试使用

DPOS 新增共识替代POW

既然是源码分析,主要读者群体应该是看代码的人,读者须要结合代码看此类文章。明白此类文章的作用是:提供一个分析的切入口,将散落的代码按某种内在逻辑串起来,用图文的形式叙述代码的大意,引领读者有一个系统化的认知,同时对自己阅读代码过程中不理解的地方起到一定参考作用。

2 DPOS的共识逻辑

DPOS的基本逻辑可以概述为:成为候选人-获得他人投票-被选举为验证人-在周期内轮流出块。

从这个过程可以看到,成为候选人和投票是用户主动发起的行为,获得投票和被选为验证人是系统行为。DPOS的主要功能就是成为候选人、投票(对方获得投票),以及系统定期自动执行的选举。

2.1 最初的验证人

验证人就是出块人,在创世的时候,系统还没运行,用户自然不能投票,本系统采用的方法是,在创世配置文件中定义好最初的一批出块验证人(Validator),由这一批验证人在第一个出块周期内轮流出块,默认是21个验证人。

</>复制代码

  1. {
  2. "config": {
  3. "chainId": 8888,
  4. "eip155Block": 0,
  5. "eip158Block": 0,
  6. "byzantiumBlock":0,
  7. "dpos":{
  8. "validators":[
  9. "0x8807fa0db2c60675a8f833dd010469e408428b83",
  10. "0xdf5f5a7abc5d0821c50deb4368528d8691f18737",
  11. "0xe0d64bfb1a30d66ae0f06ce36d5f4edf6835cd7c"
  12. ……
  13. ]
  14. }
  15. },
  16. "nonce": "0x0000000000000042",
  17. "difficulty": "0x020000",
  18. "mixHash": "0x0000000000000000000000000000000000000000000000000000000000000000",
  19. "coinbase": "0x0000000000000000000000000000000000000000",
  20. "timestamp": "0x00",
  21. "parentHash": "0x0000000000000000000000000000000000000000000000000000000000000000",
  22. "extraData": "0x11bbe8db4e347b4e8c937c1c8370e4b5ed33adb3db69cbdb7a38e1e50b1b82fa",
  23. "gasLimit": "0x500000",
  24. "alloc": {}
  25. }
2.2 成为候选人

系统运行之后,任何人随时可以投票,同时也可以获得他人投票。因为只有候选人才允许获得投票,所以任何人被投票之前都要先成为候选人(candidate)。


从外部用户角度看,成为候选人只需要自己发一笔交易即可:

</>复制代码

  1. eth.sendTransaction({
  2. from: "0x646ba1fa42eb940aac67103a71e9a908ef484ec3",
  3. to: "0x646ba1fa42eb940aac67103a71e9a908ef484ec3",
  4. value: 0,
  5. type: 1
  6. })

在系统内部,成为候选人和投票均被定义为交易,其实DPOS定义的所有交易有四种类型,是针对这两种行为的正向和反向操作。

</>复制代码

  1. type TxType uint8
  2. const (
  3. Binary TxType = iota
  4. LoginCandidate //成为候选人
  5. LogoutCandidate //取消候选人
  6. Delegate //投票
  7. UnDelegate //取消投票
  8. )
  9. type txdata struct {
  10. Type TxType `json:"type"
  11. ……
  12. }

成为候选人代码非常简单,就是更新(插入)一下candidateTrie,这棵树的键和值都是候选人的地址,它保存着所有当前时间的候选人。

</>复制代码

  1. func (d *DposContext) BecomeCandidate(candidateAddr common.Address) error {
  2. candidate := candidateAddr.Bytes()
  3. return d.candidateTrie.TryUpdate(candidate, candidate)
  4. }

具体执行交易的时候,它取的地址是from,这意味着只能将自己设为候选人。

</>复制代码

  1. case types.LoginCandidate:
  2. dposContext.BecomeCandidate(msg.From())

除了这里提到的candidateTrie,DPOS总共有五棵树:

</>复制代码

  1. type DposContext struct {
  2. epochTrie *trie.Trie //记录出块周期内的验证人列表 ("validator",[]validator)
  3. delegateTrie *trie.Trie //(append(candidate, delegator...), delegator)
  4. voteTrie *trie.Trie //(delegator, candidate)
  5. candidateTrie *trie.Trie //(candidate, candidate)
  6. mintCntTrie *trie.Trie //记录验证人在周期内的出块数目(append(epoch, validator.Bytes()...),count) 这里的epoch=header.Time/86400
  7. db ethdb.Database
  8. }

</>复制代码

  1. delegator是投票人
2.3 投票

从外部用户角度看,投票也是一笔交易:

</>复制代码

  1. eth.sendTransaction({
  2. from: "0x646ba1fa42eb940aac67103a71e9a908ef484ec3",
  3. to: "0x5b76fff970bf8a351c1c9ebfb5e5a9493e956ffffd",
  4. value: 0,
  5. type: 3
  6. })

系统内部的投票代码,主要更新delegateTrie和voteTrie:

</>复制代码

  1. func (d *DposContext) Delegate(delegatorAddr, candidateAddr common.Address) error {
  2. delegator, candidate := delegatorAddr.Bytes(), candidateAddr.Bytes()
  3. // 获得投票的候选人一定要在candidateTrie中
  4. candidateInTrie, err := d.candidateTrie.TryGet(candidate)
  5. if err != nil {
  6. return err
  7. }
  8. if candidateInTrie == nil {
  9. return errors.New("invalid candidate to delegate")
  10. }
  11. // delete old candidate if exists
  12. oldCandidate, err := d.voteTrie.TryGet(delegator)
  13. if err != nil {
  14. if _, ok := err.(*trie.MissingNodeError); !ok {
  15. return err
  16. }
  17. }
  18. if oldCandidate != nil {
  19. d.delegateTrie.Delete(append(oldCandidate, delegator...))
  20. }
  21. if err = d.delegateTrie.TryUpdate(append(candidate, delegator...), delegator); err != nil {
  22. return err
  23. }
  24. return d.voteTrie.TryUpdate(delegator, candidate)
  25. }
2.4 选举

投票虽然随时可以进行,但是验证人的选出,则是周期性的触发。
选举周期默认设定为24小时,每过24小时,对验证人进行一次重新选举。
每次区块被打包的时候(Finalize)都会调用选举函数,选举函数判断是否到了重新选举的时刻,它根据当前块和上一块的时间,计算两块是否属于同一个选举周期,如果是同一个周期,不触发重选,如果不是同一个周期,则说明当前块是新周期的第一块,触发重选。

选举函数:

</>复制代码

  1. func (ec *EpochContext) tryElect(genesis, parent *types.Header) error {
  2. genesisEpoch := genesis.Time.Int64() / epochInterval //0
  3. prevEpoch := parent.Time.Int64() / epochInterval
  4. //ec.TimeStamp从Finalize传过来的当前块的header.Time
  5. currentEpoch := ec.TimeStamp / epochInterval
  6. prevEpochIsGenesis := prevEpoch == genesisEpoch
  7. if prevEpochIsGenesis && prevEpoch < currentEpoch {
  8. prevEpoch = currentEpoch - 1
  9. }
  10. prevEpochBytes := make([]byte, 8)
  11. binary.BigEndian.PutUint64(prevEpochBytes, uint64(prevEpoch))
  12. iter := trie.NewIterator(ec.DposContext.MintCntTrie().PrefixIterator(prevEpochBytes))
  13. //currentEpoch只有在比prevEpoch至少大于1的时候执行下面代码。
  14. //大于1意味着当前块的时间,距离上一块所处的周期起始时间,已经超过epochInterval即24小时了。
  15. //大于2过了48小时……
  16. for i := prevEpoch; i < currentEpoch; i++ {
  17. // 如果前一个周期不是创世周期,触发踢出验证人规则
  18. if !prevEpochIsGenesis && iter.Next() {
  19. if err := ec.kickoutValidator(prevEpoch); err != nil {
  20. return err
  21. }
  22. }
  23. //计票,按票数从高到低得出safeSize个验证人
  24. // 候选人的票数cnt=所有投他的delegator的账户余额之和
  25. votes, err := ec.countVotes()
  26. if err != nil {
  27. return err
  28. }
  29. candidates := sortableAddresses{}
  30. for candidate, cnt := range votes {
  31. candidates = append(candidates, &sortableAddress{candidate, cnt})
  32. }
  33. if len(candidates) < safeSize {
  34. return errors.New("too few candidates")
  35. }
  36. sort.Sort(candidates)
  37. if len(candidates) > maxValidatorSize {
  38. candidates = candidates[:maxValidatorSize]
  39. }
  40. // shuffle candidates
  41. //用父块的hash和当前周期编号做验证人列表随机乱序的种子
  42. //打乱验证人列表顺序,由seed确保每个节点计算出来的验证人顺序都是一致的。
  43. seed := int64(binary.LittleEndian.Uint32(crypto.Keccak512(parent.Hash().Bytes()))) + i
  44. r := rand.New(rand.NewSource(seed))
  45. for i := len(candidates) - 1; i > 0; i-- {
  46. j := int(r.Int31n(int32(i + 1)))
  47. candidates[i], candidates[j] = candidates[j], candidates[i]
  48. }
  49. sortedValidators := make([]common.Address, 0)
  50. for _, candidate := range candidates {
  51. sortedValidators = append(sortedValidators, candidate.address)
  52. }
  53. epochTrie, _ := types.NewEpochTrie(common.Hash{}, ec.DposContext.DB())
  54. ec.DposContext.SetEpoch(epochTrie)
  55. ec.DposContext.SetValidators(sortedValidators)
  56. log.Info("Come to new epoch", "prevEpoch", i, "nextEpoch", i+1)
  57. }
  58. return nil
  59. }

当epochContext最终调用了dposContext的SetValidators()后,新的一批验证人就产生了,这批新的验证人将开始轮流出块。

2.5 DPOS相关类图

EpochContext是选举周期(默认24小时)相关实体类,所以主要功能是仅在周期时刻发生的事情,包括选举、计票、踢出验证人。它是更大范围上的存在,不直接操作DPOS的五棵树,而是通过它聚合的DposContext对五棵树进行增删改查。

DposContext和Trie是强组合关系,DPOS的交易行为(成为候选人、取消为候选人、投票、取消投票、设置验证人)就是它的主要功能。

Dpos is a engine,实现Engine接口。

</>复制代码

  1. func (self *worker) mintBlock(now int64) {
  2. engine, ok := self.engine.(*dpos.Dpos)
  3. ……
  4. }
3 DPOS引擎实现

DPOS是共识引擎的具体实现,Engine接口定义了九个方法。

3.1 Author

</>复制代码

  1. func (d *Dpos) Author(header *types.Header) (common.Address, error) {
  2. return header.Validator, nil
  3. }

这个接口的意思是返回出块人。在POW共识中,返回的是header.Coinbase。
DPOS中Header增加了一个Validator,是有意将Coinbase和Validator的概念分开。Validator默认等于Coinbase,也可以设为不一样的地址。

3.2 VerifyHeader

验证header里的一些字段是否符合dpos共识规则。
符合以下判断都是错的:

</>复制代码

  1. header.Time.Cmp(big.NewInt(time.Now().Unix())) > 0
  2. len(header.Extra) < extraVanity+extraSeal //32+65
  3. header.MixDigest != (common.Hash{})
  4. header.Difficulty.Uint64() != 1
  5. header.UncleHash != types.CalcUncleHash(nil)
  6. parent == nil || parent.Number.Uint64() != number-1 || parent.Hash() != header.ParentHash
  7. //与父块出块时间间隔小于了10(blockInterval)秒
  8. parent.Time.Uint64()+uint64(blockInterval) > header.Time.Uint64()
3.3 VerifyHeaders

批量验证header

3.4 VerifyUncles

dpos里不应有uncles。

</>复制代码

  1. func (d *Dpos) VerifyUncles(chain consensus.ChainReader, block *types.Block) error {
  2. if len(block.Uncles()) > 0 {
  3. return errors.New("uncles not allowed")
  4. }
  5. return nil
  6. }
3.5 Prepare

为Header准备部分字段:
Nonce为空;
Extra预留为32+65个0字节,Extra字段包括32字节的extraVanity前缀和65字节的extraSeal后缀,都为预留字节,extraSeal在区块Seal的时候写入验证人的签名。
Difficulty置为1;
Validator设置为signer;signer是在启动挖矿的时候设置的,其实就是本节点的验证人(Ethereum.validator)。

</>复制代码

  1. func (d *Dpos) Prepare(chain consensus.ChainReader, header *types.Header) error {
  2. header.Nonce = types.BlockNonce{}
  3. number := header.Number.Uint64()
  4. //如果header.Extra不足32字节,则用0填充满32字节。
  5. if len(header.Extra) < extraVanity {
  6. header.Extra = append(header.Extra, bytes.Repeat([]byte{0x00}, extraVanity-len(header.Extra))...)
  7. }
  8. header.Extra = header.Extra[:extraVanity]
  9. //header.Extra再填65字节
  10. header.Extra = append(header.Extra, make([]byte, extraSeal)...)
  11. parent := chain.GetHeader(header.ParentHash, number-1)
  12. if parent == nil {
  13. return consensus.ErrUnknownAncestor
  14. }
  15. header.Difficulty = d.CalcDifficulty(chain, header.Time.Uint64(), parent)
  16. //header.Validator赋值为Dpos的signer。
  17. header.Validator = d.signer
  18. return nil
  19. }

</>复制代码

  1. 关于难度

  2. 在DPOS里,不需要求难度值,给定一个即可。

  3. </>复制代码

    1. func (d *Dpos) CalcDifficulty(chain consensus.ChainReader, time uint64, parent *types.Header) *big.Int {
    2. return big.NewInt(1)
    3. }
  4. 而在POW中,难度是根据父块和最新块的时间差动态调整的,小于10增加难度,大于等于20减小难度。

  5. </>复制代码

    1. block_diff = parent_diff + 难度调整 + 难度炸弹
    2. 难度调整 = parent_diff // 2048 * MAX(1 - (block_timestamp - parent_timestamp) // 10, -99)
    3. 难度炸弹 = INT(2^((block_number // 100000) - 2))
  6. 关于singer

  7. 调用API,人为设置本节点的验证人

  8. </>复制代码

    1. func (api *PrivateMinerAPI) SetValidator(validator common.Address) bool {
    2. api.e.SetValidator(validator) //e *Ethereum
    3. return true
    4. }
  9. </>复制代码

    1. func (self *Ethereum) SetValidator(validator common.Address) {
    2. self.lock.Lock() //lock sync.RWMutex
    3. self.validator = validator
    4. self.lock.Unlock()
    5. }
  10. 节点启动挖矿时调用了dpos.Authorize将验证人赋值给了dpos.signer

  11. </>复制代码

    1. func (s *Ethereum) StartMining(local bool) error {
    2. validator, err := s.Validator()
    3. ……
    4. if dpos, ok := s.engine.(*dpos.Dpos); ok {
    5. wallet, err := s.accountManager.Find(accounts.Account{Address: validator})
    6. if wallet == nil || err != nil {
    7. log.Error("Coinbase account unavailable locally", "err", err)
    8. return fmt.Errorf("signer missing: %v", err)
    9. }
    10. dpos.Authorize(validator, wallet.SignHash)
    11. }
    12. ……
    13. }
  12. </>复制代码

    1. func (s *Ethereum) Validator() (validator common.Address, err error) {
    2. s.lock.RLock() //lock sync.RWMutex
    3. validator = s.validator
    4. s.lock.RUnlock()
    5. ……
    6. }
  13. </>复制代码

    1. func (d *Dpos) Authorize(signer common.Address, signFn SignerFn) {
    2. d.mu.Lock()
    3. d.signer = signer
    4. d.signFn = signFn
    5. d.mu.Unlock()
    6. }
3.6 Finalize


生成一个新的区块,不过不是最终的区块。该函数功能请看注释。

</>复制代码

  1. func (d *Dpos) Finalize(……){
  2. //把奖励打入Coinbase,拜占庭版本以后奖励3个eth,之前奖励5个
  3. AccumulateRewards(chain.Config(), state, header, uncles)
  4. //调用选举,函数内部判断是否到了新一轮选举周期
  5. err := epochContext.tryElect(genesis, parent)
  6. //每出一个块,将该块验证人的出块数+1,即更新DposContext.mintCntTrie。
  7. updateMintCnt(parent.Time.Int64(), header.Time.Int64(), header.Validator, dposContext)
  8. //给区块设置header,transactions,Bloom,uncles;
  9. //给header设置TxHash,ReceiptHash,UncleHash;
  10. return types.NewBlock(header, txs, uncles, receipts), nil
  11. }
3.7 Seal


dpos的Seal主要是给新区块进行签名,即把签名写入header.Extra,返回最终状态的区块。
d.signFn是个函数类型的声明,首先源码定义了一个钱包接口SignHash用于给一段hash进行签名,然后将这个接口作为形参调用dpos.Authorize,这样d.signFn就被赋予了这个函数,而具体实现是keystoreWallet.SignHash,所以d.signFn的执行就是在执行keystoreWallet.SignHash。

</>复制代码

  1. func (d *Dpos) Seal(chain consensus.ChainReader, block *types.Block, stop <-chan struct{}) (*types.Block, error) {
  2. header := block.Header()
  3. number := header.Number.Uint64()
  4. // Sealing the genesis block is not supported
  5. if number == 0 {
  6. return nil, errUnknownBlock
  7. }
  8. now := time.Now().Unix()
  9. delay := NextSlot(now) - now
  10. if delay > 0 {
  11. select {
  12. case <-stop:
  13. return nil, nil
  14. //等到下一个出块时刻slot,如10秒1块的节奏,10秒内等到第10秒,11秒则要等到第20秒,以此类推。
  15. case <-time.After(time.Duration(delay) * time.Second):
  16. }
  17. }
  18. block.Header().Time.SetInt64(time.Now().Unix())
  19. // time"s up, sign the block
  20. sighash, err := d.signFn(accounts.Account{Address: d.signer}, sigHash(header).Bytes())
  21. if err != nil {
  22. return nil, err
  23. }
  24. //将签名赋值给header.Extra的后缀。这里数组索引不会为负,因为在Prepare的时候,Extra就保留了32(前缀)+65(后缀)个字节。
  25. copy(header.Extra[len(header.Extra)-extraSeal:], sighash)
  26. return block.WithSeal(header), nil
  27. }

</>复制代码

  1. func (b *Block) WithSeal(header *Header) *Block {
  2. cpy := *header
  3. return &Block{
  4. header: &cpy,
  5. transactions: b.transactions,
  6. uncles: b.uncles,
  7. // add dposcontext
  8. DposContext: b.DposContext,
  9. }
  10. }
3.8 VerifySeal

Seal接口是区块产生的最后一道工序,也是各种共识算法最核心的实现,VerifySeal就是对这种封装的真伪验证。

1)从epochTrie里获取到验证人列表,(epochTrie的key就是字面量“validator”,它全局唯一,每轮选举后都会被覆盖更新)再用header的时间计算本区块验证人所在列表的偏移量(作为验证人列表数组索引),获得验证人地址。

</>复制代码

  1. validator, err := epochContext.lookupValidator(header.Time.Int64())

2)用Dpos的签名还原出这个验证人的地址。两者进行对比,看是否一致,再用还原的地址和header.Validator对比看是否一致。

</>复制代码

  1. if err := d.verifyBlockSigner(validator, header); err != nil {
  2. return err
  3. }

</>复制代码

  1. func (d *Dpos) verifyBlockSigner(validator common.Address, header *types.Header) error {
  2. signer, err := ecrecover(header, d.signatures)
  3. if err != nil {
  4. return err
  5. }
  6. if bytes.Compare(signer.Bytes(), validator.Bytes()) != 0 {
  7. return ErrInvalidBlockValidator
  8. }
  9. if bytes.Compare(signer.Bytes(), header.Validator.Bytes()) != 0 {
  10. return ErrMismatchSignerAndValidator
  11. }
  12. return nil
  13. }

其中:
header.Validator是在Prepare接口中被赋值的。
d.signatures这个签名是怎么赋值的?不要顾名思义它存的不是签名,它的类型是一种有名的缓存,(key,value)分别是(区块头hash,验证人地址),它的赋值也是在ecrecover里进行的。ecrecover根据区块头hash从缓存中获取到验证人地址,如果没有就从header.Extra的签名部分还原出验证人地址。

3)VerifySeal经过上面两步验证后,最后这个操作待详细分析。

</>复制代码

  1. return d.updateConfirmedBlockHeader(chain)
3.9 APIs

用于容纳API。

</>复制代码

  1. func (d *Dpos) APIs(chain consensus.ChainReader) []rpc.API {
  2. return []rpc.API{{
  3. Namespace: "dpos",
  4. Version: "1.0",
  5. Service: &API{chain: chain, dpos: d},
  6. Public: true,
  7. }}
  8. }

它在eth包里被赋值具体API

</>复制代码

  1. apis = append(apis, s.engine.APIs(s.BlockChain())...)
  2. func (s *Ethereum) APIs() []rpc.API {
  3. apis := ethapi.GetAPIs(s.ApiBackend)
  4. // Append any APIs exposed explicitly by the consensus engine
  5. apis = append(apis, s.engine.APIs(s.BlockChain())...)
  6. // Append all the local APIs and return
  7. return append(apis, []rpc.API{
  8. {
  9. Namespace: "eth",
  10. Version: "1.0",
  11. Service: NewPublicEthereumAPI(s),
  12. Public: true,
  13. }, {
  14. Namespace: "eth",
  15. Version: "1.0",
  16. Service: NewPublicMinerAPI(s),
  17. Public: true,
  18. }, {
  19. Namespace: "eth",
  20. Version: "1.0",
  21. Service: downloader.NewPublicDownloaderAPI(s.protocolManager.downloader, s.eventMux),
  22. Public: true,
  23. }, {
  24. Namespace: "miner",
  25. Version: "1.0",
  26. Service: NewPrivateMinerAPI(s),
  27. Public: false,
  28. }, {
  29. Namespace: "eth",
  30. Version: "1.0",
  31. Service: filters.NewPublicFilterAPI(s.ApiBackend, false),
  32. Public: true,
  33. }, {
  34. Namespace: "admin",
  35. Version: "1.0",
  36. Service: NewPrivateAdminAPI(s),
  37. }, {
  38. Namespace: "debug",
  39. Version: "1.0",
  40. Service: NewPublicDebugAPI(s),
  41. Public: true,
  42. }, {
  43. Namespace: "debug",
  44. Version: "1.0",
  45. Service: NewPrivateDebugAPI(s.chainConfig, s),
  46. }, {
  47. Namespace: "net",
  48. Version: "1.0",
  49. Service: s.netRPCService,
  50. Public: true,
  51. },
  52. }...)
  53. }

这些赋值的其实是结构体,通过结构体可以访问到自身的方法,这些结构体大多都是Ethereum,只不过区分了Namespace用于不同场景。

</>复制代码

  1. type PublicEthereumAPI struct {
  2. e *Ethereum
  3. }
  4. type PublicMinerAPI struct {
  5. e *Ethereum
  6. }
  7. type PublicDownloaderAPI struct {
  8. d *Downloader
  9. mux *event.TypeMux
  10. installSyncSubscription chan chan interface{}
  11. uninstallSyncSubscription chan *uninstallSyncSubscriptionRequest
  12. }
  13. type PrivateMinerAPI struct {
  14. e *Ethereum
  15. }
  16. type PublicDebugAPI struct {
  17. eth *Ethereum
  18. }

看看都有哪些API服务:

showImg("https://segmentfault.com/img/remote/1460000017505461?w=1130&h=1030");

在mintLoop方法里,worker无限循环,阻塞监听stopper通道,每秒调用一次mintBlock。
用户主动停止以太坊节点的时候,stopper通道被关闭,worker就停止了。

4.2 mintBlock挖矿函数分析

这个函数的作用即用引擎(POW、DPOS)出块。在POW版本中,worker还需要启动agent(分为CpuAgent和何RemoteAgent两种实现),agent进行Seal操作。在DPOS中,去掉了agent这一层,直接在mintBlock里Seal。

mintLoop每秒都调用mintBlock,但并非每秒都出块,逻辑在下面分析。

</>复制代码

  1. func (self *worker) mintLoop() {
  2. ticker := time.NewTicker(time.Second).C
  3. for {
  4. select {
  5. case now := <-ticker:
  6. self.mintBlock(now.Unix())
  7. case <-self.stopper:
  8. close(self.quitCh)
  9. self.quitCh = make(chan struct{}, 1)
  10. self.stopper = make(chan struct{}, 1)
  11. return
  12. }
  13. }
  14. }
  15. func (self *worker) mintBlock(now int64) {
  16. engine, ok := self.engine.(*dpos.Dpos)
  17. if !ok {
  18. log.Error("Only the dpos engine was allowed")
  19. return
  20. }
  21. err := engine.CheckValidator(self.chain.CurrentBlock(), now)
  22. if err != nil {
  23. switch err {
  24. case dpos.ErrWaitForPrevBlock,
  25. dpos.ErrMintFutureBlock,
  26. dpos.ErrInvalidBlockValidator,
  27. dpos.ErrInvalidMintBlockTime:
  28. log.Debug("Failed to mint the block, while ", "err", err)
  29. default:
  30. log.Error("Failed to mint the block", "err", err)
  31. }
  32. return
  33. }
  34. work, err := self.createNewWork()
  35. if err != nil {
  36. log.Error("Failed to create the new work", "err", err)
  37. return
  38. }
  39. result, err := self.engine.Seal(self.chain, work.Block, self.quitCh)
  40. if err != nil {
  41. log.Error("Failed to seal the block", "err", err)
  42. return
  43. }
  44. self.recv <- &Result{work, result}
  45. }

如时序图和源码所示,mintBlock函数包含3个主要方法:

4.2.1 CheckValidator出块前验证

该函数判断当前出块人(validator)是否与dpos规则计算得到的validator一样,同时判断是否到了出块时间点。

</>复制代码

  1. </>复制代码

    1. func (self *worker) mintBlock(now int64) {
    2. ……
    3. //检查出块验证人validator是否正确
    4. //CurrentBlock()是截止当前时间,最后加入到链的块
    5. //CurrentBlock()是BlockChain.insert的时候赋的值
    6. err := engine.CheckValidator(self.chain.CurrentBlock(), now)
    7. ……
    8. }

</>复制代码

  1. func (d *Dpos) CheckValidator(lastBlock *types.Block, now int64) error {
  2. //检查是否到达出块间隔最后1秒(slot),出块间隔设置为10秒
  3. if err := d.checkDeadline(lastBlock, now); err != nil {
  4. return err
  5. }
  6. dposContext, err := types.NewDposContextFromProto(d.db, lastBlock.Header().DposContext)
  7. if err != nil {
  8. return err
  9. }
  10. epochContext := &EpochContext{DposContext: dposContext}
  11. //根据dpos规则计算:先从epochTrie里获得本轮选举周期的验证人列表
  12. //然后根据当前时间计算偏移量,获得应该由谁挖掘当前块的验证人
  13. validator, err := epochContext.lookupValidator(now)
  14. if err != nil {
  15. return err
  16. }
  17. //判断dpos规则计算得到的validator和d.signer即节点设置的validator是否一致
  18. if (validator == common.Address{}) || bytes.Compare(validator.Bytes(), d.signer.Bytes()) != 0 {
  19. return ErrInvalidBlockValidator
  20. }
  21. return nil
  22. }
  23. func (d *Dpos) checkDeadline(lastBlock *types.Block, now int64) error {
  24. prevSlot := PrevSlot(now)
  25. nextSlot := NextSlot(now)
  26. //假如当前时间是1542117655,则prevSlot = 1542117650,nextSlot = 1542117660
  27. if lastBlock.Time().Int64() >= nextSlot {
  28. return ErrMintFutureBlock
  29. }
  30. // nextSlot-now <= 1是要求出块时间需要接近出块间隔最后1秒
  31. if lastBlock.Time().Int64() == prevSlot || nextSlot-now <= 1 {
  32. return nil
  33. }
  34. //时间不到,就返回等待错误
  35. return ErrWaitForPrevBlock
  36. }

CheckValidator()判断不通过则跳出mintBlock,继续下一秒mintBlock循环。
判断通过进入createNewWork()。

4.2.2 createNewWork生成新块并定型

这个函数涉及具体执行交易、生成收据和日志、向监听者发送相关事件、调用dpos引擎Finalize打包、将未Seal的新块加入未确认块集等事项。

4.2.2.1 挖矿时序图

</>复制代码

  1. func (self *worker) createNewWork() (*Work, error) {
  2. self.mu.Lock()
  3. defer self.mu.Unlock()
  4. self.uncleMu.Lock()
  5. defer self.uncleMu.Unlock()
  6. self.currentMu.Lock()
  7. defer self.currentMu.Unlock()
  8. tstart := time.Now()
  9. parent := self.chain.CurrentBlock()
  10. tstamp := tstart.Unix()
  11. if parent.Time().Cmp(new(big.Int).SetInt64(tstamp)) >= 0 {
  12. tstamp = parent.Time().Int64() + 1
  13. }
  14. // this will ensure we"re not going off too far in the future
  15. if now := time.Now().Unix(); tstamp > now+1 {
  16. wait := time.Duration(tstamp-now) * time.Second
  17. log.Info("Mining too far in the future", "wait", common.PrettyDuration(wait))
  18. time.Sleep(wait)
  19. }
  20. num := parent.Number()
  21. header := &types.Header{
  22. ParentHash: parent.Hash(),
  23. Number: num.Add(num, common.Big1),
  24. GasLimit: core.CalcGasLimit(parent),
  25. GasUsed: new(big.Int),
  26. Extra: self.extra,
  27. Time: big.NewInt(tstamp),
  28. }
  29. // Only set the coinbase if we are mining (avoid spurious block rewards)
  30. if atomic.LoadInt32(&self.mining) == 1 {
  31. header.Coinbase = self.coinbase
  32. }
  33. if err := self.engine.Prepare(self.chain, header); err != nil {
  34. return nil, fmt.Errorf("got error when preparing header, err: %s", err)
  35. }
  36. // If we are care about TheDAO hard-fork check whether to override the extra-data or not
  37. if daoBlock := self.config.DAOForkBlock; daoBlock != nil {
  38. // Check whether the block is among the fork extra-override range
  39. limit := new(big.Int).Add(daoBlock, params.DAOForkExtraRange)
  40. if header.Number.Cmp(daoBlock) >= 0 && header.Number.Cmp(limit) < 0 {
  41. // Depending whether we support or oppose the fork, override differently
  42. if self.config.DAOForkSupport {
  43. header.Extra = common.CopyBytes(params.DAOForkBlockExtra)
  44. } else if bytes.Equal(header.Extra, params.DAOForkBlockExtra) {
  45. header.Extra = []byte{} // If miner opposes, don"t let it use the reserved extra-data
  46. }
  47. }
  48. }
  49. // Could potentially happen if starting to mine in an odd state.
  50. err := self.makeCurrent(parent, header)
  51. if err != nil {
  52. return nil, fmt.Errorf("got error when create mining context, err: %s", err)
  53. }
  54. // Create the current work task and check any fork transitions needed
  55. work := self.current
  56. if self.config.DAOForkSupport && self.config.DAOForkBlock != nil && self.config.DAOForkBlock.Cmp(header.Number) == 0 {
  57. misc.ApplyDAOHardFork(work.state)
  58. }
  59. pending, err := self.eth.TxPool().Pending()
  60. if err != nil {
  61. return nil, fmt.Errorf("got error when fetch pending transactions, err: %s", err)
  62. }
  63. txs := types.NewTransactionsByPriceAndNonce(self.current.signer, pending)
  64. work.commitTransactions(self.mux, txs, self.chain, self.coinbase)
  65. // compute uncles for the new block.
  66. var (
  67. uncles []*types.Header
  68. badUncles []common.Hash
  69. )
  70. for hash, uncle := range self.possibleUncles {
  71. if len(uncles) == 2 {
  72. break
  73. }
  74. if err := self.commitUncle(work, uncle.Header()); err != nil {
  75. log.Trace("Bad uncle found and will be removed", "hash", hash)
  76. log.Trace(fmt.Sprint(uncle))
  77. badUncles = append(badUncles, hash)
  78. } else {
  79. log.Debug("Committing new uncle to block", "hash", hash)
  80. uncles = append(uncles, uncle.Header())
  81. }
  82. }
  83. for _, hash := range badUncles {
  84. delete(self.possibleUncles, hash)
  85. }
  86. // Create the new block to seal with the consensus engine
  87. if work.Block, err = self.engine.Finalize(self.chain, header, work.state, work.txs, uncles, work.receipts, work.dposContext); err != nil {
  88. return nil, fmt.Errorf("got error when finalize block for sealing, err: %s", err)
  89. }
  90. work.Block.DposContext = work.dposContext
  91. // update the count for the miner of new block
  92. // We only care about logging if we"re actually mining.
  93. if atomic.LoadInt32(&self.mining) == 1 {
  94. log.Info("Commit new mining work", "number", work.Block.Number(), "txs", work.tcount, "uncles", len(uncles), "elapsed", common.PrettyDuration(time.Since(tstart)))
  95. self.unconfirmed.Shift(work.Block.NumberU64() - 1)
  96. }
  97. return work, nil
  98. }
4.2.2.2 准备区块头

先调用dpos引擎的Prepare填充区块头字段。

</>复制代码

  1. ……
  2. num := parent.Number()
  3. header := &types.Header{
  4. ParentHash: parent.Hash(),
  5. Number: num.Add(num, common.Big1),
  6. GasLimit: core.CalcGasLimit(parent),
  7. GasUsed: new(big.Int),
  8. Extra: self.extra,
  9. Time: big.NewInt(tstamp),
  10. }
  11. // 确保出块时间不要偏离太大(过早或过晚)
  12. if atomic.LoadInt32(&self.mining) == 1 {
  13. header.Coinbase = self.coinbase
  14. }
  15. self.engine.Prepare(self.chain, header)
  16. ……

此时,即将产生的区块Header的GasUsed和Extra都为空,Extra通过前面引擎分析的时候,我们知道会在Prepare里用0字节填充32+65的前后缀,除了Extra,Prepare还将填充其他的Header字段(详见3.5 Prepare分析),当Prepare执行完成,大部分字段都设置好了,还有少部分待填。

4.2.2.3 准备挖矿环境

接下来把父块和本块的header传给makeCurrent方法执行。

</>复制代码

  1. err := self.makeCurrent(parent, header)
  2. if err != nil {
  3. return nil, fmt.Errorf("got error when create mining context, err: %s", err)
  4. }
  5. // Create the current work task and check any fork transitions needed
  6. work := self.current
  7. if self.config.DAOForkSupport && self.config.DAOForkBlock != nil && self.config.DAOForkBlock.Cmp(header.Number) == 0 {
  8. misc.ApplyDAOHardFork(work.state)
  9. }

makeCurrent先新建stateDB和dposContext,然后组装一个Work结构体。

</>复制代码

  1. func (self *worker) makeCurrent(parent *types.Block, header *types.Header) error {
  2. state, err := self.chain.StateAt(parent.Root())
  3. if err != nil {
  4. return err
  5. }
  6. dposContext, err := types.NewDposContextFromProto(self.chainDb, parent.Header().DposContext)
  7. if err != nil {
  8. return err
  9. }
  10. work := &Work{
  11. config: self.config,
  12. signer: types.NewEIP155Signer(self.config.ChainId),
  13. state: state,
  14. dposContext: dposContext,
  15. ancestors: set.New(),
  16. family: set.New(),
  17. uncles: set.New(),
  18. header: header,
  19. createdAt: time.Now(),
  20. }
  21. // when 08 is processed ancestors contain 07 (quick block)
  22. for _, ancestor := range self.chain.GetBlocksFromHash(parent.Hash(), 7) {
  23. for _, uncle := range ancestor.Uncles() {
  24. work.family.Add(uncle.Hash())
  25. }
  26. work.family.Add(ancestor.Hash())
  27. work.ancestors.Add(ancestor.Hash())
  28. }
  29. // Keep track of transactions which return errors so they can be removed
  30. work.tcount = 0
  31. self.current = work
  32. return nil
  33. }

Work结构体中,ancestors存储的是6个祖先块,family存储的是6个祖先块和它们各自的叔块,组装后的Work结构体赋值给*worker.current。

4.2.2.3 从交易池获取pending交易集

然后从交易池里获取所有pending状态的交易,这些交易按账户分组,每个账户里的交易按nonce排序后返回交易集,这里暂且叫S1:

</>复制代码

  1. pending, err := self.eth.TxPool().Pending() //S1 = pending
  2. txs := types.NewTransactionsByPriceAndNonce(self.current.signer, pending)
4.2.2.4 交易集结构化处理

再然后通过NewTransactionsByPriceAndNonce函数对交易集进行结构化,它把S1集合里每个账户的第一笔交易分离出来作为heads集合,返回如下结构:

</>复制代码

  1. return &TransactionsByPriceAndNonce{
  2. txs: txs, //S1集合中每个账户除去第一个交易后的交易集
  3. heads: heads, //这个集合由每个账户的第一个交易组成
  4. signer: signer,
  5. }
4.2.2.5 交易执行过程分析

调用commitTransactions方法,执行新区块包含的所有交易。

这个方法是对处理后的交易集txs的具体执行,所谓执行交易,笼统地说就是把转账、合约或dpos交易类型的数据写入对应的内存Trie,再从Trie刷到本地DB中去。

</>复制代码

  1. func (env *Work) commitTransactions(mux *event.TypeMux, txs *types.TransactionsByPriceAndNonce, bc *core.BlockChain, coinbase common.Address) {
  2. gp := new(core.GasPool).AddGas(env.header.GasLimit)
  3. var coalescedLogs []*types.Log
  4. for {
  5. // Retrieve the next transaction and abort if all done
  6. tx := txs.Peek()
  7. if tx == nil {
  8. break
  9. }
  10. // Error may be ignored here. The error has already been checked
  11. // during transaction acceptance is the transaction pool.
  12. //
  13. // We use the eip155 signer regardless of the current hf.
  14. from, _ := types.Sender(env.signer, tx)
  15. // Check whether the tx is replay protected. If we"re not in the EIP155 hf
  16. // phase, start ignoring the sender until we do.
  17. if tx.Protected() && !env.config.IsEIP155(env.header.Number) {
  18. log.Trace("Ignoring reply protected transaction", "hash", tx.Hash(), "eip155", env.config.EIP155Block)
  19. txs.Pop()
  20. continue
  21. }
  22. // Start executing the transaction
  23. env.state.Prepare(tx.Hash(), common.Hash{}, env.tcount)
  24. err, logs := env.commitTransaction(tx, bc, coinbase, gp)
  25. switch err {
  26. case core.ErrGasLimitReached:
  27. // Pop the current out-of-gas transaction without shifting in the next from the account
  28. log.Trace("Gas limit exceeded for current block", "sender", from)
  29. txs.Pop()
  30. case core.ErrNonceTooLow:
  31. // New head notification data race between the transaction pool and miner, shift
  32. log.Trace("Skipping transaction with low nonce", "sender", from, "nonce", tx.Nonce())
  33. txs.Shift()
  34. case core.ErrNonceTooHigh:
  35. // Reorg notification data race between the transaction pool and miner, skip account =
  36. log.Trace("Skipping account with hight nonce", "sender", from, "nonce", tx.Nonce())
  37. txs.Pop()
  38. case nil:
  39. // Everything ok, collect the logs and shift in the next transaction from the same account
  40. coalescedLogs = append(coalescedLogs, logs...)
  41. env.tcount++
  42. txs.Shift()
  43. default:
  44. // Strange error, discard the transaction and get the next in line (note, the
  45. // nonce-too-high clause will prevent us from executing in vain).
  46. log.Debug("Transaction failed, account skipped", "hash", tx.Hash(), "err", err)
  47. txs.Shift()
  48. }
  49. }
  50. if len(coalescedLogs) > 0 || env.tcount > 0 {
  51. // make a copy, the state caches the logs and these logs get "upgraded" from pending to mined
  52. // logs by filling in the block hash when the block was mined by the local miner. This can
  53. // cause a race condition if a log was "upgraded" before the PendingLogsEvent is processed.
  54. cpy := make([]*types.Log, len(coalescedLogs))
  55. for i, l := range coalescedLogs {
  56. cpy[i] = new(types.Log)
  57. *cpy[i] = *l
  58. }
  59. go func(logs []*types.Log, tcount int) {
  60. if len(logs) > 0 {
  61. mux.Post(core.PendingLogsEvent{Logs: logs})
  62. }
  63. if tcount > 0 {
  64. mux.Post(core.PendingStateEvent{})
  65. }
  66. }(cpy, env.tcount)
  67. }
  68. }

该方法对结构化处理后的txs遍历执行,分为几步:

Work.state.Prepare()
这是给StateDB设置交易hash、区块hash(此时为空)、交易索引。
StateDB是用来操作整个账户树也即world state trie的,每执行一笔交易就更改一次world state trie。
交易索引是指在对txs.heads进行遍历的时候的自增数,这个索引在本区块内唯一,因为它是本区块包含的所有pending交易涉及的账户及各账户下所有交易的总递增。

commitTransactions函数对txs的遍历方式是:从遍历txs.heads开始,获取第一个账户的第一笔交易,然后获取同一账户的第二笔交易以此类推,如果该账户没有交易了,继续txs.heads的下一个账户。
也就是按账户优先级先遍历其下的所有交易,其次遍历所有账户(堆级别操作),txs结构化就是为这种循环方式准备的。

</>复制代码

  1. func (self *StateDB) Prepare(thash, bhash common.Hash, ti int) {
  2. self.thash = thash
  3. self.bhash = bhash
  4. self.txIndex = ti
  5. }

Work.commitTransaction()
执行单笔交易,先对stateDB这个大结构做一个版本号快照,也要对dpos的五棵树上下文即dposContext做一个备份,然后调用core.ApplyTransaction()方法,如果出错就退回快照和备份,执行成功后把交易加入Work.txs,(这个txs是为Finalize的时候传参用的,因为在遍历执行交易的时候会把原txs结构破坏,做个备份)交易收据加入Work.receipts,最后返回收据日志。

</>复制代码

  1. func (env *Work) commitTransaction(tx *types.Transaction, bc *core.BlockChain, coinbase common.Address, gp *core.GasPool) (error, []*types.Log) {
  2. snap := env.state.Snapshot()
  3. dposSnap := env.dposContext.Snapshot()
  4. receipt, _, err := core.ApplyTransaction(env.config, env.dposContext, bc, &coinbase, gp, env.state, env.header, tx, env.header.GasUsed, vm.Config{})
  5. if err != nil {
  6. env.state.RevertToSnapshot(snap)
  7. env.dposContext.RevertToSnapShot(dposSnap)
  8. return err, nil
  9. }
  10. env.txs = append(env.txs, tx)
  11. env.receipts = append(env.receipts, receipt)
  12. return nil, receipt.Logs
  13. }

看一下ApplyTransaction()是如何具体执行交易的:

</>复制代码

  1. func ApplyTransaction(config *params.ChainConfig, dposContext *types.DposContext, bc *BlockChain, author *common.Address, gp *GasPool, statedb *state.StateDB, header *types.Header, tx *types.Transaction, usedGas *big.Int, cfg vm.Config) (*types.Receipt, *big.Int, error) {
  2. msg, err := tx.AsMessage(types.MakeSigner(config, header.Number))
  3. if err != nil {
  4. return nil, nil, err
  5. }
  6. if msg.To() == nil && msg.Type() != types.Binary {
  7. return nil, nil, types.ErrInvalidType
  8. }
  9. // Create a new context to be used in the EVM environment
  10. context := NewEVMContext(msg, header, bc, author)
  11. // Create a new environment which holds all relevant information
  12. // about the transaction and calling mechanisms.
  13. vmenv := vm.NewEVM(context, statedb, config, cfg)
  14. // Apply the transaction to the current state (included in the env)
  15. _, gas, failed, err := ApplyMessage(vmenv, msg, gp)
  16. if err != nil {
  17. return nil, nil, err
  18. }
  19. if msg.Type() != types.Binary {
  20. if err = applyDposMessage(dposContext, msg); err != nil {
  21. return nil, nil, err
  22. }
  23. }
  24. // Update the state with pending changes
  25. var root []byte
  26. if config.IsByzantium(header.Number) {
  27. statedb.Finalise(true)
  28. } else {
  29. root = statedb.IntermediateRoot(config.IsEIP158(header.Number)).Bytes()
  30. }
  31. usedGas.Add(usedGas, gas)
  32. // Create a new receipt for the transaction, storing the intermediate root and gas used by the tx
  33. // based on the eip phase, we"re passing wether the root touch-delete accounts.
  34. receipt := types.NewReceipt(root, failed, usedGas)
  35. receipt.TxHash = tx.Hash()
  36. receipt.GasUsed = new(big.Int).Set(gas)
  37. // if the transaction created a contract, store the creation address in the receipt.
  38. if msg.To() == nil {
  39. receipt.ContractAddress = crypto.CreateAddress(vmenv.Context.Origin, tx.Nonce())
  40. }
  41. // Set the receipt logs and create a bloom for filtering
  42. receipt.Logs = statedb.GetLogs(tx.Hash())
  43. receipt.Bloom = types.CreateBloom(types.Receipts{receipt})
  44. return receipt, gas, err
  45. }

NewEVMContext是构建一个EVM执行环境,这个环境如下:

</>复制代码

  1. return vm.Context{
  2. //是否能够转账函数,会判断发起交易账户余额是否大于转账数量
  3. CanTransfer: CanTransfer,
  4. //转账函数,给转账地址减去转账额,同时给接收地址加上转账额
  5. Transfer: Transfer,
  6. //区块头hash
  7. GetHash: GetHashFn(header, chain),
  8. Origin: msg.From(),
  9. Coinbase: beneficiary,
  10. BlockNumber: new(big.Int).Set(header.Number),
  11. Time: new(big.Int).Set(header.Time),
  12. Difficulty: new(big.Int).Set(header.Difficulty),
  13. GasLimit: new(big.Int).Set(header.GasLimit),
  14. GasPrice: new(big.Int).Set(msg.GasPrice()),
  15. }

==beneficiary是Coinbase,这里是指如果没有指定coinbase就从header里获取validator的地址作为coinbase。==
NewEVM是创建一个携带了EVM环境和编译器的虚拟机。

然后调用ApplyMessage(),这个函数最主要的是对当前交易进行状态转换TransitionDb()。

TransitionDb详解

</>复制代码

  1. func (st *StateTransition) TransitionDb() (ret []byte, requiredGas, usedGas *big.Int, failed bool, err error) {
  2. if err = st.preCheck(); err != nil {
  3. return
  4. }
  5. msg := st.msg
  6. sender := st.from() // err checked in preCheck
  7. homestead := st.evm.ChainConfig().IsHomestead(st.evm.BlockNumber)
  8. contractCreation := msg.To() == nil
  9. // Pay intrinsic gas
  10. // TODO convert to uint64
  11. intrinsicGas := IntrinsicGas(st.data, contractCreation, homestead)
  12. if intrinsicGas.BitLen() > 64 {
  13. return nil, nil, nil, false, vm.ErrOutOfGas
  14. }
  15. if err = st.useGas(intrinsicGas.Uint64()); err != nil {
  16. return nil, nil, nil, false, err
  17. }
  18. var (
  19. evm = st.evm
  20. // vm errors do not effect consensus and are therefor
  21. // not assigned to err, except for insufficient balance
  22. // error.
  23. vmerr error
  24. )
  25. if contractCreation {
  26. ret, _, st.gas, vmerr = evm.Create(sender, st.data, st.gas, st.value)
  27. } else {
  28. // Increment the nonce for the next transaction
  29. st.state.SetNonce(sender.Address(), st.state.GetNonce(sender.Address())+1)
  30. ret, st.gas, vmerr = evm.Call(sender, st.to().Address(), st.data, st.gas, st.value)
  31. }
  32. if vmerr != nil {
  33. log.Debug("VM returned with error", "err", vmerr)
  34. // The only possible consensus-error would be if there wasn"t
  35. // sufficient balance to make the transfer happen. The first
  36. // balance transfer may never fail.
  37. if vmerr == vm.ErrInsufficientBalance {
  38. return nil, nil, nil, false, vmerr
  39. }
  40. }
  41. requiredGas = new(big.Int).Set(st.gasUsed())
  42. st.refundGas()
  43. st.state.AddBalance(st.evm.Coinbase, new(big.Int).Mul(st.gasUsed(), st.gasPrice))
  44. return ret, requiredGas, st.gasUsed(), vmerr != nil, err
  45. }

其中preCheck检查当前交易nonce和发送账户当前nonce是否一致,同时检查发送账户余额是否大于GasLimit,足够的话就先将余额减去gaslimit(过度状态转换),不足就返回一个常见的错误:“insufficient balance to pay for gas”。

IntrinsicGas()是计算交易所需固定费用:如果是创建合约交易,固定费用为53000gas,转账交易固定费用是21000gas,如果交易携带数据,这个数据对于创建合约是合约代码数据,对于转账交易是转账的附加说明数据,这些数据按字节存储收费,非0字节每位68gas,0字节每位4gas,总计起来就是执行交易所需的gas费。

useGas()判断提供的gas是否满足上面计算出的内部所需费用,足够的话从提供的gas里扣除内部所需费用(状态转换)。

因为ApplyTransaction传的参数msg已经将dpos类型且to为空的交易排除出去了。

所以当这里msg.To() == nil的时候,只剩下msg.Type == 0这一种原始交易的可能了。msg.To为空说明该交易不是转账、不是合约调用,只能是创建合约交易,根据msg.To是否为空,分两种情况,Create创建合约和Call调用合约,这两种情况都覆盖了转账行为。

1)if contractCreation{…},即to==nil,说明是创建合约交易,调用evm.Create()。

</>复制代码

  1. // Create creates a new contract using code as deployment code.
  2. func (evm *EVM) Create(caller ContractRef, code []byte, gas uint64, value *big.Int) (ret []byte, contractAddr common.Address, leftOverGas uint64, err error) {
  3. // Depth check execution. Fail if we"re trying to execute above the
  4. // limit.
  5. if evm.depth > int(params.CallCreateDepth) {
  6. return nil, common.Address{}, gas, ErrDepth
  7. }
  8. if !evm.CanTransfer(evm.StateDB, caller.Address(), value) {
  9. return nil, common.Address{}, gas, ErrInsufficientBalance
  10. }
  11. // Ensure there"s no existing contract already at the designated address
  12. nonce := evm.StateDB.GetNonce(caller.Address())
  13. evm.StateDB.SetNonce(caller.Address(), nonce+1)
  14. contractAddr = crypto.CreateAddress(caller.Address(), nonce)
  15. contractHash := evm.StateDB.GetCodeHash(contractAddr)
  16. if evm.StateDB.GetNonce(contractAddr) != 0 || (contractHash != (common.Hash{}) && contractHash != emptyCodeHash) {
  17. return nil, common.Address{}, 0, ErrContractAddressCollision
  18. }
  19. // Create a new account on the state
  20. snapshot := evm.StateDB.Snapshot()
  21. evm.StateDB.CreateAccount(contractAddr)
  22. if evm.ChainConfig().IsEIP158(evm.BlockNumber) {
  23. evm.StateDB.SetNonce(contractAddr, 1)
  24. }
  25. evm.Transfer(evm.StateDB, caller.Address(), contractAddr, value)
  26. // initialise a new contract and set the code that is to be used by the
  27. // E The contract is a scoped evmironment for this execution context
  28. // only.
  29. contract := NewContract(caller, AccountRef(contractAddr), value, gas)
  30. contract.SetCallCode(&contractAddr, crypto.Keccak256Hash(code), code)
  31. if evm.vmConfig.NoRecursion && evm.depth > 0 {
  32. return nil, contractAddr, gas, nil
  33. }
  34. ret, err = run(evm, snapshot, contract, nil)
  35. // check whether the max code size has been exceeded
  36. maxCodeSizeExceeded := evm.ChainConfig().IsEIP158(evm.BlockNumber) && len(ret) > params.MaxCodeSize
  37. // if the contract creation ran successfully and no errors were returned
  38. // calculate the gas required to store the code. If the code could not
  39. // be stored due to not enough gas set an error and let it be handled
  40. // by the error checking condition below.
  41. if err == nil && !maxCodeSizeExceeded {
  42. createDataGas := uint64(len(ret)) * params.CreateDataGas
  43. if contract.UseGas(createDataGas) {
  44. evm.StateDB.SetCode(contractAddr, ret)
  45. } else {
  46. err = ErrCodeStoreOutOfGas
  47. }
  48. }
  49. // When an error was returned by the EVM or when setting the creation code
  50. // above we revert to the snapshot and consume any gas remaining. Additionally
  51. // when we"re in homestead this also counts for code storage gas errors.
  52. if maxCodeSizeExceeded || (err != nil && (evm.ChainConfig().IsHomestead(evm.BlockNumber) || err != ErrCodeStoreOutOfGas)) {
  53. evm.StateDB.RevertToSnapshot(snapshot)
  54. if err != errExecutionReverted {
  55. contract.UseGas(contract.Gas)
  56. }
  57. }
  58. // Assign err if contract code size exceeds the max while the err is still empty.
  59. if maxCodeSizeExceeded && err == nil {
  60. err = errMaxCodeSizeExceeded
  61. }
  62. return ret, contractAddr, contract.Gas, err
  63. }

注意这里传入的gas是已经扣除了固定费用的剩余gas。evm是基于栈的简单虚拟机,最多支持1024栈深度,超过就报错。

然后在这里调用evmContext的CanTransfer()判断发起交易地址余额是否大于转账数量,是的话就将发起交易的账户的nonce+1。

生成合约账户地址:合约账户的地址生成规则是,由发起交易的地址和该nonce计算生成,生成地址后,此时仅有地址,根据地址获取该合约账户的nonce应该为0、codeHash应该为空hash,不符合这些判断说明地址冲突,报错退出。

紧接着创建一个新账户evm.StateDB.CreateAccount(contractAddr),这个函数创建的是一个普通账户(即EOA和Contract账户的未分化形式)。
新账户的地址就是上面计算生成的地址,Nonce设为0,Balance设为0,但是如果之前已存在同样地址的账户那么Balance就设为之前账户的余额,CodeHash设为空hash注意不是空。EIP158之后的新账号nonce设为1。

evm.Transfer():如果创建账户的时候有资助代币(eth),则将代币从发起地址转移到新账户地址。

然后NewContract()构建一个合约上下文环境contract。

SetCallCode(),给contract环境对象设置入参Code、CodeHash。

run():EVM编译、执行合约的创建,执行EVM栈操作。
run执行返回合约body字节码(code storage),如果长度超过24576也存储不了,然后计算存储这个合约字节码的gas费用=长度*200。最后给stateObject对象设置code,给账户(Account)设置codeHash,这样那个新账户就成了一个合约账户。

2)else{…}如果不是创建合约交易(即to!=nil),调用evm.Call()。这个Call是执行合约交易,包括转账类型的交易、调用合约交易。

</>复制代码

  1. func (evm *EVM) Call(caller ContractRef, addr common.Address, input []byte, gas uint64, value *big.Int) (ret []byte, leftOverGas uint64, err error) {
  2. if evm.vmConfig.NoRecursion && evm.depth > 0 {
  3. return nil, gas, nil
  4. }
  5. // Fail if we"re trying to execute above the call depth limit
  6. if evm.depth > int(params.CallCreateDepth) {
  7. return nil, gas, ErrDepth
  8. }
  9. // Fail if we"re trying to transfer more than the available balance
  10. if !evm.Context.CanTransfer(evm.StateDB, caller.Address(), value) {
  11. return nil, gas, ErrInsufficientBalance
  12. }
  13. var (
  14. to = AccountRef(addr)
  15. snapshot = evm.StateDB.Snapshot()
  16. )
  17. if !evm.StateDB.Exist(addr) {
  18. precompiles := PrecompiledContractsHomestead
  19. if evm.ChainConfig().IsByzantium(evm.BlockNumber) {
  20. precompiles = PrecompiledContractsByzantium
  21. }
  22. if precompiles[addr] == nil && evm.ChainConfig().IsEIP158(evm.BlockNumber) && value.Sign() == 0 {
  23. return nil, gas, nil
  24. }
  25. evm.StateDB.CreateAccount(addr)
  26. }
  27. evm.Transfer(evm.StateDB, caller.Address(), to.Address(), value)
  28. // initialise a new contract and set the code that is to be used by the
  29. // E The contract is a scoped environment for this execution context
  30. // only.
  31. contract := NewContract(caller, to, value, gas)
  32. contract.SetCallCode(&addr, evm.StateDB.GetCodeHash(addr), evm.StateDB.GetCode(addr))
  33. ret, err = run(evm, snapshot, contract, input)
  34. // When an error was returned by the EVM or when setting the creation code
  35. // above we revert to the snapshot and consume any gas remaining. Additionally
  36. // when we"re in homestead this also counts for code storage gas errors.
  37. if err != nil {
  38. evm.StateDB.RevertToSnapshot(snapshot)
  39. if err != errExecutionReverted {
  40. contract.UseGas(contract.Gas)
  41. }
  42. }
  43. return ret, contract.Gas, err
  44. }

Call函数先来三个判断:evm编译器被禁用或者evm执行栈深超过1024或者转账数额超过余额就报错。

注意以下几个Call步骤和Create的区别:

evm.StateDB.Exist(addr)是从stateObjects这个所有stateObject的map集合中查找是否存to地址,如果不存在,则调用evm.StateDB.CreateAccount(addr)创建一个新账户,这和Create里调的是同一个函数,即CreateAccount创建的是一个普通账户。

evm.Transfer():将代币从发起地址转移到to地址(包括纯转账类型的交易、给合约地址转入代币等)

NewContract()构建一个合约上下文环境contract。

SetCallCode():这个函数和Create里的SetCallCode()传的入参不一样,它是从to地址获取code,然后才给to账户设置code、codehash等,这隐含了两种可能性,如果获取到了code那么这个账户自然是合约账户,如果没有获取到,那这个账户就是外部拥有账户(EOA)

run():EVM编译、执行EVM栈操作。

这个Call除了转账、调用合约,还包括执dpos交易,当交易是dpos类型的交易的时候,它其实是个空合约,之所以要执行dpos这类空合约是要计算其gas。

TransitionDB()在交易执行完后,将剩余gas返退回给发起者账户地址,同时把挖矿节点设置的Coinbase的余额增加上消耗的gas。

除了Call(),evm还提供了另外3个合约调用方法:
CallCode(),已经弃用,由DelegateCall()替代
DelegateCall()
StaticCall()暂时未用

</>复制代码

  1. type CallContext interface {
  2. // Call another contract
  3. Call(env *EVM, me ContractRef, addr common.Address, data []byte, gas, value *big.Int) ([]byte, error)
  4. // Take another"s contract code and execute within our own context
  5. CallCode(env *EVM, me ContractRef, addr common.Address, data []byte, gas, value *big.Int) ([]byte, error)
  6. // Same as CallCode except sender and value is propagated from parent to child scope
  7. DelegateCall(env *EVM, me ContractRef, addr common.Address, data []byte, gas *big.Int) ([]byte, error)
  8. // Create a new contract
  9. Create(env *EVM, me ContractRef, data []byte, gas, value *big.Int) ([]byte, common.Address, error)
  10. }

我们上面讨论的是交易,根据黄皮书的定义交易就两种:创建合约、消息调用。区分二者的标志就是to是否为空。由外部用户触发的才能叫交易,所以用户发起创建合约、用户发起合约调用都叫交易,对应的就是我们上面分析的Create和Call两种情况。

转账这种交易执行的是Call()而不是Create(),因为to不为空。

用户调用合约A,这叫交易,执行的是Call(),紧接着A里边又调用了合约B,那么这不叫交易叫内部调用,执行的就不是Call(),而是DelegateCall()了,Call和DelegateCall的区别是:Call总是直接改变to的的storage,而DelegateCall改变的是caller(即A)的storage,而不是to的storage。那个NewContract上下文构造函数就是做msg.caller、to等指向工作的。
至于DelegateCall为什么替代CallCode,是修改了一点即msg.sender在DelegateCall里永远指向用户,而CallCode里的sender则指向的是caller。

ApplyMessage()结束后,判断一下是否属于DPOS交易,是的话就执行applyDposMessage中对应的交易,即dpos的四种交易:成为候选人、退出候选人、投票、取消投票,具体执行就是更改对应的Trie。
然后调用statedb.Finalise删除掉空账户,再更新状态树,得到最新的world state root hash(intermediate root)。
然后生成一个收据,收据里包括:

</>复制代码

  1. 交易的hash
    执行成败状态
    消耗的费用
    若是创建合约交易就把合约地址也写到收据的ContractAddress字段里
    日志
    Bloom

  2. 关于日志,栈操作的时候会记录下日志,日志信息如下:

  3. </>复制代码

    1. type Log struct {
    2. // Consensus fields:
    3. // address of the contract that generated the event
    4. Address common.Address `json:"address" gencodec:"required"`
    5. // list of topics provided by the contract.
    6. Topics []common.Hash `json:"topics" gencodec:"required"`
    7. // supplied by the contract, usually ABI-encoded
    8. Data []byte `json:"data" gencodec:"required"`
    9. // Derived fields. These fields are filled in by the node
    10. // but not secured by consensus.
    11. // block in which the transaction was included
    12. BlockNumber uint64 `json:"blockNumber"`
    13. // hash of the transaction
    14. TxHash common.Hash `json:"transactionHash" gencodec:"required"`
    15. // index of the transaction in the block
    16. TxIndex uint `json:"transactionIndex" gencodec:"required"`
    17. // hash of the block in which the transaction was included
    18. BlockHash common.Hash `json:"blockHash"`
    19. // index of the log in the receipt
    20. Index uint `json:"logIndex" gencodec:"required"`
    21. // The Removed field is true if this log was reverted due to a chain reorganisation.
    22. // You must pay attention to this field if you receive logs through a filter query.
    23. Removed bool `json:"removed"`
    24. }

ApplyTransaction()最终返回收据。
至此,单笔交易执行过程commitTransaction()结束。

如此循环执行,直到所有交易执行完成。
在循环执行交易的过程中,我们把所有交易收据的日志写入了一个集合,等交易全部执行完成,异步将这个日志集合向所有已注册的事件接收者发送:

</>复制代码

  1. mux.Post(core.PendingLogsEvent{Logs: logs})
  2. mux.Post(core.PendingStateEvent{})

</>复制代码

  1. func (mux *TypeMux) Post(ev interface{}) error {
  2. event := &TypeMuxEvent{
  3. Time: time.Now(),
  4. Data: ev,
  5. }
  6. rtyp := reflect.TypeOf(ev)
  7. mux.mutex.RLock()
  8. if mux.stopped {
  9. mux.mutex.RUnlock()
  10. return ErrMuxClosed
  11. }
  12. subs := mux.subm[rtyp]
  13. mux.mutex.RUnlock()
  14. for _, sub := range subs {
  15. sub.deliver(event)
  16. }
  17. return nil
  18. }

投递相应的事件到TypeMuxSubscription的postC通道中。

</>复制代码

  1. func (s *TypeMuxSubscription) deliver(event *TypeMuxEvent) {
  2. // Short circuit delivery if stale event
  3. if s.created.After(event.Time) {
  4. return
  5. }
  6. // Otherwise deliver the event
  7. s.postMu.RLock()
  8. defer s.postMu.RUnlock()
  9. select {
  10. case s.postC <- event:
  11. case <-s.closing:
  12. }
  13. }

关于事件的订阅、发送单列章节讲。

commitTransactions()结束,现在回到了createNewWork中,代码继续遍历叔块和损坏的叔块,这段代码其实在DPOS中已经不需要了,因为DPOS中没有叔块,chainSideCh事件被删除,possibleUncles没有被赋值的机会了。

4.2.2.6 Finalize定型新块

把header、账户状态、交易、收据等信息传给dpos引擎去定型。参见3.6节。

4.2.2.7 检查之前的块是否上链

</>复制代码

  1. 注意:是检查本节点之前挖的块是否上链,而不是当前挖出的块。当前块离上链为时尚早。

每个以太坊节点会维护一个未确认块集,集合内有个环状容器,这个容器容纳仅由自身挖出的块,在最乐观的情况下(即连续由本节点挖出块的情况下),最大容纳5个块。当第6个连续的块由本节点挖出的时候就会触发unconfirmedBlocks.Shift()的执行(这里“执行”的上下文含义是满足函数内部的判断条件,而不仅仅指函数被调用,下同)。

但大多数情况下,一个节点不会连续出块,那么可能在本节点第二次挖出块的时候,当前区块链高度就已经超过之前挖出的那个块6个高度了,也会触发unconfirmedBlocks.Shift()执行。换句话说就是通常情况下检查自己出的前一个块有没有加入到链上。

Shift的作用,是检查未确认块集,这个未确认集并不是说真的就全是一直未被加入到链上的块,而是当该节点满足上面两段描述的“执行”条件时,都会检查一下之前挖出的块有没有被确认(加入区块链),如果当前区块链高度,高于未确认集环状容器内那些块6个高度之后,那些块还没有被加入到链上,就从未确认块集合中删除那些块。

这个函数的意思着重表达:在至少6个高度的==时间==之后,才会去检查是否加入到链上,至于上没上链它也不能改变什么,就是给本节点一个之前的块被怎么处理了的通知。为什么是这样的时点呢?可能是要留出6个高度的时间等所有节点都确认吧,后文再说。

</>复制代码

  1. func (set *unconfirmedBlocks) Shift(height uint64) {
  2. set.lock.Lock()
  3. defer set.lock.Unlock()
  4. for set.blocks != nil {
  5. // Retrieve the next unconfirmed block and abort if too fresh
  6. next := set.blocks.Value.(*unconfirmedBlock)
  7. if next.index+uint64(set.depth) > height {
  8. break
  9. }
  10. // Block seems to exceed depth allowance, check for canonical status
  11. header := set.chain.GetHeaderByNumber(next.index)
  12. switch {
  13. case header == nil:
  14. log.Warn("Failed to retrieve header of mined block", "number", next.index, "hash", next.hash)
  15. case header.Hash() == next.hash:
  16. log.Info("

文章版权归作者所有,未经允许请勿转载,若此文章存在违规行为,您可以联系管理员删除。

转载请注明本文地址:https://www.ucloud.cn/yun/24502.html

相关文章

  • 区块链技术阅读列表

    摘要:有很多值得学习的区块链技术资源,在这里稍微总结了一下。是一个一键起链的区块链框架,你可以理解成之类的前端框架,由带领的团队打造,是前以太坊,团队可能是这个世界上目前区块链技术最强的团队了虽然之前出过多签事故,值得期待。 有很多值得学习的区块链技术资源,在这里稍微总结了一下。因为不想再多一个 markdown repo,所以把它放在了 blockchain-tutorial 的 wiki...

    Anchorer 评论0 收藏0
  • FIBOS 与 Ethereum 技术对比

    摘要:区块的产生是由个轮流出块,产生的区块需要以上的确认才能够被区块链认可。手续费资源在中使用区块链上的资源需要消耗,消耗的作为区块打包的费用支付给矿工。是区块链的通用库,具有以下功能使用提供的包管理。是一个区块链数据服务框架,基于框架实现。 共识机制 Ethereum 使用的是 PoW 共识机制,未来几年里将会换成 PoS 共识机制。Ethereum 区块是由矿工计算哈希产生,在 PoW ...

    JinB 评论0 收藏0
  • 对于区块链在现实落地的一些技术业务关注点

    摘要:因为年跳槽到一家保险科技公司,因此,对于区块链早期落地非炒币有了一定的认识和理解。首先,从技术角度来看,区块链主要涵盖了以下几个技术点共识算法网络加密安全技术现在对以上三个技术点做分别的介绍。 因为2016年跳槽到一家保险科技公司,因此,对于区块链早期落地(非炒币)有了一定的认识和理解。但真正开始思考区块链的问题,应该是从2018年初开始的,因为,经典互联网技术在我过去一年的产品化商业...

    chanthuang 评论0 收藏0
  • 基于以太坊的视频直播平台 Livepeer白皮书中文概览

    摘要:说明的视频片段分发现在没做出什么成果作者还提了一句,协议有望成为直播内容的传播协议。仿佛也没能掩饰住不知道怎么分发视频片段的尴尬说了这么多,看了代码发现视频片段还是通过分发总结最终将建立一个可扩展的,即用即付的直播网络 Background Livepeer旨在构建带有激励机制的视频直播分布式网络 Blockchain 以太坊 智能合约和交易基于Ethereum以太坊网络 DP...

    Eric 评论0 收藏0

发表评论

0条评论

neu

|高级讲师

TA的文章

阅读更多
最新活动
阅读需要支付1元查看
<