溫馨提示×

溫馨提示×

您好,登錄后才能下訂單哦!

密碼登錄×
登錄注冊×
其他方式登錄
點擊 登錄注冊 即表示同意《億速云用戶服務(wù)條款》

以太坊DPOS共識機(jī)制源碼分析

發(fā)布時間:2022-01-18 10:31:35 來源:億速云 閱讀:214 作者:iii 欄目:互聯(lián)網(wǎng)科技

本文小編為大家詳細(xì)介紹“以太坊DPOS共識機(jī)制源碼分析”,內(nèi)容詳細(xì),步驟清晰,細(xì)節(jié)處理妥當(dāng),希望這篇“以太坊DPOS共識機(jī)制源碼分析”文章能幫助大家解決疑惑,下面跟著小編的思路慢慢深入,一起來學(xué)習(xí)新知識吧。

一、前言:

任何共識機(jī)制都必須回答包括但不限于如下的問題:

  1. 下一個添加到數(shù)據(jù)庫的新區(qū)塊應(yīng)該由誰來生成?

  2. 下一個塊應(yīng)該何時產(chǎn)生?

  3. 該區(qū)塊應(yīng)包含哪些交易?

  4. 怎樣對本協(xié)議進(jìn)行修改?

  5. 該如何解決交易歷史的競爭問題?

二、功能描述:

每一個持有比特股的人按照持股比例進(jìn)行對候選受托人的投票;從中選取投票數(shù)最多的前21位代表(也可以是其他數(shù)字,具體由區(qū)塊鏈項目方?jīng)Q定) 成為權(quán)力完全相等的21個超級節(jié)點(真正:受托人/見證人);通過每隔3秒輪詢方式產(chǎn)出區(qū)塊;而其他候選受托人無權(quán)產(chǎn)出區(qū)塊;

1、持股人:比特股持有所有人;每個賬戶按照持幣數(shù)給證人投票;可以隨時更換投票;也可以不投;但投只能投贊成票;

2、見證人(受托人/代表,類似比特幣的礦工):

注冊成為候選受托人需要支付一筆保證金,這筆保證金就是為了防止節(jié)點出現(xiàn)作惡的情況,一般來說,如果成為受托人,也就成為超級節(jié)點進(jìn)行挖礦,超級節(jié)點需要大概兩周的時間才能達(dá)到損益平衡,這就促使超級節(jié)點至少挖滿兩周不作惡。

3、選定代表(實現(xiàn)步驟中未考慮實現(xiàn)它)

 代表也是通過類似選舉證人的方式選舉出來。 創(chuàng)始賬戶(the genesis account)有權(quán)對網(wǎng)絡(luò)參數(shù)提出修改,而代表就是該特殊賬戶的共同簽署者。這些參數(shù)包括交易費(fèi)用,區(qū)塊大小,證人工資和區(qū)塊間隔等。 在大多數(shù)代表批準(zhǔn)了提議的變更后,股東有2周的復(fù)審期(review period),在此期間他們可以投票踢出代表并作廢被提議的變更。

4.出塊規(guī)則:每隔3秒輪詢受托人;并且每個證人會輪流地在固定的預(yù)先計劃好的2秒內(nèi)生產(chǎn)一個區(qū)塊。 當(dāng)所有證人輪完之后,將被洗牌。 如果某個證人沒有在他自己的時間段內(nèi)生產(chǎn)一個區(qū)塊,那么該時間段將被跳過,下一個證人繼續(xù)產(chǎn)生下一個區(qū)塊。每當(dāng)證人生產(chǎn)一個區(qū)塊時,他們都會獲取相應(yīng)的服務(wù)費(fèi)。 證人的薪酬水平由股東選出的代表(delegate)來制定。 如果某個證人沒有生產(chǎn)出區(qū)塊,那么就不會給他支付薪酬,并可能在未來被投票踢出。

5.算法主要包含兩個核心部分:塊驗證人選舉和塊驗證人調(diào)度

