以太坊源码深入分析(2)-- go-ethereum 客户端入口和Node分析

易烨磊
2023-12-01

一,geth makefile 以及编译逻辑

上篇提到用 make geth 来编译geth客户端。我们来看看make file做了什么:

  geth:

  build/env.sh go run build/ci.go install ./cmd/geth

  @echo "Done building."

  @echo "Run \"$(GOBIN)/geth\" to launch geth."

执行了env.sh

# Create fake Go workspace if it doesn'texist yet.

workspace="$PWD/build/_workspace"

root="$PWD"

ethdir="$workspace/src/github.com/ethereum"

if [ ! -L "$ethdir/go-ethereum"]; then

   mkdir -p "$ethdir"

   cd "$ethdir"

   ln -s ../../../../../. go-ethereum

   cd "$root"

fi

# Set up the environment to use theworkspace.

GOPATH="$workspace"

export GOPATH

 

# Run the command inside the workspace.

cd "$ethdir/go-ethereum"

PWD="$ethdir/go-ethereum"

里面做了两件事情

1,ln -s命令在build/_workspace/ 目录上生成了go-etherum的一个文件镜像,不占用磁盘空间,与源文件同步更新

2,把工作目录 workspace加入GOPATH环境变量

跟踪进ci.go 关键函数

 func doInstall(cmdline []string)

这个方法拼接了完整的geth 编译命令:

go install -ldflags -X main.gitCommit=722bac84fa503199b9c485c1a2bfba03bc487d -v ./cmd/geth

二,geth 客户端main函数以及geth的启动入口

geth启动命令:

build/bin/geth --datadir =./data/00 --networkid 1 --fast --cache = 1024 --etherbase“[yourpreferred account]”console >>geth.log

--datadir设置区块链数据存放路径

--networkid 网络设置启动的区块链网路默认值是1表示以太坊公司,0,2,3表示测试网路,大于4表示本地私有网路

--fast同步方式,默认为fast要选择完全同步使用命令--syncmode full

--cache设置缓存大小(最小16MB /数据库强制)(默认值:128)

console表示启动控制台>>geth.log将控制台显示内容输出到文件geth.log中去

geth的源码入口main()函数在/cmd/geth/main.go 

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

// geth is the main entry point into the system if no special subcommand is ran.
// It creates a default node based on the command line arguments and runs it in
// blocking mode, waiting for it to be shut down.
func geth(ctx *cli.Context) error {
	node := makeFullNode(ctx)
	startNode(ctx, node)
	node.Wait()
	return nil
}

main.go/main() -> app.go/Run() - > app.go/HandleAction() - > main.go/geth()

通过这几个函数跳转来到了GETH启动入口。

 从启动入口可以看到第一个启动的模块是node模块,通过makeFullNode函数来创建一个节点对象(在ETH里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)
		}
		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()方法 

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,函数获取以太坊相关服务(Eth Node Shh DashBoard)的默认配置 2,通过Node的默认配置来创建一个Node。返回node对象和cfg

 makeFullNode把创建ethservice、创建DashBoard service、创建Shhservice以及创建ethStats service注册到Node


从这里我们可以看出来eth DashBoard Shh ethStats都是从node.Service接口派生出来的,它们的实例化对象需要实现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
}

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

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

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

Stop() 停止service所有的goroutines,并阻塞线程知道所有goroutines都终止

接下来调用startNode来启动

// startNode boots up the system node and all registered protocols, after which
// it unlocks any requested accounts, and starts the RPC/IPC interfaces and the
// miner.
func startNode(ctx *cli.Context, stack *node.Node) {
	// 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 an 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)

				if event.Wallet.URL().Scheme == "ledger" {
					event.Wallet.SelfDerive(accounts.DefaultLedgerBaseDerivationPath, stateReader)
				} else {
					event.Wallet.SelfDerive(accounts.DefaultBaseDerivationPath, 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.GlobalBool(utils.LightModeFlag.Name) || 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)
		}
		// Use a reduced number of threads if requested
		if threads := ctx.GlobalInt(utils.MinerThreadsFlag.Name); threads > 0 {
			type threaded interface {
				SetThreads(threads int)
			}
			if th, ok := ethereum.Engine().(threaded); ok {
				th.SetThreads(threads)
			}
		}
		// Set the gas price to the limits from the CLI and start mining
		ethereum.TxPool().SetGasPrice(utils.GlobalBig(ctx, utils.GasPriceFlag.Name))
		if err := ethereum.StartMining(true); err != nil {
			utils.Fatalf("Failed to start mining: %v", err)
		}
	}
}

首先node start自己。node将之前注册的所有service交给p2p.Server, 然后启动p2p.Server对象,Server对象会逐个启动每个Service。

解锁账号,并注册钱包反馈事件。

启动RPC。(RPC 提供一种能通过网络或者其他I/O连接访问的能力,将在后续章节分析)

如果配置支持挖矿,则启动挖矿。

node.Wait()阻塞住程序,直到nodestop。


三,小结

Node就好像一个组装工厂,把以太坊相关功能装配起来,连接了以太坊的前端和后端,启动RPC供远程调用,启动了P2P server跟网络中的其他节点建立联系,开始了console 供命令行操作。

Node模块并没有做任何跟区块链实质相关的事情,甚至它都不直接创建以太坊相关模块,而让它们封装成一个个的service,每个service自身自灭。就好比一个卖场,各个商家都可以来卖东西,但我不关心你们的死活,你们的死活也不影响我。

这给我们扩展以太坊客户端的功能提供一个思路,我们可以把扩展功能封装成一个Service,塞给Node。



 类似资料: