go-ethereum中core-genesis源码学习

genesis就是创世区块,我们在运行一个区块链节点时都要使用创世区块的配置文件对区块链进行初始化,不同的网络又不同的创世区块。

数据结构

type Genesis struct {
	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"`

	// These fields are used for consensus tests. Please don't use them
	// in actual genesis blocks.
	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"` 
}

type ChainConfig struct {
	ChainID *big.Int `json:"chainId"` 

	HomesteadBlock *big.Int `json:"homesteadBlock,omitempty"` 

	DAOForkBlock   *big.Int `json:"daoForkBlock,omitempty"`   
	DAOForkSupport bool     `json:"daoForkSupport,omitempty"` 


	EIP150Block *big.Int    `json:"eip150Block,omitempty"` 
	EIP150Hash  common.Hash `json:"eip150Hash,omitempty"`  

	EIP155Block *big.Int `json:"eip155Block,omitempty"` 
	EIP158Block *big.Int `json:"eip158Block,omitempty"` 

	ByzantiumBlock      *big.Int `json:"byzantiumBlock,omitempty"`      
	ConstantinopleBlock *big.Int `json:"constantinopleBlock,omitempty"` 
	PetersburgBlock     *big.Int `json:"petersburgBlock,omitempty"`     
	EWASMBlock          *big.Int `json:"ewasmBlock,omitempty"`          

	Ethash *EthashConfig `json:"ethash,omitempty"`
	Clique *CliqueConfig `json:"clique,omitempty"`
}

type EthashConfig struct{}

type CliqueConfig struct {
	Period uint64 `json:"period"` 
	Epoch  uint64 `json:"epoch"`  
}

上面给出了创世区块的完整定义,每个字段后面都有json标签,说明可以进行json的序列化和反序列化操作,事实上我们在指定一个创世区块时就是通过json文件配置的,官网给出了一个创世区块json文件的例子

{
  "config": {
        "chainId": 0,
        "homesteadBlock": 0,
        "eip155Block": 0,
        "eip158Block": 0
    },
  "alloc": {
        "0x0000000000000000000000000000000000000001": {"balance": "111111111"},
        "0x0000000000000000000000000000000000000002": {"balance": "222222222"}
    },
  "coinbase"   : "0x0000000000000000000000000000000000000000",
  "difficulty" : "0x20000",
  "extraData"  : "",
  "gasLimit"   : "0x2fefd8",
  "nonce"      : "0x0000000000000042",
  "mixhash"    : "0x0000000000000000000000000000000000000000000000000000000000000000",
  "parentHash" : "0x0000000000000000000000000000000000000000000000000000000000000000",
  "timestamp"  : "0x00"
}

其中,config中的chainId是网络标识,注意1表示以太坊主网;homesteadBlock、eip155Block和eip158Block是表示几个版本升级时硬分叉的高度,为nil表示不分叉。剩余的基本就是区块中一些固定的内容。

SetupGenesisBlock

在初始化区块链的时候就是调用这个方法来配置创世区块

func SetupGenesisBlock(db ethdb.Database, genesis *Genesis) (*params.ChainConfig, common.Hash, error) {
	return SetupGenesisBlockWithOverride(db, genesis, nil)
}

func SetupGenesisBlockWithOverride(db ethdb.Database, genesis *Genesis, constantinopleOverride *big.Int) (*params.ChainConfig, common.Hash, error) {
	if genesis != nil && genesis.Config == nil { 
		return params.AllEthashProtocolChanges, common.Hash{}, errGenesisNoConfig
	}
	
	stored := rawdb.ReadCanonicalHash(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
	}

	if genesis != nil {
		hash := genesis.ToBlock(nil).Hash()
		if hash != stored {
			return genesis.Config, hash, &GenesisMismatchError{stored, hash}
		}
	}

	newcfg := genesis.configOrDefault(stored)
	if constantinopleOverride != nil {
		newcfg.ConstantinopleBlock = constantinopleOverride
		newcfg.PetersburgBlock = constantinopleOverride
	}
	storedcfg := rawdb.ReadChainConfig(db, stored)
	if storedcfg == nil {
		log.Warn("Found genesis block without chain config")
		rawdb.WriteChainConfig(db, stored, newcfg)
		return newcfg, stored, nil
	}

	if genesis == nil && stored != params.MainnetGenesisHash {
		return storedcfg, stored, nil
	}

	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)
	if compatErr != nil && *height != 0 && compatErr.RewindTo != 0 {
		return newcfg, stored, compatErr
	}
	rawdb.WriteChainConfig(db, stored, newcfg)
	return newcfg, stored, nil
}

