通过前面章节学习了以太坊的基本架构之后,我们通过自己搭建一个单节点,并覆盖以太坊主要流程来讲解代码。在这一节,你将学会:如何初始化创世区块
以太坊源码 V 1.8.0
golang 1.9+
windows 系统下 goland 2018+
本系列文章主要是研究以太坊源码,所以以太坊的编译工作不详细展开,有需要的可以参考这篇文章。
假设你已经在 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
函数到底干了啥。
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
对象中["chaindata", "lightchaindata"]
,根据遍历出来的名称打开对应的底层数据库 chaindb
core.SetupGenesisBlock()
函数,将 genesis 对象中的内容设置到底层数据库中,如果成功,更新数据库,否则报错退出。注意:函数的第一个返回值在这里被忽略。在 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
为通过 genesis
和 stored
综合得出的链配置,如果 genesis 存在,使用其配置,否则使用默认配置。总体来说,这一步还是处理异常情况。
8)接下来就检查兼容性了。在这之前,先解决特殊情况,那就是,对于 genesis
不存在而数据库中的创世块非主网创世块,那么退出。然后就检查兼容性了,这里的逻辑其实不是很懂,后面有空再看。
在这一章,主要是找到初始化创世块的函数 initGenesis
函数,然后看了怎么设置创世块的逻辑,感觉前面都还好,后面的逻辑有点不是很懂,还要再研究研究。