以太坊源碼解析 Ethash算法解讀

Ethash

前面我們分析了以太坊挖礦的源碼嗦明,挖了一個(gè)共識(shí)引擎的坑钉答,研究了DAG有向無環(huán)圖的算法,這些都是本文要研究的Ethash的基礎(chǔ)遵馆。Ethash是目前以太坊基于POW工作量證明的一個(gè)共識(shí)引擎(也叫挖礦算法)京腥。它的前身是Dagger Hashimoto算法赦肃。

Dagger Hashimoto

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

  • 抵制礦機(jī)(ASIC,專門用于挖礦的芯片)
  • 輕客戶端驗(yàn)證
  • 全鏈數(shù)據(jù)存儲(chǔ)

Dagger和Hashimoto其實(shí)是兩個(gè)東西他宛,

Hashimoto算法

是這個(gè)人Thaddeus Dryja創(chuàng)造的船侧。旨在通過IO限制來抵制礦機(jī)。在挖礦過程中厅各,使內(nèi)存讀取限制條件镜撩,由于內(nèi)存設(shè)備本身會(huì)比計(jì)算設(shè)備更加便宜以及普遍,在內(nèi)存升級(jí)優(yōu)化方面队塘,全世界的大公司也都投入巨大袁梗,以使內(nèi)存能夠適應(yīng)各種用戶場景,所以有了隨機(jī)訪問內(nèi)存的概念RAM憔古,因此,現(xiàn)有的內(nèi)存可能會(huì)比較接近最優(yōu)的評(píng)估算法遮怜。Hashimoto算法使用區(qū)塊鏈作為源數(shù)據(jù),滿足了上面的1和3的要求投放。

Dagger算法

是這個(gè)人Vitalik Buterin發(fā)明的奈泪。它利用了有向無環(huán)圖DAG同時(shí)實(shí)現(xiàn)了Memory-Hard Function內(nèi)存計(jì)算困難但易于驗(yàn)證Memory-easy verification的特性(我們知道這是哈希算法的重要特性之一)。它的理論依據(jù)是基于每個(gè)特定場合nonce只需要大型數(shù)據(jù)總量樹的一小部分灸芳,并且針對(duì)每個(gè)特定場合nonce的子樹的再計(jì)算是被禁止挖礦的涝桅。因此,需要存儲(chǔ)樹但也支持一個(gè)獨(dú)立場合nonce的驗(yàn)證價(jià)值烙样。Dagger算法注定要替代現(xiàn)存的僅內(nèi)存計(jì)算困難的算法冯遂,例如Scrypt(萊特幣采用的),它是計(jì)算困難同時(shí)驗(yàn)證亦困難的算法谒获,當(dāng)他們的內(nèi)存計(jì)算困難度增加至真正安全的水平蛤肌,驗(yàn)證的困難度也隨之難上加難。然而批狱,Dagger算法被證明是容易受到Sergio Lerner發(fā)明的共享內(nèi)存硬件加速技術(shù)裸准,隨后在其他路徑的研究方面,該算法被遺棄了赔硫。

Memory-Hard Function

直接翻譯過來是內(nèi)存困難函數(shù)炒俱,這是為了地址礦機(jī)而誕生的一種思想。我們知道挖礦是靠我們的電腦爪膊,但是有些硬件廠商會(huì)制造專門用于挖礦的硬件設(shè)備权悟,它們并不是一臺(tái)完整的PC機(jī),例如ASIC推盛、GPU以及FPGAs(我們經(jīng)常能聽到GPU挖礦等)峦阁。所以這些作為礦機(jī)的設(shè)備是超越普通PC挖礦的存在,這是不符合我們區(qū)塊鏈的去中心化精神的耘成,所以我們要讓挖礦設(shè)備平等榔昔。

那么該如何讓挖礦設(shè)備是平等的呢驹闰?

上面談到Dagger算法的時(shí)候其實(shí)提到了,這里換一種方式再來介紹一下撒会,現(xiàn)在CPU都是多核的疮方,如果從計(jì)算能力來講,CPU有幾核就可以模擬幾臺(tái)設(shè)備同時(shí)平行挖礦茧彤,自然效率就高些,但是這里采用的衡量對(duì)象是內(nèi)存疆栏,一臺(tái)電腦只有一個(gè)總內(nèi)存曾掂。我們做過java多線程開發(fā)的朋友知道,無論機(jī)器性能有多高壁顶,但我們寫的程序就是單線程的珠洗,那么這個(gè)程序運(yùn)行在高配多核電腦和低配單核電腦的區(qū)別不大,只要他們的單核運(yùn)算能力和內(nèi)存大小一樣即可若专。所以也是這個(gè)原理许蓖,通過Dagger算法,我們將挖礦流程鎖定在以內(nèi)存為衡量標(biāo)準(zhǔn)的硬件性能上调衰,只要通過“塞一堆數(shù)據(jù)到內(nèi)存中”的方式膊爪,讓多核平行處理發(fā)揮不出來,降低硬件的運(yùn)算優(yōu)勢嚎莉,只與內(nèi)存大小有關(guān)米酬,這樣無論是PC機(jī)還是ASIC、GPU以及FPGAs趋箩,都可達(dá)到平等挖礦的訴求赃额,這也是ASIC-resistant原理,目前抵制礦機(jī)的主要手段叫确。

