以太坊原始碼分析(二):以太坊啟動流程分析
點選 ofollow,noindex" target="_blank"> 區塊鏈技術培訓課程 獲取更多區塊鏈技術學習資料。
一、前言
本章節主要通過分析原始碼來了解以太坊的啟動流程,本文基於以太坊的原始碼版本是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-
本文完,更多資訊敬請關注微信公眾號“區塊鏈工程師”
來源:鏈塊學院
本文由布洛克專欄作者釋出,不代表布洛克觀點,版權歸作者所有
——TheEnd——
關注“布洛克科技”