區塊鏈100講:以太坊源碼研究之PoW及共識算法深究

image

本講將介紹「挖礦「獲得新區塊的整個過程,以及不一樣共識算法的實現細節。算法

1

待挖掘區塊須要組裝

在Ethereum 代碼中,名爲miner的包(package)負責向外提供一個「挖礦」獲得的新區塊,其主要結構體的UML關係圖以下圖所示:數據庫

image

處於入口的類是Miner,它做爲公共類型,向外暴露mine功能;它有一個worker類型的成員變量,負責管理mine過程;worker內部有一組Agent接口類型對象,每一個Agent均可以完成單個區塊的mine,目測這些Agent之間應該是競爭關係;Work結構體主要用以攜帶數據,被視爲挖掘一個區塊時所需的數據環境。數組

主要的數據傳輸發生在worker和它的Agent(們)之間:在合適的時候,worker把一個Work對象發送給每一個Agent,而後任何一個Agent完成mine時,將一個通過受權確認的Block加上那個更新過的Work,組成一個Result對象發送回worker。緩存

有意思的是<>接口,儘管調用方worker內部聲明瞭一個Agent數組,但目前只有一個實現類CpuAgent的對象會被加到該數組,可能這個Agent數組是爲未來的擴展做的預留吧。CpuAgent經過全局的<>對象,藉助共識算法完成最終的區塊受權。安全

另外,unconfirmedBlocks 也挺特別,它會以unconfirmedBlock的形式存儲最近一些本地挖掘出的區塊。在一段時間以後,根據區塊的Number和Hash,再肯定這些區塊是否已經被收納進主幹鏈(canonical chain)裏,以輸出Log的方式來告知用戶。網絡

對於一個新區塊被挖掘出的過程,代碼實現上基本分爲兩個環節:一是組裝出一個新區塊,這個區塊的數據基本完整,包括成員Header的部分屬性,以及交易列表txs,和叔區塊組uncles[],而且全部交易已經執行完畢,全部收據(Receipt)也已收集完畢,這部分主要由worker完成;二是填補該區塊剩餘的成員屬性,好比Header.Difficulty等,並完成受權,這些工做是由Agent調用接口實現體,利用共識算法來完成的。數據結構

新區塊的組裝流程多線程

挖掘新區塊的流程入口在Miner裏,略顯奇葩的是,具體入口在Miner結構體的建立函數(避免稱之爲‘構造函數’)裏。app

image

Miner的函數ide

New()

在New()裏,針對新對象miner的各個成員變量初始化完成後,會緊跟着建立worker對象,而後將Agent對象登記給worker,最後用一個單獨線程去運行miner.Update()函數;而worker的建立函數裏也如法炮製,分別用單獨線程去啓動worker.updater()和wait();最後worker.CommitNewWork()會開始準備一個新區塊所需的基本數據,如Header,Txs, Uncles等。注意此時Agent還沒有啓動。

Update()

這個update()會訂閱(監聽)幾種事件,均跟Downloader相關。當收到Downloader的StartEvent時,意味者此時本節點正在從其餘節點下載新區塊,這時miner會當即中止進行中的挖掘工做,並繼續監聽;若是收到DoneEvent或FailEvent時,意味本節點的下載任務已結束-不管下載成功或失敗-此時均可以開始挖掘新區塊,而且此時會退出Downloader事件的監聽。

從miner.Update()的邏輯能夠看出,對於任何一個Ethereum網絡中的節點來講,挖掘一個新區塊和從其餘節點下載、同步一個新區塊,根本是相互衝突的。這樣的規定,保證了在某個節點上,一個新區塊只可能有一種來源,這能夠大大下降可能出現的區塊衝突,並避免全網中計算資源的浪費。

worker的函數

這裏咱們主要關注worker.updater()和wait()

image

update()

worker.update()分別監聽ChainHeadEvent,ChainSideEvent,TxPreEvent幾個事件,每一個事件會觸發worker不一樣的反應。ChainHeadEvent是指區塊鏈中已經加入了一個新的區塊做爲整個鏈的鏈頭,這時worker的迴應是當即開始準備挖掘下一個新區塊(也是夠忙的);ChainSideEvent指區塊鏈中加入了一個新區塊做爲當前鏈頭的旁支,worker會把這個區塊收納進possibleUncles[]數組,做爲下一個挖掘新區塊可能的Uncle之一;TxPreEvent是TxPool對象發出的,指的是一個新的交易tx被加入了TxPool,這時若是worker沒有處於挖掘中,那麼就去執行這個tx,並把它收納進Work.txs數組,爲下次挖掘新區塊備用。