兩個(gè)問題的研究

在Dagger以及Dagger Hashimoto算法中跳芳,有兩個(gè)問題的研究是被擱置的,

  • 基于區(qū)塊鏈的工作量證明:一個(gè)POW函數(shù)包括了運(yùn)行區(qū)塊鏈上的合約竹勉。該方法被拋棄是因?yàn)檫@是一個(gè)長期的攻擊缺陷飞盆,因?yàn)楣粽吣軌騽?chuàng)建分叉,然后通過一個(gè)包含秘密的快速“trapdoor”井蓋門的運(yùn)行機(jī)制的合約在該分叉上殖民饶米。
  • 隨機(jī)環(huán)路:一個(gè)POW函數(shù)由這個(gè)人Vlad Zamfir開發(fā)桨啃,包含了每1000個(gè)場合nonces就生成一個(gè)新的程序的功能。本質(zhì)上來講檬输,每次選擇一個(gè)新的哈希函數(shù)照瘾,會(huì)比可重配置的FPGAs(可重編程的芯片,不必重新焊接電路板就可通過軟件技術(shù)重新自定義硬件功能)更快丧慈。該方法被暫時(shí)擱置析命,是因?yàn)樗茈y看到有什么機(jī)制可以用來生成隨機(jī)程序是足夠全面主卫,因此它的專業(yè)化收益是較低的。然而鹃愤,我們并沒有看到為什么這個(gè)概念無法讓它生效的根本原因簇搅,所以暫時(shí)擱置。

Dagger Hashimoto算法

(區(qū)別于Hashimoto)Dagger Hashimoto不是直接將區(qū)塊鏈作為數(shù)據(jù)源软吐,而是使用一個(gè)1GB的自定義生成的數(shù)據(jù)集cache瘩将。

這個(gè)數(shù)據(jù)集是基于區(qū)塊數(shù)據(jù)每N個(gè)塊就會(huì)更新。該數(shù)據(jù)集是使用Dagger算法生成凹耙,允許一個(gè)自己的高效計(jì)算姿现,特定于每個(gè)輕客戶端校驗(yàn)算法的場合nonce。

(區(qū)別于Dagger)Dagger Hashimoto克服了Dagger的缺陷肖抱,它用于查詢區(qū)塊數(shù)據(jù)的數(shù)據(jù)集是半永久的备典,只有在偶然的間隔才會(huì)被更新(例如每周一次)。這意味著生成數(shù)據(jù)集將非常容易意述,所以Sergio Lerner的爭議共享內(nèi)存加速變得微不足道了提佣。

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

Ethash算法包含找到一個(gè)nonce值輸入到一個(gè)算法中荤崇,得到的結(jié)果是低于一個(gè)基于特定困難度的閥值拌屏。

POW算法的關(guān)鍵點(diǎn)是除了暴力枚舉,沒有任何辦法可以找到這個(gè)nonce值术荤,但對(duì)于驗(yàn)證輸出的結(jié)果是非常簡單容易的槐壳。如果輸出結(jié)果有一個(gè)均勻分布,我們就可以保證找到一個(gè)nonce值的平均所需時(shí)間取決于那個(gè)難度閥值喜每,因此我們可以通過調(diào)整難度閥值來控制找到一個(gè)新塊的時(shí)間务唐,這就是控制出塊速度的原理。

DAG

Ethash的POW是memory-hard带兜,支持礦機(jī)抵御枫笛。這意味著POW計(jì)算需要選擇一個(gè)固定的依賴于nonce值和塊頭的資源的子集。

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

epoch

每3萬個(gè)塊會(huì)花幾個(gè)小時(shí)的時(shí)間生成一個(gè)有向無環(huán)圖DAG刚照。這個(gè)DAG被稱為epoch刑巧。DAG只取決于區(qū)塊高度,它可以被預(yù)生成无畔,如果沒有預(yù)生成的話啊楚,客戶端需要等待預(yù)生成流程結(jié)束以后才能繼續(xù)出塊操作。除非客戶端真實(shí)的提前預(yù)緩存了DAG浑彰,否則在每個(gè)epoch的過渡期間恭理,網(wǎng)絡(luò)可能會(huì)經(jīng)歷一個(gè)巨大的區(qū)塊延遲。

特例:當(dāng)你從頭啟動(dòng)一個(gè)結(jié)點(diǎn)時(shí)郭变,挖礦工作只會(huì)在創(chuàng)建了現(xiàn)世DAG以后啟動(dòng)颜价。

挖礦獎(jiǎng)勵(lì)

