以太坊源码分析(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 不存在而数据库中的创世块非主网创世块,那么退出。然后就检查兼容性了,这里的逻辑其实不是很懂,后面有空再看。

三、 总结

在这一章,主要是找到初始化创世块的函数 initGenesis 函数,然后看了怎么设置创世块的逻辑,感觉前面都还好,后面的逻辑有点不是很懂,还要再研究研究。

  • 1
    点赞
  • 5
    收藏
    觉得还不错? 一键收藏
  • 1
    评论

“相关推荐”对你有帮助么?

  • 非常没帮助
  • 没帮助
  • 一般
  • 有帮助
  • 非常有帮助
提交
评论 1
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值