須要稍稍注意的是,ChainHeadEvent並不必定是外部源發出。因爲worker對象有個成員變量chain(eth.BlockChain),因此當worker本身完成挖掘一個新區塊,並把它寫入數據庫,加進區塊鏈裏成爲新的鏈頭時,worker本身也能夠調用chain發出一個ChainHeadEvent,從而被worker.update()函數監聽到,進入下一次區塊挖掘。

wait()

worker.wait()會在一個channel處一直等待Agent完成挖掘發送回來的新Block和Work對象。這個Block會被寫入數據庫,加入本地的區塊鏈試圖成爲最新的鏈頭。注意,此時區塊中的全部交易,假設都已經被執行過了,因此這裏的操做,不會再去執行這些交易對象。

當這一切都完成,worker就會發送一條事件(NewMinedBlockEvent{}),等於通告天下:我挖出了一個新區塊!這樣監聽到該事件的其餘節點,就會根據自身的情況,來決定是否接受這個新區塊成爲全網中公認的區塊鏈新的鏈頭。至於這個公認過程如何實現,就屬於共識算法的範疇了。

commitNewWork()

commitNewWork()會在worker內部多處被調用,注意它每次都是被直接調用,並無以goroutine的方式啓動。commitNewWork()內部使用sync.Mutex對所有操做作了隔離。這個函數的基本邏輯以下:

  • 準備新區塊的時間屬性Header.Time,通常均等於系統當前時間,不過要確保父區塊的時間(parentBlock.Time())要早於新區塊的時間,父區塊固然來自當前區塊鏈的鏈頭了。

  • 建立新區塊的Header對象,其各屬性中:Num可肯定(父區塊Num +1);Time可肯定;ParentHash可肯定;其他諸如Difficulty,GasLimit等,均留待以後共識算法中肯定。

  • 調用Engine.Prepare()函數,完成Header對象的準備。

  • 根據新區塊的位置(Number),查看它是否處於DAO硬分叉的影響範圍內,若是是,則賦值予header.Extra。

  • 根據已有的Header對象,建立一個新的Work對象,並用其更新worker.current成員變量。

  • 若是配置信息中支持硬分叉,在Work對象的StateDB裏應用硬分叉。

  • 準備新區塊的交易列表,來源是TxPool中那些最近加入的tx,並執行這些交易。

  • 準備新區塊的叔區塊uncles[],來源是worker.possibleUncles[],而possibleUncles[]中的每一個區塊都從事件ChainSideEvent中搜集獲得。注意叔區塊最多有兩個。

  • 調用Engine.Finalize()函數,對新區塊「定型」,填充上Header.Root, TxHash, ReceiptHash, UncleHash等幾個屬性。

  • 若是上一個區塊(即舊的鏈頭區塊)處於unconfirmedBlocks中,意味着它也是由本節點挖掘出來的,嘗試去驗證它已經被吸納進主幹鏈中。

  • 把建立的Work對象,經過channel發送給每個登記過的Agent,進行後續的挖掘。

以上步驟中,4和6都是僅僅在該區塊配置中支持DAO硬分叉,而且該區塊的位置正好處於DAO硬分叉影響範圍內時纔會發生;其餘步驟是廣泛性的。commitNewWork()完成了待挖掘區塊的組裝,block.Header建立完畢,交易數組txs,叔區塊Uncles[]都已取得,而且因爲全部交易被執行完畢,相應的Receipt[]也已得到。萬事俱備,能夠交給Agent進行‘挖掘’了。

CpuAgent的函數

CpuAgent中與mine相關的函數,主要是update()和mine():

image

CpuAgent.update()就是worker.commitNewWork()結束後發出Work對象的會一直監聽相關channel,若是收到Work對象(顯然由worker.commitNewWork()結束後發出),就啓動mine()函數;若是收到中止(mine)的消息,就退出一切相關操做。

