1. 程式人生 > >以太坊的挖礦和難度調整過程

以太坊的挖礦和難度調整過程

以太坊挖礦過程

在比特幣的挖礦過程中,僅僅需要較為簡單的雜湊運算,而不需要額外的計算資源(記憶體等),於是比特幣的挖礦過程逐漸成為了算力的競爭,於是就出現了ASIC礦機,這種礦機相比於個人計算機,進行普通的計算,其算力是個人計算機的數千倍,剛好適用於進行比特幣中的挖礦,因此,普通人要想挖礦,就得有更專業的裝置,挖礦這一行業都出現了中心化的現象,這與比特幣當初設計之初的去中心化理念背道而馳了。

於是,設計以太坊挖礦的時候,採用了全新的挖礦過程以做到ASIC Resistance。ASIC只能進行運算,卻沒有額外儲存空間,於是以太坊在挖礦過程中設計了兩個資料結構,分別為Cache和dataset。其中Cache是16M大小,而dataset是1G大小,對於礦工來說,每次選取一個nonce之後的挖礦操作,都要從dataset中讀取資料,這就要求礦機有儲存能力,挖礦過程中引入了讀取記憶體的操作,這就極大的降低了ASIC礦機的算力,使其挖礦的優勢不那麼明顯,而普通的個人計算機也能進行挖礦。具體的挖礦過程如下:

  1. 根據當前區塊資訊生成一個Seed種子。
  2. 根據Seed種子生成16M大小的Cache,Cache是一個List結構,其資料前後相關。
  3. 根據Cache來生成1G大小的Dataset(又稱為Dag)。
  4. 礦機每次選取一個Nonce之後,從Dataset中讀取兩個數進行挖礦測試,直到找到合適的Nonce。

區塊鏈中每30000個區塊的時候,Cache和Dataset的大小都會增加1128,也就是說Cache會增加128K,而Dataset會增加8M。生成Cache的演算法如下:

def mkcache(cache_size, seed):
    cache = [hash(seed)]
    for
i in range(1,cache_size): cache.append(hash[cache[-1]]) return cache

dataset的生成來源於cache,具體來說,dataset[i]個元素的生成需要cache和cache[i]的參與。dataset中第i個元素的生成程式碼如下:

def cal_dataset_i(cahce, i):# 計算dataset[i]
    cache_size = cache.size
    mix = hash(cache[i%cache_size]^i)# cache遠遠小於dataset,讓i也參與運算,從而使得mix不會重複
for j in range(256): cache_index = get_int(mix); mix = make_item(mix,cache[cache_index%cache_size]) return hash(mix)

上述程式碼是虛擬碼,省略了大部分細節,重點在於展示原理。

  1. 先通過cache中的第i%cache_size個元素生成初始的mix,因為兩個不同的dataset元素可能對應同一個cache中的元素,為了保證每個初始的mix都不同,注意到i也參與了雜湊計算。
  2. 隨後迴圈256次,每次通過get_int來根據當前的mix值求得下一個要訪問的cache元素的下標,用這個cache元素和mix通過make_item求得新的mix值。注意到由於初始的mix值都不同,所以訪問cache的序列也都是不同的。
    最終返回mix的雜湊值,得到第i個dataset中的元素。
  3. 多次呼叫這個函式,就可以得到完整的dataset。

通過cache生成dataset的元素時,下一個用到的cache中的元素的位置是通過當前用到的cache的元素的值計算得到的,這樣具體的訪問順序事先不可預知,滿足偽隨機性。生稱dataset的程式碼如下:

def calc_dataset(full_size, cache):
    return [calc_dataset_item(cache,i) for i in range(full_size)]

生成dataset之後礦工就可以開始挖礦,根據特定的過程計算出一個雜湊值,其程式碼如下所示。其中的迴圈64次並沒有額外的原因,就是想增加挖礦過程中的訪問記憶體操作。礦工為了增加挖礦速度,就必須要將dataset儲存在記憶體中。