传入的参数就是一个已经解析过的Genesis对象。首先判断config字段是否为空,因为这里面至少定义了一个网络ID。接下来调用了ReadCanonicalHash方法,ReadCanonicalHash实际上就是ethdb的get方法封装,这里读取了编号为0的区块hash,也就是尝试从数据库中读取创世区块。

接下来分四种情况,在源码开头已经给出了:

//                          genesis == nil       genesis != nil
//                       +------------------------------------------
//     db has no genesis |  main-net default  |  genesis
//     db has genesis    |  from DB           |  genesis (if compatible)
//

情况一:外部genesis与数据库中genesis都为空

首先如果读取到的为空表示没有初始化过。接下来如果传入的genesis对象也为为空时,符合注释中的第一种情况,就调用DefaultGenesisBlock配置默认的创世区块,也就是主网下创世区块。这一般在默认连接主网时出现

func DefaultGenesisBlock() *Genesis {
	return &Genesis{
		Config:     params.MainnetChainConfig,
		Nonce:      66,
		ExtraData:  hexutil.MustDecode("0x11bbe8db4e347b4e8c937c1c8370e4b5ed33adb3db69cbdb7a38e1e50b1b82fa"),
		GasLimit:   5000,
		Difficulty: big.NewInt(17179869184),
		Alloc:      decodePrealloc(mainnetAllocData),
	}
}

	MainnetChainConfig = &ChainConfig{
		ChainID:             big.NewInt(1),
		HomesteadBlock:      big.NewInt(1150000),
		DAOForkBlock:        big.NewInt(1920000),
		DAOForkSupport:      true,
		EIP150Block:         big.NewInt(2463000),
		EIP150Hash:          common.HexToHash("0x2086799aeebeae135c246c65021c82b4e15a2c451340993aacfd2751886514f0"),
		EIP155Block:         big.NewInt(2675000),
		EIP158Block:         big.NewInt(2675000),
		ByzantiumBlock:      big.NewInt(4370000),
		ConstantinopleBlock: big.NewInt(7280000),
		PetersburgBlock:     big.NewInt(7280000),
		Ethash:              new(EthashConfig),
	}

主网的创世区块配置中的Alloc字段是在core/genesis_alloc.go中写的。另外可以看到主网的ID是1,还有几次硬分叉的高度。

情况二:只有数据库中的genesis为空

回到SetupGenesisBlock中,如果我们传来的创世区块不为空,但是本地数据库中没有存储过创世区块的话,这一般在指定配置文件时第一次初始化出现,则调用commit方法将我们指定的genesis提交到数据库,commit方法后面再介绍

情况三:外部genesis与数据库中genesis都不为空

如果本地数据库存的有genesis数据,外部指定的也有genesis数据,先对二者的区块进行比较,就是对hash比较,不一样的报错返回。只有在一样的情况下继续执行,这时先通过configOrDefault获取我们指定的配置信息,然后利用ReadChainConfig从数据库读取旧的配置信息,然后在获取区块链高度,之后进行兼容性判断,兼容的话利用WriteChainConfig把我们指定的配置信息写入数据库

情况四:只有外部的genesis为空

对应这种情况,也是先通过configOrDefault根据本地存储的创世区块hash值获取某个预制的配置信息,在获取的配置信息不是主网配置信息的情况下不做任何改变,也就是说是其他配置信息的话就和有外部Genesis一样进行兼容性检测人后写入。

configOrDefault

func (g *Genesis) configOrDefault(ghash common.Hash) *params.ChainConfig {
	switch {
	case g != nil:
		return g.Config
	case ghash == params.MainnetGenesisHash:
		return params.MainnetChainConfig
	case ghash == params.TestnetGenesisHash:
		return params.TestnetChainConfig
	default:
		return params.AllEthashProtocolChanges
	}
}

commit

commit方法是将我们指定的创世区块写入数据库

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.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
}

ToBlock

首先调用了ToBlock方法创建了一个区块