CpuAgent.mine()會直接調用Engine.Seal()函數,利用Engine實現體的共識算法對傳入的Block進行最終的受權,若是成功,就將Block同Work一塊兒經過channel發還給worker,那邊worker.wait()會接收並處理。

顯然,這兩個函數都沒作什麼實質性工做,它們只是負責調用接口實現體,待受權完成後將區塊數據發送回worker。挖掘出一個區塊的真正奧妙全在Engine實現體所表明的共識算法裏。

2

共識算法完成挖掘

共識算法族對外暴露的是Engine接口,其有兩種實現體,分別是基於運算能力的Ethash算法和基於「同行」認證的的Clique算法。

image

在Engine接口的聲明函數中,VerifyHeader(),VerifyHeaders(),VerifyUncles()用來驗證區塊相應數據成員是否合理合規,能否放入區塊;Prepare()函數每每在Header建立時調用,用來對Header.Difficulty等屬性賦值;Finalize()函數在區塊區塊的數據成員都已具有時被調用,好比叔區塊(uncles)已經具有,所有交易Transactions已經執行完畢,所有收據(Receipt[])也已收集完畢,此時Finalize()會最終生成Root,TxHash,UncleHash,ReceiptHash等成員。

而Seal()和VerifySeal()是Engine接口全部函數中最重要的。Seal()函數可對一個調用過Finalize()的區塊進行受權或封印,並將封印過程產生的一些值賦予區塊中剩餘還沒有賦值的成員(Header.Nonce, Header.MixDigest)。Seal()成功時返回的區塊所有成員齊整,可視爲一個正常區塊,可被廣播到整個網絡中,也能夠被插入區塊鏈等。因此,對於挖掘一個新區塊來講,全部相關代碼裏Engine.Seal()是其中最重要,也是最複雜的一步。VerifySeal()函數基於跟Seal()徹底同樣的算法原理,經過驗證區塊的某些屬性(Header.Nonce,Header.MixDigest等)是否正確,來肯定該區塊是否已經通過Seal操做。

在兩種共識算法的實現中,Ethash是產品環境下以太坊真正使用的共識算法,Clique主要針對以太坊的測試網絡運做,兩種共識算法的差別,主要體如今Seal()的實現上。

Ethash共識算法

Ethash算法又被稱爲Proof-of-Work(PoW),是基於運算能力的受權/封印過程。Ethash實現的Seal()函數,其基本原理可簡單表示成如下公式:

RAND(h, n)  <=  M / d

這裏M表示一個極大的數,好比2^256-1;d表示Header成員Difficulty。RAND()是一個概念函數,它表明了一系列複雜的運算,並最終產生一個相似隨機的數。這個函數包括兩個基本入參:h是Header的哈希值(Header.HashNoNonce()),n表示Header成員Nonce。整個關係式能夠大體理解爲,在最大不超過M的範圍內,以某個方式試圖找到一個數,若是這個數符合條件(<=M/d),那麼就認爲Seal()成功。

咱們能夠先定性的驗證一個推論:d的大小對整個關係式的影響。假設h,n均不變,若是d變大,則M/d變小,那麼對於RAND()生成一個知足該條件的數值,顯然其機率是降低的,即意味着難度將加大。考慮到以上變量的含義,當Header.Difficulty逐漸變大時,這個對應區塊被挖掘出的難度(恰爲Difficulty本義)也在緩慢增大,挖掘所需時間也在增加,因此上述推論是合理的。

mine()函數

Ethash.Seal()函數實現中,會以多線程(goroutine)的方式並行調用mine()函數,線程個數等於Ethash.threads;若是Ethash.threads被設爲0,則Ethash選擇以本地CPU中的總核數做爲開啓線程的個數。

image

以上代碼就是mine()函數的主要業務邏輯。入參@id是線程編號,用來發送log告知上層;函數內部首先定義一組局部變量,包括以後調用hashimotoFull()時傳入的hash、nonce、巨大的輔助數組dataset,以及結果比較的target;而後是一個無限循環,每次調用hashimotoFull()進行一系列複雜運算,一旦它的返回值符合條件,就複製Header對象(深度拷貝),並賦值Nonce、MixDigest屬性,返回通過受權的區塊。注意到在每次循環運算時,nonce還會自增+1,使得每次循環中的計算都各不相同。