有三部分:

  • 靜態(tài)區(qū)塊創(chuàng)建獎(jiǎng)勵(lì)涯保,精確發(fā)放3以太幣作為獎(jiǎng)勵(lì)。
  • 當(dāng)前區(qū)塊包含的所有交易的gas錢周伦,隨著時(shí)間推移夕春,gas會(huì)越來越便宜,獲得的gas總和獎(jiǎng)勵(lì)會(huì)低于靜態(tài)區(qū)塊創(chuàng)建獎(jiǎng)勵(lì)专挪。
  • 叔塊獎(jiǎng)勵(lì)及志,整塊獎(jiǎng)勵(lì)的1/32。

Ethash

Ethash算法路線圖:

  • 存在一個(gè)種子seed寨腔,通過掃描塊頭為每個(gè)塊計(jì)算出來那個(gè)點(diǎn)困肩。
  • 根據(jù)這個(gè)種子seed,可以計(jì)算一個(gè)16MB的偽隨機(jī)緩存cache脆侮,輕客戶端存儲(chǔ)這個(gè)緩存。
  • 從這個(gè)緩存cache中勇劣,我們能夠生成一個(gè)1GB的數(shù)據(jù)集靖避,該數(shù)據(jù)集中的每一項(xiàng)都取決于緩存中的一小部分。完整客戶端和礦工存儲(chǔ)了這個(gè)數(shù)據(jù)集比默,數(shù)據(jù)集隨著時(shí)間線性增長幻捏。
  • 挖礦工作包含了抓取數(shù)據(jù)集的隨機(jī)片以及運(yùn)用哈希函數(shù)計(jì)算他們。校驗(yàn)工作能夠在低內(nèi)存的環(huán)境下完成命咐,通過使用緩存再次生成所需的特性數(shù)據(jù)集的片段篡九,所以你只需要存儲(chǔ)緩存cache即可。

以上提到的大數(shù)據(jù)集是每3萬個(gè)塊更新一次醋奠,所以絕大多數(shù)的礦工的工作是讀取該數(shù)據(jù)集而不是改變它榛臼。

pkg ethash源碼分析

以上我們將所有的概念抽象梳理了一下,包括POW窜司,挖礦沛善,Ethash原理流程等,下面我們帶著這些理論知識(shí)走進(jìn)源代碼中去分析具體的實(shí)現(xiàn)塞祈。正如我們的題目金刁,本文主要分析的是ethash算法,因此整個(gè)源碼范圍僅限于go-ethereum/consensus/ethash包议薪,該包實(shí)現(xiàn)了ethash pow的共識(shí)引擎尤蛮。

入口

在go-ethereum/consensus/consensus.go 接口中定義了如下的方法,該接口方法的定義如下:

Seal(chain ChainReader, block *types.Block, stop <-chan struct{}) (*types.Block, error)//該方法通過輸入一個(gè)包含本地礦工挖出的最高區(qū)塊在主干上生成一個(gè)新塊斯议。

參數(shù)有ChainReader产捞,Block,stop結(jié)構(gòu)體信號(hào)哼御,返回一個(gè)主鏈上的新出的塊實(shí)體轧葛。

  • ChainReader
// 定義了一些方法搂抒,用于在區(qū)塊頭驗(yàn)證以及叔塊驗(yàn)證期間,訪問本地區(qū)塊鏈尿扯。
type ChainReader interface {
    // 獲取區(qū)塊鏈的鏈配置
    Config() *params.ChainConfig

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

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

    // 通過number從主鏈中獲取一個(gè)區(qū)塊頭
    GetHeaderByNumber(number uint64) *types.Header

    // 通過hash從主鏈中獲取一個(gè)區(qū)塊頭
    GetHeaderByHash(hash common.Hash) *types.Header

    // 通過hash和number從主鏈中獲取一個(gè)區(qū)塊
    GetBlock(hash common.Hash, number uint64) *types.Block
}

總結(jié)求晶,ChainReader定義了幾個(gè)方法:從本地區(qū)塊鏈獲取配置、區(qū)塊頭衷笋,從主鏈中獲取區(qū)塊頭芳杏、區(qū)塊,參數(shù)條件包括hash和number辟宗,隨意組合爵赵。

  • Block
// Block代表以太坊區(qū)塊鏈中的一個(gè)完整的區(qū)塊
type Block struct {
    header       *Header // 區(qū)塊包括頭
    uncles       []*Header // 叔塊
    transactions Transactions // 交易集合

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

    // Td用于core包存儲(chǔ)所有的鏈上的難度
    td *big.Int

    // 這些字段用于eth包來跟蹤inter-peer內(nèi)部端點(diǎn)區(qū)塊的接替
    ReceivedAt   time.Time
    ReceivedFrom interface{}
}

總結(jié),Block除了我們熟知的區(qū)塊中必有的區(qū)塊頭泊脐、叔塊以及打包存儲(chǔ)的交易信息空幻,還有cache緩存的內(nèi)容,以及每個(gè)塊之于鏈的難度值容客,還有用于跟蹤內(nèi)部端點(diǎn)的字段秕铛。

  • stop

stop是一個(gè)空結(jié)構(gòu)體作為信號(hào)源。

