本文首發於深入淺出區塊鏈社群
原文連結:以太坊創世區塊與鏈配置載入分析,原文已更新,請讀者前往原文閱讀。
創世區塊作為第零個區塊,其他區塊直接或間接引用到創世區塊。因此節點啟動之初必須載入正確的創世區塊資訊,且不得任意修改。
以太坊允許通過創世配置檔案來初始化創世區塊,也可使用選擇使用內建的多個網路環境的創世配置。預設使用以太坊主網創世配置。
創世配置檔案
如果你需要搭建以太坊私有鏈,那麼瞭解創世配置是必須的,否則你大可不關心創世配置。下面是一份 JSON 格式的創世配置示例:
{
"config": {
"chainId": 1,
"homesteadBlock": 1150000,
"daoForkBlock": 1920000,
"daoForkSupport": true,
"eip150Block": 2463000,
"eip150Hash": "0x2086799aeebeae135c246c65021c82b4e15a2c451340993aacfd2751886514f0",
"eip155Block": 2675000,
"eip158Block": 2675000,
"byzantiumBlock": 4370000,
"constantinopleBlock": 7280000,
"petersburgBlock": 7280000,
"ethash": {}
},
"nonce": "0x42",
"timestamp": "0x0",
"extraData": "0x11bbe8db4e347b4e8c937c1c8370e4b5ed33adb3db69cbdb7a38e1e50b1b82fa",
"gasLimit": "0x1388",
"difficulty": "0x400000000",
"mixHash": "0x0000000000000000000000000000000000000000000000000000000000000000",
"coinbase": "0x0000000000000000000000000000000000000000",
"number": "0x0",
"gasUsed": "0x0",
"parentHash": "0x0000000000000000000000000000000000000000000000000000000000000000",
"alloc": {
"000d836201318ec6899a67540690382780743280": {
"balance": "0xad78ebc5ac6200000"
},
"001762430ea9c3a26e5749afdb70da5f78ddbb8c": {
"balance": "0xad78ebc5ac6200000"
}
}
}
根據配置用途可分為三大類:
- 鏈配置
config
項是定義鏈配置,會影響共識協議,雖然鏈配置對創世影響不大,但新區塊的出塊規則均依賴鏈配置。 - 創世區塊頭資訊配置
nonce
:隨機數,對應創世區塊Nonce
欄位。timestamp
:UTC時間戳,對應創世區塊Time
欄位。extraData
:額外資料,對應創世區塊Extra
欄位。gasLimit
:必填,燃料上限,對應創世區塊GasLimit
欄位。difficulty
:必填,難度係數,對應創世區塊Difficulty
欄位。搭建私有鏈時,需要根據情況選擇合適的難度值,以便調整出塊。minHash
:一個雜湊值,對應創世區塊的MixDigest
欄位。和 nonce 值一起證明在區塊上已經進行了足夠的計算。coinbase
:一個地址,對應創世區塊的Coinbase
欄位。
- 初始賬戶資產配置
alloc
項是創世中初始賬戶資產配置。在生成創世區塊時,將此資料集中的賬戶資產寫入區塊中,相當於預挖礦。這對開發測試和私有鏈非常好用,不需要挖礦就可以直接為任意多個賬戶分配資產。
自定義創世
如果你計劃部署以太坊私有網路或者一個獨立的測試環境,那麼需要自定義創世,並初始化它。為了統一溝通,推薦先在使用者根目錄建立一個資料夾 deepeth
,以做為《以太坊設計與實現》電子書學習工作目錄。
mkdir $HOME/deepeth && cd $HOME/deepeth
再準備兩個以太坊賬戶,以便在創世時存入資產。
geth --datadir $HOME/deepeth account new
因為是學習使用,推薦使用統一密碼 foobar
,執行兩次命令,建立好兩個賬戶。這裡使用 --datadir
引數指定以太坊執行時資料存放目錄,是讓大家將資料統一存放在一個本課程學習資料夾中。
再將下面配置內容儲存到 $HOME/deepeth/genesis.json
檔案,其中 alloc
項替換成剛剛建立的兩個以太坊賬戶地址。
{
"config": {
"chainId": 8888,
"homesteadBlock": 0,
"daoForkBlock": 0,
"daoForkSupport": true,
"eip150Block": 0,
"eip155Block": 0,
"eip158Block": 0,
"byzantiumBlock": 0,
"constantinopleBlock": 0,
"petersburgBlock": 0,
"ethash": {}
},
"nonce": "0x42",
"timestamp": "0x0",
"extraData": "0x11bbe8db4e347b4e8c937c1c8370e4b5ed33adb3db69cbdb7a38e1e50b1b82fa",
"gasLimit": "0x1388",
"difficulty": "0x1",
"alloc": {
"093f59f1d91017d30d8c2caa78feb5beb0d2cfaf": {
"balance": "0xffffffffffffffff"
},
"ddf7202cbe0aaed1c2d5c4ef05e386501a054406": {
"balance": "0xffffffffffffffff"
}
}
}
然後,執行 geth 子命令 init 初始化創世區塊。
geth --datadir $HOME/deepeth init genesis.json
執行成功後,便可啟動該私有鏈:
geth --maxpeers 0 --datadir $HOME/deepeth console
執行如下命令,可以檢視到前面建立的兩個賬戶,均已有資產:
eth.getBalance(eth.accounts[0])
// 18446744073709551615
eth.getBalance(eth.accounts[1])
// 18446744073709551615
至此,我們已完成創世定製版。
內建的創世配置
上面我已完成自定義創世,但以太坊作為去中心平臺,需要許多節點一起參與。僅僅為了測試,多個節點來搭建私有鏈比較麻煩。如果希望和別人一起聯調,或者需要在測試網路中測試DAPP時,該怎麼辦呢?那麼,可使用以太坊測試網路。以太坊公開的測試網路有 5 個,目前仍在執行的有 4 個,具體見下表格。
測試網 | 共識機制 | 出塊間隔 | 提供方 | 上線時間 | 備註 | 狀態 | |
---|---|---|---|---|---|---|---|
Morden | PoW | 以太坊官方 | 2015.7 | 因難度炸彈被迫退役 | stopped | ||
Ropsten | PoW | 30秒 | 以太坊官方 | 2016.11 | 接替Morden | running | |
Kovan | PoA | 4秒 | 以太坊錢包Parity開發團隊 | 2017.3 | 不支援geth | running | |
Rinkeby | PoA | 15秒 | 以太坊官方 | 2017.4 | 最常用,只支援geth | running | |
Sokol | PoA | 5秒 | 以太坊官方POA.network團隊 | 2017.12 | 不支援geth | running | |
Görli | PoA | 15秒 | 以太坊柏林社群 | 2018.9 | 首個以太坊2.0實驗場 | running |
支援 geth 的3個測試網路的創世配置已內建在以太坊程式碼中,具體見 core/genesis.go
檔案:
// DefaultTestnetGenesisBlock returns the Ropsten network genesis block.
func DefaultTestnetGenesisBlock() *Genesis{}
// DefaultRinkebyGenesisBlock returns the Rinkeby network genesis block.
func DefaultRinkebyGenesisBlock() *Genesis
// DefaultGoerliGenesisBlock returns the Görli network genesis block.
func DefaultGoerliGenesisBlock() *Genesis{}
當然不會缺以太坊主網創世配置,也是 geth 執行的預設配置。
// DefaultGenesisBlock returns the Ethereum main net genesis block.
func DefaultGenesisBlock() *Genesis{}
如果你不想自定義創世配置檔案用於開發測試,那麼以太坊也提供一份專用於本地開發的配置。
// DeveloperGenesisBlock returns the 'geth --dev' genesis block. Note, this must
// be seeded with the
func DeveloperGenesisBlock(period uint64, faucet common.Address) *Genesis
執行 geth --dev console
可臨時執行使用。但如果需要長期使用此模式,則需要指定 datadir
。
geth --dev --datadir $HOME/deepeth/dev console
首次執行 dev 模式會自動建立一個空密碼的賬戶,並開啟挖礦。當有新交易時,將立刻打包出塊。
geth 創世區塊載入流程
在執行 geth 時需根據配置檔案載入創世配置以及創世區塊,並校驗其合法性。如果配置資訊隨意變更,易引起共識校驗不通過等問題。只有在載入並檢查通過時,才能繼續執行程式。
上圖是一個簡要流程,下面分別講解“載入創世配置”和“安裝創世區塊”兩個子流程。
載入創世配置
應使用哪種創世配置,由使用者在啟動 geth 時決定。下圖是創世配置選擇流程圖:
通過 geth 命令引數可選擇不同網路配置,可以通過 networkid
選擇,也可使用網路名稱啟用。
- 使用 networkid:
不同網路使用不同ID標識。- 1=Frontier,主網環境,是預設選項。
- 2=Morden 測試網路,但已禁用。
- 3=Ropsten 測試網路。
- 4=Rinkeby 測試網路。
- 直接使用網路名稱:
- testnet: Ropsten 測試網路。
- rinkeby: Rinkeby 測試網路。
- goerli: Görli 測試網路。
- dev: 本地開發環境。
geth 啟動時根據不同引數選擇載入不同網路配置,並對應不同網路環境。如果不做任何選擇,雖然在此不會做出選擇,但在後面流程中會預設使用主網配置。
安裝創世區塊
上面已初步選擇創世配置,而這一步則根據配置載入或者初始化創世單元。下圖是處理流程:
首先,需要從資料庫中根據區塊高度 0 讀取創世區塊雜湊。如果不存在則說明本地屬於第一次啟動,直接使用執行時創世配置來構建創世區塊。屬於首次,還需要儲存創世區塊和鏈配置。
如果存在,則需要使用執行時創世配置構建創世區塊並和本次已儲存的創世區塊雜湊進行對比。一旦不一致,則返回錯誤,不得繼續。
隨後,還需要檢查鏈配置。先從資料庫獲取鏈配置,如果不存在,則無需校驗直接使用執行時鏈配置。否則,需要檢查執行時鏈配置是否正確,只有正確時才能替換更新。但有一個例外:主網配置不得隨意更改,由程式碼控制而非人為指定。
總的來說,以太坊預設使用主網配置,只有在首次執行時才建立和儲存創世區塊,其他時候僅僅用於校驗。而鏈配置除主網外則在規則下可隨時變更。
構建建立區塊
上面我們已知曉總體流程,這裡再細說下以太坊是如何根據創世配置生成創世區塊。核心程式碼位於 core/genesis.go:229
。
func (g *Genesis) ToBlock(db ethdb.Database) *types.Block{
if db == nil {
db = rawdb.NewMemoryDatabase()
}
statedb, _ := state.New(common.Hash{}, state.NewDatabase(db))//❶
for addr, account := range g.Alloc { //❷
statedb.AddBalance(addr, account.Balance)
statedb.SetCode(addr, account.Code)
statedb.SetNonce(addr, account.Nonce)
for key, value := range account.Storage {
statedb.SetState(addr, key, value)
}
}
root := statedb.IntermediateRoot(false)//❸
head := &types.Header{//❹
Number: new(big.Int).SetUint64(g.Number),
Nonce: types.EncodeNonce(g.Nonce),
Time: g.Timestamp,
ParentHash: g.ParentHash,
Extra: g.ExtraData,
GasLimit: g.GasLimit,
GasUsed: g.GasUsed,
Difficulty: g.Difficulty,
MixDigest: g.Mixhash,
Coinbase: g.Coinbase,
Root: root,
}
//❺
if g.GasLimit == 0 {
head.GasLimit = params.GenesisGasLimit
}
if g.Difficulty == nil {
head.Difficulty = params.GenesisDifficulty
}
statedb.Commit(false)//❻
statedb.Database().TrieDB().Commit(root, true)//❼
return types.NewBlock(head, nil, nil, nil)//❽
}
上面程式碼是根據創世配置生成創世區塊的程式碼邏輯,細節如下:
- ❶ 創世區塊無父塊,從零初始化全新的
state
(後續文章會詳細講解state
物件)。 ❷ 遍歷配置中
Alloc
項賬戶集合資料,直接寫入 state 中。
這裡不單可以設定balance
,還可以設定code
、nonce
以及任意多個storage
資料。
意味著創世時便可以直接部署智慧合約。例如下面配置則在創世時部署了一個名為093f59f1d91017d30d8c2caa78feb5beb0d2cfaf
的智慧合約。"alloc": { "093f59f1d91017d30d8c2caa78feb5beb0d2cfaf": { "balance": "0xffffffffffffffff", "nonce": "0x3", "code":"0x606060", "storage":{ "11bbe8db4e347b4e8c937c1c8370e4b5ed33adb3db69cbdb7a38e1e50b1b82fa":"1234ff" } } }
- ❸ 將賬戶資料寫入 state 後,便可以計算出 state 資料的默克爾樹的根值,稱之為
StateRoot
。
此值記錄在區塊頭Root
欄位中。 - ❹ 創世配置的一部分配置,則直接對映到區塊頭中,完成創世區塊頭的構建。
- ❺ 因為
GasLimit
和Difficulty
直接影響到下一個區塊出塊處理。
因此未設定時使用預設配置(Difficulty=131072,GasLimit=4712388)。 - ❻ 提交 state,將 state 資料提交到底層的記憶體 trie 資料中。
- ❼ 將記憶體 trie 資料更新到 db 中。
這是多餘的一步,因為提交到資料庫是由外部進行,這裡只需要負責生成區塊。 ❽ 利用區塊頭建立區塊,且區塊中無交易記錄。
深入淺出區塊鏈 - 系統學習區塊鏈,學區塊鏈都在這裡,打造最好的區塊鏈技術部落格。