死磕以太坊源碼分析之Ethash共識算法git
代碼分支:https://github.com/ethereum/go-ethereum/tree/v1.9.9github
目前以太坊中有兩個共識算法的實現:clique
和ethash
。而ethash
是目前以太坊主網(Homestead
版本)的POW
共識算法。算法
ethash
模塊位於以太坊項目目錄下的consensus/ethash
目錄下。api
Dagger-Hashimoto
算法的全部功能,好比生成cache
和dataset
、根據Header
和Nonce
計算挖礦哈希等。RPC
使用的api
方法。Verify
系列方法(VerifyHeader
、VerifySeal
等)、Prepare
和Finalize
、CalcDifficulty
、Author
、SealHash
。cache
結構體和dataset
結構體及它們各自的方法、MakeCache
/MakeDataset
函數、Ethash
對象的New
函數,和Ethash
的內部方法。Seal
方法,和Ethash
的內部方法mine
。這些方法實現了ethash
的挖礦功能。以太坊設計共識算法時,指望達到三個目的:數組
ASIC
性:爲算法建立專用硬件的優點應儘量小,讓普通計算機用戶也能使用CPU進行開採。
ASIC
使用礦機內存昂貴)ethash
要計算哈希,須要先有一塊數據集。這塊數據集較大,初始大小大約有1G
,每隔 3 萬個區塊就會更新一次,且每次更新都會比以前變大8M
左右。計算哈希的數據源就是從這塊數據集中來的;而決定使用數據集中的哪些數據進行哈希計算的,纔是header
的數據和Nonce
字段。這部分是由Dagger
算法實現的。緩存
Dagger
算法是用來生成數據集Dataset
的,核心的部分就是Dataset
的生成方式和組織結構。app
能夠把Dataset
想成多個item
(dataItem)組成的數組,每一個item
是64
字節的byte數組(一條哈希)。dataset
的初始大小約爲1G
,每隔3萬個區塊(一個epoch
區間)就會更新一次,且每次更新都會比以前變大8M
左右。異步
Dataset
的每一個item
是由一個緩存塊(cache
)生成的,緩存塊也能夠看作多個item
(cacheItem)組成,緩存塊佔用的內存要比dataset
小得多,它的初始大小約爲16M
。同dataset
相似,每隔 3 萬個區塊就會更新一次,且每次更新都會比以前變大128K
左右。ide
生成一條dataItem
的程是:從緩存塊中「隨機」(這裏的「隨機」不是真的隨機數,而是指事前不能肯定,但每次計算獲得的都是同樣的值)選擇一個cacheItem
進行計算,得的結果參與下次計算,這個過程會循環 256 次。函數
緩存塊是由seed
生成的,而seed
的值與塊的高度有關。因此生成dataset
的過程以下圖所示:
Dagger
還有一個關鍵的地方,就是肯定性。即同一個epoch
內,每次計算出來的seed
、緩存、dataset
都是相同的。不然對於同一個區塊,挖礦的人和驗證的人使用不一樣的dataset
,就無法進行驗證了。
是Thaddeus Dryja
創造的。旨在經過IO
限制來抵制礦機。在挖礦過程當中,使內存讀取限制條件,因爲內存設備自己會比計算設備更加便宜以及廣泛,在內存升級優化方面,全世界的大公司也都投入巨大,以使內存可以適應各類用戶場景,因此有了隨機訪問內存的概念RAM
,所以,現有的內存可能會比較接近最優的評估算法。Hashimoto
算法使用區塊鏈做爲源數據,知足了上面的 1 和 3 的要求。
它的做用就是使用區塊Header的哈希和Nonce字段、利用dataset數據,生成一個最終的哈希值。
generate
函數位於ethash.go
文件中,主要是爲了生成dataset
,其中包擴如下內容。
cache size
主要某個特定塊編號的ethash驗證緩存的大小 *, epochLength
爲 30000,若是epoch
小於 2048,則從已知的epoch
返回相應的cache size
,不然從新計算epoch
cache
的大小是線性增加的,size
的值等於(224 + 217 * epoch - 64),用這個值除以 64 看結果是不是一個質數,若是不是,減去128 再從新計算,直到找到最大的質數爲止。
csize := cacheSize(d.epoch*epochLength + 1)
func cacheSize(block uint64) uint64 { epoch := int(block / epochLength) if epoch < maxEpoch { return cacheSizes[epoch] } return calcCacheSize(epoch) }
func calcCacheSize(epoch int) uint64 { size := cacheInitBytes + cacheGrowthBytes*uint64(epoch) - hashBytes for !new(big.Int).SetUint64(size / hashBytes).ProbablyPrime(1) { // Always accurate for n < 2^64 size -= 2 * hashBytes } return size }
dataset Size
主要某個特定塊編號的ethash驗證緩存的大小 , 相似上面生成cache size
dsize := datasetSize(d.epoch*epochLength + 1)
func datasetSize(block uint64) uint64 { epoch := int(block / epochLength) if epoch < maxEpoch { return datasetSizes[epoch] } return calcDatasetSize(epoch) }
seedHash是用於生成驗證緩存和挖掘數據集的種子。長度爲 32。
seed := seedHash(d.epoch*epochLength + 1)
func seedHash(block uint64) []byte { seed := make([]byte, 32) if block < epochLength { return seed } keccak256 := makeHasher(sha3.NewLegacyKeccak256()) for i := 0; i < int(block/epochLength); i++ { keccak256(seed, seed) } return seed }
generateCache(cache, d.epoch, seed)
接下來分析generateCache
的關鍵代碼:
先了解一下hashBytes,在下面的計算中都是以此爲單位,它的值爲 64 ,至關於一個keccak512
哈希的長度,下文以item稱呼[hashBytes]byte
。
①:初始化cache
此循環用來初始化cache
:先將seed
的哈希填入cache
的第一個item
,隨後使用前一個item
的哈希,填充後一個item
。
for offset := uint64(hashBytes); offset < size; offset += hashBytes { keccak512(cache[offset:], cache[offset-hashBytes:offset]) atomic.AddUint32(&progress, 1) }
②:對cache中數據按規則作異或
爲對於每個item
(srcOff
),「隨機」選一個item
(xorOff
)與其進行異或運算;將運算結果的哈希寫入dstOff
中。這個運算邏輯將進行cacheRounds
次。
兩個須要注意的地方:
srcOff
是從尾部向頭部變化的,而dstOff
是從頭部向尾部變化的。而且它倆是對應的,即當srcOff
表明倒數第x個item時,dstOff
則表明正數第x個item。xorOff
的選取。注意咱們剛纔的「隨機」是打了引號的。xorOff
的值看似隨機,由於在給出seed
以前,你沒法知道xorOff的值是多少;但一旦seed
的值肯定了,那麼每一次xorOff
的值都是肯定的。而seed的值是由區塊的高度決定的。這也是同一個epoch
內老是能獲得相同cache
數據的緣由。for i := 0; i < cacheRounds; i++ { for j := 0; j < rows; j++ { var ( srcOff = ((j - 1 + rows) % rows) * hashBytes dstOff = j * hashBytes xorOff = (binary.LittleEndian.Uint32(cache[dstOff:]) % uint32(rows)) * hashBytes ) bitutil.XORBytes(temp, cache[srcOff:srcOff+hashBytes], cache[xorOff:xorOff+hashBytes]) keccak512(cache[dstOff:], temp) atomic.AddUint32(&progress, 1) } }
dataset
大小的計算和cache
相似,量級不一樣:230 + 223 * epoch - 128,而後每次減256尋找最大質數。
生成數據是一個循環,每次生成64個字節,主要的函數是generateDatasetItem
:
generateDatasetItem
的數據來源就是cache
數據,而最終的dataset值會存儲在mix變量中。整個過程也是由多個循環構成。
①:初始化mix
變量
根據cache值對mix
變量進行初始化。其中hashWords
表明的是一個hash
裏有多少個word
值:一個hash
的長度爲hashBytes
即64字節,一個word
(uint32類型)的長度爲 4 字節,所以hashWords
值爲 16。選取cache
中的哪一項數據是由參數index
和i
變量決定的。
mix := make([]byte, hashBytes) binary.LittleEndian.PutUint32(mix, cache[(index%rows)*hashWords]^index) for i := 1; i < hashWords; i++ { binary.LittleEndian.PutUint32(mix[i*4:], cache[(index%rows)*hashWords+uint32(i)]) } keccak512(mix, mix)
②:將mix
轉換成[]uint32
類型
intMix := make([]uint32, hashWords) for i := 0; i < len(intMix); i++ { intMix[i] = binary.LittleEndian.Uint32(mix[i*4:]) }
③:將cache
數據聚合進intmix
for i := uint32(0); i < datasetParents; i++ { parent := fnv(index^i, intMix[i%16]) % rows fnvHash(intMix, cache[parent*hashWords:]) }
FNV
哈希算法,是一種不須要使用密鑰的哈希算法。
這個算法很簡單:a乘以FNV質數0x01000193,而後再和b異或。
首先用這個算法算出一個索引值,利用這個索引從cache
中選出一個值(data
),而後對mix
中的每一個字節都計算一次FNV
,獲得最終的哈希值。
func fnv(a, b uint32) uint32 { return a*0x01000193 ^ b } func fnvHash(mix []uint32, data []uint32) { for i := 0; i < len(mix); i++ { mix[i] = mix[i]*0x01000193 ^ data[i] } }
④:將intMix
又恢復成mix
並計算mix
的哈希返回
for i, val := range intMix { binary.LittleEndian.PutUint32(mix[i*4:], val) } keccak512(mix, mix) return mix
generateCache
和generateDataset
是實現Dagger
算法的核心函數,到此整個生成哈希數據集的的過程結束。
代碼位於consensus.go
①:Author
// 返回coinbase, coinbase是打包第一筆交易的礦工的地址 func (ethash *Ethash) Author(header *types.Header) (common.Address, error) { return header.Coinbase, nil }
②:VerifyHeader
主要有兩步檢查,第一步檢查header是否已知或者是未知的祖先,第二步是ethash
的檢查:
2.1 header.Extra 不能超過32字節
if uint64(len(header.Extra)) > params.MaximumExtraDataSize { // 不超過32字節 return fmt.Errorf("extra-data too long: %d > %d", len(header.Extra), params.MaximumExtraDataSize) }
2.2 時間戳不能超過15秒,15秒之後的就被認定爲將來的塊
if !uncle { if header.Time > uint64(time.Now().Add(allowedFutureBlockTime).Unix()) { return consensus.ErrFutureBlock } }
2.3 當前header的時間戳小於父塊的
if header.Time <= parent.Time { // 當前header的時間小於等於父塊的 return errZeroBlockTime }
2.4 根據時間戳和父塊的難度來驗證塊的難度
expected := ethash.CalcDifficulty(chain, header.Time, parent) if expected.Cmp(header.Difficulty) != 0 { return fmt.Errorf("invalid difficulty: have %v, want %v", header.Difficulty, expected) }
2.5驗證gas limit
小於263 -1
cap := uint64(0x7fffffffffffffff) if header.GasLimit > cap { return fmt.Errorf("invalid gasLimit: have %v, max %v", header.GasLimit, cap) }
2.6 確認gasUsed
爲<= gasLimit
if header.GasUsed > header.GasLimit { return fmt.Errorf("invalid gasUsed: have %d, gasLimit %d", header.GasUsed, header.GasLimit) }
2.7 驗證塊號是父塊加1
if diff := new(big.Int).Sub(header.Number, parent.Number); diff.Cmp(big.NewInt(1)) != 0 { return consensus.ErrInvalidNumber }
2.8檢查給定的塊是否知足pow難度要求
if seal { if err := ethash.VerifySeal(chain, header); err != nil { return err } }
③:VerifyUncles
3.1叔叔塊最多兩個
if len(block.Uncles()) > maxUncles { return errTooManyUncles }
3.2收集叔叔塊和祖先塊
number, parent := block.NumberU64()-1, block.ParentHash() for i := 0; i < 7; i++ { ancestor := chain.GetBlock(parent, number) if ancestor == nil { break } ancestors[ancestor.Hash()] = ancestor.Header() for _, uncle := range ancestor.Uncles() { uncles.Add(uncle.Hash()) } parent, number = ancestor.ParentHash(), number-1 } ancestors[block.Hash()] = block.Header() uncles.Add(block.Hash())
3.3 確保叔塊只被獎勵一次且叔塊有個有效的祖先
for _, uncle := range block.Uncles() { // Make sure every uncle is rewarded only once hash := uncle.Hash() if uncles.Contains(hash) { return errDuplicateUncle } uncles.Add(hash) // Make sure the uncle has a valid ancestry if ancestors[hash] != nil { return errUncleIsAncestor } if ancestors[uncle.ParentHash] == nil || uncle.ParentHash == block.ParentHash() { return errDanglingUncle } if err := ethash.verifyHeader(chain, uncle, ancestors[uncle.ParentHash], true, true); err != nil { return err }
④:Prepare
初始化
header
的Difficulty
字段
parent := chain.GetHeader(header.ParentHash, header.Number.Uint64()-1) if parent == nil { return consensus.ErrUnknownAncestor } header.Difficulty = ethash.CalcDifficulty(chain, header.Time, parent) return nil
⑤:Finalize
會執行交易後的全部狀態修改(例如,區塊獎勵),但不會組裝該區塊。
5.1累積任何塊和叔塊的獎勵
accumulateRewards(chain.Config(), state, header, uncles)
5.2計算狀態樹的根哈希並提交到header
header.Root = state.IntermediateRoot(chain.Config().IsEIP158(header.Number))
⑥:FinalizeAndAssemble
運行任何交易後狀態修改(例如,塊獎勵),並組裝最終塊。
func (ethash *Ethash) FinalizeAndAssemble(chain consensus.ChainReader, header *types.Header, state *state.StateDB, txs []*types.Transaction, uncles []*types.Header, receipts []*types.Receipt) (*types.Block, error) { accumulateRewards(chain.Config(), state, header, uncles) header.Root = state.IntermediateRoot(chain.Config().IsEIP158(header.Number)) return types.NewBlock(header, txs, uncles, receipts), nil }
很明顯就是比Finalize
多了 types.NewBlock
⑦:SealHash
返回在seal
以前塊的哈希(會跟seal
以後的塊哈希不一樣)
func (ethash *Ethash) SealHash(header *types.Header) (hash common.Hash) { hasher := sha3.NewLegacyKeccak256() rlp.Encode(hasher, []interface{}{ header.ParentHash, header.UncleHash, header.Coinbase, header.Root, header.TxHash, header.ReceiptHash, header.Bloom, header.Difficulty, header.Number, header.GasLimit, header.GasUsed, header.Time, header.Extra, }) hasher.Sum(hash[:0]) return hash }
⑧:Seal
給定的輸入塊生成一個新的密封請求(挖礦),並將結果推送到給定的通道中。
注意,該方法將當即返回並將異步發送結果。 根據共識算法,可能還會返回多個結果。這部分會在下面的挖礦中具體分析,這裏跳過。
你們在閱讀本文時有任何疑問都可留言給我,我必定會及時回覆。若是以爲寫得不錯能夠關注最下方參考的
github項目
,能夠第一時間關注做者文章動態。
挖礦的核心接口定義:
Seal(chain ChainReader, block *types.Block, results chan<- *types.Block, stop <-chan struct{}) error
進入到seal
函數:
①:若是運行錯誤的POW
,直接返回空的nonce
和MixDigest
,同時塊也是空塊。
if ethash.config.PowMode == ModeFake || ethash.config.PowMode == ModeFullFake { header := block.Header() header.Nonce, header.MixDigest = types.BlockNonce{}, common.Hash{} select { case results <- block.WithSeal(header): default: ethash.config.Log.Warn("Sealing result is not read by miner", "mode", "fake", "sealhash", ethash.SealHash(block.Header())) } return nil }
②:共享pow
的話,則轉到它的共享對象執行Seal
操做
if ethash.shared != nil { return ethash.shared.Seal(chain, block, results, stop) }
③:獲取種子源,並根據其生成ethash
須要的種子
f ethash.rand == nil { // 得到種子 seed, err := crand.Int(crand.Reader, big.NewInt(math.MaxInt64)) if err != nil { ethash.lock.Unlock() return err } ethash.rand = rand.New(rand.NewSource(seed.Int64())) // 給rand賦值 }
④:挖礦的核心工做交給mine
for i := 0; i < threads; i++ { pend.Add(1) go func(id int, nonce uint64) { defer pend.Done() ethash.mine(block, id, nonce, abort, locals) // 真正執行挖礦的動做 }(i, uint64(ethash.rand.Int63())) }
⑤:處理挖礦的結果
go func() { var result *types.Block select { case <-stop: close(abort) case result = <-locals: select { case results <- result: //其中一個線程挖到正確塊,停止其餘全部線程 default: ethash.config.Log.Warn("Sealing result is not read by miner", "mode", "local", "sealhash", ethash.SealHash(block.Header())) } close(abort) case <-ethash.update: close(abort) if err := ethash.Seal(chain, block, results, stop); err != nil { ethash.config.Log.Error("Failed to restart sealing after update", "err", err) } }
由上能夠知道seal
的核心工做是由mine
函數完成的,重點介紹一下。
mine
函數其實也比較簡單,它是真正的pow
礦工,用來搜索一個nonce
值,nonce
值開始於seed
值,seed
值是能最終產生正確的可匹配可驗證的區塊難度
①:從區塊頭中提取相關數據,放在全局變量域中
var ( header = block.Header() hash = ethash.SealHash(header).Bytes() target = new(big.Int).Div(two256, header.Difficulty) // 這是用來驗證的target number = header.Number.Uint64() dataset = ethash.dataset(number, false) )
②:開始產生隨機nonce
,直到咱們停止或找到一個好的nonce
var ( attempts = int64(0) nonce = seed )
③: 彙集完整的dataset
數據,爲特定的header和nonce產生最終哈希值
func hashimotoFull(dataset []uint32, hash []byte, nonce uint64) ([]byte, []byte) { //定義一個lookup函數,用於在數據集中查找數據 lookup := func(index uint32) []uint32 { offset := index * hashWords //hashWords是上面定義的常量值= 16 return dataset[offset : offset+hashWords] } return hashimoto(hash, nonce, uint64(len(dataset))*4, lookup) }
能夠發現實際上hashimotoFull
函數作的工做就是將原始數據集進行了讀取分割,而後傳給hashimoto
函數。接下來重點分析hashimoto
函數:
3.1根據seed獲取區塊頭
rows := uint32(size / mixBytes) ① seed := make([]byte, 40) ② copy(seed, hash) ③ binary.LittleEndian.PutUint64(seed[32:], nonce)④ seed = crypto.Keccak512(seed)⑤ seedHead := binary.LittleEndian.Uint32(seed)⑥
header+nonce
到一個 40 字節的seed
hash
拷貝到seed
中nonce
值填入seed
的後(40-32=8)字節中去,(nonce自己就是uint64
類型,是 64 位,對應 8 字節大小),正好把hash
和nonce
完整的填滿了 40 字節的 seedKeccak512
加密seed
seed
中獲取區塊頭3.2 從複製的種子開始混合
mixBytes
常量= 128,mix
的長度爲 32,元素爲uint32
,是 32位,對應爲 4 字節大小。因此mix
總共大小爲 4*32=128 字節大小mix := make([]uint32, mixBytes/4) for i := 0; i < len(mix); i++ { mix[i] = binary.LittleEndian.Uint32(seed[i%16*4:]) }
3.3 混合隨機數據集節點
temp := make([]uint32, len(mix))//與mix結構相同,長度相同 for i := 0; i < loopAccesses; i++ { parent := fnv(uint32(i)^seedHead, mix[i%len(mix)]) % rows for j := uint32(0); j < mixBytes/hashBytes; j++ { copy(temp[j*hashWords:], lookup(2*parent+j)) } fnvHash(mix, temp) }
3.4 壓縮混合
for i := 0; i < len(mix); i += 4 { mix[i/4] = fnv(fnv(fnv(mix[i], mix[i+1]), mix[i+2]), mix[i+3]) } mix = mix[:len(mix)/4] digest := make([]byte, common.HashLength) for i, val := range mix { binary.LittleEndian.PutUint32(digest[i*4:], val) } return digest, crypto.Keccak256(append(seed, digest...))
最終返回的是digest
和digest
與seed
的哈希;而digest
其實就是mix
的[]byte
形式。在前面Ethash.mine
的代碼中咱們已經看到使用第二個返回值與target
變量進行比較,以肯定這是不是一個有效的哈希值。
挖礦信息的驗證有兩部分:
Header.Difficulty
是否正確Header.MixDigest
和Header.Nonce
是否正確①:驗證Header.Difficulty
的代碼主要在Ethash.verifyHeader
中:
func (ethash *Ethash) verifyHeader(chain consensus.ChainReader, header, parent *types.Header, uncle bool, seal bool) error { ...... expected := ethash.CalcDifficulty(chain, header.Time.Uint64(), parent) if expected.Cmp(header.Difficulty) != 0 { return fmt.Errorf("invalid difficulty: have %v, want %v", header.Difficulty, expected) } }
經過區塊高度和時間差做爲參數來計算Difficulty
值,而後與待驗證的區塊的Header.Difficulty
字段進行比較,若是相等則認爲是正確的。
②:MixDigest
和Nonce
的驗證主要是在Header.verifySeal
中:
驗證的方式:使用Header.Nonce
和頭部哈希經過hashimoto
從新計算一遍MixDigest
和result
哈希值,而且驗證的節點是不須要dataset數據的。
https://github.com/blockchainGuide ☆☆☆
https://eth.wiki/concepts/ethash/design-rationale
https://eth.wiki/concepts/ethash/dag
https://www.vijaypradeep.com/blog/2017-04-28-ethereums-memory-hardness-explained/