這裏hashimotoFull()函數經過調用hashimoto()函數完成運算,而同時還有另一個相似的函數hashimoLight()函數。

image

以上兩個函數,最終都調用了hashimoto()。它們的差異,在於各自調用hashimoto()函數的入參@size uint 和 @lookup func()不一樣。相比於Light(),Full()函數調用的size更大,以及一個從更大數組中獲取數據的查詢函數lookup()。hashimotoFull()函數是被Seal()調用的,而hashimotoLight()是爲VerifySeal()準備的。

這裏的lookup()函數其實很重要,它實際上是一個以非線性表查找方式進行的哈希函數! 這種哈希函數的性能不只取決於查找的邏輯,更多的依賴於所查找的表格的數據規模大小。lookup()會以函數型參數的形式傳遞給hashimoto()

核心的運算函數hashimoto()

最終爲Ethash共識算法的Seal過程執行運算任務的是hashimoto()函數,它的函數類型以下:

image

hashimoto()函數的入參包括:區塊哈希值@hash, 區塊nonce成員@nonce,和非線性表查找的哈希函數lookup(),及其所查找的非線性表格的容量@size。返回值digest和result,都是32 bytes長的字節串。

hashimoto()的邏輯比較複雜,包含了屢次、多種哈希運算。下面嘗試從其中數據結構變化的角度來簡單描述之:

image

簡單介紹一下上圖所表明的代碼流程:

  • 首先,hashimoto()函數將入參@hash和@nonce合併成一個40 bytes長的數組,取它的SHA-512哈希值取名seed,長度爲64 bytes。

  • 而後,將seed[]轉化成以uint32爲元素的數組mix[],注意一個uint32數等於4 bytes,故而seed[]只能轉化成16個uint32數,而mix[]數組長度32,因此此時mix[]數組先後各半是等值的。

  • 接着,lookup()函數登場。用一個循環,不斷調用lookup()從外部數據集中取出uint32元素類型數組,向mix[]數組中混入未知的數據。循環的次數可用參數調節,目前設爲64次。每次循環中,變化生成參數index,從而使得每次調用lookup()函數取出的數組都各不相同。這裏混入數據的方式是一種相似向量「異或」的操做,來自於FNV算法。

  • 待混淆數據完成後,獲得一個基本上面目全非的mix[],長度爲32的uint32數組。這時,將其摺疊(壓縮)成一個長度縮小成原長1/4的uint32數組,摺疊的操做方法仍是來自FNV算法。

  • 最後,將摺疊後的mix[]由長度爲8的uint32型數組直接轉化成一個長度32的byte數組,這就是返回值@digest;同時將以前的seed[]數組與digest合併再取一次SHA-256哈希值,獲得的長度32的byte數組,即返回值@result。

最終通過一系列屢次、多種的哈希運算,hashimoto()返回兩個長度均爲32的byte數組 - digest[]和result[]。回憶一下ethash.mine()函數中,對於hashimotoFull()的兩個返回值,會直接以big.int整型數形式比較result和target;若是符合要求,則將digest取SHA3-256的哈希值(256 bits),並存於Header.MixDigest中,待之後Ethash.VerifySeal()能夠加以驗證。

以非線性表查找方式進行的哈希運算

上述hashimoto()函數中,函數型入參lookup()其實表示的是一次以非線性表查找方式進行的哈希運算,lookup()以入參爲key,從所關聯的數據集中按照定義好的一段邏輯取出64 bytes長的數據做爲hash value並返回,注意返回值以uint32的形式則相應變成16個uint32長。返回的數據會在hashimoto()函數被其餘的哈希運算所使用。

以非線性表的查找方式進行的哈希運算(hashing by nonlinear table lookup),屬於衆多哈希函數中的一種實現,在Ethash算法的核心環節有大量使用,所使用到的非線性表被定義成兩種結構體,分別叫cache{}和dataset{}。兩者的差別主要是表格的規模和調用場景:dataset{}中的數據規模更加巨大,從而會被hashimotoFull()調用從而服務於Ethash.Seal();cache{}內含數據規模相對較小,會被hashimotoLight()調用並服務於Ethash.VerifySeal()。

image

