鏈客,專爲開發者而生,有問必答!html
此文章來自區塊鏈技術社區,未經容許拒絕轉載。算法
clique
以太坊的官方共識算法是ethash算法,這在前文已經有了詳細的分析:數據庫
它是基於POW的共識機制的,礦工須要經過計算nonce值,會消耗大量算力來匹配target值。json
若是在聯盟鏈或者私鏈的方案裏,繼續使用ethash就會浪費算力,POW也沒有存在的意義。因此以太坊有了另外一種共識方案:基於POA的clique。api
POA, Proof of Authority。權力證實,不一樣於POW的工做量證實,POA是可以直接肯定幾個節點具有出塊的權力,這幾個節點出的塊會被全網其餘節點驗證爲有效塊。數組
創建私鏈緩存
經過這篇文章的操做能夠創建一個私有鏈,觀察這個流程能夠看到,經過puppeth工具創建創世塊時,會提示你選擇哪一種共識方式,有ethash和clique兩個選項,說到這裏咱們就明白了爲何文章中默認要選擇clique。安全
源碼分析
講過了基本概念,下面咱們深刻以太坊源碼來仔細分析clique算法的具體實現。併發
入口仍然選擇seal方法,這裏與前文分析ethash算法的入口是保持一致的,由於他們是Seal的不一樣實現。app
// 咱們的註釋能夠對比着來看,clique的seal函數的目的是:嘗試經過本地簽名認證(權力簽名與認證,找到有權力的結點)來建立一個已密封的區塊。
func (c Clique) Seal(chain consensus.ChainReader, block types.Block, stop <-chan struct{}) (*types.Block, error) {
header := block.Header() number := header.Number.Uint64() if number == 0 {// 不容許密封創世塊 return nil, errUnknownBlock } // 跳轉到下方Clique對象的分析。不支持0-period的鏈,同時拒絕密封空塊,沒有獎勵可是可以旋轉密封 if c.config.Period == 0 && len(block.Transactions()) == 0 { return nil, errWaitTransactions } // 在整個密封區塊的過程當中,不要持有signer簽名者字段。 c.lock.RLock() // 上鎖獲取config中的簽名者和簽名方法。 signer, signFn := c.signer, c.signFn c.lock.RUnlock() snap, err := c.snapshot(chain, number-1, header.ParentHash, nil)// snapshot函數見下方分析 // 校驗處理:若是咱們未經受權去簽名了一個區塊 if err != nil { return nil, err } if _, authorized := snap.Signers[signer]; !authorized { return nil, errUnauthorized } // 若是咱們是【最近簽名者】的一員,則等待下一個區塊,// 見下方[底層機制三](http://www.cnblogs.com/Evsward/p/clique.html#%E4%B8%89%E8%AE%A4%E8%AF%81%E7%BB%93%E7%82%B9%E7%9A%84%E5%87%BA%E5%9D%97%E6%9C%BA%E4%BC%9A%E5%9D%87%E7%AD%89) for seen, recent := range snap.Recents { if recent == signer { // Signer當前簽名者在【最近簽名者】中,若是當前區塊沒有剔除他的話只能繼續等待。 if limit := uint64(len(snap.Signers)/2 + 1); number < limit || seen > number-limit { log.Info("Signed recently, must wait for others") <-stop return nil, nil } } } // 經過以上校驗,到了這裏說明協議已經容許咱們來簽名這個區塊,等待此工做完成 delay := time.Unix(header.Time.Int64(), 0).Sub(time.Now()) if header.Difficulty.Cmp(diffNoTurn) == 0 { // 這不是咱們的輪次來簽名,delay wiggle := time.Duration(len(snap.Signers)/2+1) * wiggleTime // wiggleTime = 500 * time.Millisecond // 隨機推延,從而容許併發簽名(針對每一個簽名者) delay += time.Duration(rand.Int63n(int64(wiggle))) log.Trace("Out-of-turn signing requested", "wiggle", common.PrettyDuration(wiggle)) } log.Trace("Waiting for slot to sign and propagate", "delay", common.PrettyDuration(delay)) select { case <-stop: return nil, nil case <-time.After(delay): } // 核心工做:開始簽名 sighash, err := signFn(accounts.Account{Address: signer}, sigHash(header).Bytes())// signFn函數見下方 if err != nil { return nil, err } copy(header.Extra[len(header.Extra)-extraSeal:], sighash)//將簽名結果替換區塊頭的Extra字段(專門支持記錄額外信息的) return block.WithSeal(header), nil //經過區塊頭從新組裝一個區塊
}
Clique對象的分析
// Clique是POA共識引擎,計劃在Ropsten攻擊之後,用來支持以太坊私測試鏈testnet(也能夠本身搭建聯盟鏈或者私有鏈)
type Clique struct {
config *params.CliqueConfig // 共識引擎配置參數,見下方CliqueConfig源碼介紹 db ethdb.Database // 數據庫,用來存儲以及獲取快照檢查點 recents *lru.ARCCache // 最近區塊的快照,用來加速快照重組 signatures *lru.ARCCache // 最近區塊的簽名,用來加速挖礦 proposals map[common.Address]bool // 目前咱們正在推進的提案清單,存的是地址和布爾值的鍵值對映射 signer common.Address // 簽名者的以太坊地址 signFn SignerFn // 簽名方法,用來受權哈希 lock sync.RWMutex // 鎖,保護簽名字段
}
CliqueConfig源碼分析
// CliqueConfig是POA挖礦的共識引擎的配置字段。
type CliqueConfig struct {
Period uint64 `json:"period"` // 在區塊之間執行的秒數(能夠理解爲距離上一塊出塊後的流逝時間秒數) Epoch uint64 `json:"epoch"` // Epoch['iːpɒk]長度,重置投票和檢查點
}
snapshot函數分析
// snapshot函數可經過給定點獲取認證快照
func (c Clique) snapshot(chain consensus.ChainReader, number uint64, hash common.Hash, parents []types.Header) (*Snapshot, error) {
// 在內存或磁盤上搜索一個快照以檢查檢查點。 var ( headers []*types.Header// 區塊頭 snap *Snapshot// 快照對象,見下方 ) for snap == nil { // 若是找到一個內存裏的快照,使用如下方案: if s, ok := c.recents.Get(hash); ok { snap = s.(*Snapshot) break } // 若是一個在磁盤檢查點的快照被找到,使用如下方案: if number%checkpointInterval == 0 {// checkpointInterval = 1024 // 區塊號,在數據庫中保存投票快照的區塊。 if s, err := loadSnapshot(c.config, c.signatures, c.db, hash); err == nil {// loadSnapshot函數見下方 log.Trace("Loaded voting snapshot form disk", "number", number, "hash", hash) snap = s break } } // 若是咱們在創世塊,則作一個快照 if number == 0 { genesis := chain.GetHeaderByNumber(0) if err := c.VerifyHeader(chain, genesis, false); err != nil { return nil, err } signers := make([]common.Address, (len(genesis.Extra)-extraVanity-extraSeal)/common.AddressLength) for i := 0; i < len(signers); i++ { copy(signers[i][:], genesis.Extra[extraVanity+i*common.AddressLength:]) } snap = newSnapshot(c.config, c.signatures, 0, genesis.Hash(), signers)// 建立一個新的快照的函數,見下方 if err := snap.store(c.db); err != nil { return nil, err } log.Trace("Stored genesis voting snapshot to disk") break } // 沒有針對這個區塊頭的快照,則收集區塊頭並向後移動 var header *types.Header if len(parents) > 0 { // 若是咱們有明確的父類,從這裏強制挑揀出來。 header = parents[len(parents)-1] if header.Hash() != hash || header.Number.Uint64() != number { return nil, consensus.ErrUnknownAncestor } parents = parents[:len(parents)-1] } else { // 若是沒有明確父類(或者沒有更多的),則轉到數據庫 header = chain.GetHeader(hash, number) if header == nil { return nil, consensus.ErrUnknownAncestor } } headers = append(headers, header) number, hash = number-1, header.ParentHash } // 找到了先前的快照,那麼將全部pending的區塊頭都放在它的上面。 for i := 0; i < len(headers)/2; i++ { headers[i], headers[len(headers)-1-i] = headers[len(headers)-1-i], headers[i] } snap, err := snap.apply(headers)//經過區塊頭生成一個新的snapshot對象 if err != nil { return nil, err } c.recents.Add(snap.Hash, snap)//將當前快照區塊的hash存到recents中。 // 若是咱們生成了一個新的檢查點快照,保存到磁盤上。 if snap.Number%checkpointInterval == 0 && len(headers) > 0 { if err = snap.store(c.db); err != nil { return nil, err } log.Trace("Stored voting snapshot to disk", "number", snap.Number, "hash", snap.Hash) } return snap, err
}
Snapshot對象源碼分析:
// Snapshot對象是在給定點的一個認證投票的狀態
type Snapshot struct {
config *params.CliqueConfig // 配置參數 sigcache *lru.ARCCache // 簽名緩存,最近的區塊簽名加速恢復。 Number uint64 `json:"number"` // 快照創建的區塊號 Hash common.Hash `json:"hash"` // 快照創建的區塊哈希 Signers map[common.Address]struct{} `json:"signers"` // 當下認證簽名者的集合 Recents map[uint64]common.Address `json:"recents"` // 最近簽名區塊地址的集合 Votes []*Vote `json:"votes"` // 按時間順序排列的投票名單。 Tally map[common.Address]Tally `json:"tally"` // 當前的投票結果,避免從新計算。
}
loadSnapshot函數源碼分析:
// loadSnapshot函數用來從數據庫中加載一個現存的快照,參數列表中不少都是Snapshot對象的關鍵字段屬性。
func loadSnapshot(config params.CliqueConfig, sigcache lru.ARCCache, db ethdb.Database, hash common.Hash) (*Snapshot, error) {
blob, err := db.Get(append([]byte("clique-"), hash[:]...))// ethdb使用的是leveldb,對外開放接口Dababase見下方 if err != nil { return nil, err } snap := new(Snapshot) if err := json.Unmarshal(blob, snap); err != nil { return nil, err } snap.config = config snap.sigcache = sigcache return snap, nil
}
ethdb數據庫對外開放接口:
// Database接口包裹了全部的數據庫相關操做,全部的方法都是線程安全的。
type Database interface {
Putter Get(key []byte) ([]byte, error)//經過某key獲取值 Has(key []byte) (bool, error)//某key是否包含有效值 Delete(key []byte) error Close() NewBatch() Batch
}
newSnapshot函數源碼:
// newSnapshot函數建立了一個新的快照,經過給出的特定的啓動參數。這個方法沒有初始化最近簽名者的集合,因此只有使用創世塊。
func newSnapshot(config params.CliqueConfig, sigcache lru.ARCCache, number uint64, hash common.Hash, signers []common.Address) *Snapshot {
snap := &Snapshot{// 就是組裝一個Snapshot對象,安裝相應參數 config: config, sigcache: sigcache, Number: number, Hash: hash, Signers: make(map[common.Address]struct{}), Recents: make(map[uint64]common.Address), Tally: make(map[common.Address]Tally), } for _, signer := range signers { snap.Signers[signer] = struct{}{} } return snap
}
signFn函數:
// SignerFn是一個簽名者的回調函數,用來請求一個可以被後臺帳戶簽名生成的哈希
type SignerFn func(accounts.Account, []byte) ([]byte, error)
clique常量配置:
blockPeriod = uint64(15) // clique規定,兩個區塊的生成時間至少間隔15秒,timestamp類型。
Clique底層機制
在進入共識引擎以前,當前結點已經生成了一個完整的區塊,包括區塊頭和密封的交易列表,而後進入seal函數,經過ethash或者clique算法引擎來操做出塊確權。本文重點講述了針對clique算法的源碼分析,clique算法基於POA共識,是在結點中找出有權力的幾個「超級結點」,只有這些結點能夠生成合法區塊,其餘結點的出塊都會直接丟棄。
一:clique是如何肯定簽名者以及簽名方法的?
我在clique文件中搜索,發現有一個方法作了這個工做:
// Authorize函數注入共識引擎clique一個私鑰地址(簽名者)以及簽名方法signFn,用來挖礦新塊
func (c *Clique) Authorize(signer common.Address, signFn SignerFn) {
c.lock.Lock() defer c.lock.Unlock() c.signer = signer c.signFn = signFn
}
那麼繼續搜索,該函數是在什麼時候被調用的,找到了位於/eth/backend.go中的函數StartMining:
func (s *Ethereum) StartMining(local bool) error {
eb, err := s.Etherbase()// 用戶地址 if err != nil { log.Error("Cannot start mining without etherbase", "err", err)//未找到以太帳戶地址,報錯 return fmt.Errorf("etherbase missing: %v", err) } // 若是是clique共識算法,則走if分支,若是是ethash則跳過if。 if clique, ok := s.engine.(*clique.Clique); ok {// Comma-ok斷言語法見下方分析。 wallet, err := s.accountManager.Find(accounts.Account{Address: eb})// 經過用戶地址得到wallet對象 if wallet == nil || err != nil { log.Error("Etherbase account unavailable locally", "err", err) return fmt.Errorf("signer missing: %v", err) } clique.Authorize(eb, wallet.SignHash)//在這裏!注入了簽名者以及經過wallet對象獲取到簽名方法 } if local { // 若是本地CPU挖礦已啓動,咱們能夠禁止注入機制以加速同步時間。 // CPU挖礦在主網是荒誕的,因此沒有人能碰到這個路徑,然而一旦CPU挖礦同步標誌完成之後,將保證私網工做也在一個獨立礦工結點。 atomic.StoreUint32(&s.protocolManager.acceptTxs, 1) } go s.miner.Start(eb)//併發啓動挖礦工做 return nil
}
最終,經過miner.Start(eb),調用到work -> agent -> CPUAgent -> update -> seal,回到最上方咱們的入口。
這裏要補充一點,挖礦機制是從miner.start()做爲入口開始分析的,而上面的StartMining函數是在miner.start()以前的。這樣就把整個這一條線串起來了。
Go語法補充:Comma-ok斷言
if clique, ok := s.engine.(*clique.Clique); ok {
這段語句很使人迷惑,通過搜查,以上語法被稱做Comma-ok斷言。
value, ok = element.(T)
value是element變量的值,ok是布爾類型用來表達斷言結果,element是接口變量,T是斷言類型。
套入以上代碼段,翻譯過來即:
若是s.engine是Clique類型,則ok爲true,同時clique就等於s.engine。
二:Snapshot起到的做用是什麼?
Snapshot對象在Seal方法中是經過調用snapshot構造函數來獲取到的。而snapshot構造函數內部有較長的函數體,包括newSnapshot方法以及loadSnapshot方法的處理。從這個分析來看,咱們也能夠知道Snapshot是快照,也是緩存的一種機制,同時它也不只僅是緩存,由於它存儲了最近簽名者的map集合。
Snapshot能夠從內存(即程序中的變量)或是磁盤上(即經過數據庫leveldb)獲取或者存儲,實際上這就是二級緩存的概念了。
三:認證結點的出塊機會均等
首先將上文Seal方法的源碼遺留代碼段展現以下。
for seen, recent := range snap.Recents {
if recent == signer { if limit := uint64(len(snap.Signers)/2 + 1); number < limit || seen > number-limit { log.Info("Signed recently, must wait for others") <-stop return nil, nil } }
}
其中
if recent == signer {
若是當前結點最近簽名過,則跳過,爲保證機會均等,避免某個認證結點能夠連續出塊,從而做惡。
if limit := uint64(len(snap.Signers)/2 + 1); number < limit || seen > number-limit {
實際上到了這裏就已經在決定出塊權了。咱們依次來看,
snap.Signers是全部的認證結點。
limit的值是全部認證結點的數量的一半加1,也就是說能夠保證limit>50%好的認證結點個數(安全性考慮:掌握大於50%的控制權)。結合上面的機會均等,clique要求認證結點在每輪limit個區塊中只能生成一個區塊。
number是當前區塊號
seen是 「for seen, recent := range snap.Recents {」 中Recents的index,從0開始,最大值爲Recents的總數-1。
接着,咱們來分析控制程序停止的條件表達式:
number < limit || seen > number-limit
number < limit, 若是區塊高度小於limit
seen > number - limit,緩存中最近簽發者序號已經超過了區塊高度與limit之差。number-limit是最多的壞節點,索引seen大於壞節點也要中斷(TODO: number區塊高度與認證結點的關係)
在這兩種狀況下,會中斷程序,中止簽名以及出塊操做。
四:出塊難度
// inturn函數經過給定的區塊高度和簽發者返回該簽發者是否在輪次內
func (s *Snapshot) inturn(number uint64, signer common.Address) bool {
// 方法體的內容就是區塊高度與認證簽發者集合長度的餘數是否等於該簽發者的下標值 signers, offset := s.signers(), 0 for offset < len(signers) && signers[offset] != signer { offset++ } return (number % uint64(len(signers))) == uint64(offset)
}
一句話,clique要求籤發者必須按照其在snapshot中的認證簽發者集合按照字典排序的順序出塊。
符合以上條件的話,難度爲2,不然爲1。
diffInTurn = big.NewInt(2) // 簽名在輪次內的區塊難度爲2。
diffNoTurn = big.NewInt(1) // 簽名未在輪次內的區塊難度爲1。
clique的出塊難度比較容易理解,這是在POW中大書特書的部分但在clique中卻十分簡單,當inturn的結點離線時,其餘結點會來競爭,難度值降爲1。然而正常出塊時,limit中的全部認證結點包括一個inturn和其餘noturn的結點,clique是採用了給noturn加延遲時間的方式來支持inturn首先出塊,避免noturn的結點無謂生成區塊。這部分代碼在下面再貼一次。
wiggle := time.Duration(len(snap.Signers)/2+1) wiggleTime // wiggleTime = 500 time.Millisecond // 隨機推延,從而容許併發簽名(針對每一個簽名者)
delay += time.Duration(rand.Int63n(int64(wiggle)))
clique承認難度值最高的鏈爲主鏈,因此徹底inturn結點出的塊組成的鏈會是最理想的主鏈。
五:區塊校驗
// 一樣位於clique文件中的verifySeal函數,顧名思義是結點用來校驗別的結點廣播過來的區塊信息的。
func (c Clique) verifySeal(chain consensus.ChainReader, header types.Header, parents []*types.Header) error {
// 創世塊的話不校驗 number := header.Number.Uint64() if number == 0 { return errUnknownBlock } // 取到所需snapshot對象,用來校驗區塊頭而且將其緩存。 snap, err := c.snapshot(chain, number-1, header.ParentHash, parents) if err != nil { return err } // 處理受權祕鑰,檢查是否違背認證簽名者集合 signer, err := ecrecover(header, c.signatures)// 從區塊頭中解密出Extra字段,找到簽名字符串,得到簽名者地址信息。能夠跳轉到下面ecrecover函數的源碼分析。 if err != nil { return err } if _, ok := snap.Signers[signer]; !ok { return errUnauthorized } // 與Seal相同的處理,機會均等 for seen, recent := range snap.Recents { if recent == signer { if limit := uint64(len(snap.Signers)/2 + 1); seen > number-limit { return errUnauthorized } } } // 區分是否inturn,設置區塊困難度,上面也介紹過了。 inturn := snap.inturn(header.Number.Uint64(), signer) if inturn && header.Difficulty.Cmp(diffInTurn) != 0 { return errInvalidDifficulty } if !inturn && header.Difficulty.Cmp(diffNoTurn) != 0 { return errInvalidDifficulty } return nil
}
ecrecover函數的源碼分析:
// ecrecover函數從一個簽名的區塊頭中解壓出以太坊帳戶地址
func ecrecover(header types.Header, sigcache lru.ARCCache) (common.Address, error) {
// 若是簽名已經被緩存,返回它。 hash := header.Hash() if address, known := sigcache.Get(hash); known { return address.(common.Address), nil } // 從區塊頭的Extra字段取得簽名內容。 if len(header.Extra) < extraSeal { return common.Address{}, errMissingSignature } signature := header.Extra[len(header.Extra)-extraSeal:] // 經過密碼學技術從簽名內容中解密出公鑰和以太坊地址。 pubkey, err := crypto.Ecrecover(sigHash(header).Bytes(), signature)// 具體源碼見下方 if err != nil { return common.Address{}, err } var signer common.Address copy(signer[:], crypto.Keccak256(pubkey[1:])[12:])//將公鑰利用keccak256解密賦值給signer。 sigcache.Add(hash, signer)//加入緩存 return signer, nil
}
crypto包的Ecrecover函數:
func Ecrecover(hash, sig []byte) ([]byte, error) {
return secp256k1.RecoverPubkey(hash, sig)
}
Ecrecover函數是使用secp256k1來解密公鑰。
下面咱們從VerifySeal函數反推,找出調用該函數的位置在miner/remote_agent.go,
// SubmitWork函數嘗試注入一個pow解決方案(共識引擎)到遠程代理,返回這個解決方案是否被接受。(不能同時是一個壞的pow也不能有其餘任何錯誤,例如沒有工做被pending
func (a *RemoteAgent) SubmitWork(nonce types.BlockNonce, mixDigest, hash common.Hash) bool {
a.mu.Lock() defer a.mu.Unlock() // 保證被提交的工做不是空 work := a.work[hash] if work == nil { log.Info("Work submitted but none pending", "hash", hash) return false } // 保證引擎是真實有效的。 result := work.Block.Header() result.Nonce = nonce result.MixDigest = mixDigest if err := a.engine.VerifySeal(a.chain, result); err != nil {//在這裏,VerifySeal方法被調用。 log.Warn("Invalid proof-of-work submitted", "hash", hash, "err", err) return false } block := work.Block.WithSeal(result) // 解決方案看上去是有效的,返回到礦工而且通知接受結果。 a.returnCh <- &Result{work, block} delete(a.work, hash) return true
}
這個SubmitWork位於挖礦的pkg中,主要工做是對work的校驗,包括work自己是否爲空,work中的區塊頭以及區塊頭中包含的字段的有效性,而後是對區塊頭的VerifySeal(該函數的功能在上面已經介紹到了,主要是對區塊簽名者的認證,區塊難度值的確認)
繼續反推找到SubmitWork函數被調用的位置:
// SubmitWork函數可以被外部礦工用來提交他們的POW。
func (api *PublicMinerAPI) SubmitWork(nonce types.BlockNonce, solution, digest common.Hash) bool {
return api.agent.SubmitWork(nonce, digest, solution)
}
總結
區塊的校驗是外部結點自動執行PublicMinerAPI的SubmitWork方法,從而層層調用,經過檢查區塊頭內的簽名內容,經過secp256k1方法恢復公鑰,而後利用Keccak256將公鑰加密爲一個以太地址做爲簽名地址,得到簽名地址之後,去本地認證結點緩存中檢查,看該簽名地址是否符合要求。最終只要經過層層校驗,就不會報出errUnauthorized的錯誤。
注意:簽名者地址common.Address在Seal時被簽名signature存在區塊頭的Extra字段中,而後在VerifySeal中被從區塊頭中取出簽名signature。該簽名的解密方式比較複雜:要先經過secp256k1恢復一個公鑰,而後利用這個公鑰和Keccak256加密出簽名者地址common.Address。
common.Address自己就是結點公鑰的Keccak256加密結果。請參照common/types.go:
// Hex函數返回了一個十六禁止的字符串,表明了以太坊地址。
func (a Address) Hex() string {
unchecksummed := hex.EncodeToString(a[:]) sha := sha3.NewKeccak256()//這裏就不展開了,能夠看出是經過Keccak256方法將未檢查的明文Address加密爲一個標準以太坊地址 sha.Write([]byte(unchecksummed)) hash := sha.Sum(nil) result := []byte(unchecksummed) for i := 0; i < len(result); i++ { hashByte := hash[i/2] if i%2 == 0 { hashByte = hashByte >> 4 } else { hashByte &= 0xf } if result[i] > '9' && hashByte > 7 { result[i] -= 32 } } return "0x" + string(result)
}
六: 基於投票的認證結點的運行機制
上面咱們分析了clique的認證結點的出塊,校驗等細節,那麼這裏引出終極問題:如何確認一個普通結點是不是認證結點呢?
答:clique是基於投票機制來確認認證結點的。
先來看投票實體類,存在於snapshot源碼中。
// Vote表明了一個獨立的投票,這個投票能夠受權一個簽名者,更改受權列表。
type Vote struct {
Signer common.Address `json:"signer"` // 已受權的簽名者(經過投票) Block uint64 `json:"block"` // 投票區塊號 Address common.Address `json:"address"` // 被投票的帳戶,修改它的受權 Authorize bool `json:"authorize"` // 對一個被投票帳戶是否受權或解受權
}
這個Vote是存在於Snapshot的屬性字段中,因此投票機制離不開Snapshot,咱們在這裏再次將Snapshot實體源碼從新分析一遍,上面註釋過的內容我再也不復述,而是直接關注在投票機制相關字段內容上。
type Snapshot struct {
config *params.CliqueConfig sigcache *lru.ARCCache Number uint64 `json:"number"` Hash common.Hash `json:"hash"` Signers map[common.Address]struct{} `json:"signers"` // 認證節點集合 Recents map[uint64]common.Address `json:"recents"` Votes []*Vote `json:"votes"` // 上面的Vote對象數組 Tally map[common.Address]Tally `json:"tally"` // 也是一個自定義類型,見下方
}
Tally結構體:
// Tally是一個簡單的用來保存當前投票分數的計分器
type Tally struct {
Authorize bool `json:"authorize"` // 受權true或移除false Votes int `json:"votes"` // 該提案已獲票數
}
另外Clique實體中還有個有爭議的字段proposals,當時並無分析清楚,何謂提案?
proposal是能夠經過rpc申請加入或移除一個認證節點,結構爲待操做地址(節點地址)和狀態(加入或移除)
投票中某些概念的肯定
投票的範圍是在委員會,委員會的意思就是全部礦工。
概念介紹:checkpoint,checkpointInterval = 1024 ,每過1024個區塊,則保存snapshot到數據庫
概念介紹:Epoch,與ethash同樣,一個Epoch是三萬個區塊
投票流程
首先委員會某個成員(即節點礦工)經過rpc調用consensus/clique/api.go中的propose方法
// Propose注入一個新的受權提案,能夠受權一個簽名者或者移除一個。
func (api *API) Propose(address common.Address, auth bool) {
api.clique.lock.Lock() defer api.clique.lock.Unlock() api.clique.proposals[address] = auth// true:受權,false:移除
}
上面rpc提交過來的propose會寫入Clique.proposals集合中。
在挖礦開始之後,會在miner.start()中提交一個commitNewWork,其中涉及到準備區塊頭Prepare的方法,咱們進入到clique的實現,其中涉及到對上面的Clique.proposals的處理:
// 若是存在pending的proposals,則投票
if len(addresses) > 0 {
header.Coinbase = addresses[rand.Intn(len(addresses))]//將投票節點的地址賦值給區塊頭的Coinbase字段。 // 下面是經過提案內容來組裝區塊頭的隨機數字段。 if c.proposals[header.Coinbase] { copy(header.Nonce[:], nonceAuthVote) } else { copy(header.Nonce[:], nonceDropVote) }
}
// nonceAuthVote和nonceDropVote常量的聲明與初始化
nonceAuthVote = hexutil.MustDecode("0xffffffffffffffff") // 受權簽名者的必要隨機數
nonceDropVote = hexutil.MustDecode("0x0000000000000000") // 移除簽名者的必要隨機數
整個區塊組裝好之後(其餘的內容再也不復述),會被廣播到外部結點校驗,若是沒有問題該塊被成功出了,則區塊頭中的這個提案也會被記錄在主鏈上。
區塊在生成時,會建立Snapshot,在snapshot構造函數中,會涉及到對proposal的處理apply方法。
// apply經過接受一個給定區塊頭建立了一個新的受權
func (s Snapshot) apply(headers []types.Header) (*Snapshot, error) {
if len(headers) == 0 { return s, nil } for i := 0; i < len(headers)-1; i++ { if headers[i+1].Number.Uint64() != headers[i].Number.Uint64()+1 { return nil, errInvalidVotingChain } } if headers[0].Number.Uint64() != s.Number+1 { return nil, errInvalidVotingChain } snap := s.copy() // 投票的處理核心代碼 for _, header := range headers { // Remove any votes on checkpoint blocks number := header.Number.Uint64() // 若是區塊高度正好在Epoch結束,則清空投票和計分器 if number%s.config.Epoch == 0 { snap.Votes = nil snap.Tally = make(map[common.Address]Tally) } if limit := uint64(len(snap.Signers)/2 + 1); number >= limit { delete(snap.Recents, number-limit) } // 從區塊頭中解密出來簽名者地址 signer, err := ecrecover(header, s.sigcache) if err != nil { return nil, err } if _, ok := snap.Signers[signer]; !ok { return nil, errUnauthorized } for _, recent := range snap.Recents { if recent == signer { return nil, errUnauthorized } } snap.Recents[number] = signer // 區塊頭認證,無論該簽名者以前的任何投票 for i, vote := range snap.Votes { if vote.Signer == signer && vote.Address == header.Coinbase { // 從緩存計數器中移除該投票 snap.uncast(vote.Address, vote.Authorize) // 從按時間排序的列表中移除投票 snap.Votes = append(snap.Votes[:i], snap.Votes[i+1:]...) break // only one vote allowed } } // 從簽名者中計數新的投票 var authorize bool switch { case bytes.Equal(header.Nonce[:], nonceAuthVote): authorize = true case bytes.Equal(header.Nonce[:], nonceDropVote): authorize = false default: return nil, errInvalidVote } if snap.cast(header.Coinbase, authorize) { snap.Votes = append(snap.Votes, &Vote{ Signer: signer, Block: number, Address: header.Coinbase, Authorize: authorize, }) } // 判斷票數是否超過一半的投票者,若是投票經過,更新簽名者列表 if tally := snap.Tally[header.Coinbase]; tally.Votes > len(snap.Signers)/2 { if tally.Authorize { snap.Signers[header.Coinbase] = struct{}{} } else { delete(snap.Signers, header.Coinbase) if limit := uint64(len(snap.Signers)/2 + 1); number >= limit { delete(snap.Recents, number-limit) } for i := 0; i < len(snap.Votes); i++ { if snap.Votes[i].Signer == header.Coinbase { snap.uncast(snap.Votes[i].Address, snap.Votes[i].Authorize) snap.Votes = append(snap.Votes[:i], snap.Votes[i+1:]...) i-- } } } // 無論以前的任何投票,直接改變帳戶 for i := 0; i < len(snap.Votes); i++ { if snap.Votes[i].Address == header.Coinbase { snap.Votes = append(snap.Votes[:i], snap.Votes[i+1:]...) i-- } } delete(snap.Tally, header.Coinbase) } } snap.Number += uint64(len(headers)) snap.Hash = headers[len(headers)-1].Hash() return snap, nil
}
關鍵控制的代碼是tally.Votes > len(snap.Signers)/2,意思是計分器中的票數大於一半的簽名者,就表示該投票經過,下面就是要更改snapshot中的認證簽名者列表緩存,同時要同步給其餘節點,並刪除該投票相關信息。
總結
本覺得clique比較簡單,沒必要調查這麼長,然而POA的共識算法仍是比較有難度的,它和POW是基於徹底不一樣的兩種場景的實現方式,出塊方式也徹底不一樣。下面我嘗試用簡短的語言來總結Clique的共識機制。
clique共識是基於委員會選舉認證節點來確認出塊權力的方式實現的。投票方式經過rpc請求propose,snapshot二級緩存機制,唱票,執行投票結果。認證節點出塊機會均等,困難度經過輪次(是否按照緩存認證順序出塊)肯定,區塊頭Extra存儲簽名,keccak256加密以太地址,secp256k1解密簽名爲公鑰,經過認證結點出塊的邏輯能夠反推區塊校驗。
到目前爲止,咱們對POA共識機制,以及以太坊clique的實現有了深入的理解與認識,相信若是讓咱們去實現一套POA,也是徹底有能力的。你們在閱讀本文時有任何疑問都可留言給我,我必定會及時回覆。