以太坊挖礦源碼:ethash算法

鏈客,專爲開發者而生,有問必答!html

此文章來自區塊鏈技術社區,未經容許拒絕轉載。
圖片描述java

Ethash
前面咱們分析了以太坊挖礦的源碼,挖了一個共識引擎的坑,研究了DAG有向無環圖的算法,這些都是本文要研究的Ethash的基礎。Ethash是目前以太坊基於POW工做量證實的一個共識引擎(也叫挖礦算法)。它的前身是Dagger Hashimoto算法。golang

Dagger Hashimoto
做爲以太坊挖礦算法Ethash的前身,Dagger Hashimoto的目的是:算法

抵制礦機(ASIC,專門用於挖礦的芯片)
輕客戶端驗證
全鏈數據存儲
Dagger和Hashimoto實際上是兩個東西,編程

Hashimoto算法
是這我的Thaddeus Dryja創造的。旨在經過IO限制來抵制礦機。在挖礦過程當中,使內存讀取限制條件,因爲內存設備自己會比計算設備更加便宜以及廣泛,在內存升級優化方面,全世界的大公司也都投入巨大,以使內存可以適應各類用戶場景,因此有了隨機訪問內存的概念RAM,所以,現有的內存可能會比較接近最優的評估算法。Hashimoto算法使用區塊鏈做爲源數據,知足了上面的1和3的要求。json

Dagger算法
是這我的Vitalik Buterin發明的。它利用了有向無環圖DAG同時實現了Memory-Hard Function內存計算困難但易於驗證Memory-easy verification的特性(咱們知道這是哈希算法的重要特性之一)。它的理論依據是基於每一個特定場合nonce只須要大型數據總量樹的一小部分,而且針對每一個特定場合nonce的子樹的再計算是被禁止挖礦的。所以,須要存儲樹但也支持一個獨立場合nonce的驗證價值。Dagger算法註定要替代現存的僅內存計算困難的算法,例如Scrypt(萊特幣採用的),它是計算困難同時驗證亦困難的算法,當他們的內存計算困難度增長至真正安全的水平,驗證的困難度也隨之難上加難。然而,Dagger算法被證實是容易受到Sergio Lerner發明的共享內存硬件加速技術,隨後在其餘路徑的研究方面,該算法被遺棄了。數組

Memory-Hard Function
直接翻譯過來是內存困難函數,這是爲了地址礦機而誕生的一種思想。咱們知道挖礦是靠咱們的電腦,可是有些硬件廠商會製造專門用於挖礦的硬件設備,它們並非一臺完整的PC機,例如ASIC、GPU以及FPGAs(咱們常常能聽到GPU挖礦等)。因此這些做爲礦機的設備是超越普通PC挖礦的存在,這是不符合咱們區塊鏈的去中心化精神的,因此咱們要讓挖礦設備平等。緩存

那麼該如何讓挖礦設備是平等的呢?安全

上面談到Dagger算法的時候其實提到了,這裏換一種方式再來介紹一下,如今CPU都是多核的,若是從計算能力來說,CPU有幾核就能夠模擬幾臺設備同時平行挖礦,天然效率就高些,可是這裏採用的衡量對象是內存,一臺電腦只有一個總內存。咱們作過java多線程開發的朋友知道,不管機器性能有多高,但咱們寫的程序就是單線程的,那麼這個程序運行在高配多核電腦和低配單核電腦的區別不大,只要他們的單核運算能力和內存大小同樣便可。因此也是這個原理,經過Dagger算法,咱們將挖礦流程鎖定在之內存爲衡量標準的硬件性能上,只要經過「塞一堆數據到內存中」的方式,讓多核平行處理髮揮不出來,下降硬件的運算優點,只與內存大小有關,這樣不管是PC機仍是ASIC、GPU以及FPGAs,均可達到平等挖礦的訴求,這也是ASIC-resistant原理,目前抵制礦機的主要手段。網絡

兩個問題的研究
在Dagger以及Dagger Hashimoto算法中,有兩個問題的研究是被擱置的,