以cache{}的結構體聲明爲例,成員cache就是實際使用的一塊內存Buffer,mmap是內存映射對象,dump是該內存buffer存儲於磁盤空間的文件對象,epoch是共享這個cache{}對象的一組區塊的序號。從上述UML圖來看,cache和dataset的結構體聲明基本同樣,這也暗示了它們不管是原理仍是行爲都極爲類似。

cache{}對象的生成

dataset{}和cache{}的生成過程很相似,都是經過內存映射的方式讀/寫磁盤文件。

image

以cache{}爲例,Ethash.cache(uint64)會確保該區塊所用到的cache{}對象已經建立,它的入參是區塊的Number,用Number/epochLength能夠獲得該區塊所對應的epoch號。epochLength被定義成常量30000,意味着每連續30000個區塊共享一個cache{}對象。

有意思的是內存映射相關的函數,memoryMapAndGenerate()會首先調用memoryMapFile()生成一個文件並映射到內存中的一個數組,並調用傳入的函數型參數generator() 進行數據的填入,因而這個內存數組以及所映射的磁盤文件就同時變得十分巨大,注意此時傳入memoryMapFile()的文件操做權限是可寫的。而後再關閉全部文件操做符,調用memoryMapFile()從新打開這個磁盤文件並映射到內存的一個數組,注意此時的文件操做權限是隻讀的。可見這組函數的coding很精細。

Ethash中分別用一個map結構來存放不一樣epoch對應的cache對象和dataset對象,緩存成員fcache和fdataset,用以提早建立cache{}和dataset{}對象以免下次使用時再花費時間。

咱們以cache{}爲例,看看cache.generate()方法的具體邏輯:

image

上圖是cache.generate()方法的基本流程:若是是測試用途,則沒必要考慮磁盤文件,直接調用generateCache()建立buffer;若是文件夾爲空,那也無法拼接出文件路徑,一樣直接調用generateCache()建立buffer;而後根據拼接出的文件路徑,先嚐試讀取磁盤上已有文件,若是成功,說明文件已存在並可以使用;若是文件不存在,那隻好建立一個新文件,定義文件容量,而後利用內存映射將其導入內存。很明顯,直接爲cache{]建立buffer的generateCache()函數是這裏的核心操做,包括memoryMapAndGenerate()方法,都將generateCache()做爲一個函數型參數引入操做的。

參數size的生成

參數size是生成buffer的容量,它在上述cache.generate()中生成。

image

上述就是生成size的代碼,cacheSize()的入參雖然是跟區塊Number相關,但實際上對於處於同一epoch組的區塊來講效果是同樣的,每組個數epochLength。Ethash在代碼裏預先定義了一個數組cacheSizes[],存放了前2048個epoch組所用到的cache size。若是當前區塊的epoch處於這個範圍內,則取用之;若沒有,則如下列公式賦初始值。

size = cacheInitBytes + cacheGrowthBytes * epoch - hashBytes

這裏cacheInitBytes = 2^24,cacheGrowthBytes = 2^17,hashBytes = 64,可見size的取值有多麼巨大了。注意到cacheSize()中在對size賦值後還要不斷調整,保證最終size是個質數,這是出於密碼學的須要。

粗略計算一下size的取值範圍,size = 2^24 + 2^17 * epoch,因爲epoch > 2048 = 2^11,因此size  > 2^28,生成的buffer至少有256MB,而這還僅僅是VerifySeal()使用的cache{},Seal()使用的dataset{}還要大的多,可見這些數據集有多麼龐大了。

參數seed[]的生成

參數seed是generateCache()中對buffer進行哈希運算的種子數據,它也在cache.generate()函數中生成。

image

上述seedHash()函數用來生成所需的seed[]數組,它的長度32bytes,與common.Address類型長度相同。makeHasher()函數利用入參的哈希函數接口生成一個哈希函數,這裏用了SHA3-256哈希函數。注意seedHash()中利用生成的哈希函數keccak256()對seed[]作的原地哈希,而原地哈希運算的次數跟當前區塊所處的epoch序號有關,因此每一個不一樣的cache{}所用到的seed[]也是徹底不一樣的,這個不一樣經過更屢次的哈希運算來實現。

generateCache()函數

