元宇宙 碳中和 区块链 快讯 正文
热门: 元宇宙的组成(元宇宙的本质特征是五大融合) 厦门碳中和产业投融资峰会(旗下机构完成首宗海洋碳汇交易 金圆力量助推厦门全国碳中和领域走前头) 西南石油大学碳中和研究院(围绕党中央重大战略决策:教育部下发文件,西南石油大学率先行动) 三好街比特币(比特币上演“疯牛”行情 2日连涨10%) 比特币用信用卡买(购买比特币受限 美国银行信用卡已调整) 华闻集团是元宇宙吗(华闻集团——NFT和元宇宙VR领域的深度介入者)

以太坊源码解读(「链块技术57期」以太坊启动流程分析)

原文链接:http://www.liankuai.tech/technology/180.html

本章节主要通过分析以太坊启动入口的源代码来了解以太坊的启动流程。

一、前言

本章节主要通过分析源代码来了解以太坊的启动流程,本文基于以太坊的源码版本是go-ethereum-release-1.8。

二、启动入口

Geth的启动入口位置在go-ethereum/cmd/main.go的253行main函数。

func main() { if err := app.Run(os.Args); err != nil { fmt.Fprintln(os.Stderr, err) os.Exit(1) } }

Geth的命令行处理使用了urfave/cli这个库,urfave/cli这个库抽象了flag、commnad等模块,用户只需要简单的配置,urfave/cli会帮我们完成参数的解析和关联,也能够自动生成帮助信息。