基於區塊鏈的工做量證實:一個POW函數包括了運行區塊鏈上的合約。該方法被拋棄是由於這是一個長期的攻擊缺陷,由於攻擊者可以建立分叉,而後經過一個包含祕密的快速「trapdoor」井蓋門的運行機制的合約在該分叉上殖民。
隨機環路:一個POW函數由這我的Vlad Zamfir開發,包含了每1000個場合nonces就生成一個新的程序的功能。本質上來說,每次選擇一個新的哈希函數,會比可重配置的FPGAs(可重編程的芯片,沒必要從新焊接電路板就可經過軟件技術從新自定義硬件功能)更快。該方法被暫時擱置,是由於它很難看到有什麼機制能夠用來生成隨機程序是足夠全面,所以它的專業化收益是較低的。然而,咱們並無看到爲何這個概念沒法讓它生效的根本緣由,因此暫時擱置。
Dagger Hashimoto算法
(區別於Hashimoto)Dagger Hashimoto不是直接將區塊鏈做爲數據源,而是使用一個1GB的自定義生成的數據集cache。

這個數據集是基於區塊數據每N個塊就會更新。該數據集是使用Dagger算法生成,容許一個本身的高效計算,特定於每一個輕客戶端校驗算法的場合nonce。

(區別於Dagger)Dagger Hashimoto克服了Dagger的缺陷,它用於查詢區塊數據的數據集是半永久的,只有在偶然的間隔纔會被更新(例如每週一次)。這意味着生成數據集將很是容易,因此Sergio Lerner的爭議共享內存加速變得微不足道了。

挖礦補充
前面我已經寫了一盤關於挖礦的文章了,這一節是挖礦的補充內容。

以太坊將過渡到POS(proof-of-stake),代替傳統的POW,挖礦將會被淘汰掉,因此如今不推薦再去作一名礦工(前期購買設備等成本較大,POS實現前未必能回本)。

挖掘以太幣=網絡安全=驗證估算

目前以太坊的POW算法是Ethash,

Ethash算法包含找到一個nonce值輸入到一個算法中,獲得的結果是低於一個基於特定困難度的閥值。

POW算法的關鍵點是除了暴力枚舉,沒有任何辦法能夠找到這個nonce值,但對於驗證輸出的結果是很是簡單容易的。若是輸出結果有一個均勻分佈,咱們就能夠保證找到一個nonce值的平均所需時間取決於那個難度閥值,所以咱們能夠經過調整難度閥值來控制找到一個新塊的時間,這就是控制出塊速度的原理。

DAG
Ethash的POW是memory-hard,支持礦機抵禦。這意味着POW計算須要選擇一個固定的依賴於nonce值和塊頭的資源的子集。

這個資源(大約1G大小)就是DAG!

一世epoch
每3萬個塊會花幾個小時的時間生成一個有向無環圖DAG。這個DAG被稱爲epoch,一世(爲了好記,refer個秦二世)。DAG只取決於區塊高度,它能夠被預生成,若是沒有預生成的話,客戶端須要等待預生成流程結束之後才能繼續出塊操做。除非客戶端真實的提早預緩存了DAG,不然在每一個epoch的過渡期間,網絡可能會經歷一個巨大的區塊延遲。

特例:當你從頭啓動一個結點時,挖礦工做只會在建立了現世DAG之後啓動。

挖礦獎勵
有三部分:

靜態區塊建立獎勵,精確發放3以太幣做爲獎勵。
當前區塊包含的全部交易的gas錢,隨着時間推移,gas會愈來愈便宜,得到的gas總和獎勵會低於靜態區塊建立獎勵。
叔塊獎勵,整塊獎勵的1/32。
Ethash
Ethash算法路線圖:

存在一個種子seed,經過掃描塊頭爲每一個塊計算出來那個點。
根據這個種子seed,能夠計算一個16MB的僞隨機緩存cache,輕客戶端存儲這個緩存。
從這個緩存cache中,咱們可以生成一個1GB的數據集,該數據集中的每一項都取決於緩存中的一小部分。完整客戶端和礦工存儲了這個數據集,數據集隨着時間線性增加。
挖礦工做包含了抓取數據集的隨機片以及運用哈希函數計算他們。校驗工做可以在低內存的環境下完成,經過使用緩存再次生成所需的特性數據集的片斷,因此你只須要存儲緩存cache便可。
以上提到的大數據集是每3萬個塊更新一次,因此絕大多數的礦工的工做是讀取該數據集而不是改變它。