generateCache()函數在給定種子數組seed[]的狀況下,對固定容量的一塊buffer進行一系列操做,使得buffer的數值分佈變得隨機、無規律可循,最終buffer做爲cache{}中的數組(非線性表)返回,還可做爲數據源幫助生成dataset{}。generateCache()函數主體分兩部分,首先用SHA3-512哈希函數做爲哈希生成器(hasher),對buffer數組分段(每次64bytes)進行哈希化,而後利用StrictMemoryHardFunction中的RandMemoHash算法對buffer再進行處理。這個RandMemoHash算法來自2014年密碼學方向的一篇學術論文,有興趣的朋友能夠搜搜看。

內存映射

因爲Ethash(PoW)算法中用到的隨機數據集cache{}和dataset{}過於龐大,將其以文件形式存儲在磁盤上就顯得頗有必要。一樣因爲這些文件過於龐大,使用時又須要一次性總體讀入內存(由於對其的使用是隨意截取其中的一段數據),使用內存映射能夠大大減輕I/O負擔。cache{}和dataset{}結構體中,均有一個mmap對象用以操做內存映射,以及一個系統文件對象dump,對應於打開的磁盤文件。

Ethash算法總結

回看一下Ethash共識算法最基本的形態,若是把整個result[]的生成過程視做那個概念上的函數RAND(),則如何能更加隨機,分佈更加均勻的生成數組,關係到整個Ethash算法的安全性。畢竟若是result[]生成過程存在被破譯的途徑,那麼必然有方法能夠更快地找到符合條件的數組,經過更快的挖掘出區塊,在整個以太坊系統中逐漸佔據主導。因此Ethash共識算法應用了很是複雜的一系列運算,包含了屢次、多種不一樣的哈希函數運算:

  • 大量使用SHA3哈希函數,包括256-bit和512-bit形式的,用它們來對數據(組)做哈希運算,或者充當其餘更復雜哈希計算的某個原型 -- 好比調用makeHasher()。而SHA3哈希函數,是一種典型的可應對長度變化的輸入數據的哈希函數,輸出結果長度統一(可指定256bits或512bits)。

  • lookup()函數提供了非線性表格查找方式的哈希函數,相關聯的dataset{}和cache{}規模巨大,其中數據的生成/填充過程當中也大量使用哈希函數。

  • 在一些計算過程當中,有意將[]byte數組轉化爲uint32或uint64整型數進行操做(好比XOR,以及類XOR的FNV()函數)。由於理論證明,在32位或64位CPU機器上,以32位/64位整型數進行操做時,速度更快。

Clique共識算法

Clique算法又稱Proof-of-Authortiy(PoA),它實現的Seal()實際上是一個標準的數字簽名加密過程,可由下列公式表示:

n = F(pr, h)

其中F()是數字簽名函數,n是生成的數字簽名,pr是公鑰,h是被加密的內容。具體到Clique應用中,n是一個65 bytes長的字符串,pr是一個common.Address類型的(長度20 bytes)地址,h是一個common.Hash類型(32 bytes)的哈希值,而簽名算法F(),目前採用的正是橢圓曲線數字簽名算法(ECDSA)。

沒錯,就是這個被用來生成交易(Transaction)對象的數字簽名的ECDSA。在Clique的實現中,這裏用做公鑰的Address類型地址有一個限制,它必須是已認證的(authorized)。因此Clique.Seal()函數的基本邏輯就是:有一個Address類型地址打算用做數字簽名的公鑰(不是區塊的做者地址Coinbase);若是它是已認證的,則執行指定的數字簽名算法。而其中涉及到的動態管理全部認證地址的機制,纔是Clique算法(PoA)的精髓。

基於投票的地址認證機制

首先了解一下Clique的認證機制authorization所包括的一些設定:

  • 全部的地址(Address類型)分爲兩類,分別是通過認證的,和未通過認證的。

  • 已認證地址(authorized)能夠變成未認證的,反之亦然。不過這些變化都必須經過投票機制完成。

  • 一張投票包括:投票方地址,被投票地址,和被投票地址的新認證狀態。有效投票必須知足:被投票地址的新認證地址與其現狀相反。

  • 任意地值A只能給地址B投一張票

這些設定理解起來並不困難,把這裏的地址替換成日常生活中的普通個體,這就是個很普通的投票制度。Clique算法中的投票系統的巧妙之處在於,每張投票並非某個投票方主動「投」出來的,而是隨機組合出來的。