# 根據nonce計算出一個雜湊值
def get_hash_value(header, nonce, full_size, dataset):
    hash_value = hash(header, nonce);
    for i in range(64):
        dataset_index = get_int(hash_value )%full_size
        hash_value = make_item(hash_value , dataset[dataset_index)
        hash_value = make_item(hash_value , dataset[dataset_index+1])
    return hash(hash_value )

如果一個nonce不合適,就需要更換一個nonce,直到找到合適的nonce,整個挖礦過程虛擬碼如下:

def mine(full_size, dataset, header, target):
    max_nonce = 2**64
    nonce = random.randint(0, max_nonce )
    while get_hash_value(header, nonce, full_size, dataset) > target:
        nonce = (nonce+1)%max_nonce
    return nonce

為什麼要挖礦中設計cache呢,貌似礦工挖礦的時候根本沒有用到cache,為什麼要多此一舉?這是為了方便輕節點對區塊進行驗證。對於輕節點來說,不可能儲存很大的dataset,但是輕節點可以通過儲存cache,驗證某個區塊塊頭時,根據cache生成dataset中某個元素,隨後驗證區塊的合法性。輕節點驗證區塊合法性的虛擬碼如下:

def varify(header, nonce, full_size, cache):
    hash_value = hash(header, nonce)
    for i in range(64):
        index = get_int(hash_value)%full_size
        hash_value = hash(hash_value, cal_dataset_i(cache,index))# 計算生成dataset中的資料
        hash_value = hash(hash_value,cal_dataset_i(cache,index+1))
    return hash(hash_value)

礦工需要驗證大量的nonce,若每次都要從16M的cache中重新生成,那麼挖礦的效率就大打折扣,而且會有大量的重複計算:隨機選取的dataset的元素中有很多是重複的,可能是之前嘗試別的nonce時用過的。所以,礦工採取以空間換時間的策略,把整個dataset儲存下來。而輕節點由於只驗證一個nonce,驗證的時候就直接生成要用到的dataset中的元素就行了。

以太坊挖礦難度調整

以太坊中的區塊的難度調整公式如下圖所示。


引數說明

  1. 區塊鏈難度調整中,創始塊的難度被設定為D0=131072 ,此後每個區塊的難度都與其父區塊的難度相關。D(H)是本區塊的難度,由P(H)Hd+x×ζ2和難度炸彈ϵ構成。

  2. P(H)Hd為父區塊的難度,每個區塊的難度都是在父區塊難度的基礎上進行調整。

  3. x×ζ2用於自適應調節出塊難度,維持穩定的出塊速度。

  4. ϵ表示難度炸彈。
  5. 難度有最低下限,即不能低於D0=131072

其中xϵ2的計算方式如下圖所示。

  • x 是父區塊難度的12048的取整,是調整的單位
  • ϵ調整係數,其小隻能是-99。
  • y的取值依賴於父區塊是否包含叔父區塊,如果包含,則y=2,否則y=1。
  • HS是本區塊的時間戳,P(H)Hs是父區塊的時間戳,單位為秒,並且HS>P(H)Hs
  • 難度降低的上界設定為−99 ,主要是應對被黑客攻擊或其他目前想不到的黑天鵝事件。

假設當父區塊不帶叔父區塊的時候(y=1),調整過程如下:

  • 出塊時間在[1,8]之間,出塊時間過短,難度調大一個單位
  • 出塊時間在[9,17]之間,出塊時間可以接受,難度保持不變
  • 出塊時間在[18,26]之間,出塊時間過長,難度調小一個單位

  • 這裡發現,出塊時間變長,區塊的整體難度就會調小,假若有的礦工,故意將區塊的時間戳改的比較晚,那麼是不是就可以搶先發布區塊呢?比如說將時間戳延遲寫15秒,會怎麼樣呢?這樣就會導致該礦工計算出來的難度比別的礦工計算的難度低,其他礦工15秒釋出一個區塊,而該礦工可以在10秒內釋出區塊,可以拿到區塊獎勵。但是問題在於假如剛好也有別的區塊在10秒內釋出了區塊,此時根據POW的規則,另外一個礦工釋出的區塊難度更大,因此其他礦工會以最大工作量標準,選擇15秒內挖出的區塊所在的鏈作為主鏈,而該礦工釋出的區塊便成了叔父區塊。

難度炸彈計算公式如下圖所示。

  • ϵ是2的指數函式,每十萬個塊擴大一倍,後期增長非常快,這就是難度“炸彈”的由來。

  • Hi稱為fake block number,由真正的block number Hi減少三百萬得到。之所以減少三百萬,是因為目前proof of stake的工作量證明方式還存在一些問題,pos協議涉及不夠完善,但是難度炸彈已經導致挖礦時間變成了30秒左右,為了減小難度,就會減去三百萬。

設定難度炸彈的原因是要降低遷移到PoS協議時發生fork的風險,假若礦工聯合起來抵制POS的工作量證明模式,那就會導致以太坊產生硬分叉;有了難度炸彈,挖礦難度越來越大,礦工就有意願遷移到PoS協議上了。難度炸彈的威力,可以通過下圖看出。

區塊數量到370萬之後,挖礦難度突然遞增,到430萬時,難度已經非常之大了,這時候挖礦時間已經變為為30秒,但是POS協議還沒有完善,於是以太坊將挖礦難度公式進行調整,使得每次計算時,當前區塊號減去三百萬,這樣就降低了挖礦難度,並且在這個時期,對以太坊出塊獎勵進行了調整,從原來的5個ETH變為3個ETH。

以太坊中難度計算公式如下圖所示,由於目前處於以太坊發展的Metropolis中的Byzantium階段,所以難度計算公式的函式名稱為calcDifficultyByzantium

// calcDifficultyByzantium is the difficulty adjustment algorithm. It returns
// the difficulty that a new block should have when created at time given the
// parent block's time and difficulty. The calculation uses the Byzantium rules.
func calcDifficultyByzantium(time uint64, parent *types.Header) *big.Int {
    // https://github.com/ethereum/EIPs/issues/100.
    // algorithm:這裡給出了難度計算公式的整體註釋
    // diff = (parent_diff +
    // (parent_diff / 2048 * max((2 if len(parent.uncles) else 1)
    // - ((timestamp - parent.timestamp) // 9), -99))) + 2^(periodCount - 2)
    // 獲取當前時間和父區塊的時間戳
    bigTime := new(big.Int).SetUint64(time)
    bigParentTime := new(big.Int).Set(parent.Time)

    // holds intermediate values to make the algo easier to read & audit
    x := new(big.Int)
    y := new(big.Int)

    //這裡求出當前區塊時間戳和父區塊的時間戳,然後求差之後除以9
    // (2 if len(parent_uncles) else 1)-(block_timestamp - parent_timestamp) // 9
    x.Sub(bigTime, bigParentTime)
    x.Div(x, big9)
    if parent.UncleHash == types.EmptyUncleHash {
        x.Sub(big1, x)
    } else {
        x.Sub(big2, x)
    }

    // max((2 if len(parent_uncles) else 1)-(block_timestamp - parent_timestamp) // 9, -99)
    if x.Cmp(bigMinus99) < 0 {
        x.Set(bigMinus99)
    }
    // parent_diff + (parent_diff / 2048 * max((2 if len(parent.uncles) else 1) - 
    //((timestamp - parent.timestamp) // 9), -99))
    y.Div(parent.Difficulty, params.DifficultyBoundDivisor)
    x.Mul(y, x)
    x.Add(parent.Difficulty, x)

    // minimum difficulty can ever be (before exponential factor)
    // MinumumDifficulty = big.NewInt(131072)
    if x.Cmp(params.MinimumDifficulty) < 0 {
        x.Set(params.MinimumDifficulty)
    }
    // calculate a fake block number for the ice-age delay:
    // https://github.com/ethereum/EIPs/pull/669
    // fake_block_number = max(0, block.number - 3_000_000)
    fakeBlockNumber := new(big.Int)
    if parent.Number.Cmp(big2999999) >= 0 {
        // Note, parent is 1 less than the actual block number
        fakeBlockNumber = fakeBlockNumber.Sub(parent.Number, big2999999) 
    }
    // for the exponential factor
    periodCount := fakeBlockNumber
    periodCount.Div(periodCount, expDiffPeriod)

    // the exponential factor, commonly referred to as "the bomb"
    // diff = diff + 2^(periodCount - 2)
    if periodCount.Cmp(big1) > 0 {
        y.Sub(periodCount, big2)
        y.Exp(big2, y, nil)
        x.Add(x, y)
    }
    return x
}

至此,以太坊的挖礦過程和難度調整過程告一段落。