pkg ethash源碼分析
以上咱們將全部的概念抽象梳理了一下,包括POW,挖礦,Ethash原理流程等,下面咱們帶着這些理論知識走進源代碼中去分析具體的實現。正如咱們的題目,本文主要分析的是ethash算法,所以整個源碼範圍僅限於go-ethereum/consensus/ethash包,該包實現了ethash pow的共識引擎。

入口
分析源碼要有個入口,這個入口就是在《以太坊源碼機制:挖礦》中挖下的坑「Seal方法」,原文留下了這個印子,在本文進行展開討論。

在go-ethereum/consensus/consensus.go 接口中定義了以下的方法,正是對應上面的「Seal方法」,該接口方法的定義以下:

Seal(chain ChainReader, block types.Block, stop <-chan struct{}) (types.Block, error)//該方法經過輸入一個包含本地礦工挖出的最高區塊在主幹上生成一個新塊。
參數有ChainReader,Block,stop結構體信號,返回一個主鏈上的新出的塊實體。

ChainReader
// 定義了一些方法,用於在區塊頭驗證以及叔塊驗證期間,訪問本地區塊鏈。
type ChainReader interface {

// 獲取區塊鏈的鏈配置
Config() *params.ChainConfig

// 從本地鏈獲取當前塊頭
CurrentHeader() *types.Header

// 經過hash和number從主鏈中獲取一個區塊頭
GetHeader(hash common.Hash, number uint64) *types.Header

// 經過number從主鏈中獲取一個區塊頭
GetHeaderByNumber(number uint64) *types.Header

// 經過hash從主鏈中獲取一個區塊頭
GetHeaderByHash(hash common.Hash) *types.Header

// 經過hash和number從主鏈中獲取一個區塊
GetBlock(hash common.Hash, number uint64) *types.Block

}
總結,ChainReader定義了幾個方法:從本地區塊鏈獲取配置、區塊頭,從主鏈中獲取區塊頭、區塊,參數條件包括hash和number,隨意組合。

Block
// Block表明以太坊區塊鏈中的一個完整的區塊
type Block struct {

header       *Header // 區塊包括頭
uncles       []*Header // 叔塊
transactions Transactions // 交易集合

// caches緩存
hash atomic.Value
size atomic.Value

// Td用於core包存儲全部的鏈上的難度
td *big.Int

// 這些字段用於eth包來跟蹤inter-peer內部端點區塊的接替
ReceivedAt   time.Time
ReceivedFrom interface{}

}
總結,Block除了咱們熟知的區塊中必有的區塊頭、叔塊以及打包存儲的交易信息,還有cache緩存的內容,以及每一個塊之於鏈的難度值,還有用於跟蹤內部端點的字段。

stop
stop是一個空結構體做爲信號源。

關於空結構體的討論,爲何go裏面常常出現struct{}?

go中除了struct{}類型之外,其餘類型都是width,佔有存儲,而struct{}沒有字段,沒有方法,width爲0,靈活性高,不佔內存空間,這多是讓Gopher青睞的緣由。

sealer
seal方法有兩個實現,咱們選擇ethash,該方法存在於consensus/ethash/sealer.go文件中,第一個函數就是seal的實現,先來看該方法的聲明部分:

