以太坊Ethash算法

Ethash

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

Dagger Hashimoto

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

  • 抵制礦機(ASIC,專門用於挖礦的芯片)
  • 輕客戶端驗證
  • 全鏈數據存儲

Dagger和Hashimoto其實是兩個東西,

Hashimoto算法

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

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,也是完全有能力的。大家在閱讀本文時有任何疑問均可留言給我,我一定會及時回覆。

參考資料

go-ethereum源碼,以太坊官方文檔,網絡名詞解釋文章

發表評論
所有評論
還沒有人評論,想成為第一個評論的人麼? 請在上方評論欄輸入並且點擊發布.
相關文章