以太坊源碼分析(4)——初始化創世區塊

一、前言

通過前面章節學習了以太坊的基本架構之後,我們通過自己搭建一個單節點,並覆蓋以太坊主要流程來講解代碼。在這一節,你將學會:如何初始化創世區塊

二、代碼研究

2.1 準備工作

  • 以太坊源碼 V 1.8.0

  • golang 1.9+

  • windows 系統下 goland 2018+

本系列文章主要是研究以太坊源碼,所以以太坊的編譯工作不詳細展開,有需要的可以參考這篇文章

2.2 genesis.json 文件

假設你已經在 goland 正確設置好了項目,那麼下面使用一個示例創世文件初始化自己的私有網絡創世塊。

{
  "config": {
    "chainId": 399,
    "homesteadBlock": 0,
    "eip150Block": 0,
    "eip155Block": 0,
    "eip158Block": 0,
    "byzantiumBlock": 0,
    "constantinopleBlock": 0,
    "petersburgBlock": 0
  },
  "alloc": {
	"0x0000000000000000000000000000000000000001": {
    	"balance": "0x84595161401484a000000"
    },
  },
  "coinbase": "0x0000000000000000000000000000000000000000",
  "difficulty": "0x20000",
  "extraData": "",
  "gasLimit": "0x2fefd8",
  "nonce": "0x000000000000ff42",
  "mixhash": "0x0000000000000000000000000000000000000000000000000000000000000000",
  "parentHash": "0x0000000000000000000000000000000000000000000000000000000000000000",
  "timestamp": "0x00"
}
  • 保存創世文件
    在項目目錄下,src 同級目錄新建一個測試數據文件夾 testdata,將上面的內容保存到創世文件 genesis.json 中,並存放在 testdata 文件夾。

  • 準備配置運行參數
    接着,使用 goland 打開 Ethereum-V1.8.0 的工程,找到 go-ethereum/cmd/geth 文件夾 - 右鍵 - 選擇 Create Run Configuration - 點擊 go build github.com/...

  • 配置運行參數
    點擊後,在配置菜單中 Program arguments 欄設置 --datadir=./testdata init ./testdir/genesis.json,點擊 “OK”。保存配置。

  • 設置斷點,開始調試。
    然後按住組合鍵 Ctrl+Shift+F 查找 initGenesis 函數。在函數入口設置斷點。點擊debug 按鈕,程序停在斷點處。

接下來,就看下 initGenesis 函數到底幹了啥。

2.3 initGeness 函數

initGenesis 函數在命令行參數中設置 “init” 命令時被調用,用給定 Json 格式的創世文件初來始化創世塊,如果失敗,創世文件將不寫入創世塊。

// initGenesis will initialise the given JSON format genesis file and writes it as
// the zero'd block (i.e. genesis) or will fail hard if it can't succeed.
func initGenesis(ctx *cli.Context) error {
	// Make sure we have a valid genesis JSON
	genesisPath := ctx.Args().First()
	if len(genesisPath) == 0 {
		utils.Fatalf("Must supply path to genesis JSON file")
	}
	file, err := os.Open(genesisPath)
	if err != nil {
		utils.Fatalf("Failed to read genesis file: %v", err)
	}
	defer file.Close()

	genesis := new(core.Genesis)
	if err := json.NewDecoder(file).Decode(genesis); err != nil {
		utils.Fatalf("invalid genesis file: %v", err)
	}
	// Open an initialise both full and light databases
	stack := makeFullNode(ctx)
	for _, name := range []string{"chaindata", "lightchaindata"} {
		chaindb, err := stack.OpenDatabase(name, 0, 0)
		if err != nil {
			utils.Fatalf("Failed to open database: %v", err)
		}
		_, hash, err := core.SetupGenesisBlock(chaindb, genesis)
		if err != nil {
			utils.Fatalf("Failed to write genesis block: %v", err)
		}
		log.Info("Successfully wrote genesis state", "database", name, "hash", hash)
	}
	return nil
}

函數執行如下 :

  • 從命令行中讀取創世文件的路徑,並打開文件,在函數結束時關閉文件
  • 將讀取的創世文件編碼到 genesis 對象中
  • 根據上下文創建 stake 對象
  • 遍歷字符串數組 ["chaindata", "lightchaindata"] ,根據遍歷出來的名稱打開對應的底層數據庫 chaindb
  • 調用 core.SetupGenesisBlock() 函數,將 genesis 對象中的內容設置到底層數據庫中,如果成功,更新數據庫,否則報錯退出。注意:函數的第一個返回值在這裏被忽略。