// 嘗試找到一個nonce值可以知足區塊難度需求。
func (ethash Ethash) Seal(chain consensus.ChainReader, block types.Block, stop <-chan struct{}) (*types.Block, error) {
能夠看出這個方法是屬於Ethash的指針對象的,

type Ethash struct {

// cache配置
cachedir     string // 緩存位置
cachesinmem  int    // 在內存中緩存的數量
cachesondisk int    // 在硬盤中緩存的數量

// DAG挖礦數據集配置
dagdir       string // DAG位置,存儲所有挖礦數據集
dagsinmem    int    // 在內存中DAG的數量
dagsondisk   int    // 在硬盤中DAG的數量

// 內存cache
caches   map[uint64]*cache   // 內存緩存,可反覆使用避免再生太頻繁
fcache   *cache              // 爲了下一世估算的預生產緩存

// 內存數據集
datasets map[uint64]*dataset // 內存數據集,可反覆使用避免再生太頻繁
fdataset *dataset            // 爲了下一世估算的預生產數據集

// 挖礦相關字段
rand     *rand.Rand    // 隨機工具,用來爲nonce作適當的種子
threads  int           // 若是在挖礦,表明挖礦的線程編號
update   chan struct{} // 更新挖礦中參數的通道
hashrate metrics.Meter // 測量跟蹤平均哈希率

// 如下字段是用於測試
tester    bool          // 是否使用一個小型測試數據集的標誌位
shared    *Ethash       // 共享pow模式,沒法再生緩存
fakeMode  bool          // Fake模式,是否取消POW檢查的標誌位
fakeFull  bool          // 是否取消全部共識規則的標誌位
fakeFail  uint64        // 未經過POW檢查的區塊號(包含fake模式)
fakeDelay time.Duration // 驗證工做返回消息前的休眠延遲時間

lock sync.Mutex // 爲了內存中的緩存和挖礦字段,保證線程安全

}
爲了更好的讀懂以後的代碼,咱們要對區塊頭的數據結構進行一個分析:

type Header struct {

ParentHash  common.Hash    `json:"parentHash"       gencodec:"required"`
UncleHash   common.Hash    `json:"sha3Uncles"       gencodec:"required"`
Coinbase    common.Address `json:"miner"            gencodec:"required"`
Root        common.Hash    `json:"stateRoot"        gencodec:"required"`
TxHash      common.Hash    `json:"transactionsRoot" gencodec:"required"`
ReceiptHash common.Hash    `json:"receiptsRoot"     gencodec:"required"`
Bloom       Bloom          `json:"logsBloom"        gencodec:"required"`
Difficulty  *big.Int       `json:"difficulty"       gencodec:"required"`
Number      *big.Int       `json:"number"           gencodec:"required"`
GasLimit    *big.Int       `json:"gasLimit"         gencodec:"required"`
GasUsed     *big.Int       `json:"gasUsed"          gencodec:"required"`
Time        *big.Int       `json:"timestamp"        gencodec:"required"`
Extra       []byte         `json:"extraData"        gencodec:"required"`
MixDigest   common.Hash    `json:"mixHash"          gencodec:"required"`
Nonce       BlockNonce     `json:"nonce"            gencodec:"required"`

}
能夠看到一個區塊頭包含了父塊hash值,叔塊hash值,Coinbase結點帳戶地址,狀態根,交易hash,接受者hash,日誌,難度值,塊編號,最低支付gas,花費的gas,時間戳,額外數據,混合hash,nonce值(8個byte)。咱們要對這些區塊頭的成員屬性瞭然於胸,後面的源碼內容才能更好的理解。下面咱們繼續Seal方法,下面展現完整代碼:

func (ethash Ethash) Seal(chain consensus.ChainReader, block types.Block, stop <-chan struct{}) (*types.Block, error) {

// fake模式當即返回0 nonce
if ethash.fakeMode {
    header := block.Header()
    header.Nonce, header.MixDigest = types.BlockNonce{}, common.Hash{}
    return block.WithSeal(header), nil
}
// 共享pow的話,則轉到它的共享對象執行Seal操做
if ethash.shared != nil {
    return ethash.shared.Seal(chain, block, stop)
}
// 建立一個runner以及它指揮的多重搜索線程
abort := make(chan struct{})
found := make(chan *types.Block)

ethash.lock.Lock() // 線程上鎖,保證內存的緩存(包含挖礦字段)安全
threads := ethash.threads // 挖礦的線程s
if ethash.rand == nil {// rand爲空,則爲ethash的字段rand賦值
    // 得到種子
    seed, err := crand.Int(crand.Reader, big.NewInt(math.MaxInt64))
    if err != nil {// 執行失敗,有報錯
        ethash.lock.Unlock() // 先解鎖
        return nil, err // 程序停止,直接返回空塊和報錯信息
    }
    ethash.rand = rand.New(rand.NewSource(seed.Int64())) // 執行成功,拿到合法種子seed,經過其得到rand對象,賦值。
}
ethash.lock.Unlock() // 解鎖
if threads == 0 {// 挖礦線程編號爲0,則經過方法返回當前物理上可用CPU編號
    threads = runtime.NumCPU()
}
if threads < 0 { // 非法結果
    threads = 0 // 置爲0,容許在本地或遠程沒有額外邏輯的狀況下,取消本地挖礦操做
}
var pend sync.WaitGroup // 建立一個倒計時鎖對象,go語法參照 http://www.cnblogs.com/Evsward/p/goPipeline.html#sync.waitgroup
for i := 0; i < threads; i++ {
    pend.Add(1)
    go func(id int, nonce uint64) {// 核心代碼經過閉包多線程技術來執行。
        defer pend.Done()
        ethash.mine(block, id, nonce, abort, found) // Seal核心工做
    }(i, uint64(ethash.rand.Int63()))//閉包第二個參數表達式uint64(ethash.rand.Int63())經過上面準備好的rand函數隨機數結果做爲nonce實參傳入方法體
}
// 直到seal操做被停止或者找到了一個nonce值,不然一直等
var result *types.Block // 定義一個區塊對象result,用於接收操做結果並做爲返回值返回上一層
select { // go語法參照 http://www.cnblogs.com/Evsward/p/go.html#select
case <-stop:
    // 外部意外停止,中止全部挖礦線程
    close(abort)
case result = <-found:
    // 其中一個線程挖到正確塊,停止其餘全部線程
    close(abort)
case <-ethash.update:
    // ethash對象發生改變,中止當前全部操做,重啓當前方法
    close(abort)
    pend.Wait()
    return ethash.Seal(chain, block, stop)
}
// 等待全部礦工中止或者返回一個區塊
pend.Wait()
return result, nil

}
以上Seal方法體,針對ethash的各類狀態進行了校驗和流程處理,以及對線程資源的控制,下面看Seal核心工做的內容(sealer.go文件只有兩個函數,一個是Seal方法,另外一個就是mine方法,能夠看出Seal方法是對外的,而mine方法是內部方法,只能被當前ethash包域調用):mine方法

// mine函數是真正的pow礦工,用來搜索一個nonce值,nonce值開始於seed值,seed值是能最終產生正確的可匹配可驗證的區塊難度
func (ethash Ethash) mine(block types.Block, id int, seed uint64, abort chan struct{}, found chan *types.Block) {

// 從區塊頭中提取出一些數據,放在一個全局變量域中
var (
    header = block.Header()
    hash   = header.HashNoNonce().Bytes()
    target = new(big.Int).Div(maxUint256, header.Difficulty) // 後面有大用,這是用來驗證的target

    number  = header.Number.Uint64()
    dataset = ethash.dataset(number)
)
// 開始生成隨機nonce值知道咱們停止或者成功找到了一個合適的值
var (
    attempts = int64(0) // 初始化一個嘗試次數的變量,下面會利用該變量耍一些花槍
    nonce    = seed // 初始化爲seed值,後面每次嘗試之後會累加
)
logger := log.New("miner", id)
logger.Trace("Started ethash search for new nonces", "seed", seed)
for {
    select {
    case <-abort: // 停止命令
        // 挖礦停止,更新狀態,停止當前操做,返回空
        logger.Trace("Ethash nonce search aborted", "attempts", nonce-seed)
        ethash.hashrate.Mark(attempts)
        return

    default: // 默認執行
        // 咱們不必在每一次嘗試nonce值的時候更新hash率,能夠在嘗試了2的X次方nonce值之後再更新便可
        attempts++ // 經過次數attemp來控制
        if (attempts % (1 << 15)) == 0 {// 這裏是定的2的15次方,位操做符請參考 http://www.cnblogs.com/Evsward/p/go.html#%E5%B8%B8%E9%87%8F
            ethash.hashrate.Mark(attempts) // 知足條件了之後,要更新ethash的hash率字段的狀態值
            attempts = 0 // 重置嘗試次數
        }
        // 爲這個nonce值計算pow值
        digest, result := hashimotoFull(dataset, hash, nonce) // 調用的hashimotoFull函數在本包的算法庫中,後面會介紹。
        if new(big.Int).SetBytes(result).Cmp(target) <= 0 { // 驗證標準,後面介紹
            // 找到正確nonce值,建立一個基於它的新的區塊頭
            header = types.CopyHeader(header)
            header.Nonce = types.EncodeNonce(nonce) // 將輸入的整型值轉換爲一個區塊nonce值
            header.MixDigest = common.BytesToHash(digest) // 將字節數組轉換爲Hash對象【Hash是32位的根據任意輸入數據的Keccak256哈希算法的返回值】

            // 封裝返回一個區塊
            select {
            case found <- block.WithSeal(header):
                logger.Trace("Ethash nonce found and reported", "attempts", nonce-seed, "nonce", nonce)
            case <-abort:
                logger.Trace("Ethash nonce found but discarded", "attempts", nonce-seed, "nonce", nonce)
            }
            return
        }
        nonce++ // 累加nonce
    }
}

}
mine方法主要就是對nonce的操做,以及對區塊頭的重建操做,註釋中咱們也留了一個坑就是對於nonce嘗試的工做,這部份內容會轉到算法庫中來介紹。

algorithm
ethash包中包含幾個algorithm開頭的文件,這些文件的內容是pow核心算法,用來支持挖礦操做。首先咱們繼續上面留的坑繼續研究。

hashimotoFull函數
該函數位於ethash/algorithm.go文件中,

// 在傳入的數據集中經過hash和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]
}
// hashimotoFull函數作的工做就是將原始數據集進行了讀取分割,而後傳給hashimoto函數。
return hashimoto(hash, nonce, uint64(len(dataset))*4, lookup)

}
hashimoto函數
繼續分析,上面的hashimotoFull函數返回的是hashimoto函數的返回值,hashimoto算法咱們在上面概念部分已經介紹過了,讀源碼的朋友不理解的能夠翻回上面仔細瞭解一番再回到這裏繼續研究。

