以太坊啟動過程原始碼解析

mindcarver發表於2020-10-13

啟動引數

以太坊是如何啟動一個網路節點的呢?

./geth --datadir "../data0" --nodekeyhex "27aa615f5fa5430845e4e97229def5f23e9525a20640cc49304f40f3b43824dc" --bootnodes $enodeid --mine --debug --metrics --syncmode="full" --gcmode=archive  --gasprice 0 --port 30303 --rpc --rpcaddr "0.0.0.0" --rpcport 8545 --rpcapi "db,eth,net,web3,personal" --nat any --allow-insecure-unlock  2>>log 1>>log 0>>log >>log &

引數說明:

  • geth : 編譯好的geth程式,可以起別名
  • datadir:資料庫和keystore金鑰的資料目錄
  • nodekeyhex: 十六進位制的P2P節點金鑰
  • bootnodes:用於P2P發現引導的enode urls
  • mine:開啟挖礦
  • debug:突出顯示呼叫位置日誌(檔名及行號)
  • metrics: 啟用metrics收集和報告
  • syncmode:同步模式 ("fast", "full", or "light")
  • gcmode:表示即時將記憶體中的資料寫入到檔案中,否則重啟節點可能會導致區塊高度歸零而丟失資料
  • gasprice:挖礦接受交易的最低gas價格
  • port:網路卡監聽埠(預設值:30303)
  • rpc:啟用HTTP-RPC伺服器
  • rpcaddr:HTTP-RPC伺服器介面地址(預設值:“localhost”)
  • rpcport:HTTP-RPC伺服器監聽埠(預設值:8545)
  • rpcapi:基於HTTP-RPC介面提供的API
  • nat: NAT埠對映機制 (any|none|upnp|pmp|extip:) (預設: “any”)
  • allow-insecure-unlock:用於解鎖賬戶

詳細的以太坊啟動引數可以參考我的以太坊理論系列,裡面有對引數的詳細解釋。


原始碼分析

geth位於cmd/geth/main.go檔案中,入口如下:

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

image-20201012152238541

我們通過這張圖可以看出來:main()並不是真正意義上的入口,在初始化完常量和變數以後,會先呼叫模組的init()函式,然後才是main()函式。所以初始化的工作是在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-2019 The go-ethereum Authors"
	app.Commands = []cli.Command{
    ....
    ....
    ...
  }

從這我們找到了入口函式geth:

func geth(ctx *cli.Context) error {
	if args := ctx.Args(); len(args) > 0 {
		return fmt.Errorf("invalid command: %q", args[0])
	}
	prepare(ctx)
	node := makeFullNode(ctx)
	defer node.Close()
	startNode(ctx, node)
	node.Wait()
	return nil
}

主要做了以下幾件事:

  1. 準備操作記憶體快取配額並設定度量系統
  2. 載入配置和註冊服務
  3. 啟動節點
  4. 守護當前執行緒

載入配置和註冊服務

makeFullNode

1.載入配置

makeConfigNode

首先載入預設配置(作為主網節點啟動):

cfg := gethConfig{
		Eth:  eth.DefaultConfig,
		Shh:  whisper.DefaultConfig,
		Node: defaultNodeConfig(),
	}
  • eth.DefaultConfig : 以太坊節點的主要引數配置。主要包括: 同步模式(fast)、chainid、交易池配置、gasprice、挖礦配置等;
  • whisper.DefaultConfig : 主要用於配置網路間通訊;
  • defaultNodeConfig() : 主要用於配置對外提供的RPC節點服務;
  • dashboard.DefaultConfig : 主要用於對外提供看板資料訪問服務。

接著載入自定義配置(適用私有鏈):

if file := ctx.GlobalString(configFileFlag.Name); file != "" {
    if err := loadConfig(file, &cfg); err != nil {
        utils.Fatalf("%v", err)
    }
}

最後載入命令視窗引數(開發階段):

utils.SetNodeConfig(ctx, &cfg.Node) // 本地節點配置
utils.SetEthConfig(ctx, stack, &cfg.Eth)// 以太坊配置
utils.SetShhConfig(ctx, stack, &cfg.Shh)// whisper配置

2.RegisterEthService

func RegisterEthService(stack *node.Node, cfg *eth.Config) {
	var err error
	if cfg.SyncMode == downloader.LightSync {
		err = stack.Register(func(ctx *node.ServiceContext) (node.Service, error) {
			return les.New(ctx, cfg)
		})
	} else {
		err = stack.Register(func(ctx *node.ServiceContext) (node.Service, error) {
			fullNode, err := eth.New(ctx, cfg)
			if fullNode != nil && cfg.LightServ > 0 {
				ls, _ := les.NewLesServer(fullNode, cfg)
				fullNode.AddLesServer(ls)
			}
			return fullNode, err
		})
	}
	if err != nil {
		Fatalf("Failed to register the Ethereum service: %v", err)
	}
}

出現了兩個新型別:ServiceContext和Service。

先看一下ServiceContext的定義:

type ServiceContext struct {
	config         *Config
	services       map[reflect.Type]Service // Index of the already constructed services
	EventMux       *event.TypeMux           // Event multiplexer used for decoupled notifications
	AccountManager *accounts.Manager        // Account manager created by the node.
}

ServiceContext主要是儲存了一些從結點(或者叫協議棧)那裡繼承過來的、和具體Service無關的一些資訊,比如結點config、account manager等。其中有一個services欄位儲存了當前正在執行的所有Service.

接下來看一下Service的定義:

