深入原始碼理解 Ethereum 的獎勵機制

Photo by Thought Catalog on Unsplash
Photo by Thought Catalog on Unsplash

大家都知道在區塊鏈上,礦工(miner)可以透過挖礦(mining)的方式來獲得獎勵,也就是得到虛擬貨幣,一般來說,在整個區塊鏈上會有許多礦工在進行挖礦,來爭奪獎勵。在本文章中,會簡單介紹關於 Bitcoin 和 Ethereum 的基礎知識,理解礦工如何透過挖礦的方式取得獎勵,然後這中間進行了哪些事情,最後會透過原始碼的方式針對 Ethereum 的獎勵機制進行說明,讓大家可以更清楚地理解 Ethereum 的獎勵機制。

在本文中也會對於 Bitcoin 以及 Ethterum 常見的相關的名詞做說明,希望大家之後看到名詞之後,不會再對名詞產生疑惑,如果你對於區塊鏈還沒有很清楚的概念,可以參考 @fukuball 大大寫的 Ethereum 開發筆記系列,讓你可以對區塊鏈可以有更多的初步認識。

如果文章內容有任何錯誤請盡快告知我,謝謝!


⚖️ Consensus

區塊鏈是一個去中心化的系統,整個區塊鏈就像是一個大帳本,每個區塊上面紀錄了許多交易資訊,若某個節點若想要建立一個區塊,要如何讓分散在全球各地的節點可以一致認同這個區塊的建立,需要透過共識機制(Consensus)

共識機制維護了整個區塊鏈的運作順序和公平性,也保障整個區塊鏈的安全性和一致性,其中也包含一些獎勵機制,讓整個區塊鏈系統可以有效的運作,本文內容就是要討論關於共識機制下的獎勵機制。


🇧itcoin

Bitcoin 主要是透過工作量證明最長鏈兩個部分作為共識機制:

⛏ Proof Of Work (PoW)

每個節點收集許多有效但是尚未被確認的交易,這些交易會組成一顆 Merkle Tree,得到一個 Merkle Root 的 Hash 值:

Source: Wikipedia, Merkle Tree

接著再結合區塊的另外 6 個 Header(總共為 7 個 Header)並且經過 2 次SHA256 計算取得區塊的 Hash 值:

bitcoin-block-header
Source: parity-bitcoin, Bitcoin Block Header
  • Version:區塊版號
  • hashPrevBlock:先前區塊的 Hash 值
  • hashMerkleRoot:通過 Merkle Root 來表示交易的 Hash 值
  • Timestamp:時間戳記
  • Bits:區塊計算難度
  • Nonce:隨機值,也就是 PoW 所要計算的值

公式:

SHA256(SHA256(Version + hashPrevBlock + hashMerkleRoot + Timestamp + Bits + Nonce))

一個有效的區塊要求 Hash 值必須小於 Bits 難度(Difficulty)值,實際上挖礦其實就是不斷通過修改 Nonce 參數來取得新的 Hash 值,直到符合要求難度的 Hash 值。因為 Hash 函式的特性,當難度值越大,若要計算出符合的 Hash 值就必須花上更多時間,在這個過程中可以證明你付出了一定的工作量,所以稱之為工作量證明

進行工作量證明之前,每個節點都會進行以下準備:

  • 收集廣播中尚未被記錄的交易
  • 檢查每筆交易中,付款的地址是否有足夠的餘額
  • 驗證交易是否有正確的簽名
  • 打包通過驗證的交易
  • 為自己的地址新增一筆獎勵交易
  • 贏得記帳權的節點,可以贏得獎勵

在 Bitcoin 中,一段時間內(平均 10 分鐘以內)只有一個節點可以取得記帳的權利,礦工會透過工作量證明競爭來取得唯一記帳的權利,當某個節點(礦工)計算出符合難度的 Hash 時,說明該節點已經挖礦成功,它會將該區塊放入到自己本機的區塊鏈上,並同時廣播該區塊給其他的節點,其他節點在收到該區塊時會對進行驗證,確認該區塊是有效的。當各個節點驗證該區塊沒問題之後,便會把這個區塊放入自己本機上的區塊鏈,挖礦成功的礦工則可以獲得獎勵。

🔗 最長鏈規則 (Longest Chain Rule)