func (g *Genesis) ToBlock(db ethdb.Database) *types.Block {
	if db == nil { 
		db = ethdb.NewMemDatabase()
	}

	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:       new(big.Int).SetUint64(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 //默认为4712388  go-ethereum\params\protocol_params.go
	}
	if g.Difficulty == nil {
		head.Difficulty = params.GenesisDifficulty //默认为131072
	}
	statedb.Commit(false)
	statedb.Database().TrieDB().Commit(root, true)

	return types.NewBlock(head, nil, nil, nil)
}

在ToBlock,如果传入的数据库对象为空,则建立一个内存数据库,这个我们在分析ethdb源码时分析过,随后调用了state.New方法,关于state见这篇文章,这里返回的StateDB用于操作账户。随后遍历了alloc字段,这是预分配余额,对于每个账户都写入相应的余额、代码、随机数和存储的数据。之后调用IntermediateRoot获取临时的根hash,接着构建了区块头,填入了头部的各项信息,基本都是按照创世区块来的。随后调用statedb.Commit把前面statedb所做的变动进行提交,注意IntermediateRoot只是暂时算出当前状态树的根并没有持久化到数据库。

接着又调用了statedb.Database().TrieDB().Commit(root, true),我们一段一段分析,statedb.Database()是获取的StateDB的db对象,这个变量我们前面在创建statedb时通过state.NewDatabase方法构建,他返回一个cachingDB对象,而cachingDB的TrieDB返回它自己的db对象,这是一个trie包里的Database对象,这个对象保存了我们最初传入的ethdb.Database对象。它的Commit方法实际上就是将树最后写入数据库中,关于这一部分见前面tire源码分析

回到ToBlock,最后调用NewBlock构建了一个区块对象

func NewBlock(header *Header, txs []*Transaction, uncles []*Header, receipts []*Receipt) *Block {
	b := &Block{header: CopyHeader(header), td: new(big.Int)}

	if len(txs) == 0 {
		b.header.TxHash = EmptyRootHash
	} else {
		b.header.TxHash = DeriveSha(Transactions(txs))
		b.transactions = make(Transactions, len(txs))
		copy(b.transactions, txs)
	}

	if len(receipts) == 0 {
		b.header.ReceiptHash = EmptyRootHash
	} else {
		b.header.ReceiptHash = DeriveSha(Receipts(receipts))
		b.header.Bloom = CreateBloom(receipts)
	}

	if len(uncles) == 0 {
		b.header.UncleHash = EmptyUncleHash
	} else {
		b.header.UncleHash = CalcUncleHash(uncles)
		b.uncles = make([]*Header, len(uncles))
		for i := range uncles {
			b.uncles[i] = CopyHeader(uncles[i])
		}
	}

	return b
}

传入的参数有区块头,交易集合、叔块头和收据,我们在这里还要补全交易树和收据树的根,我们看一下根hash如何计算

// go-ethereum\core\types\derive_sha.go
func DeriveSha(list DerivableList) common.Hash {
	keybuf := new(bytes.Buffer)
	trie := new(trie.Trie)
	for i := 0; i < list.Len(); i++ {
		keybuf.Reset()
		rlp.Encode(keybuf, uint(i))
		trie.Update(keybuf.Bytes(), list.GetRlp(i))
	}
	return trie.Hash()
}

func (s Transactions) GetRlp(i int) []byte {
	enc, _ := rlp.EncodeToBytes(s[i])
	return enc
}

首先新建一棵树,要插入的key就是其所在集合中的编号,值时经过rlp编码的数据,最后计算树的根hash即可。

对于叔块的hash计算如下:

func CalcUncleHash(uncles []*Header) common.Hash {
	return rlpHash(uncles)
}

func rlpHash(x interface{}) (h common.Hash) {
	hw := sha3.NewLegacyKeccak256()
	rlp.Encode(hw, x)
	hw.Sum(h[:0])
	return h
}

首先进行rlp编码然后计算sha3-256的值即可。

写数据库

回到Genesis中,ToBlock返回了一个区块对象也就是创世区块,接下来调用了rawdb包中的许多方法,rawdb实际上就是一个包装类,封装了一些操作数据库的方法,底层还是使用ethdb的相关方法。首先写入了总难度