關(guān)于空結(jié)構(gòu)體的討論缩挑,為什么go里面經(jīng)常出現(xiàn)struct{}?

go中除了struct{}類型以外但两,其他類型都是width,占有存儲(chǔ)供置,而struct{}沒有字段谨湘,沒有方法,width為0芥丧,靈活性高紧阔,不占內(nèi)存空間,這可能是讓Gopher青睞的原因续担。

sealer

seal方法有兩個(gè)實(shí)現(xiàn)寓辱,我們選擇ethash,該方法存在于consensus/ethash/sealer.go文件中赤拒,第一個(gè)函數(shù)就是seal的實(shí)現(xiàn)秫筏,先來看該方法的聲明部分:

// 嘗試找到一個(gè)nonce值能夠滿足區(qū)塊難度需求。
func (ethash *Ethash) Seal(chain consensus.ChainReader, block *types.Block, stop <-chan struct{}) (*types.Block, error) {

可以看出這個(gè)方法是屬于Ethash的指針對(duì)象的挎挖,

type Ethash struct {
    // cache配置
    cachedir     string // 緩存位置
    cachesinmem  int    // 在內(nèi)存中緩存的數(shù)量
    cachesondisk int    // 在硬盤中緩存的數(shù)量

    // DAG挖礦數(shù)據(jù)集配置
    dagdir       string // DAG位置这敬,存儲(chǔ)全部挖礦數(shù)據(jù)集
    dagsinmem    int    // 在內(nèi)存中DAG的數(shù)量
    dagsondisk   int    // 在硬盤中DAG的數(shù)量

    // 內(nèi)存cache
    caches   map[uint64]*cache   // 內(nèi)存緩存,可反復(fù)使用避免再生太頻繁
    fcache   *cache              // 為了下一世估算的預(yù)生產(chǎn)緩存

    // 內(nèi)存數(shù)據(jù)集
    datasets map[uint64]*dataset // 內(nèi)存數(shù)據(jù)集蕉朵,可反復(fù)使用避免再生太頻繁
    fdataset *dataset            // 為了下一世估算的預(yù)生產(chǎn)數(shù)據(jù)集

    // 挖礦相關(guān)字段
    rand     *rand.Rand    // 隨機(jī)工具崔涂,用來為nonce做適當(dāng)?shù)姆N子
    threads  int           // 如果在挖礦,代表挖礦的線程編號(hào)
    update   chan struct{} // 更新挖礦中參數(shù)的通道
    hashrate metrics.Meter // 測量跟蹤平均哈希率

    // 以下字段是用于測試
    tester    bool          // 是否使用一個(gè)小型測試數(shù)據(jù)集的標(biāo)志位
    shared    *Ethash       // 共享pow模式始衅,無法再生緩存
    fakeMode  bool          // Fake模式冷蚂,是否取消POW檢查的標(biāo)志位
    fakeFull  bool          // 是否取消所有共識(shí)規(guī)則的標(biāo)志位
    fakeFail  uint64        // 未通過POW檢查的區(qū)塊號(hào)(包含fake模式)
    fakeDelay time.Duration // 驗(yàn)證工作返回消息前的休眠延遲時(shí)間

    lock sync.Mutex // 為了內(nèi)存中的緩存和挖礦字段缭保,保證線程安全
}

為了更好的讀懂之后的代碼,我們要對(duì)區(qū)塊頭的數(shù)據(jù)結(jié)構(gòu)進(jìn)行一個(gè)分析:

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"`
}

