摘要:的主要功能就是成為候選人投票對方獲得投票,以及系統定期自動執行的選舉。它是更大范圍上的存在,不直接操作的五棵樹,而是通過它聚合的對五棵樹進行增刪改查。在共識中,返回的是。
1 導語
區塊鏈的主要工作就是出塊,出塊的制度、方式叫做共識;
塊里的內容是不可篡改的信息記錄,塊連接成鏈就是區塊鏈。
出塊又叫挖礦,有各種挖礦的方式,比如POW、DPOS,本文主要分析DPOS共識源碼。
以太坊存在多種共識:
PoW (etash)在主網使用
PoA(clique) 在測試網使用
FakePow 在單元測試使用
DPOS 新增共識替代POW
既然是源碼分析,主要讀者群體應該是看代碼的人,讀者須要結合代碼看此類文章。明白此類文章的作用是:提供一個分析的切入口,將散落的代碼按某種內在邏輯串起來,用圖文的形式敘述代碼的大意,引領讀者有一個系統化的認知,同時對自己閱讀代碼過程中不理解的地方起到一定參考作用。
2 DPOS的共識邏輯DPOS的基本邏輯可以概述為:成為候選人-獲得他人投票-被選舉為驗證人-在周期內輪流出塊。
從這個過程可以看到,成為候選人和投票是用戶主動發起的行為,獲得投票和被選為驗證人是系統行為。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)。
從外部用戶角度看,成為候選人只需要自己發一筆交易即可:
eth.sendTransaction({ from: "0x646ba1fa42eb940aac67103a71e9a908ef484ec3", to: "0x646ba1fa42eb940aac67103a71e9a908ef484ec3", value: 0, type: 1 })
在系統內部,成為候選人和投票均被定義為交易,其實DPOS定義的所有交易有四種類型,是針對這兩種行為的正向和反向操作。
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: "0x5b76fff970bf8a351c1c9ebfb5e5a9493e956ffffd", 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相關類圖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 Authorfunc (d *Dpos) Author(header *types.Header) (common.Address, error) { return header.Validator, nil }
這個接口的意思是返回出塊人。在POW共識中,返回的是header.Coinbase。
DPOS中Header增加了一個Validator,是有意將Coinbase和Validator的概念分開。Validator默認等于Coinbase,也可以設為不一樣的地址。
驗證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 VerifyUnclesdpos里不應有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 }
3.6 Finalize關于難度
在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() }
生成一個新的區塊,不過不是最終的區塊。該函數功能請看注釋。
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
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服務:
showImg("https://segmentfault.com/img/remote/1460000017505461?w=1130&h=1030");
在mintLoop方法里,worker無限循環,阻塞監聽stopper通道,每秒調用一次mintBlock。
用戶主動停止以太坊節點的時候,stopper通道被關閉,worker就停止了。
這個函數的作用即用引擎(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()。
這個函數涉及具體執行交易、生成收據和日志、向監聽者發送相關事件、調用dpos引擎Finalize打包、將未Seal的新塊加入未確認塊集等事項。
4.2.2.1 挖礦時序圖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執行環境,這個環境如下:
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,這里是指如果沒有指定coinbase就從header里獲取validator的地址作為coinbase。==
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。
除了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("
文章版權歸作者所有,未經允許請勿轉載,若此文章存在違規行為,您可以聯系管理員刪除。
轉載請注明本文地址:http://specialneedsforspecialkids.com/yun/24502.html
摘要:區塊的產生是由個輪流出塊,產生的區塊需要以上的確認才能夠被區塊鏈認可。手續費資源在中使用區塊鏈上的資源需要消耗,消耗的作為區塊打包的費用支付給礦工。是區塊鏈的通用庫,具有以下功能使用提供的包管理。是一個區塊鏈數據服務框架,基于框架實現。 共識機制 Ethereum 使用的是 PoW 共識機制,未來幾年里將會換成 PoS 共識機制。Ethereum 區塊是由礦工計算哈希產生,在 PoW ...
摘要:因為年跳槽到一家保險科技公司,因此,對于區塊鏈早期落地非炒幣有了一定的認識和理解。首先,從技術角度來看,區塊鏈主要涵蓋了以下幾個技術點共識算法網絡加密安全技術現在對以上三個技術點做分別的介紹。 因為2016年跳槽到一家保險科技公司,因此,對于區塊鏈早期落地(非炒幣)有了一定的認識和理解。但真正開始思考區塊鏈的問題,應該是從2018年初開始的,因為,經典互聯網技術在我過去一年的產品化商業...
摘要:說明的視頻片段分發現在沒做出什么成果作者還提了一句,協議有望成為直播內容的傳播協議。仿佛也沒能掩飾住不知道怎么分發視頻片段的尷尬說了這么多,看了代碼發現視頻片段還是通過分發總結最終將建立一個可擴展的,即用即付的直播網絡 Background Livepeer旨在構建帶有激勵機制的視頻直播分布式網絡 Blockchain 以太坊 智能合約和交易基于Ethereum以太坊網絡 DP...
閱讀 3457·2021-11-17 17:00
閱讀 3818·2021-08-09 13:46
閱讀 2866·2019-08-30 15:54
閱讀 627·2019-08-30 13:54
閱讀 2945·2019-08-29 17:13
閱讀 3218·2019-08-29 14:00
閱讀 2975·2019-08-29 11:11
閱讀 1379·2019-08-26 10:15