2.4 core.SetupGenesisBlock 函數

在 initGenesis 函數中,我們看到了,設置創世區塊的內容是由 SetupGenesisBlock 函數來完成的。如果是對 Ethereum 不熟悉的同學,直接看這個函數的邏輯可能容易被搞糊塗。還是老樣子,我們可以先看看註釋:

// SetupGenesisBlock writes or updates the genesis block in db.
// The block that will be used is:
//
//                          genesis == nil       genesis != nil
//                       +------------------------------------------
//     db has no genesis |  main-net default  |  genesis
//     db has genesis    |  from DB           |  genesis (if compatible)
//
// The stored chain configuration will be updated if it is compatible (i.e. does not
// specify a fork block below the local head block). In case of a conflict, the
// error is a *params.ConfigCompatError and the new, unwritten config is returned.
//
// The returned chain configuration is never nil.

註釋對這個函數功能和主要分支做了較詳細的描述:

SetupGenesisBlock 函數用來寫入或更新數據庫中的創世區塊。根據參數的不同,會出現以下4種情況:

  • 數據庫中沒有創世區塊,且 genesis 指針爲空,默認主網配置
  • 數據庫中沒有創世區塊,但 genesis 指針不爲空,使用 genesis 參數中的配置(寫入創世塊)
  • 數據庫中存在創世區塊,且 genesis 指針爲空,使用數據庫中讀取的創世快(讀取創世塊)
  • 數據庫中存在創世區塊,但 genesis 指針不爲空,如果 genesis 參數中的配置跟數據庫中配置兼容,那麼使用 genesis 參數中的配置(更新創世塊)

函數結果影響創世塊中的鏈配置,如果(更新配置)與鏈配置兼容,保存的鏈配置將被更新,即,不會在本地頭區塊下指定一個分叉區塊。如果(更新配置)與鏈配置衝突,那麼會報配置衝突錯誤,並返回新的、未寫入的 genesis 配置。

據此我們能得到兩個信息:
1)被成功應用的新配置,將被保存到創世塊(數據庫),這也是主要功能。
2)如果有新的創世配置文件被寫入/更新,那麼首先將影響鏈配置,也就是說,如果想要更新鏈的配置,重新初始化鏈配置就行了,前提是更新的配置不可與數據庫中的配置衝突。

SetupGenesisBlock 函數

func SetupGenesisBlock(db ethdb.Database, genesis *Genesis) (*params.ChainConfig, common.Hash, error) {
	if genesis != nil && genesis.Config == nil {
		return params.AllEthashProtocolChanges, common.Hash{}, errGenesisNoConfig
	}

	// Just commit the new block if there is no stored genesis block.
	stored := GetCanonicalHash(db, 0)
	if (stored == common.Hash{}) {
		if genesis == nil {
			log.Info("Writing default main-net genesis block")
			genesis = DefaultGenesisBlock()
		} else {
			log.Info("Writing custom genesis block")
		}
		block, err := genesis.Commit(db)
		return genesis.Config, block.Hash(), err
	}
	// Check whether the genesis block is already written.
	if genesis != nil {
		hash := genesis.ToBlock(nil).Hash()
		if hash != stored {
			return genesis.Config, hash, &GenesisMismatchError{stored, hash}
		}
	}

	// Get the existing chain configuration.
	newcfg := genesis.configOrDefault(stored)
	storedcfg, err := GetChainConfig(db, stored)
	if err != nil {
		if err == ErrChainConfigNotFound {
			// This case happens if a genesis write was interrupted.
			log.Warn("Found genesis block without chain config")
			err = WriteChainConfig(db, stored, newcfg)
		}
		return newcfg, stored, err
	}
	// Special case: don't change the existing config of a non-mainnet chain if no new
	// config is supplied. These chains would get AllProtocolChanges (and a compat error)
	// if we just continued here.
	if genesis == nil && stored != params.MainnetGenesisHash {
		return storedcfg, stored, nil
	}

	// Check config compatibility and write the config. Compatibility errors
	// are returned to the caller unless we're already at block zero.
	height := GetBlockNumber(db, GetHeadHeaderHash(db))
	if height == missingNumber {
		return newcfg, stored, fmt.Errorf("missing block number for head header hash")
	}
	compatErr := storedcfg.CheckCompatible(newcfg, height)
	if compatErr != nil && height != 0 && compatErr.RewindTo != 0 {
		return newcfg, stored, compatErr
	}
	return newcfg, stored, WriteChainConfig(db, stored, newcfg)
}