區塊鏈上存在著許多節點,有可能會出現多個節點同時挖出了區塊,前面提到,當礦工完成挖礦的同時,會向全網路上的礦工(節點)進行廣播告訴大家說:「Hey 各位,我解出問題啦!」,假設 A 礦工B 礦工同時間廣播了 B1B2 區塊,這時候可能會因為網路的延遲造成其他節點收到的區塊是不一樣的,可能有些節點先收到 B1,有些則先收到 B2,因為這些區塊都是有效的,當其他節點收到後,就會將該區塊納入自己的節點中,這時候就會造成所謂的「分岔(fork)」,也就是說節點之間的區塊會有不一致的狀況:

fork
分岔(fork)的簡易示意圖

但是這種分岔情況不會一直持續下去,當下個區塊的出現,將會打破這個僵局

以上圖繼續為範例進行說明,若是當下一個新的區塊(B3)是基於 B1 所產生的,這時候可以說明,B1 這條鏈是累積工作量最多的鏈,所以就會變成最長鏈(見下圖):

fork demo

而原本基於 B2 區塊繼續挖礦的礦工,當收到新的區塊(B3)的時候,這時候就會放棄原本基於 B2 區塊的工作,並轉為開始挖掘基於 B1B3 的新區塊,這時候的 B2 就會變成所謂的「陳舊區塊(Stale Block)」,而整個區塊鏈就會收斂成只有一條鏈的狀態了。

💰 區塊獎勵(Block Reward)

Bitcoin 的獎勵機制非常簡單,每位礦工在挖礦的過程中,可以得到 2 種類型的獎勵:

  • 建立區塊的獎勵
  • 區塊的手續費

建立區塊的獎勵中,這是憑空產生 Bitcoin,又稱為 Coinbase

在中本聰設計的 Bitcoin 中,每挖出 21 萬個區塊時,區塊獎勵就會減半,也就是說:

  • 1 - 210000 => 50BTC
  • 210001 - 420000 => 25BTC
  • 420001 - 630000 => 12.5BTC
  • ...以此類推