(1)第一批塊驗證人由創(chuàng)世塊指定,后續(xù)每個周期(周期由具體實現(xiàn)定義)都會在周期開始的第一個塊重新選舉。驗證人選舉過程如下:

  1. 踢掉上個周期出塊不足的驗證人

  2. 統(tǒng)計截止選舉塊(每個周期的第一塊)產(chǎn)生時候選人的票數(shù),選出票數(shù)最高的前 N 個作為驗證人

  3. 隨機(jī)打亂驗證人出塊順序,驗證人根據(jù)隨機(jī)后的結(jié)果順序出塊

(2)驗證人調(diào)度根據(jù)選舉結(jié)果進(jìn)行出塊,其他節(jié)點根據(jù)選舉結(jié)果驗證出塊順序和選舉結(jié)果是否一致,不一致則認(rèn)為此塊不合法,直接丟棄。

三、以太坊DPOS共識機(jī)制源碼分析

以太坊DPOS共識機(jī)制源碼分析

1、啟動入口:

以太坊入口調(diào)試腳本:

以太坊DPOS共識機(jī)制源碼分析

以太坊項目的啟動:main.go中的init()函數(shù)-->調(diào)用geth方法-->調(diào)用startNode-->backend.go中的函數(shù)StartMining-->miner.go中的start函數(shù)

func init() {
   // Initialize the CLI app and start Geth
   app.Action = geth
}
func geth(ctx *cli.Context) error {
   //根據(jù)上下文配置信息獲取全量節(jié)點并將該節(jié)點注冊到以太坊服務(wù)
   //makeFullNode函數(shù)-->flags.go中函數(shù)RegisterEthService中-->eth.New-->handler.go中NewProtocolManager-->InsertChain內(nèi)進(jìn)行dpos的區(qū)塊信息校驗
   node := makeFullNode(ctx)
   //啟動節(jié)點
   startNode(ctx, node)
   node.Wait()
   return nil
}

啟動節(jié)點說明

func startNode(ctx *cli.Context, stack *node.Node) {
   //啟動當(dāng)前節(jié)點:utils.StartNode(stack)
  //解鎖注冊錢包事件并自動派生錢包
  //監(jiān)聽錢包事件
   if ctx.GlobalBool(utils.MiningEnabledFlag.Name) {
      //判斷是否全量節(jié)點,只有全量節(jié)點才有挖礦權(quán)利:
      var ethereum *eth.Ethereum
      if err := stack.Service(&ethereum); err != nil {
         utils.Fatalf("ethereum service not running: %v", err)
      }
      //設(shè)置gas價格
      ethereum.TxPool().SetGasPrice(utils.GlobalBig(ctx, utils.GasPriceFlag.Name))
      //驗證是否當(dāng)前出塊受托人:validator, err := s.Validator() ,啟動服務(wù)打包區(qū)塊
      if err := ethereum.StartMining(true); err != nil {
         utils.Fatalf("Failed to start mining: %v", err)
      }
   }
}

上面StartMining方法會調(diào)用miner.go中的start函數(shù),調(diào)用啟動函數(shù)之前已經(jīng)啟動全量節(jié)點,并進(jìn)行相關(guān)初始化工作(具體初始化內(nèi)容如下);

func (self *Miner) Start(coinbase common.Address) {
   atomic.StoreInt32(&self.shouldStart, 1)
   self.worker.setCoinbase(coinbase)
   self.coinbase = coinbase

   if atomic.LoadInt32(&self.canStart) == 0 {
      log.Info("Network syncing, will start miner afterwards")
      return
   }
   atomic.StoreInt32(&self.mining, 1)

   log.Info("Starting mining operation")
   //獲取當(dāng)前節(jié)點地址,啟動服務(wù)
   self.worker.start()
}

worker.go的start函數(shù)調(diào)用mintLoop函數(shù)

func (self *worker) mintLoop() {
   ticker := time.NewTicker(time.Second).C
   //for循環(huán)不斷監(jiān)聽self信號,當(dāng)監(jiān)測到self停止時,則調(diào)用關(guān)閉操作代碼,并直接挑出循環(huán)監(jiān)聽,函數(shù)退出。
   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
      }
   }
}

2.相關(guān)角色說明

