Ethereum DPOS源碼分析

1 導語

區塊鏈的主要工做就是出塊,出塊的制度、方式叫作共識;\
塊裏的內容是不可篡改的信息記錄,塊鏈接成鏈就是區塊鏈。node

出塊又叫挖礦,有各類挖礦的方式,好比POW、DPOS,本文主要分析DPOS共識源碼。算法

以太坊存在多種共識:數據庫

  • PoW (etash)在主網使用
  • PoA(clique) 在測試網使用
  • FakePow 在單元測試使用
  • DPOS 新增共識替代POW

既然是源碼分析,主要讀者羣體應該是看代碼的人,讀者需要結合代碼看此類文章。明白此類文章的做用是:提供一個分析的切入口,將散落的代碼按某種內在邏輯串起來,用圖文的形式敘述代碼的大意,引領讀者有一個系統化的認知,同時對本身閱讀代碼過程當中不理解的地方起到必定參考做用。json

2 DPOS的共識邏輯

DPOS的基本邏輯能夠概述爲:成爲候選人-得到他人投票-被選舉爲驗證人-在週期內輪流出塊。api

從這個過程能夠看到,成爲候選人和投票是用戶主動發起的行爲,得到投票和被選爲驗證人是系統行爲。DPOS的主要功能就是成爲候選人、投票(對方得到投票),以及系統按期自動執行的選舉。數組

2.1 最初的驗證人

驗證人就是出塊人,在創世的時候,系統還沒運行,用戶天然不能投票,本系統採用的方法是,在創世配置文件中定義好最初的一批出塊驗證人(Validator),由這一批驗證人在第一個出塊週期內輪流出塊,默認是21個驗證人。緩存

{
    "config": {
        "chainId": 8888,
        "eip155Block": 0,
        "eip158Block": 0,
        "byzantiumBlock":0,
        "dpos":{
            "validators":[
                "0x8807fa0db2c60675a8f833dd010469e408428b83",
                "0xdf5f5a7abc5d0821c50deb4368528d8691f18737",
                "0xe0d64bfb1a30d66ae0f06ce36d5f4edf6835cd7c"
                ……
            ]
        }
    },
    "nonce": "0x0000000000000042",
    "difficulty": "0x020000",
    "mixHash": "0x0000000000000000000000000000000000000000000000000000000000000000",
    "coinbase": "0x0000000000000000000000000000000000000000",
    "timestamp": "0x00",
    "parentHash": "0x0000000000000000000000000000000000000000000000000000000000000000",
    "extraData": "0x11bbe8db4e347b4e8c937c1c8370e4b5ed33adb3db69cbdb7a38e1e50b1b82fa",
    "gasLimit": "0x500000",
    "alloc": {}
}

2.2 成爲候選人

系統運行以後,任何人隨時能夠投票,同時也能夠得到他人投票。由於只有候選人才容許得到投票,因此任何人被投票以前都要先成爲候選人(candidate)。
\
\
從外部用戶角度看,成爲候選人只須要本身發一筆交易便可:app

eth.sendTransaction({
    from: '0x646ba1fa42eb940aac67103a71e9a908ef484ec3', 
    to: '0x646ba1fa42eb940aac67103a71e9a908ef484ec3', 
    value: 0, 
    type: 1
})

在系統內部,成爲候選人和投票均被定義爲交易,其實DPOS定義的全部交易有四種類型,是針對這兩種行爲的正向和反向操做。less