// 該函數與hashimotoFull有着相同的願景:在傳入的數據集中經過hash和nonce值計算加密值
func hashimoto(hash []byte, nonce uint64, size uint64, lookup func(index uint32) []uint32) ([]byte, []byte) {

// 計算數據集的理論的行數
rows := uint32(size / mixBytes)

// 合併header+nonce到一個40字節的seed
seed := make([]byte, 40) // 建立一個長度爲40的字節數組,名字爲seed
copy(seed, hash)// 將區塊頭的hash(上面提到了Hash對象是32字節大小)拷貝到seed中。
binary.LittleEndian.PutUint64(seed[32:], nonce) // 將nonce值填入seed的後(40-32=8)字節中去,(nonce自己就是uint64類型,是64位,對應8字節大小),正好把hash和nonce完整的填滿了40字節的seed

seed = crypto.Keccak512(seed) // seed經歷一遍Keccak512加密
seedHead := binary.LittleEndian.Uint32(seed) // 從seed中獲取區塊頭,代碼後面詳解

// 開始與重複seed的混合
mix := make([]uint32, mixBytes/4)// mixBytes常量= 128,mix的長度爲32,元素爲uint32,是32位,對應爲4字節大小。因此mix總共大小爲4*32=128字節大小
for i := 0; i < len(mix); i++ {
    mix[i] = binary.LittleEndian.Uint32(seed[i%16*4:])// 共循環32次,前16和後16位的元素值相同
}
// 作一個temp,與mix結構相同,長度相同
temp := make([]uint32, len(mix))

for i := 0; i < loopAccesses; i++ { // loopAccesses常量 = 64,循環64次
    parent := fnv(uint32(i)^seedHead, mix[i%len(mix)]) % rows // mix[i%len(mix)]是循環依次調用mix的元素值,fnv函數在本代碼後面詳解
    for j := uint32(0); j < mixBytes/hashBytes; j++ {
        copy(temp[j*hashWords:], lookup(2*parent+j))// 經過用種子seed生成的mix數據進行FNV哈希操做之後的數值做爲參數去查找源數據(太繞了)拷貝到temp中去。
    }
    fnvHash(mix, temp) // 將mix中全部元素都與temp中對應位置的元素進行FNV hash運算
}
// mix大混淆
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])
}
// 最後有效數據只在前8個位置,後面的數據通過上面的循環混淆之後沒有價值了,因此將mix的長度減到8,保留前8位有效數據。
mix = mix[:len(mix)/4]