但是我们在上面的启动代码中并没有看到参数配置等操作,因为main函数其实不是Geth真正的入口,Geth首先是调用go-ethereum/cmd/main.go第169行的init函数,然后再调用main函数, 当然能够先调init函数再调main函数,这是go语言所支持的特性。

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.Flags = append(app.Flags, metricsFlags...) app.Before = func(ctx *cli.Context) error { runtime.GOMAXPROCS(runtime.NumCPU()) logdir := "" if ctx.GlobalBool(utils.DashboardEnabledFlag.Name) { logdir = (&node.Config{DataDir: utils.MakeDataDir(ctx)}).ResolvePath("logs") } if err := debug.Setup(ctx, logdir); err != nil { return err } // Cap the cache allowance and tune the garbage collector 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 metrics export if enabled utils.SetupMetrics(ctx) // Start system runtime metrics collection go metrics.CollectProcessMetrics(3 * time.Second) return nil } app.After = func(ctx *cli.Context) error { debug.Exit() console.Stdin.Close() // Resets terminal mode. return nil } }

init函数主要是做了一些初始化的工作,其中app.Action = geth是设置主入口函数,也就是说urfave/cli初始化完成后, 会调用main函数,main函数中再调用app的action函数, 从而调用geth来启动以太坊。

三、geth函数

geth函数比较简单,主要调用makeFullNode函数创建节点,调用startNode启动节点中的服务,最后调用node.Wait使节点进入等待状态。

func geth(ctx *cli.Context) error { if args := ctx.Args(); len(args) > 0 { return fmt.Errorf("invalid command: %q", args[0]) } //创建节点 node := makeFullNode(ctx) //启动节点中的服务 startNode(ctx, node) // node.Wait() return nil } 3.1 makeFullNode函数

makeFullNode函数来创建一个节点,geth中node对象可以认为是以太坊网络中的一个节点,这个节点中会包含各种服务,比如网络服务、eth服务、DashBoard服务等等。然后向node中注册一个以太坊服务

func makeFullNode(ctx *cli.Context) *node.Node { stack, cfg := makeConfigNode(ctx) utils.RegisterEthservice(stack, &cfg.Eth) if ctx.GlobalBool(utils.DashboardEnabledFlag.Name) { utils.RegisterDashboardService(stack, &cfg.Dashboard, gitCommit) } // Whisper must be explicitly enabled by specifying at least 1 whisper flag or in dev mode shhEnabled := enableWhisper(ctx) shhAutoEnabled := !ctx.GlobalIsSet(utils.WhisperEnabledFlag.Name) && ctx.GlobalIsSet(utils.DeveloperFlag.Name) if shhEnabled || shhAutoEnabled { if ctx.GlobalIsSet(utils.WhisperMaxMessageSizeFlag.Name) { cfg.Shh.MaxMessageSize = uint32(ctx.Int(utils.WhisperMaxMessageSizeFlag.Name)) } if ctx.GlobalIsSet(utils.WhisperMinPOWFlag.Name) { cfg.Shh.MinimumAcceptedPOW = ctx.Float64(utils.WhisperMinPOWFlag.Name) } if ctx.GlobalIsSet(utils.WhisperRestrictConnectionBetweenLightClientsFlag.Name) { cfg.Shh.RestrictConnectionBetweenLightClients = true } utils.RegisterShhService(stack, &cfg.Shh) } // Add the Ethereum Stats daemon if requested. if cfg.Ethstats.URL != "" { utils.RegisterEthStatsService(stack, cfg.Ethstats.URL) } return stack }

makeConfigNode函数创建各一个Node对象。

func makeConfigNode(ctx *cli.Context) (*node.Node, gethConfig) { // Load defaults. cfg := gethConfig{ Eth: eth.DefaultConfig, Shh: whisper.DefaultConfig, Node: defaultNodeConfig(), Dashboard: dashboard.DefaultConfig, } // Load config file. if file := ctx.GlobalString(configFileFlag.Name); file != "" { if err := loadConfig(file, &cfg); err != nil { utils.Fatalf("%v", err) } } // Apply flags. utils.SetNodeConfig(ctx, &cfg.Node) stack, err := node.New(&cfg.Node) if err != nil { utils.Fatalf("Failed to create the protocol stack: %v", err) } utils.SetEthConfig(ctx, stack, &cfg.Eth) if ctx.GlobalIsSet(utils.EthStatsURLFlag.Name) { cfg.Ethstats.URL = ctx.GlobalString(utils.EthStatsURLFlag.Name) } utils.SetShhConfig(ctx, stack, &cfg.Shh) utils.SetDashboardConfig(ctx, &cfg.Dashboard) return stack, cfg }

makeConfigNode主要有两个任务:

1. 根据命令行的配置来初始化cfg对象, 如果用户没有配置,则使用默认配置,后面以太坊中的各个模块会根据cfg的配置来执行相应的初始化。

2. 通过Node的默认配置来创建一个Node并返回node和cfg。

Node可以看做是以太坊的各个服务的容器, 一个服务能够注册到node当中,必须实现Service 接口。

type Service interface { // Protocols retrieves the P2P protocols the service wishes to start. Protocols() []p2p.Protocol // APIs retrieves the list of RPC descriptors the service provides APIs() []rpc.API // Start is called after all services have been constructed and the networking // layer was also initialized to spawn any goroutines required by the service. Start(server *p2p.Server) error // Stop terminates all goroutines belonging to the service, blocking until they // are all terminated. Stop() error }

这样Node可以不用关心容器中的具体是哪一个服务,只要统一调用他们的接口就行。他们功能如下:

Protocols() 返回service要启动的P2P 协议列表

APIs() 返回service提供的RPC接口

Start() 启动已经初始化的service

Stop() 停止service中所有的go程,并阻塞当前go程直到service中所有go程都终止

3.2 startNode函数

我们重新回到cmd/geth/main.go的geth函数中分析startNode()函数。

startNode函数主要做4个任务:

1. 启动node中的服务,主要流程是node将之前注册的所有service交给p2p.Server, 然后启动p2p.Server对象,然后逐个启动每个Service。

2. 解锁钱包中的账号

3. 注册钱包事件

4. 启动辅助服务,比如RPC服务,如果配置支持启动挖矿,则执行启动挖矿。

func startNode(ctx *cli.Context, stack *node.Node) { debug.Memsize.Add("node", stack) // Start up the node itself utils.StartNode(stack) // Unlock any account specifically requested ks:= stack.AccountManager().Backends(keystore.KeyStoreType)[0].(*keystore.KeyStore) passwords := utils.MakePasswordList(ctx) unlocks := strings.Split(ctx.GlobalString(utils.UnlockedAccountFlag.Name), ",") for i, account := range unlocks { if trimmed := strings.TrimSpace(account); trimmed != "" { unlockAccount(ctx, ks, trimmed, i, passwords) } } // Register wallet event handlers to open and auto-derive wallets events := make(chan accounts.WalletEvent, 16) stack.AccountManager().Subscribe(events) go func() { // Create a chain state reader for self-derivation rpcClient, err := stack.Attach() if err != nil { utils.Fatalf("Failed to attach to self: %v", err) } stateReader := ethclient.NewClient(rpcClient) // Open any wallets already attached for _, wallet := range stack.AccountManager().Wallets() { if err := wallet.Open(""); err != nil { log.Warn("Failed to open wallet", "url", wallet.URL(), "err", err) } } // Listen for wallet event till termination for event := range events { switch event.Kind { case accounts.WalletArrived: if err := event.Wallet.Open(""); err != nil { log.Warn("New wallet appeared, failed to open", "url", event.Wallet.URL(), "err", err) } case accounts.WalletOpened: status, _ := event.Wallet.Status() log.Info("New wallet appeared", "url", event.Wallet.URL(), "status", status) derivationPath := accounts.DefaultBaseDerivationPath if event.Wallet.URL().Scheme == "ledger" { derivationPath = accounts.DefaultLedgerBaseDerivationPath } event.Wallet.SelfDerive(derivationPath, stateReader) case accounts.WalletDropped: log.Info("Old wallet dropped", "url", event.Wallet.URL()) event.Wallet.Close() } } }() // Start auxiliary services if enabled if ctx.GlobalBool(utils.MiningEnabledFlag.Name) || ctx.GlobalBool(utils.DeveloperFlag.Name) { // Mining only makes sense if a full Ethereum node is running if ctx.GlobalString(utils.SyncModeFlag.Name) == "light" { utils.Fatalf("Light clients do not support mining") } var ethereum *eth.Ethereum if err := stack.Service(ðereum); err != nil { utils.Fatalf("Ethereum service not running: %v", err) } // Set the gas price to the limits from the CLI and start mining gasprice := utils.GlobalBig(ctx, utils.MinerLegacyGasPriceFlag.Name) if ctx.IsSet(utils.MinerGasPriceFlag.Name) { gasprice = utils.GlobalBig(ctx, utils.MinerGasPriceFlag.Name) } ethereum.TxPool().SetGasPrice(gasprice) threads := ctx.GlobalInt(utils.MinerLegacyThreadsFlag.Name) if ctx.GlobalIsSet(utils.MinerThreadsFlag.Name) { threads = ctx.GlobalInt(utils.MinerThreadsFlag.Name) } if err := ethereum.StartMining(threads); err != nil { utils.Fatalf("Failed to start mining: %v", err) } } } 3.3 Node.Wait函数

Node.Wait函数主要是让geth的主go成进入阻塞状态,保持整个程序不退出,直到从channel中收到Stop消息, Stop消息一般是用户关闭以太坊客户端引起的。

func (n *Node) Wait() { n.lock.RLock() if n.server == nil { n.lock.RUnlock() return } stop := n.stop n.lock.RUnlock() <-stop } 四、总结

以太坊的启动过程其实就是创建一个Node对象, 接着向Node对象中添加各种服务,然后调用这些服务的Start方法启动他们。Node看起来就像一个容器,将各种模块扔进其中,然后将他们联系起来。

Node中包括P2P服务、RPC服务、以太坊服务等, 我们后面一节会着重介绍以太坊服务,以太坊服务会包含区块链相关的核心模块的初始化和启动。

-END-





推荐文章