可以看到一個(gè)區(qū)塊頭包含了父塊hash值蝙茶,叔塊hash值艺骂,Coinbase結(jié)點(diǎn)賬戶地址,狀態(tài)根隆夯,交易hash钳恕,接受者h(yuǎn)ash,日志蹄衷,難度值忧额,塊編號(hào),最低支付gas愧口,花費(fèi)的gas睦番,時(shí)間戳,額外數(shù)據(jù)耍属,混合hash托嚣,nonce值(8個(gè)byte)。我們要對(duì)這些區(qū)塊頭的成員屬性了然于胸恬涧,后面的源碼內(nèi)容才能更好的理解。下面我們繼續(xù)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的話溯捆,則轉(zhuǎn)到它的共享對(duì)象執(zhí)行Seal操作
    if ethash.shared != nil {
        return ethash.shared.Seal(chain, block, stop)
    }
    // 創(chuàng)建一個(gè)runner以及它指揮的多重搜索線程
    abort := make(chan struct{})
    found := make(chan *types.Block)

    ethash.lock.Lock() // 線程上鎖,保證內(nèi)存的緩存(包含挖礦字段)安全
    threads := ethash.threads // 挖礦的線程s
    if ethash.rand == nil {// rand為空厦瓢,則為ethash的字段rand賦值
        // 獲得種子
        seed, err := crand.Int(crand.Reader, big.NewInt(math.MaxInt64))
        if err != nil {// 執(zhí)行失敗提揍,有報(bào)錯(cuò)
            ethash.lock.Unlock() // 先解鎖
            return nil, err // 程序中止,直接返回空塊和報(bào)錯(cuò)信息
        }
        ethash.rand = rand.New(rand.NewSource(seed.Int64())) // 執(zhí)行成功煮仇,拿到合法種子seed劳跃,通過其獲得rand對(duì)象,賦值浙垫。
    }
    ethash.lock.Unlock() // 解鎖
    if threads == 0 {// 挖礦線程編號(hào)為0刨仑,則通過方法返回當(dāng)前物理上可用CPU編號(hào)
        threads = runtime.NumCPU()
    }
    if threads < 0 { // 非法結(jié)果
        threads = 0 // 置為0,允許在本地或遠(yuǎn)程沒有額外邏輯的情況下夹姥,取消本地挖礦操作
    }
    var pend sync.WaitGroup // 創(chuàng)建一個(gè)倒計(jì)時(shí)鎖對(duì)象杉武,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) {// 核心代碼通過閉包多線程技術(shù)來執(zhí)行。
            defer pend.Done()
            ethash.mine(block, id, nonce, abort, found) // Seal核心工作
        }(i, uint64(ethash.rand.Int63()))//閉包第二個(gè)參數(shù)表達(dá)式uint64(ethash.rand.Int63())通過上面準(zhǔn)備好的rand函數(shù)隨機(jī)數(shù)結(jié)果作為nonce實(shí)參傳入方法體
    }
    // 直到seal操作被中止或者找到了一個(gè)nonce值辙售,否則一直等
    var result *types.Block // 定義一個(gè)區(qū)塊對(duì)象result轻抱,用于接收操作結(jié)果并作為返回值返回上一層
    select { // go語法參照 http://www.cnblogs.com/Evsward/p/go.html#select
    case <-stop:
        // 外部意外中止,停止所有挖礦線程
        close(abort)
    case result = <-found:
        // 其中一個(gè)線程挖到正確塊旦部,中止其他所有線程
        close(abort)
    case <-ethash.update:
        // ethash對(duì)象發(fā)生改變祈搜,停止當(dāng)前所有操作较店,重啟當(dāng)前方法
        close(abort)
        pend.Wait()
        return ethash.Seal(chain, block, stop)
    }
    // 等待所有礦工停止或者返回一個(gè)區(qū)塊
    pend.Wait()
    return result, nil
}

以上Seal方法體,針對(duì)ethash的各種狀態(tài)進(jìn)行了校驗(yàn)和流程處理容燕,以及對(duì)線程資源的控制梁呈,下面看Seal核心工作的內(nèi)容(sealer.go文件只有兩個(gè)函數(shù),一個(gè)是Seal方法缰趋,另一個(gè)就是mine方法捧杉,可以看出Seal方法是對(duì)外的,而mine方法是內(nèi)部方法秘血,只能被當(dāng)前ethash包域調(diào)用):mine方法