dpos_context.go

type DposContext struct {
   epochTrie     *trie.Trie  //記錄每個周期的驗證人列表
   delegateTrie  *trie.Trie  //記錄驗證人以及對應(yīng)投票人的列表
   voteTrie      *trie.Trie //記錄投票人對應(yīng)驗證人
   candidateTrie *trie.Trie //記錄候選人列表
   mintCntTrie   *trie.Trie //記錄驗證人在周期內(nèi)的出塊數(shù)目
   db ethdb.Database 
}

以太坊MPT(Trie樹, Patricia Trie, 和Merkle樹)樹形結(jié)構(gòu)存儲,并定期同步[k,v]型底層數(shù)據(jù)庫是LevelDB數(shù)據(jù)庫

3.相關(guān)交易類型說明

以太坊DPOS共識算法中,將"成為候選人"、"退出候選人"、"投票(授權(quán))"、"取消投票(取消授權(quán))"等操作均定義為以太坊的一種交易類型

transaction.go

const (//交易類型
   Binary TxType = iota  //之前的交易主要是轉(zhuǎn)賬或者合約調(diào)用
   LoginCandidate  //成為候選人
   LogoutCandidate  //退出候選人
   Delegate   //投票(授權(quán))
   UnDelegate  //取消投票(取消授權(quán))
)

在一個新塊打包時會執(zhí)行所有塊內(nèi)的交易,如果發(fā)現(xiàn)交易類型不是之前的轉(zhuǎn)賬或者合約調(diào)用類型,那么會調(diào)用 applyDposMessage 進(jìn)行處理

在worker.go的createNewWork()-->commitTransactions函數(shù)-->commitTransaction函數(shù)-->調(diào)用state_processor.go中的ApplyTransaction函數(shù)-->applyDposMessage

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
   }

   // 創(chuàng)建EVM環(huán)境的上下文
   context := NewEVMContext(msg, header, bc, author)
   // Create a new environment which holds all relevant information
   // 創(chuàng)建EVM虛擬機(jī)處理交易及智能合約
   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 {
      //如果是非轉(zhuǎn)賬或者合約調(diào)用類型交易
      if err = applyDposMessage(dposContext, msg); err != nil {
         return nil, nil, err
      }
   }
}
func applyDposMessage(dposContext *types.DposContext, msg types.Message) error {
   switch msg.Type() {
   case types.LoginCandidate://成為候選人
      dposContext.BecomeCandidate(msg.From())
   case types.LogoutCandidate://取消候選人
      dposContext.KickoutCandidate(msg.From())
   case types.Delegate://投票
      //投票之前需要先檢查該賬號是否候選人;如果投票人之前已經(jīng)給其他人投過票則先取消之前投票,再進(jìn)行投票
      dposContext.Delegate(msg.From(), *(msg.To()))
   case types.UnDelegate://取消投票
      dposContext.UnDelegate(msg.From(), *(msg.To()))
   default:
      return types.ErrInvalidType
   }
   return nil
}

4.打包出塊過程

以太坊DPOS共識機(jī)制源碼分析

worker.go  

