深入原始碼理解 Ethereum 的獎勵機制
大家都知道在區塊鏈上,礦工(miner)可以透過挖礦(mining)的方式來獲得獎勵,也就是得到虛擬貨幣,一般來說,在整個區塊鏈上會有許多礦工在進行挖礦,來爭奪獎勵。在本文章中,會簡單介紹關於 Bitcoin 和 Ethereum 的基礎知識,理解礦工如何透過挖礦的方式取得獎勵,然後這中間進行了哪些事情,最後會透過原始碼的方式針對 Ethereum 的獎勵機制進行說明,讓大家可以更清楚地理解 Ethereum 的獎勵機制。
在本文中也會對於 Bitcoin 以及 Ethterum 常見的相關的名詞做說明,希望大家之後看到名詞之後,不會再對名詞產生疑惑,如果你對於區塊鏈還沒有很清楚的概念,可以參考 @fukuball 大大寫的 Ethereum 開發筆記系列,讓你可以對區塊鏈可以有更多的初步認識。
如果文章內容有任何錯誤請盡快告知我,謝謝!
⚖️ Consensus
區塊鏈是一個去中心化的系統,整個區塊鏈就像是一個大帳本,每個區塊上面紀錄了許多交易資訊,若某個節點若想要建立一個區塊,要如何讓分散在全球各地的節點可以一致認同這個區塊的建立,需要透過共識機制(Consensus)。
共識機制維護了整個區塊鏈的運作順序和公平性,也保障整個區塊鏈的安全性和一致性,其中也包含一些獎勵機制,讓整個區塊鏈系統可以有效的運作,本文內容就是要討論關於共識機制下的獎勵機制。
🇧itcoin
Bitcoin 主要是透過工作量證明和最長鏈兩個部分作為共識機制:
⛏ Proof Of Work (PoW)
每個節點收集許多有效但是尚未被確認的交易,這些交易會組成一顆 Merkle Tree,得到一個 Merkle Root 的 Hash 值:
接著再結合區塊的另外 6 個 Header(總共為 7 個 Header)並且經過 2 次的 SHA256
計算取得區塊的 Hash 值:
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 礦工同時間廣播了 B1 和 B2 區塊,這時候可能會因為網路的延遲造成其他節點收到的區塊是不一樣的,可能有些節點先收到 B1,有些則先收到 B2,因為這些區塊都是有效的,當其他節點收到後,就會將該區塊納入自己的節點中,這時候就會造成所謂的「分岔(fork)」,也就是說節點之間的區塊會有不一致的狀況:
但是這種分岔情況不會一直持續下去,當下個區塊的出現,將會打破這個僵局。
以上圖繼續為範例進行說明,若是當下一個新的區塊(B3)是基於 B1 所產生的,這時候可以說明,B1 這條鏈是累積工作量最多的鏈,所以就會變成最長鏈(見下圖):
而原本基於 B2 區塊繼續挖礦的礦工,當收到新的區塊(B3)的時候,這時候就會放棄原本基於 B2 區塊的工作,並轉為開始挖掘基於 B1、B3 的新區塊,這時候的 B2 就會變成所謂的「陳舊區塊(Stale Block)」,而整個區塊鏈就會收斂成只有一條鏈的狀態了。
💰 區塊獎勵(Block Reward)
Bitcoin 的獎勵機制非常簡單,每位礦工在挖礦的過程中,可以得到 2 種類型的獎勵:
- 建立區塊的獎勵
- 區塊的手續費
在建立區塊的獎勵中,這是憑空產生 Bitcoin,又稱為 Coinbase。
在中本聰設計的 Bitcoin 中,每挖出 21 萬個區塊時,區塊獎勵就會減半,也就是說:
1 - 210000
=> 50BTC210001 - 420000
=> 25BTC420001 - 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 協議則是以區塊子樹包含最多有效的區塊為主鏈。
根據上圖可以了解到:
- 如果是根據最長鏈規則,主鏈應該是
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. 叔區塊的獎勵
上圖為一個普通區塊和叔區塊的關係示意圖,白色部分為叔區塊
叔區塊獎勵 =(叔區塊號碼 + 8 - 包含叔區塊的區塊號碼)* 普通區塊的獎勵(3ETH)/ 8
根據上述的公式,讓我們透過圖片所標示的區塊號碼做為範例計算一次:
- 目前的區塊號碼: 1007
- 第一個叔區塊:1006
- 公式帶入:
$$ (1006 + 8 - 1007) * 3 / 8 $$
將上述公式整理一下:
=> (1014 + 8 - 1007) * 3 / 8=> 7 * 3 / 8=> (7 / 8) * 3
我們將計算結果整理為 (7 / 8) * 3
,也就是說第一層的叔區塊可以拿到的獎勵會是最多,如果叔區塊距離越遠,所拿到的獎勵相對的就會越少,以下表格是獎勵的整理:
間隔層數 | 報酬比例 | 獎勵 (Eth) |
---|---|---|
1 | 7/8 | 2.625 |
2 | 6/8 | 2.25 |
3 | 5/8 | 1.875 |
4 | 4/8 | 1.5 |
5 | 3/8 | 1.125 |
6 | 2/8 | 0.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 progressionblockReward := FrontierBlockRewardif config.IsByzantium(header.Number) {blockReward = ByzantiumBlockReward}// Accumulate the rewards for the miner and any included unclesreward := 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 blockByzantiumBlockReward = big.NewInt(3e+18) // Block reward in wei for successfully mining a block upward from ByzantiummaxUncles = 2 // Maximum number of uncles allowed in a single blockallowedFutureBlockTime = 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)// ÷ 8r.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 的區別:
🏊 Transaction Pool(交易池)
在 Ethereum 中,每個節點都會有一個本機的交易池(txpool),交易池中儲存的是尚未被加入到區塊的交易(Transaction)。
前面有提到在進行工作量證明之前,會收集許多有效但是尚未被確認的交易,這些交易會被放入交易池,當一個區塊競爭結束後,礦工會開始競爭下一個新的區塊,這時候就會從交易池取出尚未被確認的交易繼續下一個區塊的競爭。
這些被放入交易池的交易,通常會根據每個交易的 Gas 由高到低做排序,這可以確保礦工可以得到最高的收益。所以付出越高的 Gas 的交易可以越快被礦工所處理,每個交易池有一定的儲存上限,若交易池的交易數量達到上限就會造成阻塞,Gas 較低的交易被礦工處理的機會就越低。
小知識:在 Bitcoin 的交易池是 mempool,而在 Ethereum 的 Geth 的交易池是 txpool。
Reference
- 🔗 Bitcoin Developer Glossary
- 🔗 Bitcoin Developer Reference
- 🔗 Ethereum White Paper
- 🔗 Life Cycle of an Ethereum Transaction
- 🔗 Confirmation Times, Stale Blocks, Reverse Transaction, Double Spending and the 51% Attack in Simple Terms
- 🔗 Releasing Stuck Ethereum Transactions
- 🔗 區塊鏈 Blockchain – 共識機制之工作量證明 Proof-Of-Work
- 🔗 以太坊叔塊相關