func WriteTd(db DatabaseWriter, hash common.Hash, number uint64, td *big.Int) {
	data, err := rlp.EncodeToBytes(td)
	if err != nil {
		log.Crit("Failed to RLP encode block total difficulty", "err", err)
	}
	if err := db.Put(headerTDKey(number, hash), data); err != nil {
		log.Crit("Failed to store block total difficulty", "err", err)
	}
}

func headerTDKey(number uint64, hash common.Hash) []byte {
	return append(headerKey(number, hash), headerTDSuffix...)
}

func headerKey(number uint64, hash common.Hash) []byte {
	return append(append(headerPrefix, encodeBlockNumber(number)...), hash.Bytes()...)
}

headerPrefix  = []byte("h")

func encodeBlockNumber(number uint64) []byte {
	enc := make([]byte, 8)
	binary.BigEndian.PutUint64(enc, number)
	return enc
}

向数据库中写入总难度时,先对key进行了构造:

byte('h') + 区块编号的大端表示(长度8字节) + 区块hash + byte('t')

之后开始写入区块

func WriteBlock(db DatabaseWriter, block *types.Block) {
	WriteBody(db, block.Hash(), block.NumberU64(), block.Body())
	WriteHeader(db, block.Header())
}

先写区块体

func (b *Block) Body() *Body { return &Body{b.transactions, b.uncles} }

func WriteBody(db DatabaseWriter, hash common.Hash, number uint64, body *types.Body) {
	data, err := rlp.EncodeToBytes(body)
	if err != nil {
		log.Crit("Failed to RLP encode body", "err", err)
	}
	WriteBodyRLP(db, hash, number, data)
}

func WriteBodyRLP(db DatabaseWriter, hash common.Hash, number uint64, rlp rlp.RawValue) {
	if err := db.Put(blockBodyKey(number, hash), rlp); err != nil {
		log.Crit("Failed to store block body", "err", err)
	}
}

func blockBodyKey(number uint64, hash common.Hash) []byte {
	return append(append(blockBodyPrefix, encodeBlockNumber(number)...), hash.Bytes()...)
}

blockBodyPrefix     = []byte("b")

首先所谓的区块体就是一个只包含交易和叔块头的结构体,存储时先对其进行rlp编码,然后存入数据库,存入的key如下构成

byte('b') + 区块编号的大端表示(长度8字节) + 区块hash

写入完区块体后写入区块头

func WriteHeader(db DatabaseWriter, header *types.Header) {
	var (
		hash    = header.Hash()
		number  = header.Number.Uint64()
		encoded = encodeBlockNumber(number)
	)
	key := headerNumberKey(hash)
	if err := db.Put(key, encoded); err != nil {
		log.Crit("Failed to store hash to number mapping", "err", err)
	}

	data, err := rlp.EncodeToBytes(header)
	if err != nil {
		log.Crit("Failed to RLP encode header", "err", err)
	}
	key = headerKey(number, hash)
	if err := db.Put(key, data); err != nil {
		log.Crit("Failed to store header", "err", err)
	}
}

func headerNumberKey(hash common.Hash) []byte {
	return append(headerNumberPrefix, hash.Bytes()...)
}

headerNumberPrefix = []byte("H")

func headerKey(number uint64, hash common.Hash) []byte {
	return append(append(headerPrefix, encodeBlockNumber(number)...), hash.Bytes()...)
}

这里首先存储区块编号,构建的key结构如下

byte('H') + 区块头hash

之后再存储区块头,先进行rlp编码,再构建key,结构如下

byte('h') + 区块编号的大端表示(长度8字节) + 区块头hash 

接着又存储了收据信息,区块hash,当前区块(区块头顶部区块)hash以及当前区块头hash。还要写入配置信息,不过如果配置为空则加载一个默认配置

AllEthashProtocolChanges = &ChainConfig{big.NewInt(1337), big.NewInt(0), nil, false, big.NewInt(0), common.Hash{}, big.NewInt(0), big.NewInt(0), big.NewInt(0), big.NewInt(0), big.NewInt(0), nil, new(EthashConfig), nil}

最后commit方法返回开始创建的区块对象。回到SetupGenesisBlock中,此时创世区块配置完毕返回其配置和区块hash。

题图来自unsplash:https://unsplash.com/photos/_aASE0L1I1g