目前挖到 Bitcoin 的區塊只能獲得 12.5BTC(於 2018/09/15,目前區塊編號 #541,476),有興趣的可以看看這個網站


thereum

在本文上半段介紹了 Bitcoin 的共識機制,以及區塊的獎勵,那 Bitcoin 的共識機制和獎勵與 Ethereum 有什麼不同呢?

Ethereum 的共識機制也是基於 Bitcoin,但是與 Bitcoin 不同的是,在 Ethereum 中,產生區塊的速度非常的快,所以一定會有許多的陳舊區塊產生,而在 Bitcoin 中,那些沒有成為普通區塊的陳舊區塊,是沒有任何獎勵的;但是在 Ethereum 中,若較新的區塊將先前面的陳舊區塊打包進來,則可以獲得獎勵

延伸閱讀:Proof Of Stake

👻 GHOST(Greedy Heaviest Observed Subtree)Protocl

GHOST Protocol,中文稱為「貪婪最重可見子樹協議」,是由 Yonatan Sompolinsky 和 Aviv Zohar 於 2013 年提出的協議。

GHOST 提出的主要動機是可以快速地確認產出的區塊,避免過多的陳舊區塊造成安全性降低,因為區塊被挖出後需要一定的時間才可以擴散至整個網路。在前面有提到分岔的問題,若 A 礦工和 B 礦工同時間挖出了區塊,如果 A 礦工的區塊傳播速度較 B 礦工來得快,B 礦工的所挖的 B 區塊就會變成陳舊區塊,B 礦工等於做了白工,而且沒有為整個網路的安全做出貢獻;另外也有算力中心化的問題:假設在 P 礦池中,A 礦工的算力若是大於 B 礦工,A 礦工實際上有可以掌握更多挖礦算力的機會。

🤵🏻 Uncle Block

在 GHOST 協議中,除了目前區塊的父區塊祖先區塊也打包了那些陳舊區塊(Stale Block),這些陳舊區塊也會是工作證明的根據之一,在 Ethereum 中這些被打包的陳舊區塊稱為「叔區塊(Uncle Block)」。

為什麼叫叔區塊呢?

目前區塊的上一個區塊我們稱為「父區塊」,而和父區塊幾乎是同時間產生的,但卻是沒有放進主鏈,若目前區塊把先前的陳舊區塊打包進來,就稱作為「叔區塊」。

叔區塊定義如下,詳細可以參閱白皮書

  • 一個區塊必須指定一個父區塊,而且它必須指定 0 或多個叔區塊
  • B 的叔區塊必須具備以下屬性:
    • 它必須是 B 的第 k 代祖先區塊的直接子區塊,條件為 2 <= k <= 7
    • 它不能是 B 的祖先區塊
    • 叔區塊必須有一個有效的區塊 Header,但不必是先前驗證或者是有效的區塊
    • 叔區塊必須要是之前沒有被打包過的

🔗 選擇主鏈

在 Bitcoin 主要是以「最長鏈」為主鏈,而在 Ethereum 的 GHOST 協議則是以區塊子樹包含最多有效的區塊為主鏈。

ghost
Source: Secure High-Rate Transaction Processing in Bitcoin

根據上圖可以了解到:

  • 如果是根據最長鏈規則,主鏈應該是 0 <- 1B <- 2D <- 3F <- 3F <- 4C <- 5B
  • 如果是根據 GHOST 協議,主鏈應該是 0 <- 1B <- 2C <- 3D <- 4B,因為包含最多有效的區塊
  • 如果採用 GHOST 協議,攻擊者(紅圈處)若偽造 0 <- 1A ... <- 6A 的鏈,並不會被認為是主鏈

但 GHOST 協議並沒有辦法有效的解決關於算力中心化的問題,於是 Ethereum 對 GHOST 協議做了一些改良,讓當初挖掘叔區塊的礦工也可以拿到應有的獎勵。

接下來就是本文的重點了,Ethereum 的獎勵機制到底是如何進行的。

💰 區塊獎勵(Block Reward)

1. 普通區塊的獎勵

普通區塊獎勵如下:

  • 每個區塊固定獎勵為 3ETH
  • 區塊內包含所有 Gas Fee
  • 普通區塊打包了叔區塊,每個叔區塊可以得到額外的獎勵 3ETH 的 1/32

💡 小知識:Ethereum 在最初的區塊獎勵為 5ETH,在「拜占庭(Byzantium)」分岔後獎勵有所改變


2. 叔區塊的獎勵

Uncle Block

上圖為一個普通區塊和叔區塊的關係示意圖,白色部分為叔區塊

叔區塊獎勵 =(叔區塊號碼 + 8 - 包含叔區塊的區塊號碼)* 普通區塊的獎勵(3ETH)/ 8

根據上述的公式,讓我們透過圖片所標示的區塊號碼做為範例計算一次:

  1. 目前的區塊號碼: 1007
  2. 第一個叔區塊:1006
  3. 公式帶入:

$$ (1006 + 8 - 1007) * 3 / 8 $$

將上述公式整理一下:

=> (1014 + 8 - 1007) * 3 / 8
=> 7 * 3 / 8
=> (7 / 8) * 3

我們將計算結果整理為 (7 / 8) * 3,也就是說第一層的叔區塊可以拿到的獎勵會是最多,如果叔區塊距離越遠,所拿到的獎勵相對的就會越少,以下表格是獎勵的整理:

間隔層數報酬比例獎勵 (Eth)
17/82.625
26/82.25
35/81.875
44/81.5
53/81.125
62/80.75

3. 從原始碼理解獎勵計算

接著我們將會從原始碼來理解獎勵是如何計算的:

// Some weird constants to avoid constant memory allocs for them.
var (
big8 = big.NewInt(8)
big32 = big.NewInt(32)
)
// AccumulateRewards credits the coinbase of the given block with the mining
// reward. The total reward consists of the static block reward and rewards for
// included uncles. The coinbase of each uncle block is also rewarded.
func accumulateRewards(config *params.ChainConfig, state *state.StateDB, header *types.Header, uncles []*types.Header) {
// Select the correct block reward based on chain progression
blockReward := FrontierBlockReward
if config.IsByzantium(header.Number) {
blockReward = ByzantiumBlockReward
}
// Accumulate the rewards for the miner and any included uncles
reward := new(big.Int).Set(blockReward)
r := new(big.Int)
for _, uncle := range uncles {
r.Add(uncle.Number, big8)
r.Sub(r, header.Number)
r.Mul(r, blockReward)
r.Div(r, big8)
state.AddBalance(uncle.Coinbase, r)
r.Div(blockReward, big32)
reward.Add(reward, r)
}
state.AddBalance(header.Coinbase, reward)
}

獎勵計算的的原始碼

accumulateRewards 函式部分,blockReward 變數一開始預設為 FrontierBlockReward,也就是 5ETH,在拜占庭分岔後,礦工的獎勵機制有所改變,由原先的 5ETH 變成 3ETH,詳細可以參考原始碼

// Ethash proof-of-work protocol constants.
var (
FrontierBlockReward = big.NewInt(5e+18) // Block reward in wei for successfully mining a block
ByzantiumBlockReward = big.NewInt(3e+18) // Block reward in wei for successfully mining a block upward from Byzantium
maxUncles = 2 // Maximum number of uncles allowed in a single block
allowedFutureBlockTime = 15 * time.Second // Max time from current time allowed for blocks, before they're considered future blocks
)

這裡有一個很重要的資訊,每個區塊最多只能打包 2 個叔區塊!

接著來看如何計算區塊的獎勵:

  • reward 是建立一個 big.Int 的整數,並且設定數值為每個區塊的獎勵,這是當礦工挖到一個區塊時所可以得到的獎勵
  • r 為一個初始值,用來計算挖掘叔區塊的礦工可以得到多少獎勵
  • for 迴圈計算該區塊打包了多少個叔區塊,並且可以得到多少的獎勵

從以下程式碼可以對照先前計算叔區塊的公式:

叔區塊獎勵 =(叔區塊號碼 + 8 - 包含叔區塊的區塊號碼)* 普通區塊的獎勵(3ETH)/ 8
// (叔區塊號碼 + 8 - 包含叔區塊的區塊號碼)
r.Add(uncle.Number, big8)
r.Sub(r, header.Number)
// * 普通區塊的獎勵(3ETH)
r.Mul(r, blockReward)
// ÷ 8
r.Div(r, big8)

最後會將獎勵分給挖掘叔區塊的礦工:

state.AddBalance(uncle.Coinbase, r)

for 最後的兩段程式碼,其實就是普通區塊把叔區塊打包進來的額外獎勵:

// blockReward * (1 / 32)
r.Div(blockReward, big32)
// reward = reward + (blockReward * (1 / 32))
reward.Add(reward, r)

最後將 reward 加入到目前的區塊:

state.AddBalance(header.Coinbase, reward)

透過原始碼來看獎勵機制有種安心的感覺(?),網路上雖然很多文章寫了關於獎勵機制,但覺得可以深入看原始碼也是很不錯的理解方式。


Troubleshooting

區塊名詞的混淆

🗿 Stale Block

Stale Block,稱為「陳舊區塊」,它是一個成功被挖掘的區塊,但是並沒成為主鏈的區塊。陳舊區塊通常出現在分岔(fork)的情況下,這些區塊都具有「父節點」,只是在同個高度下,某個區塊累積較多的工作證明,成為主鏈,這些沒進到主鏈的就變成陳舊區塊了。

🏝 Orphan Block

Orphan Block,稱為「孤塊」,這是一個常常和 Stale Block 搞混的名詞(對,就是在說我!),孤塊和陳舊區塊最大的差別在於:在節點中,「孤塊指的是還找不到父區塊的區塊」,因為找不到父區塊,所以無法被驗證,會出現這樣的狀況可能是因為像是網路延遲(network latency)等之類的因素。

最後讓我們用一張圖來看 Stale Block 和 Orphan Block 的區別:

Stale And Orphan

🏊 Transaction Pool(交易池)

TxPool

在 Ethereum 中,每個節點都會有一個本機的交易池(txpool),交易池中儲存的是尚未被加入到區塊的交易(Transaction)

前面有提到在進行工作量證明之前,會收集許多有效但是尚未被確認的交易,這些交易會被放入交易池,當一個區塊競爭結束後,礦工會開始競爭下一個新的區塊,這時候就會從交易池取出尚未被確認的交易繼續下一個區塊的競爭。

這些被放入交易池的交易,通常會根據每個交易的 Gas 由高到低做排序,這可以確保礦工可以得到最高的收益。所以付出越高的 Gas 的交易可以越快被礦工所處理,每個交易池有一定的儲存上限,若交易池的交易數量達到上限就會造成阻塞,Gas 較低的交易被礦工處理的機會就越低。

小知識:在 Bitcoin 的交易池是 mempool,而在 Ethereum 的 Geth 的交易池是 txpool


Reference