func (self *worker) mintBlock(now int64) {
   engine, ok := self.engine.(*dpos.Dpos)
   if !ok {
      log.Error("Only the dpos engine was allowed")
      return
   }
   //礦工會定時(每隔3秒)檢查當(dāng)前的 validator 是否為當(dāng)前節(jié)點,如果是則說明輪詢到自己出塊了;
   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
   }
   //創(chuàng)建一個新的打塊任務(wù)
   work, err := self.createNewWork()
   if err != nil {
      log.Error("Failed to create the new work", "err", err)
      return
   }
   //Seal 會對新塊進(jìn)行簽名
   result, err := self.engine.Seal(self.chain, work.Block, self.quitCh)
   if err != nil {
      log.Error("Failed to seal the block", "err", err)
      return
   }
   //將新塊廣播到鄰近的節(jié)點,其他節(jié)點接收到新塊會根據(jù)塊的簽名以及選舉結(jié)果來看新塊是否應(yīng)該由該驗證人來出塊
   self.recv <- &Result{work, result}
}
func (self *worker) createNewWork() (*Work, error) {
   //......

   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),
   }
   // 僅在挖掘時設(shè)置coinbase(避免偽塊獎勵)
   if atomic.LoadInt32(&self.mining) == 1 {
      header.Coinbase = self.coinbase
   }
   //初始化塊頭基礎(chǔ)信息
   if err := self.engine.Prepare(self.chain, header); err != nil {
      return nil, fmt.Errorf("got error when preparing header, err: %s", err)
   }
   
   //主要是從 transaction pool 按照 gas price 將交易打包到塊中
   txs := types.NewTransactionsByPriceAndNonce(self.current.signer, pending)
   work.commitTransactions(self.mux, txs, self.chain, self.coinbase)

   // 打包區(qū)塊
   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)
   }
   // 將 prepare 和 CommitNewWork 內(nèi)容打包成新塊,同時里面還有包含出塊獎勵、選舉、更新打塊計數(shù)等功能
   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
   return work, nil
}

疑問:

(1).這里面沒看到跟pow一樣的工作量難度證明的哈希函數(shù)計算,即當(dāng)有出塊權(quán)益時,打包驗證好交易后是否直接打包,那如何會出現(xiàn)規(guī)定時間打包失敗的情況呢?是否是只有類似斷網(wǎng)或網(wǎng)絡(luò)不好時會出現(xiàn)?

(2).Seal會對新塊進(jìn)行封裝簽名;在pow算法中seal是核心是計算工作量得出隨機(jī)符合條件hash,而在dpos共識中seal是否只做了封裝簽名操作?從源碼中看是這樣

5.選舉分析

(1)選舉實現(xiàn)步驟:

  1. 根據(jù)上個周期出塊的情況把一些被選上但出塊數(shù)達(dá)不到要求的候選人踢掉

  2. 截止到上一塊為止,選出票數(shù)最高的前 N 個候選人作為驗證人

  3. 打亂驗證人順序

當(dāng)調(diào)用dpos.go中Finalize函數(shù)打包新塊時

func (d *Dpos) Finalize(chain consensus.ChainReader, header *types.Header, state *state.StateDB, txs []*types.Transaction,
   uncles []*types.Header, receipts []*types.Receipt, dposContext *types.DposContext) (*types.Block, error) {
   // 累積塊獎勵并提交最終狀態(tài)根
   AccumulateRewards(chain.Config(), state, header, uncles)
   header.Root = state.IntermediateRoot(chain.Config().IsEIP158(header.Number))

   parent := chain.GetHeaderByHash(header.ParentHash)
   epochContext := &EpochContext{
      statedb:     state,
      DposContext: dposContext,
      TimeStamp:   header.Time.Int64(),
   }
   if timeOfFirstBlock == 0 {
      if firstBlockHeader := chain.GetHeaderByNumber(1); firstBlockHeader != nil {
         timeOfFirstBlock = firstBlockHeader.Time.Int64()
      }
   }
   genesis := chain.GetHeaderByNumber(0)
   //打包每個塊之前調(diào)用 tryElect 來看看當(dāng)前塊是否是新周期的第一塊,如果是第一塊則需要觸發(fā)選舉。
   err := epochContext.tryElect(genesis, parent)
   if err != nil {
      return nil, fmt.Errorf("got error when elect next epoch, err: %s", err)
   }

   //更新驗證人在周期內(nèi)的出塊數(shù)目
   updateMintCnt(parent.Time.Int64(), header.Time.Int64(), header.Validator, dposContext)
   header.DposContext = dposContext.ToProto()
   return types.NewBlock(header, txs, uncles, receipts), nil
}

epoch_context.go中的tryElect選舉函數(shù)

