以太坊启动过程源码解析

407 阅读6分钟

文章以及资料(开源):github地址

启动参数

以太坊是如何启动一个网络节点的呢?

./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件事,包括加载配置注册服务、启动节点相关服务以及启动守护线程。