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)巡揍。