digest := make([]byte, common.HashLength) // common.HashLength=32,建立一個長度爲32的字節數組digest
for i, val := range mix {
    binary.LittleEndian.PutUint32(digest[i*4:], val)// 再把長度爲8的mix分散到32位的digest中去。
}
return digest, crypto.Keccak256(append(seed, digest...))

}
該函數除了被hashimotoFull函數調用之外,還會被hashimotoLight函數調用。顧名思義,hashimotoLight是相對於hashimotoFull的存在。hashimotoLight在後面有機會就介紹(看看能不能繞進咱們的route吧)。

下劃線與位運算|
以上代碼中的seedHead := binary.LittleEndian.Uint32(seed),咱們挑出來單練,跳轉到內部方法爲:

func (littleEndian) Uint32(b []byte) uint32 {

_ = b[3] // bounds check hint to compiler; see golang.org/issue/14808
return uint32(b[0]) | uint32(b[1])<<8 | uint32(b[2])<<16 | uint32(b[3])<<24

}
go語法補充:下劃線變量表明Go語言「垃圾桶」的意思,這個垃圾桶並非說銷燬一個對象,而是針對go語言報錯機制來處理的,因此b[3]這一行能夠是b[3]未使用防止go報「xxx未使用」的錯誤,同時觀察後面的官方註釋,也是爲了在真正使用b[3]數據前進行邊界檢查,若是b[3]爲空,則會提早報錯,不會引起程序問題。
位運算,咱們在《掌握一門語言GO》中對左移和右移進行了介紹,這裏針對或|和與&進行介紹。位運算都是將原數據轉換爲二進制進行運算,或|就是0和1或得1,例如1和2或得3,由於1的二進制表達爲01,2的二進制表達爲10,01和10或運算之後就是11,等於3。同理,與&運算就是,0和1與得0,因此1和2的與運算結果爲0,由於與&運算是隻有都爲1才能得1。
FNV hash 算法
FNV是由三位建立者的名字得來的,咱們知道hash算法最重要的目標就是要平均分佈(高度分散),避免碰撞,最好相近的源數據加密後徹底不一樣,哪怕他們只有一個字母不同,FNV hash算法就是這樣的一種算法。

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]
}

}
0x01000193是FNV hash算法的一個hash質數(Prime number,又叫素數,只能被1和其自己整除),哈希算法會基於一個常數來作散列操做。0x01000193是FNV針對32 bit數據的散列質數。