想了解更多細節免不了要深刻一些代碼,下圖是Clique算法中用到的一些結構體:

image

Clique結構體實現了共識算法接口Engine的全部方法,它可對區塊做Seal操做。它的成員signFn正是數字簽名生成函數,signer用做數字簽名的公鑰,這兩成員均由Authorize()函數進行賦值。它還有一個map類型成員proposals,用來存放全部的不記名投票,即每張投票只帶有被投票地址和投票內容(新認證狀態),因爲是map類型,顯然這裏的proposals存放的是內容不一樣的不記名投票。API結構體對外提供方法,能夠向Clique的成員變量proposals插入或刪除投票。

Snapshot結構體用來動態管理認證地址列表,在這裏認證地址是分批次存儲和更新的,一個Snapshot對象,存放的是以區塊爲時序的全部認證地址的"快照"。Snapshot的成員Number和Hash,正是區塊Block的標誌屬性;成員Signers存儲全部已認證地址。

一個Vote對象表示一張記名投票。Tally結構體用來記錄投票數據,即某個(被投票)地址總共被投了多少票,新認證狀態是什麼。Snapshot中用map型變量Tally來管理全部Tally對象數據,map的key是被投票地址,因此Snapshot.Tally記錄了被投票地址的投票次數。另外Snapshot還有一個Vote對象數組,記錄全部投票數據。

新區塊的Coinbase是一個隨機的被投票地址

Engine接口的Prepare()方法,約定在Header建立後調用,用以對Header的一些成員變量賦值,好比做者地址Coinbase。在Clique算法中,新區塊的Coinbase來自於proposals中的某個被投票地址。

image

上圖解釋了Clique.Prepare()方法中的部分邏輯。首先從proposals中篩選出有效的不記名投票,要麼是已認證地址變爲未認證,要麼反過來;而後獲取有效的被投票地址列表,從中隨機選取一個地址做爲該區塊的Coinbase,與之相應的投票內容則被區塊的Nonce屬性攜帶。而新區塊的Coinbase會在以後的更新認證地址環節,被看成一次投票的被投票地址。

ps,Ethash算法中,新區塊的Coinbase地址但是異常重要的,由於它會做爲新區塊的做者地址,被系統獎勵或補償以太幣。但Clique算法中就徹底不一樣了,因爲工做在測試網絡中,每一個賬號地址得到多少以太幣沒有實際意義,因此這裏的Coinbase任意賦值倒也無妨。

添加記名投票並更新認證地址列表

管理全部認證地址的結構體是Snapshot,具體到更新認證地址列表的方法是apply()。它的基本流程以下圖:

image

重溫一下Snapshot結構體內聲明的一組緩存成員變量:

Signers是所有已認證地址集合,注意這裏用map類型來提供Set的行爲。

Recents用來記錄最近擔當過數字簽名算法的signer的地址,經過它Snapshot能夠控制某個地址不會頻繁的擔當signer。更重要的是,因爲signer地址會充當記名投票的投票方,因此Recents能夠避免某些地址頻繁的充當投票方!Recents中map類型的key是區塊Number值。

Votes記錄了全部還沒有失效的記名投票;Tallies記錄了全部被投票地址(voted)目前的(被)投票次數。

Snapshot.apply()函數的入參是一組Header對象,它們來自區塊主鏈上按從舊到新順序排列的一組區塊,而且嚴格銜接在Snapshot當前狀態(成員Number,Hash)以後。注意,這些區塊都是當前「待挖掘」新區塊的祖先,因此它們的成員屬性都是已經肯定的。apply()方法的主要部分是迭代處理每一個Header對象,處理單個Header的流程以下:

  • 首先從數字簽名中恢復出簽名所用公鑰,轉化爲common.Address類型,做爲signer地址。數字簽名(signagure)長度65 bytes,存放在Header.Extra[]的末尾。

  • 若是signer地址是還沒有認證的,則直接退出本次迭代;若是是已認證的,則記名投票+1。因此一個父區塊可添加一張記名投票,signer做爲投票方地址,Header.Coinbase做爲被投票地址,投票內容authorized可由Header.Nonce取值肯定。

  • 更新投票統計信息。若是被投票地址的總投票次數達到已認證地址個數的一半,則經過之。

  • 該被投票地址的認證狀態當即被更改,根據是何種更改,相應的更新緩存數據,並刪除過期的投票信息。