type TxType uint8
const (
    Binary TxType = iota
    LoginCandidate  //成爲候選人
    LogoutCandidate //取消候選人
    Delegate    //投票
    UnDelegate  //取消投票
)
type txdata struct {
    Type         TxType          `json:"type"        
    …… 
}

成爲候選人代碼很是簡單,就是更新(插入)一下candidateTrie,這棵樹的鍵和值都是候選人的地址,它保存着全部當前時間的候選人。異步

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

具體執行交易的時候,它取的地址是from,這意味着只能將本身設爲候選人。

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

除了這裏提到的candidateTrie,DPOS總共有五棵樹:

type DposContext struct {
    epochTrie     *trie.Trie    //記錄出塊週期內的驗證人列表 ("validator",[]validator)
    delegateTrie  *trie.Trie    //(append(candidate, delegator...), delegator)
    voteTrie      *trie.Trie    //(delegator, candidate)
    candidateTrie *trie.Trie    //(candidate, candidate)
    mintCntTrie   *trie.Trie    //記錄驗證人在週期內的出塊數目(append(epoch, validator.Bytes()...),count) 這裏的epoch=header.Time/86400

    db ethdb.Database
}
delegator是投票人

2.3 投票

從外部用戶角度看,投票也是一筆交易:

eth.sendTransaction({
    from: '0x646ba1fa42eb940aac67103a71e9a908ef484ec3', 
    to: '0x5b76fff970bf8a351c1c9ebfb5e5a9493e956ddd', 
    value: 0, 
    type: 3
})

系統內部的投票代碼,主要更新delegateTrie和voteTrie:

func (d *DposContext) Delegate(delegatorAddr, candidateAddr common.Address) error {
    delegator, candidate := delegatorAddr.Bytes(), candidateAddr.Bytes()

    // 得到投票的候選人必定要在candidateTrie中
    candidateInTrie, err := d.candidateTrie.TryGet(candidate)
    if err != nil {
        return err
    }
    if candidateInTrie == nil {
        return errors.New("invalid candidate to delegate")
    }

    // delete old candidate if exists
    oldCandidate, err := d.voteTrie.TryGet(delegator)
    if err != nil {
        if _, ok := err.(*trie.MissingNodeError); !ok {
            return err
        }
    }
    if oldCandidate != nil {
        d.delegateTrie.Delete(append(oldCandidate, delegator...))
    }
    if err = d.delegateTrie.TryUpdate(append(candidate, delegator...), delegator); err != nil {
        return err
    }
    return d.voteTrie.TryUpdate(delegator, candidate)
}

2.4 選舉

投票雖然隨時能夠進行,可是驗證人的選出,則是週期性的觸發。\
選舉週期默認設定爲24小時,每過24小時,對驗證人進行一次從新選舉。\
每次區塊被打包的時候(Finalize)都會調用選舉函數,選舉函數判斷是否到了從新選舉的時刻,它根據當前塊和上一塊的時間,計算兩塊是否屬於同一個選舉週期,若是是同一個週期,不觸發重選,若是不是同一個週期,則說明當前塊是新週期的第一塊,觸發重選。\
\
選舉函數:

func (ec *EpochContext) tryElect(genesis, parent *types.Header) error {
    genesisEpoch := genesis.Time.Int64() / epochInterval    //0
    prevEpoch := parent.Time.Int64() / epochInterval
    //ec.TimeStamp從Finalize傳過來的當前塊的header.Time
    currentEpoch := ec.TimeStamp / epochInterval

    prevEpochIsGenesis := prevEpoch == genesisEpoch
    if prevEpochIsGenesis && prevEpoch < currentEpoch {
        prevEpoch = currentEpoch - 1
    }

    prevEpochBytes := make([]byte, 8)
    binary.BigEndian.PutUint64(prevEpochBytes, uint64(prevEpoch))
    iter := trie.NewIterator(ec.DposContext.MintCntTrie().PrefixIterator(prevEpochBytes))


    //currentEpoch只有在比prevEpoch至少大於1的時候執行下面代碼。
    //大於1意味着當前塊的時間,距離上一塊所處的週期起始時間,已經超過epochInterval即24小時了。
    //大於2過了48小時……
    for i := prevEpoch; i < currentEpoch; i++ {
        // 若是前一個週期不是創世週期,觸發踢出驗證人規則
        if !prevEpochIsGenesis && iter.Next() {
            if err := ec.kickoutValidator(prevEpoch); err != nil {
                return err
            }
        }
        //計票,按票數從高到低得出safeSize個驗證人
        // 候選人的票數cnt=全部投他的delegator的帳戶餘額之和
        votes, err := ec.countVotes()
        if err != nil {
            return err
        }
        candidates := sortableAddresses{}
        for candidate, cnt := range votes {
            candidates = append(candidates, &sortableAddress{candidate, cnt})
        }
        if len(candidates) < safeSize {
            return errors.New("too few candidates")
        }
        sort.Sort(candidates)
        if len(candidates) > maxValidatorSize {
            candidates = candidates[:maxValidatorSize]
        }

        // shuffle candidates
        //用父塊的hash和當前週期編號作驗證人列表隨機亂序的種子
        //打亂驗證人列表順序,由seed確保每一個節點計算出來的驗證人順序都是一致的。
        seed := int64(binary.LittleEndian.Uint32(crypto.Keccak512(parent.Hash().Bytes()))) + i
        r := rand.New(rand.NewSource(seed))
        for i := len(candidates) - 1; i > 0; i-- {
            j := int(r.Int31n(int32(i + 1)))
            candidates[i], candidates[j] = candidates[j], candidates[i]
        }
        sortedValidators := make([]common.Address, 0)
        for _, candidate := range candidates {
            sortedValidators = append(sortedValidators, candidate.address)
        }

        epochTrie, _ := types.NewEpochTrie(common.Hash{}, ec.DposContext.DB())
        ec.DposContext.SetEpoch(epochTrie)
        ec.DposContext.SetValidators(sortedValidators)
        log.Info("Come to new epoch", "prevEpoch", i, "nextEpoch", i+1)
    }
    return nil
}

當epochContext最終調用了dposContext的SetValidators()後,新的一批驗證人就產生了,這批新的驗證人將開始輪流出塊。

2.5 DPOS相關類圖

image

EpochContext是選舉週期(默認24小時)相關實體類,因此主要功能是僅在週期時刻發生的事情,包括選舉、計票、踢出驗證人。它是更大範圍上的存在,不直接操做DPOS的五棵樹,而是經過它聚合的DposContext對五棵樹進行增刪改查。

DposContext和Trie是強組合關係,DPOS的交易行爲(成爲候選人、取消爲候選人、投票、取消投票、設置驗證人)就是它的主要功能。

Dpos is a engine,實現Engine接口。

func (self *worker) mintBlock(now int64) {
    engine, ok := self.engine.(*dpos.Dpos)
    ……
}

3 DPOS引擎實現

DPOS是共識引擎的具體實現,Engine接口定義了九個方法。

3.1 Author

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

這個接口的意思是返回出塊人。在POW共識中,返回的是header.Coinbase。\
DPOS中Header增長了一個Validator,是有意將Coinbase和Validator的概念分開。Validator默認等於Coinbase,也能夠設爲不同的地址。

3.2 VerifyHeader

驗證header裏的一些字段是否符合dpos共識規則。\
符合如下判斷都是錯的:

header.Time.Cmp(big.NewInt(time.Now().Unix())) > 0
len(header.Extra) < extraVanity+extraSeal //32+65
header.MixDigest != (common.Hash{})
header.Difficulty.Uint64() != 1
header.UncleHash != types.CalcUncleHash(nil)
parent == nil || parent.Number.Uint64() != number-1 || parent.Hash() != header.ParentHash
//與父塊出塊時間間隔小於了10(blockInterval)秒
parent.Time.Uint64()+uint64(blockInterval) > header.Time.Uint64()

3.3 VerifyHeaders

批量驗證header

3.4 VerifyUncles

dpos裏不該有uncles。

func (d *Dpos) VerifyUncles(chain consensus.ChainReader, block *types.Block) error {
    if len(block.Uncles()) > 0 {
        return errors.New("uncles not allowed")
    }
    return nil
}

3.5 Prepare

爲Header準備部分字段:\
Nonce爲空;\
Extra預留爲32+65個0字節,Extra字段包括32字節的extraVanity前綴和65字節的extraSeal後綴,都爲預留字節,extraSeal在區塊Seal的時候寫入驗證人的簽名。\
Difficulty置爲1;\
Validator設置爲signer;signer是在啓動挖礦的時候設置的,其實就是本節點的驗證人(Ethereum.validator)。

func (d *Dpos) Prepare(chain consensus.ChainReader, header *types.Header) error {
    header.Nonce = types.BlockNonce{}
    number := header.Number.Uint64()
    //若是header.Extra不足32字節,則用0填充滿32字節。
    if len(header.Extra) < extraVanity {
        header.Extra = append(header.Extra, bytes.Repeat([]byte{0x00}, extraVanity-len(header.Extra))...)
    }
    header.Extra = header.Extra[:extraVanity]
    //header.Extra再填65字節
    header.Extra = append(header.Extra, make([]byte, extraSeal)...)
    parent := chain.GetHeader(header.ParentHash, number-1)
    if parent == nil {
        return consensus.ErrUnknownAncestor
    }
    header.Difficulty = d.CalcDifficulty(chain, header.Time.Uint64(), parent)
    //header.Validator賦值爲Dpos的signer。
    header.Validator = d.signer
    return nil
}

關於難度

在DPOS裏,不須要求難度值,給定一個便可。

func (d *Dpos) CalcDifficulty(chain consensus.ChainReader, time uint64, parent *types.Header) *big.Int {
    return big.NewInt(1)
}

而在POW中,難度是根據父塊和最新塊的時間差動態調整的,小於10增長難度,大於等於20減少難度。

block_diff = parent_diff + 難度調整 + 難度炸彈
難度調整 = parent_diff // 2048 * MAX(1 - (block_timestamp - parent_timestamp) // 10, -99)
難度炸彈 = INT(2^((block_number // 100000) - 2))

關於singer

調用API,人爲設置本節點的驗證人

func (api *PrivateMinerAPI) SetValidator(validator common.Address) bool {
    api.e.SetValidator(validator)   //e *Ethereum
    return true
}
func (self *Ethereum) SetValidator(validator common.Address) {
    self.lock.Lock()    //lock sync.RWMutex
    self.validator = validator
    self.lock.Unlock()
}

節點啓動挖礦時調用了dpos.Authorize將驗證人賦值給了dpos.signer

func (s *Ethereum) StartMining(local bool) error {
    validator, err := s.Validator()
    ……
    if dpos, ok := s.engine.(*dpos.Dpos); ok {
        wallet, err := s.accountManager.Find(accounts.Account{Address: validator})
        if wallet == nil || err != nil {
            log.Error("Coinbase account unavailable locally", "err", err)
            return fmt.Errorf("signer missing: %v", err)
        }
        dpos.Authorize(validator, wallet.SignHash)
    }
    ……
}
func (s *Ethereum) Validator() (validator common.Address, err error) {
    s.lock.RLock()  //lock sync.RWMutex
   validator = s.validator
   s.lock.RUnlock()
  ……
}
func (d *Dpos) Authorize(signer common.Address, signFn SignerFn) {
    d.mu.Lock()
    d.signer = signer
    d.signFn = signFn
    d.mu.Unlock()
}

3.6 Finalize

<span id="finalize"></span>
生成一個新的區塊,不過不是最終的區塊。該函數功能請看註釋。

func (d *Dpos) Finalize(……){
    //把獎勵打入Coinbase,拜占庭版本之後獎勵3個eth,以前獎勵5個
    AccumulateRewards(chain.Config(), state, header, uncles)
    
    //調用選舉,函數內部判斷是否到了新一輪選舉週期
    err := epochContext.tryElect(genesis, parent)

    //每出一個塊,將該塊驗證人的出塊數+1,即更新DposContext.mintCntTrie。
    updateMintCnt(parent.Time.Int64(), header.Time.Int64(), header.Validator, dposContext)

    //給區塊設置header,transactions,Bloom,uncles;
    //給header設置TxHash,ReceiptHash,UncleHash;
    return types.NewBlock(header, txs, uncles, receipts), nil
}

3.7 Seal

<span id="seal"></span>
dpos的Seal主要是給新區塊進行簽名,即把簽名寫入header.Extra,返回最終狀態的區塊。\
d.signFn是個函數類型的聲明,首先源碼定義了一個錢包接口SignHash用於給一段hash進行簽名,而後將這個接口做爲形參調用dpos.Authorize,這樣d.signFn就被賦予了這個函數,而具體實現是keystoreWallet.SignHash,因此d.signFn的執行就是在執行keystoreWallet.SignHash。

func (d *Dpos) Seal(chain consensus.ChainReader, block *types.Block, stop <-chan struct{}) (*types.Block, error) {
    header := block.Header()
    number := header.Number.Uint64()
    // Sealing the genesis block is not supported
    if number == 0 {
        return nil, errUnknownBlock
    }
    now := time.Now().Unix()
    delay := NextSlot(now) - now
    if delay > 0 {
        select {
        case <-stop:
            return nil, nil
        //等到下一個出塊時刻slot,如10秒1塊的節奏,10秒內等到第10秒,11秒則要等到第20秒,以此類推。
        case <-time.After(time.Duration(delay) * time.Second):
        }
    }
    block.Header().Time.SetInt64(time.Now().Unix())

    // time's up, sign the block
    sighash, err := d.signFn(accounts.Account{Address: d.signer}, sigHash(header).Bytes())
    if err != nil {
        return nil, err
    }
    //將簽名賦值給header.Extra的後綴。這裏數組索引不會爲負,由於在Prepare的時候,Extra就保留了32(前綴)+65(後綴)個字節。
    copy(header.Extra[len(header.Extra)-extraSeal:], sighash)
    return block.WithSeal(header), nil
}
func (b *Block) WithSeal(header *Header) *Block {
    cpy := *header

    return &Block{
        header:       &cpy,
        transactions: b.transactions,
        uncles:       b.uncles,

        // add dposcontext
        DposContext: b.DposContext,
    }
}

3.8 VerifySeal

Seal接口是區塊產生的最後一道工序,也是各類共識算法最核心的實現,VerifySeal就是對這種封裝的真僞驗證。\
\
1)從epochTrie裏獲取到驗證人列表,(epochTrie的key就是字面量「validator」,它全局惟一,每輪選舉後都會被覆蓋更新)再用header的時間計算本區塊驗證人所在列表的偏移量(做爲驗證人列表數組索引),得到驗證人地址。

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

2)用Dpos的簽名還原出這個驗證人的地址。二者進行對比,看是否一致,再用還原的地址和header.Validator對比看是否一致。

if err := d.verifyBlockSigner(validator, header); err != nil {
        return err
    }
func (d *Dpos) verifyBlockSigner(validator common.Address, header *types.Header) error {
    signer, err := ecrecover(header, d.signatures)
    if err != nil {
        return err
    }
    if bytes.Compare(signer.Bytes(), validator.Bytes()) != 0 {
        return ErrInvalidBlockValidator
    }
    if bytes.Compare(signer.Bytes(), header.Validator.Bytes()) != 0 {
        return ErrMismatchSignerAndValidator
    }
    return nil
}

其中:\
header.Validator是在Prepare接口中被賦值的。\
d.signatures這個簽名是怎麼賦值的?不要顧名思義它存的不是簽名,它的類型是一種有名的緩存,(key,value)分別是(區塊頭hash,驗證人地址),它的賦值也是在ecrecover裏進行的。ecrecover根據區塊頭hash從緩存中獲取到驗證人地址,若是沒有就從header.Extra的簽名部分還原出驗證人地址。

3)VerifySeal通過上面兩步驗證後,最後這個操做待詳細分析。

return d.updateConfirmedBlockHeader(chain)

3.9 APIs

用於容納API。

func (d *Dpos) APIs(chain consensus.ChainReader) []rpc.API {
    return []rpc.API{{
        Namespace: "dpos",
        Version:   "1.0",
        Service:   &API{chain: chain, dpos: d},
        Public:    true,
    }}
}

它在eth包裏被賦值具體API

apis = append(apis, s.engine.APIs(s.BlockChain())...)
func (s *Ethereum) APIs() []rpc.API {
    apis := ethapi.GetAPIs(s.ApiBackend)

    // Append any APIs exposed explicitly by the consensus engine
    apis = append(apis, s.engine.APIs(s.BlockChain())...)

    // Append all the local APIs and return
    return append(apis, []rpc.API{
        {
            Namespace: "eth",
            Version:   "1.0",
            Service:   NewPublicEthereumAPI(s),
            Public:    true,
        }, {
            Namespace: "eth",
            Version:   "1.0",
            Service:   NewPublicMinerAPI(s),
            Public:    true,
        }, {
            Namespace: "eth",
            Version:   "1.0",
            Service:   downloader.NewPublicDownloaderAPI(s.protocolManager.downloader, s.eventMux),
            Public:    true,
        }, {
            Namespace: "miner",
            Version:   "1.0",
            Service:   NewPrivateMinerAPI(s),
            Public:    false,
        }, {
            Namespace: "eth",
            Version:   "1.0",
            Service:   filters.NewPublicFilterAPI(s.ApiBackend, false),
            Public:    true,
        }, {
            Namespace: "admin",
            Version:   "1.0",
            Service:   NewPrivateAdminAPI(s),
        }, {
            Namespace: "debug",
            Version:   "1.0",
            Service:   NewPublicDebugAPI(s),
            Public:    true,
        }, {
            Namespace: "debug",
            Version:   "1.0",
            Service:   NewPrivateDebugAPI(s.chainConfig, s),
        }, {
            Namespace: "net",
            Version:   "1.0",
            Service:   s.netRPCService,
            Public:    true,
        },
    }...)
}

這些賦值的實際上是結構體,經過結構體能夠訪問到自身的方法,這些結構體大多都是Ethereum,只不過區分了Namespace用於不一樣場景。

type PublicEthereumAPI struct {
    e *Ethereum
}
type PublicMinerAPI struct {
    e *Ethereum
}
type PublicDownloaderAPI struct {
    d                         *Downloader
    mux                       *event.TypeMux
    installSyncSubscription   chan chan interface{}
    uninstallSyncSubscription chan *uninstallSyncSubscriptionRequest
}
type PrivateMinerAPI struct {
    e *Ethereum
}
type PublicDebugAPI struct {
    eth *Ethereum
}

看看都有哪些API服務:

<img src="https://i.loli.net/2018/11/09...; width=350>

4 DPOS引擎如何驅動以太坊挖礦

以太坊比如一臺機器,生產區塊,這臺機器的引擎上面已經講過了,接下來再看看這臺機器是如何運做的。

從控制檯啓動節點挖礦開始:

>miner.start()

這個命令將會調用api的Start方法。

4.1 以太坊啓動時序圖

image

在mintLoop方法裏,worker無限循環,阻塞監聽stopper通道,每秒調用一次mintBlock。\
用戶主動中止以太坊節點的時候,stopper通道被關閉,worker就中止了。

4.2 mintBlock挖礦函數分析

這個函數的做用即用引擎(POW、DPOS)出塊。在POW版本中,worker還須要啓動agent(分爲CpuAgent和何RemoteAgent兩種實現),agent進行Seal操做。在DPOS中,去掉了agent這一層,直接在mintBlock裏Seal。

mintLoop每秒都調用mintBlock,但並不是每秒都出塊,邏輯在下面分析。

func (self *worker) mintLoop() {
    ticker := time.NewTicker(time.Second).C
    for {
        select {
        case now := <-ticker:
            self.mintBlock(now.Unix())
        case <-self.stopper:
            close(self.quitCh)
            self.quitCh = make(chan struct{}, 1)
            self.stopper = make(chan struct{}, 1)
            return
        }
    }
}
func (self *worker) mintBlock(now int64) {
    engine, ok := self.engine.(*dpos.Dpos)
    if !ok {
        log.Error("Only the dpos engine was allowed")
        return
    }
    err := engine.CheckValidator(self.chain.CurrentBlock(), now)
    if err != nil {
        switch err {
        case dpos.ErrWaitForPrevBlock,
            dpos.ErrMintFutureBlock,
            dpos.ErrInvalidBlockValidator,
            dpos.ErrInvalidMintBlockTime:
            log.Debug("Failed to mint the block, while ", "err", err)
        default:
            log.Error("Failed to mint the block", "err", err)
        }
        return
    }
    work, err := self.createNewWork()
    if err != nil {
        log.Error("Failed to create the new work", "err", err)
        return
    }

    result, err := self.engine.Seal(self.chain, work.Block, self.quitCh)
    if err != nil {
        log.Error("Failed to seal the block", "err", err)
        return
    }
    self.recv <- &Result{work, result}
}

如時序圖和源碼所示,mintBlock函數包含3個主要方法:

4.2.1 CheckValidator出塊前驗證

該函數判斷當前出塊人(validator)是否與dpos規則計算獲得的validator同樣,同時判斷是否到了出塊時間點。

func (self *worker) mintBlock(now int64) {
    ……
    //檢查出塊驗證人validator是否正確
    //CurrentBlock()是截止當前時間,最後加入到鏈的塊
    //CurrentBlock()是BlockChain.insert的時候賦的值
    err := engine.CheckValidator(self.chain.CurrentBlock(), now)
    ……
}
func (d *Dpos) CheckValidator(lastBlock *types.Block, now int64) error {
    //檢查是否到達出塊間隔最後1秒(slot),出塊間隔設置爲10秒
    if err := d.checkDeadline(lastBlock, now); err != nil {
        return err
    }
    dposContext, err := types.NewDposContextFromProto(d.db, lastBlock.Header().DposContext)
    if err != nil {
        return err
    }
    epochContext := &EpochContext{DposContext: dposContext}
    //根據dpos規則計算:先從epochTrie裏得到本輪選舉週期的驗證人列表
    //而後根據當前時間計算偏移量,得到應該由誰挖掘當前塊的驗證人
    validator, err := epochContext.lookupValidator(now)
    if err != nil {
        return err
    }
    //判斷dpos規則計算獲得的validator和d.signer即節點設置的validator是否一致
    if (validator == common.Address{}) || bytes.Compare(validator.Bytes(), d.signer.Bytes()) != 0 {
        return ErrInvalidBlockValidator
    }
    return nil
}
func (d *Dpos) checkDeadline(lastBlock *types.Block, now int64) error {
    prevSlot := PrevSlot(now)
    nextSlot := NextSlot(now)
    //假如當前時間是1542117655,則prevSlot = 1542117650,nextSlot = 1542117660
    if lastBlock.Time().Int64() >= nextSlot {
        return ErrMintFutureBlock
    }
    // nextSlot-now <= 1是要求出塊時間須要接近出塊間隔最後1秒
    if lastBlock.Time().Int64() == prevSlot || nextSlot-now <= 1 {
        return nil
    }
    //時間不到,就返回等待錯誤
    return ErrWaitForPrevBlock
}

CheckValidator()判斷不經過則跳出mintBlock,繼續下一秒mintBlock循環。\
判斷經過進入createNewWork()。

4.2.2 createNewWork生成新塊並定型

這個函數涉及具體執行交易、生成收據和日誌、向監聽者發送相關事件、調用dpos引擎Finalize打包、將未Seal的新塊加入未確認塊集等事項。

4.2.2.1 挖礦時序圖

image

func (self *worker) createNewWork() (*Work, error) {
    self.mu.Lock()
    defer self.mu.Unlock()
    self.uncleMu.Lock()
    defer self.uncleMu.Unlock()
    self.currentMu.Lock()
    defer self.currentMu.Unlock()

    tstart := time.Now()
    parent := self.chain.CurrentBlock()

    tstamp := tstart.Unix()
    if parent.Time().Cmp(new(big.Int).SetInt64(tstamp)) >= 0 {
        tstamp = parent.Time().Int64() + 1
    }
    // this will ensure we're not going off too far in the future
    if now := time.Now().Unix(); tstamp > now+1 {
        wait := time.Duration(tstamp-now) * time.Second
        log.Info("Mining too far in the future", "wait", common.PrettyDuration(wait))
        time.Sleep(wait)
    }

    num := parent.Number()
    header := &types.Header{
        ParentHash: parent.Hash(),
        Number:     num.Add(num, common.Big1),
        GasLimit:   core.CalcGasLimit(parent),
        GasUsed:    new(big.Int),
        Extra:      self.extra,
        Time:       big.NewInt(tstamp),
    }
    // Only set the coinbase if we are mining (avoid spurious block rewards)
    if atomic.LoadInt32(&self.mining) == 1 {
        header.Coinbase = self.coinbase
    }
    if err := self.engine.Prepare(self.chain, header); err != nil {
        return nil, fmt.Errorf("got error when preparing header, err: %s", err)
    }
    // If we are care about TheDAO hard-fork check whether to override the extra-data or not
    if daoBlock := self.config.DAOForkBlock; daoBlock != nil {
        // Check whether the block is among the fork extra-override range
        limit := new(big.Int).Add(daoBlock, params.DAOForkExtraRange)
        if header.Number.Cmp(daoBlock) >= 0 && header.Number.Cmp(limit) < 0 {
            // Depending whether we support or oppose the fork, override differently
            if self.config.DAOForkSupport {
                header.Extra = common.CopyBytes(params.DAOForkBlockExtra)
            } else if bytes.Equal(header.Extra, params.DAOForkBlockExtra) {
                header.Extra = []byte{} // If miner opposes, don't let it use the reserved extra-data
            }
        }
    }

    // Could potentially happen if starting to mine in an odd state.
    err := self.makeCurrent(parent, header)
    if err != nil {
        return nil, fmt.Errorf("got error when create mining context, err: %s", err)
    }
    // Create the current work task and check any fork transitions needed
    work := self.current
    if self.config.DAOForkSupport && self.config.DAOForkBlock != nil && self.config.DAOForkBlock.Cmp(header.Number) == 0 {
        misc.ApplyDAOHardFork(work.state)
    }
    pending, err := self.eth.TxPool().Pending()
    if err != nil {
        return nil, fmt.Errorf("got error when fetch pending transactions, err: %s", err)
    }
    txs := types.NewTransactionsByPriceAndNonce(self.current.signer, pending)
    work.commitTransactions(self.mux, txs, self.chain, self.coinbase)

    // compute uncles for the new block.
    var (
        uncles    []*types.Header
        badUncles []common.Hash
    )
    for hash, uncle := range self.possibleUncles {
        if len(uncles) == 2 {
            break
        }
        if err := self.commitUncle(work, uncle.Header()); err != nil {
            log.Trace("Bad uncle found and will be removed", "hash", hash)
            log.Trace(fmt.Sprint(uncle))

            badUncles = append(badUncles, hash)
        } else {
            log.Debug("Committing new uncle to block", "hash", hash)
            uncles = append(uncles, uncle.Header())
        }
    }
    for _, hash := range badUncles {
        delete(self.possibleUncles, hash)
    }
    // Create the new block to seal with the consensus engine
    if work.Block, err = self.engine.Finalize(self.chain, header, work.state, work.txs, uncles, work.receipts, work.dposContext); err != nil {
        return nil, fmt.Errorf("got error when finalize block for sealing, err: %s", err)
    }
    work.Block.DposContext = work.dposContext

    // update the count for the miner of new block
    // We only care about logging if we're actually mining.
    if atomic.LoadInt32(&self.mining) == 1 {
        log.Info("Commit new mining work", "number", work.Block.Number(), "txs", work.tcount, "uncles", len(uncles), "elapsed", common.PrettyDuration(time.Since(tstart)))
        self.unconfirmed.Shift(work.Block.NumberU64() - 1)
    }
    return work, nil
}

4.2.2.2 準備區塊頭

先調用dpos引擎的Prepare填充區塊頭字段。

……
    num := parent.Number()
    header := &types.Header{
        ParentHash: parent.Hash(),
        Number:     num.Add(num, common.Big1),
        GasLimit:   core.CalcGasLimit(parent),
        GasUsed:    new(big.Int),
        Extra:      self.extra,
        Time:       big.NewInt(tstamp),
    }
    // 確保出塊時間不要偏離太大(過早或過晚)
    if atomic.LoadInt32(&self.mining) == 1 {
        header.Coinbase = self.coinbase
    }
    
    self.engine.Prepare(self.chain, header)
    ……

此時,即將產生的區塊Header的GasUsed和Extra都爲空,Extra經過前面引擎分析的時候,咱們知道會在Prepare裏用0字節填充32+65的先後綴,除了Extra,Prepare還將填充其餘的Header字段(詳見3.5 Prepare分析),當Prepare執行完成,大部分字段都設置好了,還有少部分待填。

4.2.2.3 準備挖礦環境

接下來把父塊和本塊的header傳給makeCurrent方法執行。

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

makeCurrent先新建stateDB和dposContext,而後組裝一個Work結構體。

func (self *worker) makeCurrent(parent *types.Block, header *types.Header) error {
    state, err := self.chain.StateAt(parent.Root())
    if err != nil {
        return err
    }
    dposContext, err := types.NewDposContextFromProto(self.chainDb, parent.Header().DposContext)
    if err != nil {
        return err
    }
    work := &Work{
        config:      self.config,
        signer:      types.NewEIP155Signer(self.config.ChainId),
        state:       state,
        dposContext: dposContext,
        ancestors:   set.New(),
        family:      set.New(),
        uncles:      set.New(),
        header:      header,
        createdAt:   time.Now(),
    }

    // when 08 is processed ancestors contain 07 (quick block)
    for _, ancestor := range self.chain.GetBlocksFromHash(parent.Hash(), 7) {
        for _, uncle := range ancestor.Uncles() {
            work.family.Add(uncle.Hash())
        }
        work.family.Add(ancestor.Hash())
        work.ancestors.Add(ancestor.Hash())
    }

    // Keep track of transactions which return errors so they can be removed
    work.tcount = 0
    self.current = work
    return nil
}

Work結構體中,ancestors存儲的是6個祖先塊,family存儲的是6個祖先塊和它們各自的叔塊,組裝後的Work結構體賦值給*worker.current。

4.2.2.3 從交易池獲取pending交易集

而後從交易池裏獲取全部pending狀態的交易,這些交易按帳戶分組,每一個帳戶裏的交易按nonce排序後返回交易集,這裏暫且叫S1:

pending, err := self.eth.TxPool().Pending() //S1 = pending

txs := types.NewTransactionsByPriceAndNonce(self.current.signer, pending)

4.2.2.4 交易集結構化處理

再而後經過NewTransactionsByPriceAndNonce函數對交易集進行結構化,它把S1集合裏每一個帳戶的第一筆交易分離出來做爲heads集合,返回以下結構:

return &TransactionsByPriceAndNonce{
        txs:    txs,    //S1集合中每一個帳戶除去第一個交易後的交易集
        heads:  heads,  //這個集合由每一個帳戶的第一個交易組成
        signer: signer,
    }

4.2.2.5 交易執行過程分析

調用commitTransactions方法,執行新區塊包含的全部交易。

這個方法是對處理後的交易集txs的具體執行,所謂執行交易,籠統地說就是把轉帳、合約或dpos交易類型的數據寫入對應的內存Trie,再從Trie刷到本地DB中去。

func (env *Work) commitTransactions(mux *event.TypeMux, txs *types.TransactionsByPriceAndNonce, bc *core.BlockChain, coinbase common.Address) {
    gp := new(core.GasPool).AddGas(env.header.GasLimit)

    var coalescedLogs []*types.Log

    for {
        // Retrieve the next transaction and abort if all done
        tx := txs.Peek()

        if tx == nil {
            break
        }
        // Error may be ignored here. The error has already been checked
        // during transaction acceptance is the transaction pool.
        //
        // We use the eip155 signer regardless of the current hf.
        from, _ := types.Sender(env.signer, tx)
        // Check whether the tx is replay protected. If we're not in the EIP155 hf
        // phase, start ignoring the sender until we do.
        if tx.Protected() && !env.config.IsEIP155(env.header.Number) {
            log.Trace("Ignoring reply protected transaction", "hash", tx.Hash(), "eip155", env.config.EIP155Block)

            txs.Pop()
            continue
        }
        // Start executing the transaction
        env.state.Prepare(tx.Hash(), common.Hash{}, env.tcount)

        err, logs := env.commitTransaction(tx, bc, coinbase, gp)
        switch err {
        case core.ErrGasLimitReached:
            // Pop the current out-of-gas transaction without shifting in the next from the account
            log.Trace("Gas limit exceeded for current block", "sender", from)
            txs.Pop()

        case core.ErrNonceTooLow:
            // New head notification data race between the transaction pool and miner, shift
            log.Trace("Skipping transaction with low nonce", "sender", from, "nonce", tx.Nonce())
            txs.Shift()

        case core.ErrNonceTooHigh:
            // Reorg notification data race between the transaction pool and miner, skip account =
            log.Trace("Skipping account with hight nonce", "sender", from, "nonce", tx.Nonce())
            txs.Pop()

        case nil:
            // Everything ok, collect the logs and shift in the next transaction from the same account
            coalescedLogs = append(coalescedLogs, logs...)
            env.tcount++
            txs.Shift()

        default:
            // Strange error, discard the transaction and get the next in line (note, the
            // nonce-too-high clause will prevent us from executing in vain).
            log.Debug("Transaction failed, account skipped", "hash", tx.Hash(), "err", err)
            txs.Shift()
        }
    }

    if len(coalescedLogs) > 0 || env.tcount > 0 {
        // make a copy, the state caches the logs and these logs get "upgraded" from pending to mined
        // logs by filling in the block hash when the block was mined by the local miner. This can
        // cause a race condition if a log was "upgraded" before the PendingLogsEvent is processed.
        cpy := make([]*types.Log, len(coalescedLogs))
        for i, l := range coalescedLogs {
            cpy[i] = new(types.Log)
            *cpy[i] = *l
        }
        go func(logs []*types.Log, tcount int) {
            if len(logs) > 0 {
                mux.Post(core.PendingLogsEvent{Logs: logs})
            }
            if tcount > 0 {
                mux.Post(core.PendingStateEvent{})
            }
        }(cpy, env.tcount)
    }
}

該方法對結構化處理後的txs遍歷執行,分爲幾步:

  • Work.state.Prepare()\
    這是給StateDB設置交易hash、區塊hash(此時爲空)、交易索引。\
    StateDB是用來操做整個帳戶樹也即world state trie的,每執行一筆交易就更改一次world state trie。\
    交易索引是指在對txs.heads進行遍歷的時候的自增數,這個索引在本區塊內惟一,由於它是本區塊包含的全部pending交易涉及的帳戶及各帳戶下全部交易的總遞增。

    commitTransactions函數對txs的遍歷方式是:從遍歷txs.heads開始,獲取第一個帳戶的第一筆交易,而後獲取同一帳戶的第二筆交易以此類推,若是該帳戶沒有交易了,繼續txs.heads的下一個帳戶。\
    也就是按帳戶優先級先遍歷其下的全部交易,其次遍歷全部帳戶(堆級別操做),txs結構化就是爲這種循環方式準備的。

func (self *StateDB) Prepare(thash, bhash common.Hash, ti int) {
    self.thash = thash
    self.bhash = bhash
    self.txIndex = ti
}
  • Work.commitTransaction()\
    執行單筆交易,先對stateDB這個大結構作一個版本號快照,也要對dpos的五棵樹上下文即dposContext作一個備份,而後調用core.ApplyTransaction()方法,若是出錯就退回快照和備份,執行成功後把交易加入Work.txs,(這個txs是爲Finalize的時候傳參用的,由於在遍歷執行交易的時候會把原txs結構破壞,作個備份)交易收據加入Work.receipts,最後返回收據日誌。

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

    看一下ApplyTransaction()是如何具體執行交易的:

    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) {
        msg, err := tx.AsMessage(types.MakeSigner(config, header.Number))
        if err != nil {
            return nil, nil, err
        }
    
        if msg.To() == nil && msg.Type() != types.Binary {
            return nil, nil, types.ErrInvalidType
        }
    
        // Create a new context to be used in the EVM environment
        context := NewEVMContext(msg, header, bc, author)
        // Create a new environment which holds all relevant information
        // about the transaction and calling mechanisms.
        vmenv := vm.NewEVM(context, statedb, config, cfg)
        // Apply the transaction to the current state (included in the env)
        _, gas, failed, err := ApplyMessage(vmenv, msg, gp)
        if err != nil {
            return nil, nil, err
        }
        if msg.Type() != types.Binary {
            if err = applyDposMessage(dposContext, msg); err != nil {
                return nil, nil, err
            }
        }
    
        // Update the state with pending changes
        var root []byte
        if config.IsByzantium(header.Number) {
            statedb.Finalise(true)
        } else {
            root = statedb.IntermediateRoot(config.IsEIP158(header.Number)).Bytes()
        }
        usedGas.Add(usedGas, gas)
    
        // Create a new receipt for the transaction, storing the intermediate root and gas used by the tx
        // based on the eip phase, we're passing wether the root touch-delete accounts.
        receipt := types.NewReceipt(root, failed, usedGas)
        receipt.TxHash = tx.Hash()
        receipt.GasUsed = new(big.Int).Set(gas)
        // if the transaction created a contract, store the creation address in the receipt.
        if msg.To() == nil {
            receipt.ContractAddress = crypto.CreateAddress(vmenv.Context.Origin, tx.Nonce())
        }
    
        // Set the receipt logs and create a bloom for filtering
        receipt.Logs = statedb.GetLogs(tx.Hash())
        receipt.Bloom = types.CreateBloom(types.Receipts{receipt})
    
        return receipt, gas, err
    }

    NewEVMContext是構建一個EVM執行環境,這個環境以下:

    var beneficiary common.Address
    if author == nil {
        beneficiary, _ = chain.Engine().Author(header) // Ignore error, we're past header validation
    } else {
        beneficiary = *author
    }
    return vm.Context{
        //是否可以轉帳函數,會判斷髮起交易帳戶餘額是否大於轉帳數量
        CanTransfer: CanTransfer,
        //轉帳函數,給轉帳地址減去轉帳額,同時給接收地址加上轉帳額
        Transfer:    Transfer,
        //區塊頭hash
        GetHash:     GetHashFn(header, chain),
        Origin:      msg.From(),
        Coinbase:    beneficiary,
        BlockNumber: new(big.Int).Set(header.Number),
        Time:        new(big.Int).Set(header.Time),
        Difficulty:  new(big.Int).Set(header.Difficulty),
        GasLimit:    new(big.Int).Set(header.GasLimit),
        GasPrice:    new(big.Int).Set(msg.GasPrice()),
    }

    beneficiary應該是Coinbase,這裏是個bug。能夠看一下core.state_processor.go裏的Procee方法調用ApplyTransaction的時候author傳的是nil,而這裏判斷author爲nil的時候從header裏取的倒是validator。
    NewEVM是建立一個攜帶了EVM環境和編譯器的虛擬機。

    而後調用ApplyMessage(),這個函數最主要的是對當前交易進行狀態轉換TransitionDb()。

TransitionDb詳解

func (st *StateTransition) TransitionDb() (ret []byte, requiredGas, usedGas *big.Int, failed bool, err error) {
    if err = st.preCheck(); err != nil {
        return
    }
    msg := st.msg
    sender := st.from() // err checked in preCheck

    homestead := st.evm.ChainConfig().IsHomestead(st.evm.BlockNumber)
    contractCreation := msg.To() == nil

    // Pay intrinsic gas
    // TODO convert to uint64
    intrinsicGas := IntrinsicGas(st.data, contractCreation, homestead)
    if intrinsicGas.BitLen() > 64 {
        return nil, nil, nil, false, vm.ErrOutOfGas
    }
    if err = st.useGas(intrinsicGas.Uint64()); err != nil {
        return nil, nil, nil, false, err
    }

    var (
        evm = st.evm
        // vm errors do not effect consensus and are therefor
        // not assigned to err, except for insufficient balance
        // error.
        vmerr error
    )
    if contractCreation {
        ret, _, st.gas, vmerr = evm.Create(sender, st.data, st.gas, st.value)
    } else {
        // Increment the nonce for the next transaction
        st.state.SetNonce(sender.Address(), st.state.GetNonce(sender.Address())+1)
        ret, st.gas, vmerr = evm.Call(sender, st.to().Address(), st.data, st.gas, st.value)
    }
    if vmerr != nil {
        log.Debug("VM returned with error", "err", vmerr)
        // The only possible consensus-error would be if there wasn't
        // sufficient balance to make the transfer happen. The first
        // balance transfer may never fail.
        if vmerr == vm.ErrInsufficientBalance {
            return nil, nil, nil, false, vmerr
        }
    }
    requiredGas = new(big.Int).Set(st.gasUsed())

    st.refundGas()
    st.state.AddBalance(st.evm.Coinbase, new(big.Int).Mul(st.gasUsed(), st.gasPrice))

    return ret, requiredGas, st.gasUsed(), vmerr != nil, err
}

其中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()。

// Create creates a new contract using code as deployment code.
func (evm *EVM) Create(caller ContractRef, code []byte, gas uint64, value *big.Int) (ret []byte, contractAddr common.Address, leftOverGas uint64, err error) {

    // Depth check execution. Fail if we're trying to execute above the
    // limit.
    if evm.depth > int(params.CallCreateDepth) {
        return nil, common.Address{}, gas, ErrDepth
    }
    if !evm.CanTransfer(evm.StateDB, caller.Address(), value) {
        return nil, common.Address{}, gas, ErrInsufficientBalance
    }
    // Ensure there's no existing contract already at the designated address
    nonce := evm.StateDB.GetNonce(caller.Address())
    evm.StateDB.SetNonce(caller.Address(), nonce+1)

    contractAddr = crypto.CreateAddress(caller.Address(), nonce)
    contractHash := evm.StateDB.GetCodeHash(contractAddr)
    if evm.StateDB.GetNonce(contractAddr) != 0 || (contractHash != (common.Hash{}) && contractHash != emptyCodeHash) {
        return nil, common.Address{}, 0, ErrContractAddressCollision
    }
    // Create a new account on the state
    snapshot := evm.StateDB.Snapshot()
    evm.StateDB.CreateAccount(contractAddr)
    if evm.ChainConfig().IsEIP158(evm.BlockNumber) {
        evm.StateDB.SetNonce(contractAddr, 1)
    }
    evm.Transfer(evm.StateDB, caller.Address(), contractAddr, value)

    // initialise a new contract and set the code that is to be used by the
    // E The contract is a scoped evmironment for this execution context
    // only.
    contract := NewContract(caller, AccountRef(contractAddr), value, gas)
    contract.SetCallCode(&contractAddr, crypto.Keccak256Hash(code), code)

    if evm.vmConfig.NoRecursion && evm.depth > 0 {
        return nil, contractAddr, gas, nil
    }
    ret, err = run(evm, snapshot, contract, nil)
    // check whether the max code size has been exceeded
    maxCodeSizeExceeded := evm.ChainConfig().IsEIP158(evm.BlockNumber) && len(ret) > params.MaxCodeSize
    // if the contract creation ran successfully and no errors were returned
    // calculate the gas required to store the code. If the code could not
    // be stored due to not enough gas set an error and let it be handled
    // by the error checking condition below.
    if err == nil && !maxCodeSizeExceeded {
        createDataGas := uint64(len(ret)) * params.CreateDataGas
        if contract.UseGas(createDataGas) {
            evm.StateDB.SetCode(contractAddr, ret)
        } else {
            err = ErrCodeStoreOutOfGas
        }
    }

    // When an error was returned by the EVM or when setting the creation code
    // above we revert to the snapshot and consume any gas remaining. Additionally
    // when we're in homestead this also counts for code storage gas errors.
    if maxCodeSizeExceeded || (err != nil && (evm.ChainConfig().IsHomestead(evm.BlockNumber) || err != ErrCodeStoreOutOfGas)) {
        evm.StateDB.RevertToSnapshot(snapshot)
        if err != errExecutionReverted {
            contract.UseGas(contract.Gas)
        }
    }
    // Assign err if contract code size exceeds the max while the err is still empty.
    if maxCodeSizeExceeded && err == nil {
        err = errMaxCodeSizeExceeded
    }
    return ret, contractAddr, contract.Gas, err
}

注意這裏傳入的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是執行合約交易,包括轉帳類型的交易、調用合約交易。

func (evm *EVM) Call(caller ContractRef, addr common.Address, input []byte, gas uint64, value *big.Int) (ret []byte, leftOverGas uint64, err error) {
    if evm.vmConfig.NoRecursion && evm.depth > 0 {
        return nil, gas, nil
    }

    // Fail if we're trying to execute above the call depth limit
    if evm.depth > int(params.CallCreateDepth) {
        return nil, gas, ErrDepth
    }
    // Fail if we're trying to transfer more than the available balance
    if !evm.Context.CanTransfer(evm.StateDB, caller.Address(), value) {
        return nil, gas, ErrInsufficientBalance
    }

    var (
        to       = AccountRef(addr)
        snapshot = evm.StateDB.Snapshot()
    )
    if !evm.StateDB.Exist(addr) {
        precompiles := PrecompiledContractsHomestead
        if evm.ChainConfig().IsByzantium(evm.BlockNumber) {
            precompiles = PrecompiledContractsByzantium
        }
        if precompiles[addr] == nil && evm.ChainConfig().IsEIP158(evm.BlockNumber) && value.Sign() == 0 {
            return nil, gas, nil
        }
        evm.StateDB.CreateAccount(addr)
    }
    evm.Transfer(evm.StateDB, caller.Address(), to.Address(), value)

    // initialise a new contract and set the code that is to be used by the
    // E The contract is a scoped environment for this execution context
    // only.
    contract := NewContract(caller, to, value, gas)
    contract.SetCallCode(&addr, evm.StateDB.GetCodeHash(addr), evm.StateDB.GetCode(addr))

    ret, err = run(evm, snapshot, contract, input)
    // When an error was returned by the EVM or when setting the creation code
    // above we revert to the snapshot and consume any gas remaining. Additionally
    // when we're in homestead this also counts for code storage gas errors.
    if err != nil {
        evm.StateDB.RevertToSnapshot(snapshot)
        if err != errExecutionReverted {
            contract.UseGas(contract.Gas)
        }
    }
    return ret, contract.Gas, err
}

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。

</td></tr></table>

除了Call(),evm還提供了另外3個合約調用方法:\
CallCode(),已經棄用,由DelegateCall()替代\
DelegateCall()\
StaticCall()暫時未用

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

咱們上面討論的是交易,根據黃皮書的定義交易就兩種:建立合約、消息調用。區分兩者的標誌就是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)。\
而後生成一個收據,收據裏包括:

交易的hash\
執行成敗狀態\
消耗的費用\
如果建立合約交易就把合約地址也寫到收據的ContractAddress字段裏\
日誌\
Bloom

關於日誌,棧操做的時候會記錄下日誌,日誌信息以下:

type Log struct {
    // Consensus fields:
    // address of the contract that generated the event
    Address common.Address `json:"address" gencodec:"required"`
    // list of topics provided by the contract.
    Topics []common.Hash `json:"topics" gencodec:"required"`
    // supplied by the contract, usually ABI-encoded
    Data []byte `json:"data" gencodec:"required"`

    // Derived fields. These fields are filled in by the node
    // but not secured by consensus.
    // block in which the transaction was included
    BlockNumber uint64 `json:"blockNumber"`
    // hash of the transaction
    TxHash common.Hash `json:"transactionHash" gencodec:"required"`
    // index of the transaction in the block
    TxIndex uint `json:"transactionIndex" gencodec:"required"`
    // hash of the block in which the transaction was included
    BlockHash common.Hash `json:"blockHash"`
    // index of the log in the receipt
    Index uint `json:"logIndex" gencodec:"required"`

    // The Removed field is true if this log was reverted due to a chain reorganisation.
    // You must pay attention to this field if you receive logs through a filter query.
    Removed bool `json:"removed"`
}

ApplyTransaction()最終返回收據。
至此,單筆交易執行過程commitTransaction()結束。

如此循環執行,直到全部交易執行完成。\
在循環執行交易的過程當中,咱們把全部交易收據的日誌寫入了一個集合,等交易所有執行完成,異步將這個日誌集合向全部已註冊的事件接收者發送:

mux.Post(core.PendingLogsEvent{Logs: logs})
mux.Post(core.PendingStateEvent{})
func (mux *TypeMux) Post(ev interface{}) error {
    event := &TypeMuxEvent{
        Time: time.Now(),
        Data: ev,
    }
    rtyp := reflect.TypeOf(ev)
    mux.mutex.RLock()
    if mux.stopped {
        mux.mutex.RUnlock()
        return ErrMuxClosed
    }
    subs := mux.subm[rtyp]
    mux.mutex.RUnlock()
    for _, sub := range subs {
        sub.deliver(event)
    }
    return nil
}

投遞相應的事件到TypeMuxSubscription的postC通道中。

func (s *TypeMuxSubscription) deliver(event *TypeMuxEvent) {
    // Short circuit delivery if stale event
    if s.created.After(event.Time) {
        return
    }
    // Otherwise deliver the event
    s.postMu.RLock()
    defer s.postMu.RUnlock()

    select {
    case s.postC <- event:
    case <-s.closing:
    }
}

關於事件的訂閱、發送單列章節講。

commitTransactions()結束,如今回到了createNewWork中,代碼繼續遍歷叔塊和損壞的叔塊,這段代碼其實在DPOS中已經不須要了,由於DPOS中沒有叔塊,chainSideCh事件被刪除,possibleUncles沒有被賦值的機會了。

4.2.2.6 Finalize定型新塊

把header、帳戶狀態、交易、收據等信息傳給dpos引擎去定型。參見3.6節

4.2.2.7 檢查以前的塊是否上鍊

注意:是檢查本節點以前挖的塊是否上鍊,而不是當前挖出的塊。當前塊離上鍊爲時尚早。

每一個以太坊節點會維護一個未確認塊集,集合內有個環狀容器,這個容器容納僅由自身挖出的塊,在最樂觀的狀況下(即連續由本節點挖出塊的狀況下),最大容納5個塊。當第6個連續的塊由本節點挖出的時候就會觸發unconfirmedBlocks.Shift()的執行(這裏「執行」的上下文含義是知足函數內部的判斷條件,而不只僅指函數被調用,下同)。

但大多數狀況下,一個節點不會連續出塊,那麼可能在本節點第二次挖出塊的時候,當前區塊鏈高度就已經超過以前挖出的那個塊6個高度了,也會觸發unconfirmedBlocks.Shift()執行。換句話說就是一般狀況下檢查本身出的前一個塊有沒有加入到鏈上。

Shift的做用,是檢查未確認塊集,這個未確認集並非說真的就全是一直未被加入到鏈上的塊,而是當該節點知足上面兩段描述的「執行」條件時,都會檢查一下以前挖出的塊有沒有被確認(加入區塊鏈),若是當前區塊鏈高度,高於未確認集環狀容器內那些塊6個高度以後,那些塊尚未被加入到鏈上,就從未確認塊集合中刪除那些塊。

這個函數的意思着重表達:在至少6個高度的==時間==以後,纔會去檢查是否加入到鏈上,至於上沒上鍊它也不能改變什麼,就是給本節點一個以前的塊被怎麼處理了的通知。爲何是這樣的時點呢?多是要留出6個高度的時間等全部節點都確認吧,後文再說。

func (set *unconfirmedBlocks) Shift(height uint64) {
    set.lock.Lock()
    defer set.lock.Unlock()

    for set.blocks != nil {
        // Retrieve the next unconfirmed block and abort if too fresh
        next := set.blocks.Value.(*unconfirmedBlock)
        if next.index+uint64(set.depth) > height {
            break
        }
        // Block seems to exceed depth allowance, check for canonical status
        header := set.chain.GetHeaderByNumber(next.index)
        switch {
        case header == nil:
            log.Warn("Failed to retrieve header of mined block", "number", next.index, "hash", next.hash)
        case header.Hash() == next.hash:
            log.Info("🔗 block reached canonical chain", "number", next.index, "hash", next.hash)
        default:
            log.Info("⑂ block  became a side fork", "number", next.index, "hash", next.hash)
        }
        // Drop the block out of the ring
        if set.blocks.Value == set.blocks.Next().Value {
            set.blocks = nil
        } else {
            //下面的代碼處於循環中,實現對for set.blocks的迭代賦值
            set.blocks = set.blocks.Move(-1)    //指向最後一個環元素
            set.blocks.Unlink(1)    //刪除原第一個
            set.blocks = set.blocks.Move(1) //指向原第二個
        }
    }
}

4.2.3 Seal封裝新塊爲最終狀態

這裏就是調用dpos引擎的Seal規則了,即給區塊簽名,參見3.7節

func (self *worker) mintBlock(now int64) {
    ……
    result, err := self.engine.Seal(self.chain, work.Block, self.quitCh)
    ……
}

這個result和work對象都被髮送到self.recv通道中去了。

func (self *worker) mintBlock(now int64) {
    ……
    self.recv <- &Result{work, result}
    ……
}

4.3 新塊入庫、廣播

work.wait()在geth運行的時候就監聽了work.recv通道,它作了以下幾件事:

func (self *worker) wait() {
    for {
        for result := range self.recv {
            atomic.AddInt32(&self.atWork, -1)

            if result == nil || result.Block == nil {
                continue
            }
            block := result.Block
            work := result.Work

            // Update the block hash in all logs since it is now available and not when the
            // receipt/log of individual transactions were created.
            for _, r := range work.receipts {
                for _, l := range r.Logs {
                    l.BlockHash = block.Hash()
                }
            }
            for _, log := range work.state.Logs() {
                log.BlockHash = block.Hash()
            }
            stat, err := self.chain.WriteBlockAndState(block, work.receipts, work.state)
            if err != nil {
                log.Error("Failed writing block to chain", "err", err)
                continue
            }
            // check if canon block and write transactions
            if stat == core.CanonStatTy {
                // implicit by posting ChainHeadEvent
            }
            // Broadcast the block and announce chain insertion event
            self.mux.Post(core.NewMinedBlockEvent{Block: block})
            var (
                events []interface{}
                logs   = work.state.Logs()
            )
            events = append(events, core.ChainEvent{Block: block, Hash: block.Hash(), Logs: logs})
            if stat == core.CanonStatTy {
                events = append(events, core.ChainHeadEvent{Block: block})
            }
            self.chain.PostChainEvents(events, logs)

            // Insert the block into the set of pending ones to wait for confirmations
            self.unconfirmed.Insert(block.NumberU64(), block.Hash())
            log.Info("Successfully sealed new block", "number", block.Number(), "hash", block.Hash())
        }
    }
}

1)寫入本節點數據庫(WriteBlockAndState)

當通道里接收到新區塊後,wait就調用chain.WriteBlockAndState()裏的WriteBlock()把區塊寫入數據庫,區塊的body部分和header部分獨立存儲在db中,body的key是「b」+blockNumber+blockHash,值是交易集、叔塊集的rlp。header的key是「h」+blockNumber。

2)Post NewMinedBlockEvent

3)PostChainEvents

4)將區塊插入unconfirmedBlocks集合

4.3.1 事件訂閱發送機制

Subscribe函數實現1個訂閱者訂閱多個事件。

4.4 新塊上鍊

後續再貼上來……

相關文章
相關標籤/搜索