前言
在上一章介绍了关于区块链的一些基础知识,这一章会分析指令 geth --datadir dev/data/02 init private-geth/genesis.json 的源码,若你的ethereum的debug环境还没有搭建,那么需要先搭建ethereum的dabug环境。
准备工作
- 创建文件 genesis.json ,内容如下:
{ "config": { "chainId": 666, //可用于网络标识,在eip155里有用到,目前来看是做重放保护的,目前eth的公网的网络id为1 "homesteadBlock": 0, //以太坊版本 "eip155Block": 0, //(Ethereum Improvement Proposals)简单重访攻击保护,由于是私有链,无硬分叉,此处我们设置为0 "eip158Block": 0 //同上 }, "coinbase" : "0x0000000000000000000000000000000000000000", //矿工账号 "difficulty" : "0x40000", //设置当前区块的难度,值越大挖矿难度越大 "extraData" : "", //附加信息,可以填写任意信息 "gasLimit" : "0xffffffff", //该值设置对GAS的消耗总量限制,用来限制区块能包含的交易信息总和 "nonce" : "0x0000000000000042", //是一个64位的随机数,用于挖矿,注意他和mixhash的设置需要满足以太坊的Yellow paper, 4.3.4. Block Header Validity, (44)章节所描述的条件。 "mixhash" : "0x0000000000000000000000000000000000000000000000000000000000000000", //与nonce配合用于挖矿 "parentHash" : "0x0000000000000000000000000000000000000000000000000000000000000000", //上一个区块的hash值,创世区块没有上一个区块,因此设置为0 "timestamp" : "0x00", //设置创世块的时间戳 "alloc": { "1fd4027fe390abaa49e5afde7896ff1e5ecacabf": { "balance": "20000000000000000000" } } //用来预置账号以及账号的以太币数量 }
指令分析
指令: geth --datadir dev/data/02 init private-geth/genesis.json
介绍:上面的指令主要的工作为:
- 生成创世区块
- 生成账号的一些信息
分析:
- dev/data/02 :eth数据保存的地址,主要保存了区块信息和账号信息,日志信息
- private-geth/genesis.json :eth初始化的一些配置参数
代码分析
接下来就让我们跟以下debug,来一探ethereum的真面目。
- 进入入口程序
由于我们使用的是 geth 指令,所以我们代开下面的代码:
- 找到以下函数
main.go:
func main() {
if err := app.Run(os.Args); err != nil {
fmt.Fprintln(os.Stderr, err)
os.Exit(1)
}
}
上面的函数是geth命令的入口函数,这段代码首先调用了 app.Run(os.Args) 这个函数, os.Args 为系统参数(例: --datadir dev/data/02 init private-geth/genesis.jso )。那么让我们来看看 app 是什么。
App:
// App is the main structure of a cli application. It is recommended that // an app be created with the cli.NewApp() function type App struct { // The name of the program. Defaults to path.Base(os.Args[0]) Name string // Full name of command for help, defaults to Name HelpName string // Description of the program. Usage string // Text to override the USAGE section of help UsageText string // Description of the program argument format. ArgsUsage string // Version of the program Version string // Description of the program Description string // List of commands to execute Commands []Command // List of flags to parse Flags []Flag // Boolean to enable bash completion commands EnableBashCompletion bool // Boolean to hide built-in help command HideHelp bool // Boolean to hide built-in version flag and the VERSION section of help HideVersion bool // Populate on app startup, only gettable through method Categories() categories CommandCategories // An action to execute when the bash-completion flag is set BashComplete BashCompleteFunc // An action to execute before any subcommands are run, but after the context is ready // If a non-nil error is returned, no subcommands are run Before BeforeFunc // An action to execute after any subcommands are run, but after the subcommand has finished // It is run even if Action() panics After AfterFunc // The action to execute when no subcommands are specified // Expects a `cli.ActionFunc` but will accept the *deprecated* signature of `func(*cli.Context) {}` // *Note*: support for the deprecated `Action` signature will be removed in a future version Action interface{} // Execute this function if the proper command cannot be found CommandNotFound CommandNotFoundFunc // Execute this function if an usage error occurs OnUsageError OnUsageErrorFunc // Compilation date Compiled time.Time // List of all authors who contributed Authors []Author // Copyright of the binary if any Copyright string // Name of Author (Note: Use App.Authors, this is deprecated) Author string // Email of Author (Note: Use App.Authors, this is deprecated) Email string // Writer writer to write output to Writer io.Writer // ErrWriter writes error output ErrWriter io.Writer // Other custom info Metadata map[string]interface{} // Carries a function which returns app specific info. ExtraInfo func() map[string]string // CustomAppHelpTemplate the text template for app help topic. // cli.go uses text/template to render templates. You can // render custom help text by setting this variable. CustomAppHelpTemplate string didSetup bool }
那么 app 是在 main.go 的 init 函数中初始化的,下面让我们来看看 init 函数。
func init() { // Initialize the CLI app and start Geth app.Action = geth app.HideVersion = true // we have a command to print the version app.Copyright = "Copyright 2013-2018 The go-ethereum Authors" app.Commands = []cli.Command{ // See chaincmd.go: initCommand, importCommand, exportCommand, importPreimagesCommand, exportPreimagesCommand, copydbCommand, removedbCommand, dumpCommand, // See monitorcmd.go: monitorCommand, // See accountcmd.go: accountCommand, walletCommand, // See consolecmd.go: consoleCommand, attachCommand, javascriptCommand, // See misccmd.go: makecacheCommand, makedagCommand, versionCommand, bugCommand, licenseCommand, // See config.go dumpConfigCommand, } sort.Sort(cli.CommandsByName(app.Commands)) app.Flags = append(app.Flags, nodeFlags...) app.Flags = append(app.Flags, rpcFlags...) app.Flags = append(app.Flags, consoleFlags...) app.Flags = append(app.Flags, debug.Flags...) app.Flags = append(app.Flags, whisperFlags...) app.Before = func(ctx *cli.Context) error { runtime.GOMAXPROCS(runtime.NumCPU()) if err := debug.Setup(ctx); err != nil { return err } // Cap the cache allowance and tune the garbage colelctor var mem gosigar.Mem if err := mem.Get(); err == nil { allowance := int(mem.Total / 1024 / 1024 / 3) if cache := ctx.GlobalInt(utils.CacheFlag.Name); cache > allowance { log.Warn("Sanitizing cache to Go's GC limits", "provided", cache, "updated", allowance) ctx.GlobalSet(utils.CacheFlag.Name, strconv.Itoa(allowance)) } } // Ensure Go's GC ignores the database cache for trigger percentage cache := ctx.GlobalInt(utils.CacheFlag.Name) gogc := math.Max(20, math.Min(100, 100/(float64(cache)/1024))) log.Debug("Sanitizing Go's GC trigger", "percent", int(gogc)) godebug.SetGCPercent(int(gogc)) // Start system runtime metrics collection go metrics.CollectProcessMetrics(3 * time.Second) utils.SetupNetwork(ctx) return nil } app.After = func(ctx *cli.Context) error { debug.Exit() console.Stdin.Close() // Resets terminal mode. return nil } }
从上面的代码,可以看到,它将所有的指令放到了 app 中缓存了起来。通过这个缓存的指令集,我们可以找到需要执行的代码。下面就让我们来看一下 app.Run(os.Args) 里面的代码。
func (a *App) Run(arguments []string) (err error) { a.Setup()//在这个里面主要做了三件事:1.初始化App中的commands(主要初始化Command.HelpName)2.增加helpCommand指令到App.Commands,一共21个指令 3.初始化App.categories,主要是给指令分类 // handle the completion flag separately from the flagset since // completion could be attempted after a flag, but before its value was put // on the command line. this causes the flagset to interpret the completion // flag name as the value of the flag before it which is undesirable // note that we can only do this because the shell autocomplete function // always appends the completion flag at the end of the command shellComplete, arguments := checkShellCompleteFlag(a, arguments) // parse flags set, err := flagSet(a.Name, a.Flags)//这里初始化了一些FlagSet,FlagSet里面存储了一些eth的默认配置,比如networkId=1 if err != nil { return err } set.SetOutput(ioutil.Discard) err = set.Parse(arguments[1:])//将命令行参数设置到set中,可以通过看里面的代码知道,命令行输入的参数有两种:1.环境配置参数以--为开头 2.命令参数,需要执行代码 nerr := normalizeFlags(a.Flags, set) context := NewContext(a, set, nil) if nerr != nil { fmt.Fprintln(a.Writer, nerr) ShowAppHelp(context) return nerr } context.shellComplete = shellComplete if checkCompletions(context) { return nil } if err != nil { if a.OnUsageError != nil { err := a.OnUsageError(context, err, false) HandleExitCoder(err) return err } fmt.Fprintf(a.Writer, "%s %s\n\n", "Incorrect Usage.", err.Error()) ShowAppHelp(context) return err } if !a.HideHelp && checkHelp(context) { ShowAppHelp(context) return nil } if !a.HideVersion && checkVersion(context) { ShowVersion(context) return nil } if a.After != nil { defer func() { if afterErr := a.After(context); afterErr != nil { if err != nil { err = NewMultiError(err, afterErr) } else { err = afterErr } } }() } if a.Before != nil { beforeErr := a.Before(context) if beforeErr != nil { ShowAppHelp(context) HandleExitCoder(beforeErr) err = beforeErr return err } } args := context.Args()//获取需要执行的命令,当前为init if args.Present() { name := args.First() c := a.Command(name)//查找是否有init命令 if c != nil { return c.Run(context)//执行init命令 } } if a.Action == nil { a.Action = helpCommand.Action } // Run default Action err = HandleAction(a.Action, context) HandleExitCoder(err) return err }
从上面的注释我们可以知道,命令行输入的参数 init 在 c.Run(context) 这行代码被执行。那么下面就让我们来 c.Run(context) 的代码。
func (c Command) Run(ctx *Context) (err error) { if len(c.Subcommands) > 0 { return c.startApp(ctx) } if !c.HideHelp && (HelpFlag != BoolFlag{}) { // append help to flags c.Flags = append( c.Flags, HelpFlag, ) } set, err := flagSet(c.Name, c.Flags) if err != nil { return err } set.SetOutput(ioutil.Discard) if c.SkipFlagParsing { err = set.Parse(append([]string{"--"}, ctx.Args().Tail()...)) } else if !c.SkipArgReorder { firstFlagIndex := -1 terminatorIndex := -1 for index, arg := range ctx.Args() { if arg == "--" { terminatorIndex = index break } else if arg == "-" { // Do nothing. A dash alone is not really a flag. continue } else if strings.HasPrefix(arg, "-") && firstFlagIndex == -1 { firstFlagIndex = index } } if firstFlagIndex > -1 { args := ctx.Args() regularArgs := make([]string, len(args[1:firstFlagIndex])) copy(regularArgs, args[1:firstFlagIndex]) var flagArgs []string if terminatorIndex > -1 { flagArgs = args[firstFlagIndex:terminatorIndex] regularArgs = append(regularArgs, args[terminatorIndex:]...) } else { flagArgs = args[firstFlagIndex:] } err = set.Parse(append(flagArgs, regularArgs...)) } else { err = set.Parse(ctx.Args().Tail())//初始化init命令的参数,该处为private-geth/genesis.json } } else { err = set.Parse(ctx.Args().Tail()) } nerr := normalizeFlags(c.Flags, set) if nerr != nil { fmt.Fprintln(ctx.App.Writer, nerr) fmt.Fprintln(ctx.App.Writer) ShowCommandHelp(ctx, c.Name) return nerr } context := NewContext(ctx.App, set, ctx) context.Command = c if checkCommandCompletions(context, c.Name) { return nil } if err != nil { if c.OnUsageError != nil { err := c.OnUsageError(context, err, false) HandleExitCoder(err) return err } fmt.Fprintln(context.App.Writer, "Incorrect Usage:", err.Error()) fmt.Fprintln(context.App.Writer) ShowCommandHelp(context, c.Name) return err } if checkCommandHelp(context, c.Name) { return nil } if c.After != nil { defer func() { afterErr := c.After(context) if afterErr != nil { HandleExitCoder(err) if err != nil { err = NewMultiError(err, afterErr) } else { err = afterErr } } }() } if c.Before != nil { err = c.Before(context) if err != nil { ShowCommandHelp(context, c.Name) HandleExitCoder(err) return err } } if c.Action == nil { c.Action = helpSubcommand.Action } err = HandleAction(c.Action, context)//这一行是用来执行init指令的,指令需要执行的代码链接到了c.Action if err != nil { HandleExitCoder(err) } return err }
那么最终 init 指令需要执行的代码是 MigrateFlags ,可以在 main.go initCommand 中看到需要执行的代码。
initCommand = cli.Command{ Action: utils.MigrateFlags(initGenesis), Name: "init", Usage: "Bootstrap and initialize a new genesis block", ArgsUsage: "<genesisPath>", Flags: []cli.Flag{ utils.DataDirFlag, utils.LightModeFlag, }, Category: "BLOCKCHAIN COMMANDS", Description: ` The init command initializes a new genesis block and definition for the network. This is a destructive action and changes the network in which you will be participating. It expects the genesis file as argument.`, }
从上面可以看到,执行 MigrateFlags 会先执行 initGenesis ,下面就来看看 initGenesis 的代码。
func initGenesis(ctx *cli.Context) error { // Make sure we have a valid genesis JSON genesisPath := ctx.Args().First()//获取命令行参数,此处为private-geth/genesis.json if len(genesisPath) == 0 { utils.Fatalf("Must supply path to genesis JSON file") } file, err := os.Open(genesisPath)//打开private-geth/genesis.json文件 if err != nil { utils.Fatalf("Failed to read genesis file: %v", err) } defer file.Close() genesis := new(core.Genesis)//构造一个Genesis if err := json.NewDecoder(file).Decode(genesis); err != nil {//读取配置文件genesis.json,构造genesis结构体 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)//这里将创世区块写入leveldb if err != nil { utils.Fatalf("Failed to write genesis block: %v", err) } log.Info("Successfully wrote genesis state", "database", name, "hash", hash) } return nil }
下面就让我们看看这里是如何构建创世区块的,构建创世区块的过程在 core.SetupGenesisBlock(chaindb, genesis) 里面。
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 := 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)//最终我们的代码会走到这里,这里将genesis写入数据库 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 := rawdb.ReadChainConfig(db, stored) if storedcfg == nil { log.Warn("Found genesis block without chain config") rawdb.WriteChainConfig(db, stored, newcfg) return newcfg, stored, nil } // 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 := 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.Commit(db) 的代码。
// Commit writes the block and state of a genesis specification to the database. // The block is committed as the canonical head block. func (g *Genesis) Commit(db ethdb.Database) (*types.Block, error) { block := g.ToBlock(db)//这里面做了两件事:1.写入状态树 2.构造创世区块 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 }
那么让我们来看看 g.ToBlock(db) 的代码。
// ToBlock creates the genesis block and writes state of a genesis specification // to the given database (or discards it if nil). 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 } if g.Difficulty == nil { head.Difficulty = params.GenesisDifficulty } statedb.Commit(false) statedb.Database().TrieDB().Commit(root, true) return types.NewBlock(head, nil, nil, nil)//这里构造了一个创世区块 }
下面让我们看下 types.Header 的结构体。
type Header struct { ParentHash common.Hash `json:"parentHash" gencodec:"required"`//父区块头的Hash值 UncleHash common.Hash `json:"sha3Uncles" gencodec:"required"`//当前区块ommers列表的Hash值 Coinbase common.Address `json:"miner" gencodec:"required"`//接收挖此区块费用的矿工钱包地址 Root common.Hash `json:"stateRoot" gencodec:"required"`//状态树根节点的Hash值 TxHash common.Hash `json:"transactionsRoot" gencodec:"required"`//包含此区块所列的所有交易的树的根节点Hash值 ReceiptHash common.Hash `json:"receiptsRoot" gencodec:"required"`//包含此区块所列的所有交易收据的树的根节点Hash值 Bloom Bloom `json:"logsBloom" gencodec:"required"`//由日志信息组成的一个Bloom过滤器 (数据结构) Difficulty *big.Int `json:"difficulty" gencodec:"required"`//挖此区块的难度 Number *big.Int `json:"number" gencodec:"required"`//区块编号,也就是区块高度 GasLimit uint64 `json:"gasLimit" gencodec:"required"`//每个区块当前的gasLimit GasUsed uint64 `json:"gasUsed" gencodec:"required"`//此区块中交易所用的总gas量 Time *big.Int `json:"timestamp" gencodec:"required"`//此区块创建时间戳 Extra []byte `json:"extraData" gencodec:"required"`//与此区块相关的附加数据 MixDigest common.Hash `json:"mixHash" gencodec:"required"`//一个Hash值,当与nonce组合时,证明此区块已经执行了足够的计算 Nonce BlockNonce `json:"nonce" gencodec:"required"`//一个Hash值,当与mixHash组合时,证明此区块已经执行了足够的计算 }
最后我们看下 types.Block 的结构体。
type Block struct { header *Header//区块头信息 uncles []*Header transactions Transactions//区块交易信息 // caches hash atomic.Value size atomic.Value // Td is used by package core to store the total difficulty // of the chain up to and including the block. td *big.Int // These fields are used by package eth to track // inter-peer block relay. ReceivedAt time.Time ReceivedFrom interface{} }
到这里这一章就介绍结束了。有什么理解错误的地方还望指正。