本文首發于深入淺出區塊鏈社區
原文鏈接:以太坊創世區塊與鏈配置載入分析,原文已更新,請讀者前往原文閱讀。
創世區塊作為第零個區塊,其他區塊直接或間接引用到創世區塊。因此節點啟動之初必須載入正確的創世區塊信息,且不得任意修改。
以太坊允許通過創世配置文件來初始化創世區塊,也可使用選擇使用內置的多個網絡環境的創世配置。默認使用以太坊主網創世配置。
創世配置文件
如果你需要搭建以太坊私有鏈,那么了解創世配置是必須的,否則你大可不關心創世配置。下面是一份 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 時需根據配置文件加載創世配置以及創世區塊,并校驗其合法性。如果配置信息隨意變更,易引起共識校驗不通過等問題。只有在加載并檢查通過時,才能繼續運行程序。
<img src="https://img.learnblockchain.cn/2019/04/07_20190407101509.png" width="400px" alt="創世加載流程">
上圖是一個簡要流程,下面分別講解“加載創世配置”和“安裝創世區塊”兩個子流程。
加載創世配置
應使用哪種創世配置,由用戶在啟動 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 中。
這是多余的一步,因為提交到數據庫是由外部進行,這里只需要負責生成區塊。? 利用區塊頭創建區塊,且區塊中無交易記錄。
深入淺出區塊鏈 - 系統學習區塊鏈,學區塊鏈都在這里,打造最好的區塊鏈技術博客。