type Service interface {
	// Protocols retrieves the P2P protocols the service wishes to start.
	// 協議檢索服務希望啟動的P2P協議
	Protocols() []p2p.Protocol

	// APIs retrieves the list of RPC descriptors the service provides
	// API檢索服務提供的RPC描述符列表
	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.
	//在所有服務都已構建完畢並且網路層也已初始化以生成服務所需的所有goroutine之後,將呼叫start。
	Start(server *p2p.Server) error

	// Stop terminates all goroutines belonging to the service, blocking until they
	// are all terminated.
	//Stop終止屬於該服務的所有goroutine,直到它們全部終止為止一直阻塞。
	Stop() error
}

在服務註冊過程中,主要註冊四個服務:EthService、DashboardService、ShhService、EthStatsService,這四種服務類均擴充套件自Service介面。其中,EthService根據同步模式的不同,分為兩種實現:

  • LightEthereum,支援LightSync模式
  • Ethereum,支援FullSync、FastSync模式

LightEthereum作為輕客戶端,與Ethereum區別在於,它只需要更新區塊頭。當需要查詢區塊體資料時,需要通過呼叫其他全節點的les服務進行查詢;另外,輕客戶端本身是不能進行挖礦的。

回到RegisterEthService程式碼,分兩個來講:

LightSync同步:

err = stack.Register(func(ctx *node.ServiceContext) (node.Service, error) {
        return les.New(ctx, cfg)
    })
func New(ctx *node.ServiceContext, config *eth.Config) (*LightEthereum, error) {
  
  1.ctx.OpenDatabase // 建立leveldb資料庫
  2.core.SetupGenesisBlockWithOverride// 根據創世配置初始化鏈資料目錄
  3.例項化本地鏈id、共識引擎、註冊peer節點、帳戶管理器以及布隆過濾器的初始化
  4.light.NewLightChain// 使用資料庫中可用的資訊返回完全初始化的輕鏈。它初始化預設的以太坊頭
  5.light.NewTxPool // 例項化交易池NewTxPool
  6.leth.ApiBackend = &LesApiBackend{ctx.ExtRPCEnabled(), leth, nil} 
  
}

FullSync/Fast同步:

  1. 引數校驗

    if config.SyncMode == downloader.LightSync {
      ....
    if !config.SyncMode.IsValid() {
      ....
    if config.Miner.GasPrice == nil || config.Miner.GasPrice.Cmp(common.Big0) <= 0 {
      ....
    if config.NoPruning && config.TrieDirtyCache > 0 {  
    
  2. 開啟資料庫

    ctx.OpenDatabaseWithFreezer
    
  3. 根據創世配置初始化鏈資料目錄

    core.SetupGenesisBlockWithOverride
    
  4. 例項化Ethereum物件

  5. 建立BlockChain例項物件

    core.NewBlockChain
    
  6. 例項化交易池

    core.NewTxPool
    
  7. 例項化協議管理器

    NewProtocolManager(...)
    
  8. 例項化對外API服務

    &EthAPIBackend{ctx.ExtRPCEnabled(), eth, nil}
    

3.RegisterShhService

註冊Whisper服務,用於p2p網路間加密通訊。

whisper.New(cfg), nil

4.RegisterEthStatsService

註冊狀態推送服務,將當前以太坊網路狀態推送至指定URL地址.

ethstats.New(url, ethServ, lesServ)

啟動節點

啟動本地節點以及啟動所有註冊的服務。

1.啟動節點

startNode

1.1 stack.Start()

  1. 例項化p2p.Server物件。

    running := &p2p.Server{Config: n.serverConfig}
    
  2. 為註冊的服務建立上下文

    for _, constructor := range n.serviceFuncs {
      ctx := &ServiceContext{
        ....
      }
    }
    
  3. 收集協議並啟動新組裝的p2p server

    for kind, service := range services {
      if err := service.Start(running); err != nil {
        ...
      }
    }
    
  4. 最後啟動配置的RPC介面

    n.startRPC(services)
    
    • startInProc (啟動程式內通訊服務)
    • startIPC (啟動IPC RPC端點)
    • startHTTP(啟動HTTP RPC端點)
    • startWS (啟動websocket RPC端點)

2.解鎖賬戶

unlockAccounts

在datadir/keystore目錄主要用於記錄在當前節點建立的帳戶keystore檔案。如果你的keystore檔案不在本地是無法進行解鎖的。

//解鎖datadir/keystore目錄中帳戶
ks := stack.AccountManager().Backends(keystore.KeyStoreType)[0].(*keystore.KeyStore)
	passwords := utils.MakePasswordList(ctx)
	for i, account := range unlocks {
		unlockAccount(ks, account, i, passwords)
	}

3.註冊錢包事件

events := make(chan accounts.WalletEvent, 16)
stack.AccountManager().Subscribe(events)

4.監聽錢包事件

	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)

				var derivationPaths []accounts.DerivationPath
				if event.Wallet.URL().Scheme == "ledger" {
					derivationPaths = append(derivationPaths, accounts.LegacyLedgerBaseDerivationPath)
				}
				derivationPaths = append(derivationPaths, accounts.DefaultBaseDerivationPath)

				event.Wallet.SelfDerive(derivationPaths, ethClient)

			case accounts.WalletDropped:
				log.Info("Old wallet dropped", "url", event.Wallet.URL())
				event.Wallet.Close()
			}
		}
	}()

5.啟動挖礦

ethereum.StartMining(threads)

啟動守護執行緒

stop通道阻塞當前執行緒,直到節點被停止。

node.Wait()

總結

以太坊啟動主要就做了3件事,包括載入配置註冊服務、啟動節點相關服務以及啟動守護執行緒。

參考:github地址


相關文章