函數邏輯可分爲兩部分:

1. 下面是數據庫中不存在創世塊的邏輯

  • 檢查 genesis 指針不空的情況下,是否有配置,如果沒有,報錯退出
  • 從數據庫中獲取創世塊的區塊哈希 stored,如果哈希爲空,即不存在創世塊,判斷入參 genesis 是否爲空:
    • 爲空,那麼使用默認的創世塊配置
    • 不空,打印日誌,提示寫入入參中的配置
    • 最後,調用 genesis.Commit() 函數提交 genesis 信息到數據庫。返回提交結果。

2.下面是數據庫中存在創世塊的邏輯

  • 首先,若 genesis 參數指針不爲空,那麼調用 genesis.ToBlock() 函數,將 genesis 的創世塊配置保存到數據庫,並計算用此配置生成的創世塊的哈希,將這個哈希與數據庫原創世塊哈希 stored 對比。如果兩個哈希不一樣,函數返回,並報 GenesisMismatchError 錯誤。
  • 調用 genesis.configOrDefault() 函數獲取最新的鏈配置信息 newcfg(即,如果 genesis 指針不空,返回 genesis 的配置,否則,返回默認配置)
  • 調用 GetChainConfig() 函數從數據庫中獲取 stored 哈希對應的鏈配置 storedcfg,如果獲取失敗且錯誤爲 ErrChainConfigNotFound (該錯誤一般情況下不會出現,只在極端情況下,寫入創世塊被打斷的時候),即數據庫存在創世塊,但沒有對應的鏈配置信息,那麼將最新配置 newcfg 寫入數據庫。然後返回錯誤。
  • 一個特殊的限制分支,如果 genesis 爲空,且保存的配置爲非主網,那麼直接返回已保存的信息,即,不改變已存在的配置,如果去除這個限制,會在後面返回 AllProtocolChanges 鏈配置及一個兼容性錯誤。
  • 獲取最後一個區塊的快高,如果獲取的數據不對,報錯退出,否則調用 storedcfg.CheckCompatible() 函數檢查配置的兼容性,如果配置衝突,報錯退出。
  • 通過 stored 區塊哈希,newcfg 最新的配置,重新保存鏈配置。

回顧

我們重新回顧前面的步驟,回過頭來看看這個函數到底想幹嘛:
1)如註釋中提到的,SetupGenesisBlock 函數將返回鏈配置,並在特定的時候,保存創世快配置,另外還更新鏈配置
2)函數進去後先檢查入參
3)然後從數據庫中獲取已保存的區塊哈希,判斷這個哈希(即,區塊)是否存在
4)如註釋中說的那樣,當數據庫中不存在創世塊時,使用默認的創世塊配置或提供的入參配置,通過 genesis.Commit() 函數完成提交區塊到數據庫。
5)如果數據庫中已經存在創世塊,執行下面的邏輯:
6)將 genesis 中的內容應用到 statedb 中,並對比通過該配置生成的創世塊的哈希跟數據庫中創世塊的哈希是否一樣,如果不一樣,返回 genesis 的配置,並報錯(做兼容性判斷,genesis 生成的創世區塊不能)。通過對比調用該函數的情況,如果該函數發生錯誤,或非 compatErr 錯誤,那麼調用函數將報錯退出,也就是說,只有使用相同配置的兩次 init 操作,該函數纔不會在此處報錯退出,但退出之前,會修改數據庫中關於創世信息的數據部分。
7) 接着,從數據庫中獲取鏈配置信息,在 genesis.Commit() 函數中,鏈配置信息最後被寫入,如果從數據庫中獲取不到鏈配置,將 newcfg 配置寫入數據庫,並退出。退出的錯誤碼爲寫數據庫時的錯誤碼,也就是說,如果寫數據庫沒發生錯誤碼,函數正常結束newcfg 爲通過 genesisstored 綜合得出的鏈配置,如果 genesis 存在,使用其配置,否則使用默認配置。總體來說,這一步還是處理異常情況。
8)接下來就檢查兼容性了。在這之前,先解決特殊情況,那就是,對於 genesis 不存在而數據庫中的創世塊非主網創世塊,那麼退出。

三、 總結

未完待續

發表評論
所有評論
還沒有人評論,想成為第一個評論的人麼? 請在上方評論欄輸入並且點擊發布.
相關文章