func (ec *EpochContext) tryElect(genesis, parent *types.Header) error {
   genesisEpoch := genesis.Time.Int64() / epochInterval
   prevEpoch := parent.Time.Int64() / epochInterval
   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))
   //根據(jù)當(dāng)前塊和上一塊的時間計算當(dāng)前塊和上一塊是否屬于同一周期,如果是同一周期,意味著當(dāng)前塊不是周期第一塊,不需要觸發(fā)選舉;如果不是同一周期,說明當(dāng)前塊是該周期的第一塊,則觸發(fā)選舉
   for i := prevEpoch; i < currentEpoch; i++ {
      // 如果前一個周期不是創(chuàng)世周期,觸發(fā)踢出候選人規(guī)則;
      if !prevEpochIsGenesis && iter.Next() {
         //踢出規(guī)則主要看上一周期是否存在候選人出塊少于特定閾值(50%),如果存在則踢出:if cnt < epochDuration/blockInterval/ maxValidatorSize /2 {
         if err := ec.kickoutValidator(prevEpoch); err != nil {
            return err
         }
      }
      //對候選人進(jìn)行計票
      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")
      }
      //將候選人按照票數(shù)由高到低排序
      sort.Sort(candidates)
      if len(candidates) > maxValidatorSize {//如果候選人大于預(yù)定受托人數(shù)量常量maxValidatorSize,則選出前maxValidatorSize個為受托人
         candidates = candidates[:maxValidatorSize]
      }

      // 重排受托人,由于使用seed是由父塊的hash以及當(dāng)前周期編號組成,所以每個節(jié)點計算出來的受托人列表也會一致;
      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
}

(2)計票實現(xiàn)

  1. 先找出候選人對應(yīng)投票人的列表

  2. 所有投票人的余額作為票數(shù)累積到候選人的總票數(shù)中

計票實現(xiàn)函數(shù)是epoch_context.go中的tryElect選舉函數(shù)中的countVotes函數(shù)

func (ec *EpochContext) countVotes() (votes map[common.Address]*big.Int, err error) {
   votes = map[common.Address]*big.Int{}
   delegateTrie := ec.DposContext.DelegateTrie()//記錄驗證人以及對應(yīng)投票人的列表
   candidateTrie := ec.DposContext.CandidateTrie()//獲取候選人列表
   statedb := ec.statedb

   iterCandidate := trie.NewIterator(candidateTrie.NodeIterator(nil))
   existCandidate := iterCandidate.Next()
   if !existCandidate {
      return votes, errors.New("no candidates")
   }
   //遍歷候選人列表
   for existCandidate {
      candidate := iterCandidate.Value
      candidateAddr := common.BytesToAddress(candidate)
      delegateIterator := trie.NewIterator(delegateTrie.PrefixIterator(candidate))
      existDelegator := delegateIterator.Next()
      if !existDelegator {
         votes[candidateAddr] = new(big.Int)
         existCandidate = iterCandidate.Next()
         continue
      }
      //遍歷后續(xù)人對應(yīng)的投票人列表
      for existDelegator {
         delegator := delegateIterator.Value
         score, ok := votes[candidateAddr]
         if !ok {
            score = new(big.Int)
         }
         delegatorAddr := common.BytesToAddress(delegator)
         //獲取投票人的余額作為票數(shù)累積到候選人的票數(shù)中
         weight := statedb.GetBalance(delegatorAddr)
         score.Add(score, weight)
         votes[candidateAddr] = score
         existDelegator = delegateIterator.Next()
      }
      existCandidate = iterCandidate.Next()
   }
   return votes, nil
}

讀到這里,這篇“以太坊DPOS共識機(jī)制源碼分析”文章已經(jīng)介紹完畢,想要掌握這篇文章的知識點還需要大家自己動手實踐使用過才能領(lǐng)會,如果想了解更多相關(guān)內(nèi)容的文章,歡迎關(guān)注億速云行業(yè)資訊頻道。

向AI問一下細(xì)節(jié)

免責(zé)聲明:本站發(fā)布的內(nèi)容(圖片、視頻和文字)以原創(chuàng)、轉(zhuǎn)載和分享為主,文章觀點不代表本網(wǎng)站立場,如果涉及侵權(quán)請聯(lián)系站長郵箱:is@yisu.com進(jìn)行舉報,并提供相關(guān)證據(jù),一經(jīng)查實,將立刻刪除涉嫌侵權(quán)內(nèi)容。

AI