在這篇文章中,作者將從Vapor節點的建立開始,進而擴充講解Vapor節點出塊過程中所涉及的原始碼。
做為Vapor原始碼解析系列的第一篇,本文首先對Vapor稍加介紹。Vapor是目前國內主流公鏈Bytom的高效能側鏈,是從Bytom主鏈中發展出來的一條獨立的高效能側鏈。Vapor是平臺最重要的區塊鏈基礎設施之一,目前採用DPoS的共識演算法,具有高效能、高安全、可擴充套件等特點,用於搭建規模化的商業應用。
Vapor節點建立及出塊模組的啟動
Vapor入口函式:
func main() {
cmd := cli.PrepareBaseCmd(commands.RootCmd, "TM", os.ExpandEnv(config.DefaultDataDir()))
cmd.Execute()
}
傳入引數node後會呼叫runNode函式並新建一個節點。
vapor/cmd/vapord/commands/run_node.go
func runNode(cmd *cobra.Command, args []string) error {
startTime := time.Now()
setLogLevel(config.LogLevel)
// Create & start node
n := node.NewNode(config)
……
}
vapor節點的結構:
type Node struct {
cmn.BaseService
config *cfg.Config
eventDispatcher *event.Dispatcher
syncManager *netsync.SyncManager
wallet *w.Wallet
accessTokens *accesstoken.CredentialStore
notificationMgr *websocket.WSNotificationManager
api *api.API
chain *protocol.Chain
blockProposer *blockproposer.BlockProposer
miningEnable bool
}
其中與出塊和共識相關的是blockProposer
欄位
新建節點的部分原始碼
func NewNode(config *cfg.Config) *Node {
//……
node := &Node{
eventDispatcher: dispatcher,
config: config,
syncManager: syncManager,
accessTokens: accessTokens,
wallet: wallet,
chain: chain,
miningEnable: config.Mining,
notificationMgr: notificationMgr,
}
node.blockProposer = blockproposer.NewBlockProposer(chain, accounts, txPool, dispatcher)
node.BaseService = *cmn.NewBaseService(nil, "Node", node)
return node
}
從這可以看到node.blockProposer本質上是一個vapor的block生成器,實際控制node啟動出塊的模組是vapor/proposal/blockproposer/blockproposer.go中的:
func (b *BlockProposer) Start() {
b.Lock()
defer b.Unlock()
// Nothing to do if the miner is already running
if b.started {
return
}
b.quit = make(chan struct{})
go b.generateBlocks() //出塊功能的關鍵模組
b.started = true
log.Infof("block proposer started")
}
出塊模組可以通過api啟動
func (a *API) startMining() Response {
a.blockProposer.Start()
if !a.IsMining() {
return NewErrorResponse(errors.New("Failed to start mining"))
}
return NewSuccessResponse("")
}
以上講解的是節點建立和出塊模組啟動所涉及的原始碼。
從generateBlocks()
函式開始,將要講解是Vapor出塊過程的具體原始碼。
Vapor的出塊機制
Vapor採用的是DPoS的共識機制進行出塊。DPoS是由被社群選舉的可信帳戶(受託人,得票數排行前10位)來建立區塊。為了成為正式受託人,使用者要去社群拉票,獲得足夠多使用者的信任。使用者根據自己持有的加密貨幣數量佔總量的百分比來投票。DPoS機制類似於股份制公司,普通股民進不了董事會,要投票選舉代表(受託人)代他們做決策。在講解Vapor的出塊流程之前,要先了解Vapor在DPoS的引數設定。
DPoS的引數資訊位於 vapor/consensus/general.go
type DPOSConfig struct {
NumOfConsensusNode int64
BlockNumEachNode uint64
RoundVoteBlockNums uint64
MinConsensusNodeVoteNum uint64
MinVoteOutputAmount uint64
BlockTimeInterval uint64
MaxTimeOffsetMs uint64
}
接下來對引數進行具體解釋
- NumOfConsensusNode是DPOS中共識節點的數量,Vapor中設定為10,通過投票選出十個負責出塊的共識節點。
- BlockNumEachNode是每個共識節點連續出塊的數量,Vapor中設定為12。
- RoundVoteBlockNums為每輪投票的出塊數,Vapor中設定為1200,也就是說每輪投票產生的共識節點會負責出塊1200個。
- MinConsensusNodeVoteNum是成為共識節點要求的最小BTM數量(單位為neu,一億分之一BTM),Vapor中設定為100000000000000,也就是說一個節點想成為共識節點,賬戶中至少需要存有100萬BTM。
- MinVoteOutputAmoun為節點進行投票所要求的最小BTM 數量(單位為neu),Vapor中設定為100000000,節點想要參與投票,賬戶中需要1BTM
- BlockTimeInterval為最短出塊時間間隔,Vapor每間隔0.5秒出一個塊。
- MaxTimeOffsetMs為塊時間允許比當前時間提前的最大秒數,在Vapor中設定為2秒。
講完DPoS的引數設定後,就可以看看Vapor上出塊的核心程式碼 generateBlocks
vapor/proposal/blockproposer/blockproposer.go
func (b *BlockProposer) generateBlocks() {
xpub := config.CommonConfig.PrivateKey().XPub()
xpubStr := hex.EncodeToString(xpub[:])
ticker := time.NewTicker(time.Duration(consensus.ActiveNetParams.BlockTimeInterval) * time.Millisecond)
defer ticker.Stop()
for {
select {
case <-b.quit:
return
case <-ticker.C:
}
//1
bestBlockHeader := b.chain.BestBlockHeader()
bestBlockHash := bestBlockHeader.Hash()
now := uint64(time.Now().UnixNano() / 1e6)
base := now
if now < bestBlockHeader.Timestamp {
base = bestBlockHeader.Timestamp
}
minTimeToNextBlock := consensus.ActiveNetParams.BlockTimeInterval - base%consensus.ActiveNetParams.BlockTimeInterval
nextBlockTime := base + minTimeToNextBlock
if (nextBlockTime - now) < consensus.ActiveNetParams.BlockTimeInterval/10 {
nextBlockTime += consensus.ActiveNetParams.BlockTimeInterval
}
//2
blocker, err := b.chain.GetBlocker(&bestBlockHash, nextBlockTime)
……
if xpubStr != blocker {
continue
}
//3
warnDuration := time.Duration(consensus.ActiveNetParams.BlockTimeInterval*warnTimeNum/warnTimeDenom) * time.Millisecond
criticalDuration := time.Duration(consensus.ActiveNetParams.BlockTimeInterval*criticalTimeNum/criticalTimeDenom) * time.Millisecond
block, err := proposal.NewBlockTemplate(b.chain, b.accountManager, nextBlockTime, warnDuration, criticalDuration)
……
//4
isOrphan, err := b.chain.ProcessBlock(block)
……
//5
log.WithFields(log.Fields{"module": logModule, "height": block.BlockHeader.Height, "isOrphan": isOrphan, "tx": len(block.Transactions)}).Info("proposer processed block")
if err = b.eventDispatcher.Post(event.NewProposedBlockEvent{Block: *block}); err != nil {
log.WithFields(log.Fields{"module": logModule, "height": block.BlockHeader.Height, "error": err}).Error("proposer fail on post block")
}
}
}
程式碼經過精簡,省略了一些無關緊要的部分,並將重要的部分,分為5個模組。
- 計算並調整出塊的時間
- 通過
GetBlocker
獲取順序下一個block的公鑰,並與當前塊比對,判斷當前塊的出塊順序是否合法。 - 通過
b.chain.ProcessBlock
根據模板生成了一個block。 - 通過
chain.ProcessBlock(block)
嘗試把block加工處理後加到本機持有的區塊鏈上。 - 使用logrus框架記錄新的塊,並像網路中廣播。
b.chain.GetBlocker
針對generateBlocks()
中幾個重要的模組進行拆分講解。
vapor/protocol/consensus_node_manager.go
GetBlocker()傳入當前高度塊的雜湊和下一個塊的出塊時間。
// 返回一個特定時間戳的Blocker
func (c *Chain) GetBlocker(prevBlockHash *bc.Hash, timeStamp uint64) (string, error) {
consensusNodeMap, err := c.getConsensusNodes(prevBlockHash)
//……
prevVoteRoundLastBlock, err := c.getPrevRoundLastBlock(prevBlockHash)
//……
startTimestamp := prevVoteRoundLastBlock.Timestamp + consensus.ActiveNetParams.BlockTimeInterval
//獲取order,xpub為公鑰
order := getBlockerOrder(startTimestamp, timeStamp, uint64(len(consensusNodeMap)))
for xPub, consensusNode := range consensusNodeMap {
if consensusNode.Order == order {
return xPub, nil
}
}
//……
}
- 通過呼叫
c.getConsensusNodes()
獲得一個儲存共識節點的Map。 - 獲取上一輪投票的最後一個塊,在加上最短出塊時間間隔,計算得到這一輪的開始時間戳。
- 呼叫
getBlockerOrder
,通過開始時間戳和當前要出塊的時間戳計算出這個時間點出塊的order。 - 最後比對
consensusNodeMap
中consensusNode.Order
,並返回公鑰。
這個模組是為了找出當前時間戳對應出塊的共識節點,並返回節點的公鑰。因為DPoS中出塊的節點和順序必須是固定的,而使用generateBlocks()
模組嘗試出塊的共識節點不一定是當前時間的合法出塊節點,因此需要本模組通過對比公鑰進行節點資格的驗證。
proposal.NewBlockTemplate
func NewBlockTemplate(chain *protocol.Chain, accountManager *account.Manager, timestamp uint64, warnDuration, criticalDuration time.Duration) (*types.Block, error) {
builder := newBlockBuilder(chain, accountManager, timestamp, warnDuration, criticalDuration)
return builder.build()
}
func newBlockBuilder(chain *protocol.Chain, accountManager *account.Manager, timestamp uint64, warnDuration, criticalDuration time.Duration) *blockBuilder {
preBlockHeader := chain.BestBlockHeader()
block := &types.Block{
BlockHeader: types.BlockHeader{
Version: 1,
Height: preBlockHeader.Height + 1,
PreviousBlockHash: preBlockHeader.Hash(),
Timestamp: timestamp,
BlockCommitment: types.BlockCommitment{},
BlockWitness: types.BlockWitness{Witness: make([][]byte, consensus.ActiveNetParams.NumOfConsensusNode)},
},
}
builder := &blockBuilder{
chain: chain,
accountManager: accountManager,
block: block,
txStatus: bc.NewTransactionStatus(),
utxoView: state.NewUtxoViewpoint(),
warnTimeoutCh: time.After(warnDuration),
criticalTimeoutCh: time.After(criticalDuration),
gasLeft: int64(consensus.ActiveNetParams.MaxBlockGas),
timeoutStatus: timeoutOk,
}
return builder
}
在Vapor上每個區塊有區塊頭和區塊的主體,區塊頭中包含版本號、高度、上一區塊的hash、時間戳等等,主體包括區塊鏈的引用模組、賬戶管理器、區塊頭、Transaction狀態(版本號和驗證狀態)、utxo檢視等。這一部分的目的是將,區塊的各種資訊通過模板包裝成一個block交給後面的ProcessBlock(block)
加工處理。
b.chain.ProcessBlock
func (c *Chain) ProcessBlock(block *types.Block) (bool, error) {
reply := make(chan processBlockResponse, 1)
c.processBlockCh <- &processBlockMsg{block: block, reply: reply}
response := <-reply
return response.isOrphan, response.err
}
func (c *Chain) blockProcesser() {
for msg := range c.processBlockCh {
isOrphan, err := c.processBlock(msg.block)
msg.reply <- processBlockResponse{isOrphan: isOrphan, err: err}
}
}
很顯然,這只是鏈更新的入口,block資料通過processBlockMsg
結構傳入了c.processBlockCh
這個管道。隨後資料通過blockProcesser()
處理後存入了msg.reply
管道,而最後處理這個block的是processBlock()
函式:
func (c *Chain) processBlock(block *types.Block) (bool, error) {
//1
blockHash := block.Hash()
if c.BlockExist(&blockHash) {
log.WithFields(log.Fields{"module": logModule, "hash": blockHash.String(), "height": block.Height}).Debug("block has been processed")
return c.orphanManage.BlockExist(&blockHash), nil
}
//2
c.markTransactions(block.Transactions...)
//3
if _, err := c.store.GetBlockHeader(&block.PreviousBlockHash); err != nil {
c.orphanManage.Add(block)
return true, nil
}
//4
if err := c.saveBlock(block); err != nil {
return false, err
}
bestBlock := c.saveSubBlock(block)
bestBlockHeader := &bestBlock.BlockHeader
c.cond.L.Lock()
defer c.cond.L.Unlock()
//5
if bestBlockHeader.PreviousBlockHash == c.bestBlockHeader.Hash() {
log.WithFields(log.Fields{"module": logModule}).Debug("append block to the end of mainchain")
return false, c.connectBlock(bestBlock)
}
//6
if bestBlockHeader.Height > c.bestBlockHeader.Height {
log.WithFields(log.Fields{"module": logModule}).Debug("start to reorganize chain")
return false, c.reorganizeChain(bestBlockHeader)
}
return false, nil
}
processBlock()
函式返回的bool
表示的是block是否為孤塊。
- 通過block的hash判斷這個block是否已經在鏈上。若已存在,則報錯並返回false(表示該block不是孤塊)
- 將block中的Transactions標記,後續會呼叫
c.knownTxs.Add()
將Transactions加入到Transaction集合中。 - 判斷是否為孤塊,如果是,則呼叫孤塊管理部分的模組處理並返回true。
- 儲存block,在
saveBlock()
中會對簽名和區塊進行驗證。 bestBlockHeader.PreviousBlockHash == c.bestBlockHeader.Hash()
的情況說明一切正常,新block被新增到鏈的末端。bestBlockHeader.Height > c.bestBlockHeader.Height
表示出現了分叉,需要回滾。
總結
本篇文章從Vapor設定出塊開始,到出塊流程結束,細節層層解析節點設定出塊和出塊部分所涉及的原始碼。雖然本文至此篇幅已經比較長,但仍有重要的問題沒有講解清楚。例如,generateBlocks()
中的第2點,程式會對出塊的順序進行查驗,但這個出塊的順序是怎麼獲得還未做細緻的解析。
那麼,下一篇文章將針對Vapor中DPoS機制的細節進行原始碼級解析。