在全部Header對象都被處理完後,Snapshot內部的Number,Hash值會被更新,代表當前Snapshot快照結構已經更新到哪一個區塊了。

Snapshot.apply()方法在Clique.Seal()中被調用,具體位於運行數字簽名算法以前,以保證即將充當公鑰的地址能夠用最新的認證地址列表加以驗證。

綜上所述,Clique算法在投票制度的安全性設計上完善了諸多細節:

  • 外部參與不記名投票的方式是經過API.Propose(),Discard()來操做Clique.proposals。因爲proposals是map類型,因此每一個投票地址(map的key)在proposals中只能存在一份,這樣就杜絕了外部經過惡意操做Clique.proposals來影響不記名投票數據的企圖。

  • 全部認證地址的動態更新,由一張張記名投票累計做用影響。而每張記名投票的投票方地址和投票內容(不記名投票),是以絕不相關、近乎隨機的方式組合起來的。因此,理論上幾乎不存在外部惡意調用代碼從而操縱記名投票數據的可能。同時,經過一些內部緩存(Snapshot.Recents),避免了某些signer地址過於頻繁的充當投票方地址。

雖然Clique共識算法並不是做用在產品環境,但它依然很精巧的設計了完整的基於投票的選拔制度,很好的踐行了"去中心化"宗旨。這對於其餘類型共識算法的設計,提供了一個不錯的樣本。

3

總結

本篇介紹了挖掘一個新區塊在代碼上的完整過程,從調用函數入口開始,沿調用過程一路向深,直至最終完成新區塊受權/封印的共識算法,並對兩種共識算法的設計思路和實現細節都進行了詳細講解。

  • 通常所說的「挖掘一個新區塊」其實包括兩部分,第一階段組裝出新區塊的全部數據成員,包括交易列表txs、叔區塊uncles等,而且全部交易都已經執行完畢,各賬號狀態更新完畢;第二階段對該區塊進行授勳/封印(Seal),沒有成功Seal的區塊不能被廣播給其餘節點。第二階段所消耗的運算資源,遠超第一階段。

  • Seal過程由共識算法(consensus algorithm)族完成,包括Ethash算法和Clique算法兩種實現。前者是產品環境下真實採用的,後者是針對測試網絡(testnet)使用的。Seal()函數並不會增長或修改區塊中任何跟有效數據有關的部分,它的目的是經過一系列複雜的步驟,或計算或公認,選拔出可以出產新區塊的個體。

  • Ethash算法(PoW)基於運算能力來篩選出挖掘區塊的獲勝者,運算過程當中使用了大量、屢次、多種的哈希函數,經過極高的計算資源消耗,來限制某些節點經過超常規的計算能力輕易造成「中心化」傾向。

  • Clique算法(PoA)利用數字簽名算法完成Seal操做,不過簽名所用公鑰,同時也是common.Address類型的地址必須是已認證的。全部認證地址基於特殊的投票地址進行動態管理,記名投票由不記名投票和投票方地址隨機組合而成,杜絕重複的不記名投票,嚴格限制外部代碼惡意操縱投票數據

  • 在實踐「去中心化」方面,以太坊還在區塊結構中增長了叔區塊(uncles)成員以加大計算資源的消耗,並經過在交易執行環節對叔區塊做者(挖掘者)的獎勵,以收益機制來調動網絡中各節點運算資源分佈更加均勻。

內容來源:博客園 做者:yong374767047

Blockathon|48小時極客競賽,區塊鏈馬拉松等你挑戰(成都)

時間:2018年9月14-16日

地點:成都高新區天府五街200號菁蓉國際廣場2號樓A座12樓中韓互聯網+新技術孵化器

  • 招募50名開發者(識別下圖二維碼或點擊「閱讀原文」便可報名)

  • 報名費100元爲參賽押金,參賽者我的緣由不能到場參加活動概不退款;參賽者全程參與活動,待活動結束後現場退還。9月14日18:00開始第一次簽到,9月15日和16日天天早上都要記得簽到哦。

  • 主辦方免費提供2天的食物、飲料,併爲每一位參會者準備一件文化衫

image
相關文章
相關標籤/搜索