// mine函數(shù)是真正的pow礦工味抖,用來搜索一個(gè)nonce值,nonce值開始于seed值灰粮,seed值是能最終產(chǎn)生正確的可匹配可驗(yàn)證的區(qū)塊難度
func (ethash *Ethash) mine(block *types.Block, id int, seed uint64, abort chan struct{}, found chan *types.Block) {
    // 從區(qū)塊頭中提取出一些數(shù)據(jù)仔涩,放在一個(gè)全局變量域中
    var (
        header = block.Header()
        hash   = header.HashNoNonce().Bytes()
        target = new(big.Int).Div(maxUint256, header.Difficulty) // 后面有大用,這是用來驗(yàn)證的target

        number  = header.Number.Uint64()
        dataset = ethash.dataset(number)
    )
    // 開始生成隨機(jī)nonce值知道我們中止或者成功找到了一個(gè)合適的值
    var (
        attempts = int64(0) // 初始化一個(gè)嘗試次數(shù)的變量粘舟,下面會(huì)利用該變量耍一些花槍
        nonce    = seed // 初始化為seed值熔脂,后面每次嘗試以后會(huì)累加
    )
    logger := log.New("miner", id)
    logger.Trace("Started ethash search for new nonces", "seed", seed)
    for {
        select {
        case <-abort: // 中止命令
            // 挖礦中止,更新狀態(tài)柑肴,中止當(dāng)前操作霞揉,返回空
            logger.Trace("Ethash nonce search aborted", "attempts", nonce-seed)
            ethash.hashrate.Mark(attempts)
            return

        default: // 默認(rèn)執(zhí)行
            // 我們沒必要在每一次嘗試nonce值的時(shí)候更新hash率,可以在嘗試了2的X次方nonce值以后再更新即可
            attempts++ // 通過次數(shù)attemp來控制
            if (attempts % (1 << 15)) == 0 {// 這里是定的2的15次方晰骑,位操作符請(qǐng)參考 http://www.cnblogs.com/Evsward/p/go.html#%E5%B8%B8%E9%87%8F
                ethash.hashrate.Mark(attempts) // 滿足條件了以后适秩,要更新ethash的hash率字段的狀態(tài)值
                attempts = 0 // 重置嘗試次數(shù)
            }
            // 為這個(gè)nonce值計(jì)算pow值
            digest, result := hashimotoFull(dataset, hash, nonce) // 調(diào)用的hashimotoFull函數(shù)在本包的算法庫中,后面會(huì)介紹硕舆。
            if new(big.Int).SetBytes(result).Cmp(target) <= 0 { // 驗(yàn)證標(biāo)準(zhǔn)秽荞,后面介紹
                // 找到正確nonce值,創(chuàng)建一個(gè)基于它的新的區(qū)塊頭
                header = types.CopyHeader(header)
                header.Nonce = types.EncodeNonce(nonce) // 將輸入的整型值轉(zhuǎn)換為一個(gè)區(qū)塊nonce值
                header.MixDigest = common.BytesToHash(digest) // 將字節(jié)數(shù)組轉(zhuǎn)換為Hash對(duì)象【Hash是32位的根據(jù)任意輸入數(shù)據(jù)的Keccak256哈希算法的返回值】

                // 封裝返回一個(gè)區(qū)塊
                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方法主要就是對(duì)nonce的操作抚官,以及對(duì)區(qū)塊頭的重建操作扬跋,注釋中我們也留了一個(gè)坑就是對(duì)于nonce嘗試的工作,這部分內(nèi)容會(huì)轉(zhuǎn)到算法庫中來介紹凌节。

algorithm

ethash包中包含幾個(gè)algorithm開頭的文件钦听,這些文件的內(nèi)容是pow核心算法,用來支持挖礦操作倍奢。首先我們繼續(xù)上面留的坑繼續(xù)研究彪见。

hashimotoFull函數(shù)

該函數(shù)位于ethash/algorithm.go文件中,

// 在傳入的數(shù)據(jù)集中通過hash和nonce值計(jì)算加密值
func hashimotoFull(dataset []uint32, hash []byte, nonce uint64) ([]byte, []byte) {
    // 本函數(shù)核心代碼段:定義一個(gè)lookup函數(shù)娱挨,用于在數(shù)據(jù)集中查找數(shù)據(jù)
    lookup := func(index uint32) []uint32 {
        offset := index * hashWords // hashWords是上面定義的常量值= 16
        return dataset[offset : offset+hashWords]
    }
    // hashimotoFull函數(shù)做的工作就是將原始數(shù)據(jù)集進(jìn)行了讀取分割余指,然后傳給hashimoto函數(shù)。
    return hashimoto(hash, nonce, uint64(len(dataset))*4, lookup)
}

hashimoto函數(shù)

繼續(xù)分析,上面的hashimotoFull函數(shù)返回的是hashimoto函數(shù)的返回值酵镜,hashimoto算法我們?cè)谏厦娓拍畈糠忠呀?jīng)介紹過了碉碉,讀源碼的朋友不理解的可以翻回上面仔細(xì)了解一番再回到這里繼續(xù)研究。

// 該函數(shù)與hashimotoFull有著相同的愿景:在傳入的數(shù)據(jù)集中通過hash和nonce值計(jì)算加密值
func hashimoto(hash []byte, nonce uint64, size uint64, lookup func(index uint32) []uint32) ([]byte, []byte) {
    // 計(jì)算數(shù)據(jù)集的理論的行數(shù)
    rows := uint32(size / mixBytes)

    // 合并header+nonce到一個(gè)40字節(jié)的seed
    seed := make([]byte, 40) // 創(chuàng)建一個(gè)長度為40的字節(jié)數(shù)組淮韭,名字為seed
    copy(seed, hash)// 將區(qū)塊頭的hash(上面提到了Hash對(duì)象是32字節(jié)大泄噶浮)拷貝到seed中。
    binary.LittleEndian.PutUint64(seed[32:], nonce) // 將nonce值填入seed的后(40-32=8)字節(jié)中去靠粪,(nonce本身就是uint64類型蜡吧,是64位,對(duì)應(yīng)8字節(jié)大姓技)昔善,正好把hash和nonce完整的填滿了40字節(jié)的seed

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

    // 開始與重復(fù)seed的混合
    mix := make([]uint32, mixBytes/4)// mixBytes常量= 128畔乙,mix的長度為32君仆,元素為uint32,是32位牲距,對(duì)應(yīng)為4字節(jié)大小返咱。所以mix總共大小為4*32=128字節(jié)大小
    for i := 0; i < len(mix); i++ {
        mix[i] = binary.LittleEndian.Uint32(seed[i%16*4:])// 共循環(huán)32次,前16和后16位的元素值相同
    }
    // 做一個(gè)temp牍鞠,與mix結(jié)構(gòu)相同咖摹,長度相同
    temp := make([]uint32, len(mix))

    for i := 0; i < loopAccesses; i++ { // loopAccesses常量 = 64,循環(huán)64次
        parent := fnv(uint32(i)^seedHead, mix[i%len(mix)]) % rows // mix[i%len(mix)]是循環(huán)依次調(diào)用mix的元素值难述,fnv函數(shù)在本代碼后面詳解
        for j := uint32(0); j < mixBytes/hashBytes; j++ {
            copy(temp[j*hashWords:], lookup(2*parent+j))// 通過用種子seed生成的mix數(shù)據(jù)進(jìn)行FNV哈希操作以后的數(shù)值作為參數(shù)去查找源數(shù)據(jù)(太繞了)拷貝到temp中去萤晴。
        }
        fnvHash(mix, temp) // 將mix中所有元素都與temp中對(duì)應(yīng)位置的元素進(jìn)行FNV hash運(yùn)算
    }
    // 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])
    }
    // 最后有效數(shù)據(jù)只在前8個(gè)位置,后面的數(shù)據(jù)經(jīng)過上面的循環(huán)混淆以后沒有價(jià)值了龄广,所以將mix的長度減到8硫眯,保留前8位有效數(shù)據(jù)蕴侧。
    mix = mix[:len(mix)/4]

    digest := make([]byte, common.HashLength) // common.HashLength=32择同,創(chuàng)建一個(gè)長度為32的字節(jié)數(shù)組digest
    for i, val := range mix {
        binary.LittleEndian.PutUint32(digest[i*4:], val)// 再把長度為8的mix分散到32位的digest中去。
    }
    return digest, crypto.Keccak256(append(seed, digest...))
}

該函數(shù)除了被hashimotoFull函數(shù)調(diào)用以外净宵,還會(huì)被hashimotoLight函數(shù)調(diào)用敲才。顧名思義,hashimotoLight是相對(duì)于hashimotoFull的存在择葡。hashimotoLight在后面有機(jī)會(huì)就介紹(看看能不能繞進(jìn)我們的route吧)紧武。

下劃線與位運(yùn)算|

以上代碼中的seedHead := binary.LittleEndian.Uint32(seed),我們挑出來單練敏储,跳轉(zhuǎn)到內(nèi)部方法為:

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
}
FNV hash 算法

