以太坊源码解读-第7讲-创世块

前言

创世块是以太坊的第一个块,随着逐步对以太坊的了解,越来越发现应该关注一下这些地方细节上的实现了。

创世块的文件在这里:core->genesis.go,这里定义了整个创世块的规则,本文主要就是分析这个文件。

配置文件的结构体

进去core->genesis.go这个文件,咋一看感觉有点乱,抽丝剥茧后,小编整理出下面的几个结构体,它们就是创世块的整个初始化原型。
这些结构体中的字段,都是用来直接解析*.json配置文件的。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
type Genesis struct {
//这个Config主要是配置chainID以及共识相关(pow和pos),还有一些硬分叉信息,在此就不列出来了
//chainID是一个标示,1标示以太坊公网,测试时,建议设置别的
Config *params.ChainConfig `json:"config"`
//随机值
Nonce uint64 `json:"nonce"`
Timestamp uint64 `json:"timestamp"`
ExtraData []byte `json:"extraData"`
GasLimit uint64 `json:"gasLimit" gencodec:"required"`
Difficulty *big.Int `json:"difficulty" gencodec:"required"`
Mixhash common.Hash `json:"mixHash"`
Coinbase common.Address `json:"coinbase"`
//加入一些账户,以太坊启动后就能直接用这几个账户
Alloc GenesisAlloc `json:"alloc" gencodec:"required"`

//下面这三个是用来测试使用的
Number uint64 `json:"number"`
GasUsed uint64 `json:"gasUsed"`
ParentHash common.Hash `json:"parentHash"`
}
//用于将账户追加到以太坊中,方便直接使用
type GenesisAlloc map[common.Address]GenesisAccount
type GenesisAccount struct {
Code []byte `json:"code,omitempty"`
Storage map[common.Hash]common.Hash `json:"storage,omitempty"`
Balance *big.Int `json:"balance" gencodec:"required"`
Nonce uint64 `json:"nonce,omitempty"`
//用来测试
PrivateKey []byte `json:"secretKey,omitempty"`
}

为了直观展示,我们看看操作部署以太坊时候,我们的配置文件,正好和上面一一对应:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
{
"config": {
"chainId": 10,
"homesteadBlock": 5,
"eip155Block": 0,
"eip158Block": 0
},
"alloc": {
"0x81e71d34e8a9e4382c36fd90c3f234549106addd": {
"balance": "20000000000000000000"
}
},
"coinbase": "0x0000000000000000000000000000000000000000",
"difficulty": "0x20000",
"extraData": "",
"gasLimit": "0x2fefd8",
"nonce": "0x0000000000000042",
"mixhash": "0x0000000000000000000000000000000000000000000000000000000000000000",
"parentHash": "0x0000000000000000000000000000000000000000000000000000000000000000",
"timestamp": "0x00"
}

为了方便测试,一般会在alloc中添加几个默认账户。

上述配置结构体的说明

文件中,你会发现还有别的结构体,在此小编专门解释一下,因为上面提到的结构体是直接解析*.json的,会有很多限制,为此官方使用了github.com/fjl/gencodec包来扩充弥补其中的不足,可以在此处了解:https://godoc.org/github.com/fjl/gencodec
文件头部,会发现有这两行内容:

1
2
//go:generate gencodec -type Genesis -field-override genesisSpecMarshaling -out gen_genesis.go
//go:generate gencodec -type GenesisAccount -field-override genesisAccountMarshaling -out gen_genesis_account.go

其中go:generate是golang1.4引入的机制,编译期间,会将后面的命令执行,生成对应文件。
总结一下:

  1. 需要了解https://godoc.org/github.com/fjl/gencodec这个
  2. 需要了解go:generate的作用

创世块的初始化设置

前面提到的是用来初始化创世块的结构体,其具体的操作是使用如下这个方法来实现的:
时间有限,暂时先写到这里

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
//若genesis为空,则进入主网
func SetupGenesisBlock(db ethdb.Database, genesis *Genesis) (*params.ChainConfig, common.Hash, error) {
//若自定义了创世块,却没有对Config进行配置,则返回错误
if genesis != nil && genesis.Config == nil {
return params.AllEthashProtocolChanges, common.Hash{}, errGenesisNoConfig
}

//初始化新配置的以太坊节点会用到
stored := rawdb.ReadCanonicalHash(db, 0)
if (stored == common.Hash{}) { //如果db中没有存储创世块
if genesis == nil { //如果没有配置genesis,则使用主网的默认配置,这会连接到主网
log.Info("Writing default main-net genesis block")
genesis = DefaultGenesisBlock()
} else { //如果genesis不为空,则按照用户要求进行
log.Info("Writing custom genesis block")
}
block, err := genesis.Commit(db) //保存在db中
return genesis.Config, block.Hash(), err //返回结果,配置、创世块hash
}

// 若是有自定义的genesis,则校验跟库中已有的是否一致
if genesis != nil {
//因为创世块的配置文件每次都可以随意改动,该步骤用来检查创世块是否有所变动
hash := genesis.ToBlock(nil).Hash() //ToBlock传入nil会返回一个标准创世块的hash
if hash != stored { //检查从数据库中读取的创世块hash和启动时候配置的创世块hash是否一致
return genesis.Config, hash, &GenesisMismatchError{stored, hash}
}
}

// 获取链的配置,注意不是创世块的配置
// 根据传入的stored检测要建立什么样的网络,主网、测试网等
newcfg := genesis.configOrDefault(stored)
storedcfg := rawdb.ReadChainConfig(db, stored)
// 如果db中没有对链的配置,则将newcfg配置进去
if storedcfg == nil {
log.Warn("Found genesis block without chain config")
rawdb.WriteChainConfig(db, stored, newcfg)
return newcfg, stored, nil
}
// 再次做配置,若genesis为空,则创世块的hash值必须是主网的值
if genesis == nil && stored != params.MainnetGenesisHash {
return storedcfg, stored, nil
}

// 检查兼容性
// 只有当前是区块0的时候,才能进行下面的操作
height := rawdb.ReadHeaderNumber(db, rawdb.ReadHeadHeaderHash(db))
if height == nil {
return newcfg, stored, fmt.Errorf("missing block number for head header hash")
}
compatErr := storedcfg.CheckCompatible(newcfg, *height)
//若已经有区块在db,则不能更改配置
if compatErr != nil && *height != 0 && compatErr.RewindTo != 0 {
return newcfg, stored, compatErr
}
//相当于在0块时更新链的配置
rawdb.WriteChainConfig(db, stored, newcfg)
return newcfg, stored, nil
}