驗證方式
咱們一直提,pow是難於計算,上面這麼長篇章深入體現了這一點,可是pow是易於驗證的,因此本節討論的是ethash的pow的驗證方式,這個驗證方式也很容易找到,就是上面mine方法中我在註釋裏留下的坑:

new(big.Int).SetBytes(result).Cmp(target) <= 0
咱們的核心計算nonce對應的加密值digest方法hashimoto算法返回了一個digest和一個result兩個值,而由這行代碼可知,與驗證方式相關的就是result的值。result在hashimoto算法中最終還通過了crypto.Keccak256(append(seed, digest...)的Keccak256加密,參數列表中也看到了digest值。獲得result值之後,就要執行上面這行代碼的表達式了。這行表達式很簡單,主要含義就是將result值和target值進行比較,若是小於等於0,即爲經過。

那麼target是什麼?

target被定義在mine方法體中靠前的變量聲明部分,

target = new(big.Int).Div(maxUint256, header.Difficulty)
能夠看出,target的定義是根據區塊頭中的難度值運算而得出的。因此,這就驗證了咱們最先在概念部分中提到的,咱們能夠經過調整Difficulty值,來控制pow運算難度,生成正確nonce的難度,達到pow工做量可控的目標。

總結代碼讀到這裏,已經完成了一個閉環,結合前面的《挖礦》,咱們已經走通了以太坊pow的所有流程,整個流程我沒有絲毫懈怠,從入口深刻到內核,咱們把源碼扒了底掉(實際上,目前爲止的流程中,以太坊的pow並未真正使用到如我所想的DAG)。到目前爲止,咱們對pow,以及以太坊ethash的實現有了深入的理解與認識,相信若是讓咱們去實現一套pow,也是徹底有能力的。你們在閱讀本文時有任何疑問都可留言給我,我必定會及時回覆。

相關文章
相關標籤/搜索