FNV是由三位創(chuàng)建者的名字得來的阻星,我們知道hash算法最重要的目標(biāo)就是要平均分布(高度分散),避免碰撞,最好相近的源數(shù)據(jù)加密后完全不同妥箕,哪怕他們只有一個(gè)字母不一樣滥酥,F(xiàn)NV 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算法的一個(gè)hash質(zhì)數(shù)(Prime number畦幢,又叫素?cái)?shù)坎吻,只能被1和其本身整除),哈希算法會(huì)基于一個(gè)常數(shù)來做散列操作宇葱。0x01000193是FNV針對(duì)32 bit數(shù)據(jù)的散列質(zhì)數(shù)瘦真。

驗(yàn)證方式

我們一直提,pow是難于計(jì)算黍瞧,上面這么長篇章深刻體現(xiàn)了這一點(diǎn)诸尽,但是pow是易于驗(yàn)證的,所以本節(jié)討論的是ethash的pow的驗(yàn)證方式雷逆,這個(gè)驗(yàn)證方式也很容易找到弦讽,就是上面mine方法中我在注釋里留下的坑:

new(big.Int).SetBytes(result).Cmp(target) <= 0

我們的核心計(jì)算nonce對(duì)應(yīng)的加密值digest方法hashimoto算法返回了一個(gè)digest和一個(gè)result兩個(gè)值,而由這行代碼可知膀哲,與驗(yàn)證方式相關(guān)的就是result的值往产。result在hashimoto算法中最終還經(jīng)過了crypto.Keccak256(append(seed, digest...)的Keccak256加密,參數(shù)列表中也看到了digest值某宪。得到result值以后仿村,就要執(zhí)行上面這行代碼的表達(dá)式了。這行表達(dá)式很簡單兴喂,主要含義就是將result值和target值進(jìn)行比較蔼囊,如果小于等于0,即為通過衣迷。

那么target是什么畏鼓?

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

target = new(big.Int).Div(maxUint256, header.Difficulty)

可以看出壶谒,target的定義是根據(jù)區(qū)塊頭中的難度值運(yùn)算而得出的云矫。所以,這就驗(yàn)證了我們最早在概念部分中提到的汗菜,我們可以通過調(diào)整Difficulty值让禀,來控制pow運(yùn)算難度,生成正確nonce的難度陨界,達(dá)到pow工作量可控的目標(biāo)巡揍。

最后編輯于
?著作權(quán)歸作者所有,轉(zhuǎn)載或內(nèi)容合作請(qǐng)聯(lián)系作者
  • 序言:七十年代末,一起剝皮案震驚了整個(gè)濱河市菌瘪,隨后出現(xiàn)的幾起案子腮敌,更是在濱河造成了極大的恐慌,老刑警劉巖,帶你破解...
    沈念sama閱讀 217,734評(píng)論 6 505
  • 序言:濱河連續(xù)發(fā)生了三起死亡事件糜工,死亡現(xiàn)場離奇詭異斗这,居然都是意外死亡,警方通過查閱死者的電腦和手機(jī)啤斗,發(fā)現(xiàn)死者居然都...
    沈念sama閱讀 92,931評(píng)論 3 394
  • 文/潘曉璐 我一進(jìn)店門表箭,熙熙樓的掌柜王于貴愁眉苦臉地迎上來,“玉大人钮莲,你說我怎么就攤上這事免钻。” “怎么了崔拥?”我有些...
    開封第一講書人閱讀 164,133評(píng)論 0 354
  • 文/不壞的土叔 我叫張陵极舔,是天一觀的道長。 經(jīng)常有香客問我链瓦,道長拆魏,這世上最難降的妖魔是什么? 我笑而不...
    開封第一講書人閱讀 58,532評(píng)論 1 293
  • 正文 為了忘掉前任慈俯,我火速辦了婚禮渤刃,結(jié)果婚禮上,老公的妹妹穿的比我還像新娘贴膘。我一直安慰自己卖子,他們只是感情好,可當(dāng)我...
    茶點(diǎn)故事閱讀 67,585評(píng)論 6 392
  • 文/花漫 我一把揭開白布刑峡。 她就那樣靜靜地躺著洋闽,像睡著了一般。 火紅的嫁衣襯著肌膚如雪突梦。 梳的紋絲不亂的頭發(fā)上诫舅,一...
    開封第一講書人閱讀 51,462評(píng)論 1 302
  • 那天,我揣著相機(jī)與錄音宫患,去河邊找鬼刊懈。 笑死,一個(gè)胖子當(dāng)著我的面吹牛撮奏,可吹牛的內(nèi)容都是我干的俏讹。 我是一名探鬼主播当宴,決...
    沈念sama閱讀 40,262評(píng)論 3 418
  • 文/蒼蘭香墨 我猛地睜開眼畜吊,長吁一口氣:“原來是場噩夢(mèng)啊……” “哼!你這毒婦竟也來了户矢?” 一聲冷哼從身側(cè)響起玲献,我...
    開封第一講書人閱讀 39,153評(píng)論 0 276
  • 序言:老撾萬榮一對(duì)情侶失蹤,失蹤者是張志新(化名)和其女友劉穎,沒想到半個(gè)月后捌年,有當(dāng)?shù)厝嗽跇淞掷锇l(fā)現(xiàn)了一具尸體瓢娜,經(jīng)...
    沈念sama閱讀 45,587評(píng)論 1 314
  • 正文 獨(dú)居荒郊野嶺守林人離奇死亡,尸身上長有42處帶血的膿包…… 初始之章·張勛 以下內(nèi)容為張勛視角 年9月15日...
    茶點(diǎn)故事閱讀 37,792評(píng)論 3 336
  • 正文 我和宋清朗相戀三年礼预,在試婚紗的時(shí)候發(fā)現(xiàn)自己被綠了眠砾。 大學(xué)時(shí)的朋友給我發(fā)了我未婚夫和他白月光在一起吃飯的照片。...
    茶點(diǎn)故事閱讀 39,919評(píng)論 1 348
  • 序言:一個(gè)原本活蹦亂跳的男人離奇死亡托酸,死狀恐怖褒颈,靈堂內(nèi)的尸體忽然破棺而出,到底是詐尸還是另有隱情励堡,我是刑警寧澤谷丸,帶...
    沈念sama閱讀 35,635評(píng)論 5 345
  • 正文 年R本政府宣布,位于F島的核電站应结,受9級(jí)特大地震影響刨疼,放射性物質(zhì)發(fā)生泄漏。R本人自食惡果不足惜鹅龄,卻給世界環(huán)境...
    茶點(diǎn)故事閱讀 41,237評(píng)論 3 329
  • 文/蒙蒙 一揩慕、第九天 我趴在偏房一處隱蔽的房頂上張望。 院中可真熱鬧扮休,春花似錦漩绵、人聲如沸。這莊子的主人今日做“春日...
    開封第一講書人閱讀 31,855評(píng)論 0 22
  • 文/蒼蘭香墨 我抬頭看了看天上的太陽。三九已至侨糟,卻和暖如春碍扔,著一層夾襖步出監(jiān)牢的瞬間,已是汗流浹背秕重。 一陣腳步聲響...
    開封第一講書人閱讀 32,983評(píng)論 1 269
  • 我被黑心中介騙來泰國打工不同, 沒想到剛下飛機(jī)就差點(diǎn)兒被人妖公主榨干…… 1. 我叫王不留,地道東北人溶耘。 一個(gè)月前我還...
    沈念sama閱讀 48,048評(píng)論 3 370
  • 正文 我出身青樓二拐,卻偏偏與公主長得像,于是被迫代替她去往敵國和親凳兵。 傳聞我的和親對(duì)象是個(gè)殘疾皇子百新,可洞房花燭夜當(dāng)晚...
    茶點(diǎn)故事閱讀 44,864評(píng)論 2 354

推薦閱讀更多精彩內(nèi)容