这个创世块的配置,其目的就是为了保证网络启动后,自始至终只能配置一次。主要的说明都记录在注释中了。其中涉及到了几个比较重要的方法:ToBlock()Commit(),之后我们一一来说明。

ToBlock()

这个方法的目的就是生成一个创世块,同时将相关信息写入到db中.

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
func (g *Genesis) ToBlock(db ethdb.Database) *types.Block {
if db == nil {
db = ethdb.NewMemDatabase() //内存db
}
statedb, _ := state.New(common.Hash{}, state.NewDatabase(db))
for addr, account := range g.Alloc {
//初始化时候设置的账户保存在db中
statedb.AddBalance(addr, account.Balance)
statedb.SetCode(addr, account.Code)
statedb.SetNonce(addr, account.Nonce)
//将账户中的各种交易状态存入(注意此时并没有提交到真正数据库中,只是存入内存db)
for key, value := range account.Storage {
statedb.SetState(addr, key, value)
}
}
// 获取db根地址
root := statedb.IntermediateRoot(false)
// 创世块的head
head := &types.Header{
//块号
Number: new(big.Int).SetUint64(g.Number),
Nonce: types.EncodeNonce(g.Nonce),
Time: new(big.Int).SetUint64(g.Timestamp),
//父hash
ParentHash: g.ParentHash,
//额外数据
Extra: g.ExtraData,
//gas限制
GasLimit: g.GasLimit,
//已使用的gas
GasUsed: g.GasUsed,
// 难度
Difficulty: g.Difficulty,
//与nonce配合用于挖矿,由上一个区块的一部分生成的hash。
MixDigest: g.Mixhash,
//挖矿人
Coinbase: g.Coinbase,
//块hash
Root: root,
}
//创世块设置的默认gaslimit:4712388
if g.GasLimit == 0 {
head.GasLimit = params.GenesisGasLimit
}
//默认难度:131072
if g.Difficulty == nil {
head.Difficulty = params.GenesisDifficulty
}
//真正将数据存入db
statedb.Commit(false)
statedb.Database().TrieDB().Commit(root, true)
//返回创世块
return types.NewBlock(head, nil, nil, nil)
}

可以看出,整个过程都是为了组装创世块,里面并没有太复杂的逻辑,能够知道的就是,默认的难度为:131072,默认的gasLimit:4712388,返回的创世块中,只有head即可。

Commit()

这个主要就是把创世块的配置相关记录到db中,
对比上面的ToBlock(),会发现有rawdb和statedb两种不同的db操作,后者主要是存储块交易的各种信息,前者可以理解为是对后者的进一步封装,目前认为其主要是存储创世块相关配置信息,后续再把这个坑补上。。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
func (g *Genesis) Commit(db ethdb.Database) (*types.Block, error) {
//获取创世块
block := g.ToBlock(db)
//只有创世块才可以操作
if block.Number().Sign() != 0 {
return nil, fmt.Errorf("can't commit genesis block with number > 0")
}
//将块的信息加入rawdb之中
rawdb.WriteTd(db, block.Hash(), block.NumberU64(), g.Difficulty)
rawdb.WriteBlock(db, block)
rawdb.WriteReceipts(db, block.Hash(), block.NumberU64(), nil)
rawdb.WriteCanonicalHash(db, block.Hash(), block.NumberU64())
rawdb.WriteHeadBlockHash(db, block.Hash())
rawdb.WriteHeadHeaderHash(db, block.Hash())

config := g.Config
if config == nil {
config = params.AllEthashProtocolChanges
}
//将创世块的配置写入
rawdb.WriteChainConfig(db, block.Hash(), config)
return block, nil
}

返回各种模式的genesis

这个是为了方便不同网络的测试和使用:GenesisBlockForTestingDefaultGenesisBlockDefaultTestnetGenesisBlockDefaultRinkebyGenesisBlockDeveloperGenesisBlock,可根据需要选择。
这个就不详述了。

总结

创世块这里,总的来说,没有太复杂的操作,主要就是配置文件的读取,校验,然后存储在db。注意区分rawdb和statedb。

Donate
  • Copyright: Copyright is owned by the author. For commercial reprints, please contact the author for authorization. For non-commercial reprints, please indicate the source.
  • Copyrights © 2017-2023 Jason
  • Visitors: | Views:

谢谢打赏~

微信