[ethereum源码分析](3) ethereum初始化指令

前言

  在上一章介绍了关于区块链的一些基础知识,这一章会分析指令 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{}
}

 

到这里这一章就介绍结束了。有什么理解错误的地方还望指正。

posted on 2018-07-30 10:38  飘云粟  阅读(1238)  评论(0编辑  收藏  举报