Geth启动流程代码解析(完整解析)

CentOS7下Geth 1.10.26版本安装避坑指南:从依赖到私有链启动的完整流程 本文详细介绍了在CentOS7系统下安装和配置Geth 1.10.26版本的完整流程,包括环境准备、二进制部署、私有链启动及智能合约交互。特别针对开发者常用的`personal`模块保留问题提供了版本选择建议,并分享了私有链部署中的常见陷阱和解决方案,帮助开发者高效搭建以太坊私有链环境。 阅读详情

Geth启动流程代码解析(完整解析)

init函数主要是做了一些初始化的工作,其中比较重要的有三个地方,app.Action=geth,app.Commands中consoleCommand。启动流程走的是consoleCommand命令语句:

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-2017 The go-ethereum Authors"
	app.Commands = []cli.Command{
		// See chaincmd.go:
		initCommand,
		importCommand,
		exportCommand,
		removedbCommand,
		dumpCommand,
		// See monitorcmd.go:
		monitorCommand,
		// See accountcmd.go:
		accountCommand,
		walletCommand,
		// See consolecmd.go:
		consoleCommand,
		attachCommand,
		javascriptCommand,
		// See misccmd.go:
		makedagCommand,
		versionCommand,
		bugCommand,
		licenseCommand,
		// See config.go
		dumpConfigCommand,
	}
	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.Before = func(ctx *cli.Context) error {
		runtime.GOMAXPROCS(runtime.NumCPU())
		if err := debug.Setup(ctx); err != nil {
			return err
		}
		// Start system runtime metrics collection
		go metrics.CollectProcessMetrics(3 * time.Second)
		utils.SetupNetwork(ctx)
		return nil
	}
	app.After = func(ctx *cli.Context) error {
		debug.Exit()
		console.Stdin.Close() // Resets terminal mode.
		return nil
	}
}

main函数的实现很简单,仅仅调用了app.Run函数,如果调用异常,则退出。

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

此处通过第三方包gopkg.in/urfave/cli.v1来衔接,app中的action的类型是「func(*Context) error」,此时将执行a(context)方法,那么此时调用那个Action呢,其实就是我们前面提到的App.init()初始化命令时的consoleCommand,接下来我们来看看cmd/geth/consolecmd中的consoleCommand:

consoleCommand = cli.Command{
		Action:   utils.MigrateFlags(localConsole),
		Name:     "console",
		Usage:    "Start an interactive JavaScript environment",
		Flags:    append(append(append(nodeFlags, rpcFlags...), consoleFlags...), whisperFlags...),
		Category: "CONSOLE COMMANDS",
		Description: `
The Geth console is an interactive shell for the JavaScript runtime environment
which exposes a node admin interface as well as the Ðapp JavaScript API.
See https://github.com/ethereum/go-ethereum/wiki/Javascipt-Console.`,
	}

其Action中的localConsole函数如下:

func localConsole(ctx *cli.Context) error {
	// Create and start the node based on the CLI flags
	node := makeFullNode(ctx)
	startNode(ctx, node)
	defer node.Stop()

	// Attach to the newly started node and start the JavaScript console
	client, err := node.Attach()
	if err != nil {
		utils.Fatalf("Failed to attach to the inproc geth: %v", err)
	}
	config := console.Config{
		DataDir: utils.MakeDataDir(ctx),
		DocRoot: ctx.GlobalString(utils.JSpathFlag.Name),
		Client:  client,
		Preload: utils.MakeConsolePreloads(ctx),
	}

	console, err := console.New(config)
	if err != nil {
		utils.Fatalf("Failed to start the JavaScript console: %v", err)
	}
	defer console.Stop(false)

	// If only a short execution was requested, evaluate and return
	if script := ctx.GlobalString(utils.ExecFlag.Name); script != "" {
		console.Evaluate(script)
		return nil
	}
	// Otherwise print the welcome screen and enter interactive mode
	console.Welcome()
	console.Interactive()

	return nil
}

该函数非常重要,主要完成以下几件事情

  • 首先会创建一个节点、同时启动该节点
  • 创建一个console的实例
  • 显示Welcome信息
  • 创建一个无限循环用于在控制台交互

首先我们来看该Node是如何创建的。makeFullNode函数的实现如下:

func makeFullNode(ctx *cli.Context) *node.Node {
	stack, cfg := makeConfigNode(ctx)

	utils.RegisterEthService(stack, &cfg.Eth)

	// 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.DevModeFlag.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)
	}

	// Add the release oracle service so it boots along with node.
	if err := stack.Register(func(ctx *node.ServiceContext) (node.Service, error) {
		config := release.Config{
			Oracle: relOracle,
			Major:  uint32(params.VersionMajor),
			Minor:  uint32(params.VersionMinor),
			Patch:  uint32(params.VersionPatch),
		}
		commit, _ := hex.DecodeString(gitCommit)
		copy(config.Commit[:], commit)
		return release.NewReleaseService(ctx, config)
	}); err != nil {
		utils.Fatalf("Failed to register the Geth release oracle service: %v", err)
	}
	return stack
}

该函数首先创建一个Node,然后注册一个Ethereum Service,我们继续分析Node是如何创建的,makeConfigNode的函数实现逻辑如下:

func makeConfigNode(ctx *cli.Context) (*node.Node, gethConfig) {
	// Load defaults.
	cfg := gethConfig{
		Eth:  eth.DefaultConfig,
		Shh:  whisper.DefaultConfig,
		Node: defaultNodeConfig(),
	}

	// 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)

	return stack, cfg
}

首先初始化gethConfig的变量,接下来调用node.New方法来创建一个Node,我们来看看New函数的实现:

func New(conf *Config) (*Node, error) {
	// Copy config and resolve the datadir so future changes to the current
	// working directory don't affect the node.
	confCopy := *conf
	conf = &confCopy
	if conf.DataDir != "" {
		absdatadir, err := filepath.Abs(conf.DataDir)
		if err != nil {
			return nil, err
		}
		conf.DataDir = absdatadir
	}
	// Ensure that the instance name doesn't cause weird conflicts with
	// other files in the data directory.
	if strings.ContainsAny(conf.Name, `/\`) {
		return nil, errors.New(`Config.Name must not contain '/' or '\'`)
	}
	if conf.Name == datadirDefaultKeyStore {
		return nil, errors.New(`Config.Name cannot be "` + datadirDefaultKeyStore + `"`)
	}
	if strings.HasSuffix(conf.Name, ".ipc") {
		return nil, errors.New(`Config.Name cannot end in ".ipc"`)
	}
	// Ensure that the AccountManager method works before the node has started.
	// We rely on this in cmd/geth.
	am, ephemeralKeystore, err := makeAccountManager(conf)
	if err != nil {
		return nil, err
	}
	// Note: any interaction with Config that would create/touch files
	// in the data directory or instance directory is delayed until Start.
	return &Node{
		accman:            am,
		ephemeralKeystore: ephemeralKeystore,
		config:            conf,
		serviceFuncs:      []ServiceConstructor{},
		ipcEndpoint:       conf.IPCEndpoint(),
		httpEndpoint:      conf.HTTPEndpoint(),
		wsEndpoint:        conf.WSEndpoint(),
		eventmux:          new(event.TypeMux),
	}, nil
}

New函数首先将保存数据的目录转换成绝对路径,然后对配置进行相关的验证,确认是否合法,接下来创建一个AccountManager实例,然后创建一个Node节点,总体来说其内部的逻辑比较的简单。由于AccountManager负责Account的相关管理操作且后续经常使用,接下来我们看看是AccountManager的具体逻辑实现。AccountManager位于/node/config.go文件中,其实现如下:

func makeAccountManager(conf *Config) (*accounts.Manager, string, error) {
	scryptN := keystore.StandardScryptN
	scryptP := keystore.StandardScryptP
	if conf.UseLightweightKDF {
		scryptN = keystore.LightScryptN
		scryptP = keystore.LightScryptP
	}

	var (
		keydir    string
		ephemeral string
		err       error
	)
	switch {
	case filepath.IsAbs(conf.KeyStoreDir):
		keydir = conf.KeyStoreDir
	case conf.DataDir != "":
		if conf.KeyStoreDir == "" {
			keydir = filepath.Join(conf.DataDir, datadirDefaultKeyStore)
		} else {
			keydir, err = filepath.Abs(conf.KeyStoreDir)
		}
	case conf.KeyStoreDir != "":
		keydir, err = filepath.Abs(conf.KeyStoreDir)
	default:
		// There is no datadir.
		keydir, err = ioutil.TempDir("", "go-ethereum-keystore")
		ephemeral = keydir
	}
	if err != nil {
		return nil, "", err
	}
	if err := os.MkdirAll(keydir, 0700); err != nil {
		return nil, "", err
	}
	// Assemble the account manager and supported backends
	backends := []accounts.Backend{
		keystore.NewKeyStore(keydir, scryptN, scryptP),
	}
	if !conf.NoUSB {
		if ledgerhub, err := usbwallet.NewLedgerHub(); err != nil {
			log.Warn(fmt.Sprintf("Failed to start Ledger hub, disabling: %v", err))
		} else {
			backends = append(backends, ledgerhub)
		}
	}
	return accounts.NewManager(backends...), ephemeral, nil
}

由于在上面,我们已经将保存数据的目录转换成了绝对路径(由于在命令行,我们已经指定了datadir且存在,如果不存在将创建一个默认的go-ethereum-keystore来保存相关信息),接下来我们来看keystore.NewKeyStore函数。该函数位于/accounts/keystore/keystore.go文件中,其实现如下:

func NewKeyStore(keydir string, scryptN, scryptP int) *KeyStore {
	keydir, _ = filepath.Abs(keydir)
	ks := &KeyStore{storage: &keyStorePassphrase{keydir, scryptN, scryptP}}
	ks.init(keydir)
	return ks
}

首先转换成绝对路径(前面已经转换,应该没有必要再次检测),接下来我们看看

ks.init(keydir)

方法的实现,其实现如下:

func (ks *KeyStore) init(keydir string) {
	// Lock the mutex since the account cache might call back with events
	ks.mu.Lock()
	defer ks.mu.Unlock()

	// Initialize the set of unlocked keys and the account cache
	ks.unlocked = make(map[common.Address]*unlocked)
	ks.cache, ks.changes = newAccountCache(keydir)

	// TODO: In order for this finalizer to work, there must be no references
	// to ks. addressCache doesn't keep a reference but unlocked keys do,
	// so the finalizer will not trigger until all timed unlocks have expired.
	runtime.SetFinalizer(ks, func(m *KeyStore) {
		m.cache.close()
	})
	// Create the initial list of wallets from the cache
	accs := ks.cache.accounts()
	ks.wallets = make([]accounts.Wallet, len(accs))
	for i := 0; i < len(accs); i++ {
		ks.wallets[i] = &keystoreWallet{account: accs[i], keystore: ks}
	}
}

该方法首先从缓存中(如果缓存存在则直接获取,否则从datadir中读取)获取Account,然后保存在keystore的钱包中,我们主要来看看ks.cache.accounts()函数,其实现逻辑如下:

func (ac *accountCache) accounts() []accounts.Account {
	ac.maybeReload()
	ac.mu.Lock()
	defer ac.mu.Unlock()
	cpy := make([]accounts.Account, len(ac.all))
	copy(cpy, ac.all)
	return cpy
}

如果账户数据未被加载到内存中,则首先加载进来,然后copy一份,防止外面对缓存的账户做修改,我们来看看ac.maybeReload是如何加载账户信息的。其实现如下:

func (ac *accountCache) maybeReload() {
	ac.mu.Lock()
	defer ac.mu.Unlock()

	if ac.watcher.running {
		return // A watcher is running and will keep the cache up-to-date.
	}
	if ac.throttle == nil {
		ac.throttle = time.NewTimer(0)
	} else {
		select {
		case <-ac.throttle.C:
		default:
			return // The cache was reloaded recently.
		}
	}
	ac.watcher.start()
	ac.reload()
	ac.throttle.Reset(minReloadInterval)
}

首先使用goroutine启动一个watcher来监测keystore目录,防止其变化。接下来调用reload方法加载账户信息,reload方法的实现如下:

func (ac *accountCache) reload() {
	accounts, err := ac.scan()
	if err != nil {
		log.Debug("Failed to reload keystore contents", "err", err)
	}
	ac.all = accounts
	sort.Sort(ac.all)
	for k := range ac.byAddr {
		delete(ac.byAddr, k)
	}
	for _, a := range accounts {
		ac.byAddr[a.Address] = append(ac.byAddr[a.Address], a)
	}
	select {
	case ac.notify <- struct{}{}:
	default:
	}
	log.Debug("Reloaded keystore contents", "accounts", len(ac.all))
}

reload方法首先加载Account信息,然后通过channel的方式通知加载结束。Scan方法就是最终从文件中加载账户信息的地方,其实现如下:

func (ac *accountCache) scan() ([]accounts.Account, error) {
	files, err := ioutil.ReadDir(ac.keydir)
	if err != nil {
		return nil, err
	}

	var (
		buf     = new(bufio.Reader)
		addrs   []accounts.Account
		keyJSON struct {
			Address string `json:"address"`
		}
	)
	for _, fi := range files {
		path := filepath.Join(ac.keydir, fi.Name())
		if skipKeyFile(fi) {
			log.Trace("Ignoring file on account scan", "path", path)
			continue
		}
		logger := log.New("path", path)

		fd, err := os.Open(path)
		if err != nil {
			logger.Trace("Failed to open keystore file", "err", err)
			continue
		}
		buf.Reset(fd)
		// Parse the address.
		keyJSON.Address = ""
		err = json.NewDecoder(buf).Decode(&keyJSON)
		addr := common.HexToAddress(keyJSON.Address)
		switch {
		case err != nil:
			logger.Debug("Failed to decode keystore key", "err", err)
		case (addr == common.Address{}):
			logger.Debug("Failed to decode keystore key", "err", "missing or zero address")
		default:
			addrs = append(addrs, accounts.Account{Address: addr, URL: accounts.URL{Scheme: KeyStoreScheme, Path: path}})
		}
		fd.Close()
	}
	return addrs, err
}

该方法首先获取该目录下的文件,然后读取文件内容,JSON解析其address字段。下面是我的datadir/keystore中文件的内容,格式化后如下:

{
	"address":"a8f8687d0da839cef651cc0d2dc41bb2dc796293",
	"crypto":{
		"cipher":"aes-128-ctr",
		"ciphertext":"142da6b140f5f4ddd48205997cfc6257312dde63e227579bdc		a427a24661b9f8",
		"cipherparams":{
		"iv":"8d45e1d8b9fbd72fa40799c3a2ca1d22"
	},
	"kdf":"scrypt",
	"kdfparams":{
		"dklen":32,
		"n":262144,
		"p":1,
		"r":8,
		"salt":"68d9776e818a5c1a92982752df1b3c66fc54a804d508a926519d33e046158959"
		},
	"mac":"898b1f94fda7731215e521ea7ca7eb7d9afd9b8255c6dccd244c78af88123e7e"
	},
	"id":"dfefe914-e328-4f7f-89a7-a85689b8e855",
	"version":3
}

从json可以看出我这里只有一个账户的信息,因为以太坊对于每个用户都创建一个单独的文件来保存其信息。对于账户信息的加载,AccountManager的创建我们分析完了,同时整个Node也创建完成,此时我们来看以太坊是如何启动一个节点的。此时回到main函数,startNode函数的代码如下:

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 and self derive 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)
			} else {
				wallet.SelfDerive(accounts.DefaultBaseDerivationPath, stateReader)
			}
		}
		// Listen for wallet event till termination
		for event := range events {
			if event.Arrive {
				if err := event.Wallet.Open(""); err != nil {
					log.Warn("New wallet appeared, failed to open", "url", event.Wallet.URL(), "err", err)
				} else {
					log.Info("New wallet appeared", "url", event.Wallet.URL(), "status", event.Wallet.Status())
					event.Wallet.SelfDerive(accounts.DefaultBaseDerivationPath, stateReader)
				}
			} else {
				log.Info("Old wallet dropped", "url", event.Wallet.URL())
				event.Wallet.Close()
			}
		}
	}()
	// Start auxiliary services if enabled
	if ctx.GlobalBool(utils.MiningEnabledFlag.Name) {
		// Mining only makes sense if a full Ethereum node is running
		var ethereum *eth.Ethereum
		if err := stack.Service(&ethereum); 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)
		}
	}
}

该函数内部首先将节点启动起来,然后建立跟RPC Server建立一个连接,用于RPC通信。我们来看看节点如何启动起来的。/cmd/utils/cmd.go文件如下:

func StartNode(stack *node.Node) {
	if err := stack.Start(); err != nil {
		Fatalf("Error starting protocol stack: %v", err)
	}
	go func() {
		sigc := make(chan os.Signal, 1)
		signal.Notify(sigc, os.Interrupt)
		defer signal.Stop(sigc)
		<-sigc
		log.Info("Got interrupt, shutting down...")
		go stack.Stop()
		for i := 10; i > 0; i-- {
			<-sigc
			if i > 1 {
				log.Warn("Already shutting down, interrupt more to panic.", "times", i-1)
			}
		}
		debug.Exit() // ensure trace and CPU profile data is flushed.
		debug.LoudPanic("boom")
	}()
}

该函数调用了Node.go中的start方法,其实现如下:

func (n *Node) Start() error {
	n.lock.Lock()
	defer n.lock.Unlock()

	// Short circuit if the node's already running
	if n.server != nil {
		return ErrNodeRunning
	}
	if err := n.openDataDir(); err != nil {
		return err
	}

	// Initialize the p2p server. This creates the node key and
	// discovery databases.
	n.serverConfig = n.config.P2P
	n.serverConfig.PrivateKey = n.config.NodeKey()
	n.serverConfig.Name = n.config.NodeName()
	if n.serverConfig.StaticNodes == nil {
		n.serverConfig.StaticNodes = n.config.StaticNodes()
	}
	if n.serverConfig.TrustedNodes == nil {
		n.serverConfig.TrustedNodes = n.config.TrustedNodes()
	}
	if n.serverConfig.NodeDatabase == "" {
		n.serverConfig.NodeDatabase = n.config.NodeDB()
	}
	running := &p2p.Server{Config: n.serverConfig}
	log.Info("Starting peer-to-peer node", "instance", n.serverConfig.Name)

	// Otherwise copy and specialize the P2P configuration
	services := make(map[reflect.Type]Service)
	for _, constructor := range n.serviceFuncs {
		// Create a new context for the particular service
		ctx := &ServiceContext{
			config:         n.config,
			services:       make(map[reflect.Type]Service),
			EventMux:       n.eventmux,
			AccountManager: n.accman,
		}
		for kind, s := range services { // copy needed for threaded access
			ctx.services[kind] = s
		}
		// Construct and save the service
		service, err := constructor(ctx)
		if err != nil {
			return err
		}
		kind := reflect.TypeOf(service)
		if _, exists := services[kind]; exists {
			return &DuplicateServiceError{Kind: kind}
		}
		services[kind] = service
	}
	// Gather the protocols and start the freshly assembled P2P server
	for _, service := range services {
		running.Protocols = append(running.Protocols, service.Protocols()...)
	}
	if err := running.Start(); err != nil {
		if errno, ok := err.(syscall.Errno); ok && datadirInUseErrnos[uint(errno)] {
			return ErrDatadirUsed
		}
		return err
	}
	// Start each of the services
	started := []reflect.Type{}
	for kind, service := range services {
		// Start the next service, stopping all previous upon failure
		if err := service.Start(running); err != nil {
			for _, kind := range started {
				services[kind].Stop()
			}
			running.Stop()

			return err
		}
		// Mark the service started for potential cleanup
		started = append(started, kind)
	}
	// Lastly start the configured RPC interfaces
	if err := n.startRPC(services); err != nil {
		for _, service := range services {
			service.Stop()
		}
		running.Stop()
		return err
	}
	// Finish initializing the startup
	n.services = services
	n.server = running
	n.stop = make(chan struct{})

	return nil
}

该方法首先打开datadir目录,接着初始化serverConfig的相关配置,接着创建一个p2p.server的一个变量,然后启动该节点,在接着把RPC的Service启动起来,我们先看看节点的启动。/p2p/server.go中Start方法如下:

func (srv *Server) Start() (err error) {
	srv.lock.Lock()
	defer srv.lock.Unlock()
	if srv.running {
		return errors.New("server already running")
	}
	srv.running = true
	log.Info("Starting P2P networking")

	// static fields
	if srv.PrivateKey == nil {
		return fmt.Errorf("Server.PrivateKey must be set to a non-nil key")
	}
	if srv.newTransport == nil {
		srv.newTransport = newRLPX
	}
	if srv.Dialer == nil {
		srv.Dialer = &net.Dialer{Timeout: defaultDialTimeout}
	}
	srv.quit = make(chan struct{})
	srv.addpeer = make(chan *conn)
	srv.delpeer = make(chan peerDrop)
	srv.posthandshake = make(chan *conn)
	srv.addstatic = make(chan *discover.Node)
	srv.removestatic = make(chan *discover.Node)
	srv.peerOp = make(chan peerOpFunc)
	srv.peerOpDone = make(chan struct{})

	// node table
	if !srv.NoDiscovery {
		ntab, err := discover.ListenUDP(srv.PrivateKey, srv.ListenAddr, srv.NAT, srv.NodeDatabase, srv.NetRestrict)
		if err != nil {
			return err
		}
		if err := ntab.SetFallbackNodes(srv.BootstrapNodes); err != nil {
			return err
		}
		srv.ntab = ntab
	}

	if srv.DiscoveryV5 {
		ntab, err := discv5.ListenUDP(srv.PrivateKey, srv.DiscoveryV5Addr, srv.NAT, "", srv.NetRestrict) //srv.NodeDatabase)
		if err != nil {
			return err
		}
		if err := ntab.SetFallbackNodes(srv.BootstrapNodesV5); err != nil {
			return err
		}
		srv.DiscV5 = ntab
	}

	dynPeers := (srv.MaxPeers + 1) / 2
	if srv.NoDiscovery {
		dynPeers = 0
	}
	dialer := newDialState(srv.StaticNodes, srv.BootstrapNodes, srv.ntab, dynPeers, srv.NetRestrict)

	// handshake
	srv.ourHandshake = &protoHandshake{Version: baseProtocolVersion, Name: srv.Name, ID: discover.PubkeyID(&srv.PrivateKey.PublicKey)}
	for _, p := range srv.Protocols {
		srv.ourHandshake.Caps = append(srv.ourHandshake.Caps, p.cap())
	}
	// listen/dial
	if srv.ListenAddr != "" {
		if err := srv.startListening(); err != nil {
			return err
		}
	}
	if srv.NoDial && srv.ListenAddr == "" {
		log.Warn("P2P server will be useless, neither dialing nor listening")
	}

	srv.loopWG.Add(1)
	go srv.run(dialer)
	srv.running = true
	return nil
}

该方法首先建立一个UDP连接,并且调用server.run启动起来,跟其它可用的节点建立连接等。关于如何建立连接,后续在讲P2P的时候,我们再来仔细的分析。到此,整个节点就启动起来了。接下来,我们来看前面4个步骤中的第二个–创建console实例。console.go中创建一个console的New方法实现如下:

func New(config Config) (*Console, error) {
	// Handle unset config values gracefully
	if config.Prompter == nil {
		config.Prompter = Stdin
	}
	if config.Prompt == "" {
		config.Prompt = DefaultPrompt
	}
	if config.Printer == nil {
		config.Printer = colorable.NewColorableStdout()
	}
	// Initialize the console and return
	console := &Console{
		client:   config.Client,
		jsre:     jsre.New(config.DocRoot, config.Printer),
		prompt:   config.Prompt,
		prompter: config.Prompter,
		printer:  config.Printer,
		histPath: filepath.Join(config.DataDir, HistoryFile),
	}
	if err := console.init(config.Preload); err != nil {
		return nil, err
	}
	return console, nil
}

该函数首先对Config做一些默认设置,接着调用其jsre.New函数,在借这个调用init方法初始化相关RPC API。jsre.New方法的实现如下:

func New(assetPath string, output io.Writer) *JSRE {
	re := &JSRE{
		assetPath:     assetPath,
		output:        output,
		closed:        make(chan struct{}),
		evalQueue:     make(chan *evalReq),
		stopEventLoop: make(chan bool),
	}
	go re.runEventLoop()
	re.Set("loadScript", re.loadScript)
	re.Set("inspect", re.prettyPrintJS)
	return re
}

该函数创建一个JSRE的变量然后使用goroutine的方式开启一个事件监听的循环,该事件为控制台输入后,经过一些处理通过channel的方式发送过来。具体接收到的命令后续处理,请读者自己跟踪其逻辑实现。总结下创建console的目的主要是监听控制台输入的命名。接下来我们来看看第三个步骤「显示Welcome信息 」,该信息在通过geth进入控制台的时候,会显示一些简单的信息,我们先来看看该函数的实现:

func (c *Console) Welcome() {
	// Print some generic Geth metadata
	fmt.Fprintf(c.printer, "Welcome to the Geth JavaScript console!\n\n")
	c.jsre.Run(`
		console.log("instance: " + web3.version.node);
		console.log("coinbase: " + eth.coinbase);
		console.log("at block: " + eth.blockNumber + " (" + new Date(1000 * eth.getBlock(eth.blockNumber).timestamp) + ")");
		console.log(" datadir: " + admin.datadir);
	`)
	// List all the supported modules for the user to call
	if apis, err := c.client.SupportedModules(); err == nil {
		modules := make([]string, 0, len(apis))
		for api, version := range apis {
			modules = append(modules, fmt.Sprintf("%s:%s", api, version))
		}
		sort.Strings(modules)
		fmt.Fprintln(c.printer, " modules:", strings.Join(modules, " "))
	}
	fmt.Fprintln(c.printer)
}

该方法首先打印「Welcome to the Geth JavaScript console!」信息,这个可以直接在控制台查看到,接着使用上面创建console的jsre执行四个控制台输出写版本、区块等信息。你可以直接复制出来在命令行执行,也能看到相同的结果。

接下来,我们来看最后的一个步骤「创建一个无限循环用于在控制台交互」,该函数的实现如下:

func (c *Console) Interactive() {
	var (
		prompt    = c.prompt          // Current prompt line (used for multi-line inputs)
		indents   = 0                 // Current number of input indents (used for multi-line inputs)
		input     = ""                // Current user input
		scheduler = make(chan string) // Channel to send the next prompt on and receive the input
	)
	// Start a goroutine to listen for promt requests and send back inputs
	go func() {
		for {
			// Read the next user input
			line, err := c.prompter.PromptInput(<-scheduler)
			if err != nil {
				// In case of an error, either clear the prompt or fail
				if err == liner.ErrPromptAborted { // ctrl-C
					prompt, indents, input = c.prompt, 0, ""
					scheduler <- ""
					continue
				}
				close(scheduler)
				return
			}
			// User input retrieved, send for interpretation and loop
			scheduler <- line
		}
	}()
	// Monitor Ctrl-C too in case the input is empty and we need to bail
	abort := make(chan os.Signal, 1)
	signal.Notify(abort, os.Interrupt)

	// Start sending prompts to the user and reading back inputs
	for {
		// Send the next prompt, triggering an input read and process the result
		scheduler <- prompt
		select {
		case <-abort:
			// User forcefully quite the console
			fmt.Fprintln(c.printer, "caught interrupt, exiting")
			return

		case line, ok := <-scheduler:
			// User input was returned by the prompter, handle special cases
			if !ok || (indents <= 0 && exit.MatchString(line)) {
				return
			}
			if onlyWhitespace.MatchString(line) {
				continue
			}
			// Append the line to the input and check for multi-line interpretation
			input += line + "\n"

			indents = countIndents(input)
			if indents <= 0 {
				prompt = c.prompt
			} else {
				prompt = strings.Repeat(".", indents*3) + " "
			}
			// If all the needed lines are present, save the command and run
			if indents <= 0 {
				if len(input) > 0 && input[0] != ' ' && !passwordRegexp.MatchString(input) {
					if command := strings.TrimSpace(input); len(c.history) == 0 || command != c.history[len(c.history)-1] {
						c.history = append(c.history, command)
						if c.prompter != nil {
							c.prompter.AppendHistory(command)
						}
					}
				}
				c.Evaluate(input)
				input = ""
			}
		}
	}
}

github地址:

https://github.com/guoshijiang/go-ethereum-code-analysis

关注问我技术公众号,加小问,拉您入技术交流群:

在这里插入图片描述

最强以太坊源码解析-01-启动流程 一、main函数 geth命令是整个以太坊的灵魂,这个命令的实现代码位置为:cmd/geth/main.go func main() { if err := app.Run(os.Args); err != nil { fmt.Fprintln(os.Stderr, err) os.Exit(1) } } 我们可以看到,有一个app变量,这是一个全局变量,在init函数中赋值 在go... 阅读详情

相关推荐

51单片机+HX711电子秤Proteus仿真保姆级教程(附完整源码与避坑指南)

本文详细介绍了51单片机与HX711电子秤在Proteus仿真中的全流程实战指南,包括电路设计、核心代码实现、典型问题排查及系统集成等关键步骤。特别提供了HX711驱动程序的编写技巧和称重数据处理算法,帮助开发者快速搭建高精度电子秤仿真系统,并避免常见的设计陷阱。

weixin_30752699的博客 414

区块链-私链创建与智能合约部署实践

准备: ECS+ubuntu 16.0+go+go-ethereum 1.创世链节点: { "config": { "chainId": 12345, "homesteadBlock":0, "eip155Block":0, "eip158Block":0 }, “coinbase”:”0x0000000000000000000000000...

gdxz123的博客 2933

四足机器人如何用对抗运动先验(AMP)实现复杂地形行走?Unitree Go1实战解析

本文深入解析了四足机器人Unitree Go1如何通过对抗运动先验(AMP)算法实现复杂地形行走。AMP结合模仿学习与对抗训练,使机器人仅凭本体感知就能适应未知地形。文章详细介绍了AMP的核心架构、仿真训练工程实践以及在Unitree Go1上的部署优化方案,展示了其在极端地形下的卓越性能表现。

weixin_29050513的博客 328

Linux下安装MySQL

1.首先上传安装包 mysql-5.7.21-linux-glibc2.12-x86_64.tar.gz  到/root下2.解压:[root@master ~]# tar -zxvf mysql-5.7.21-linux-glibc2.12-x86_64.tar.gz3.将目录移动到/usr/local/,并改名为mysql[root@master local]# mv mysql-5.7.21...

Source_00的博客 2801

ADS基础教程24 - Gerber文件的导入

一、引言二、基本概念1.仿真文件下载2.仿真文件介绍二、导入步骤1.新建workspace2.选择Layout结构3.导入设计4.选择文件类型5.导入文件6.预览文件内容7.铜皮离散问题四、总结选择。通过本文章结束,可以了解ADS中如何将导入Gerber文件,并且对导入的铜皮进行合并。下一步就针对导入的文件进行层叠设置和编辑。END!}}END!

XZ_ZC的博客 3054

QGIS安装与使用教程

QGIS安装与使用教程

weixin_46713695的博客 2万+

软连接失败,文件已经存在failed to create symbolic link '/usr/bin/bazel': File exists

-s 改成 -sf ln -sf /usr/bin/bazel-1.0.0 /usr/bin/bazel

一抹烟霞的博客 1万+

使用 Simulink 和 Simscape Electrical 工具箱设计一个 Buck 转换器系统

Buck 转换器(降压转换器)是一种常用的 DC-DC 转换器,用于将较高的输入电压转换为较低的输出电压。为了评估其抗干扰能力,我们可以在模型中引入不同的干扰源,并观察系统的响应情况。电网电压波动:输入电压的变化。负载突变:负载电阻或电感的变化。电磁干扰:高频噪声或脉冲干扰。通过模拟这些干扰,我们可以分析系统的动态响应特性,并采取相应的措施来提高系统的抗干扰能力。

xiaoheshang_123的博客 378

geth多节点私链,更新后

在新版的geth中 需要指定miner.etherbase的账户 后才可以开始挖矿 之前是unlock解锁账户后就自动绑定为挖矿地址 可以先将 --miner.etherbase后面的删掉,留一个console 先进入控制台 再输入eth.accounts查看一下我们之前创建的账户地址是多少再添加到start.bat之中。这是因为节点一 默认使用了 \\.\pipe\geth.ipc 节点二不能重复使用 所以要在节点二后面指定一下rpc --ipcpath \\.\pipe\geth2.ipc。

weixin_51776121的博客 1446

国产系统(统信 UOS )漏洞修复方式

2.1 服务器版(单漏洞更新):服务器标准版:yum upgrade-minimal --cve “CVE编号”服务器 D 版:sudo apt update&&sudo apt install “受影响的软件包”1 统信 UOS 漏洞修复方式在线升级修复桌面专业版:通过以下步骤打开在线升级:控制中心–>更新–>更新设置–>打开系统更新、安全更新。安装过程中,请勾选“我已阅读并知晓”,然后点击“开始升级”。2 点击“检查更新”–“全部下载”,如果不想进行系统更新,可以选择只下载“安全更新”。

Reyn_观极 5016

RK3588 部署 ArcFace(r100)人脸识别模型:ONNX 转 RKNN 全流程详解

本文介绍了将ArcFace人脸识别模型转换为RKNN格式并部署到RK3588 NPU的方法。ArcFace是主流的人脸特征提取模型,适用于1:1比对和1:N检索等场景。文章详细说明了模型特点(输入112x112 RGB图像,输出512维特征向量)及转换步骤:首先使用torch2onnx将PyTorch模型转为ONNX格式,再通过RKNN工具进行量化转换。提供了完整的Python转换代码,包括预处理配置、模型加载、构建和导出过程。该方法能有效提升边缘设备上的人脸识别实时性,适用于门禁、考勤等应用场景。

J。 417

GETH启动配置

【代码】GETH启动配置。

靡不有初,鲜克有终。 512

5套高颜值AI漫剧女主设定图!直接拿去做漫剧(附完整提示词)

左侧特写放大表现暖棕眼妆、豆沙唇、红宝石首饰、发丝与针织纹理,四个视图严格保持同一人物脸模、发色、服装、首饰和身体比例,柔和复古棚拍光,低饱和酒红与奶油色配色,真实布料、皮革、金属与宝石材质,高级精致角色资产设定图,高清8K。佩戴珍珠与透明水晶耳坠、纤细珍珠项链;左侧特写放大至胸部以上,清晰表现皮肤、眼妆、发丝、耳饰和针织材质,人物四个视图保持同一脸模、发型、服装、饰品和身体比例,柔和棚拍漫射光,轻微地面阴影,高级细腻材质,真实发丝,细腻皮肤,高精度角色资产设定图,高清8K质感。

欢迎来到鸾姝淡月博客,专为程序员和编程爱好者打造的技术博客。我们分享编程语言趋势、开发工具、编程哲学、职业发展等,助你掌握新技术,优化技能,激发编程热情。 1705

geth启动流程分析

geth是我们的go-ethereum最主要的一个命令行工具。 也是我们的各种网络的接入点(主网络main-net 测试网络test-net 和私有网络)。支持运行在全节点模式或者轻量级节点模式。 其他程序可以通过它暴露的JSON RPC调用来访问以太坊网络的功能。 如果什么命令都不输入直接运行geth。 就会默认启动一个全节点模式的节点。 连接到主网络。 我们看看启动的主要流程是什么,涉及到了那...

Show something 945

Outlook Express 6 (OE6) for Win7/Win8/Win10

英文版 Outlook Express6.0, 兼容Win7/Win10/WinXP,解压就可用。

jdk-8u144-linux-x64.tar.gz.

该资源为搭建Linux系统Java环境用,资源的后缀为tar.gz格式的压缩包

以太坊源码阅读6——geth启动流程

以太坊源码阅读6——geth启动流程 介绍 geth是我们的go-ethereum最主要的一个命令行工具。 也是我们的各种网络的接入点(主网络main-net 测试网络test-net 和私有网络)。支持运行在全节点模式或者轻量级节点模式。 其他程序可以通过它暴露的JSON RPC调用来访问以太坊网络的功能。如果什么命令都不输入直接运行geth。 就会默认启动一个全节点模式的节点。 连接到主网络。 init 在正式启动之前会先进行调用init函数初始化 func init() { // Initial

qq_50665031的博客 6229

Probability and Statistics - 4th edition

Probability and Statistics - 4th edition

以太坊源码学习(二)geth启动

geth命令 cmd/main/main.go func main() { if err := app.Run(os.Args); err != nil { // app是三方包,用于接收客户端命令 fmt.Fprintln(os.Stderr, err) os.Exit(1) } } func init() { // init方法先于main方法执行...

xuanfuhuo4769的博客 639

geth源码阅读——启动流程

当我们要启动geth网络,我们需要运行命令: geth --datadir=~/gethData --networkid 1 console。这期间,操作分为2步:1. 启动geth运行程序,初始化运行环境2. 运行命令一、初始化运行环境  //cmd/geth/main.gofunc init() { // 初始化CLI(命令行界面),并启动geth app.Action = get...

qq_36391986的博客 3249

如何快速上手MEV-Geth?零基础搭建以太坊MEV拍卖节点的完整教程

MEV-Geth是以太坊MEV拍卖的Go语言实现,它允许用户参与以太坊网络中的最大可提取价值(MEV)拍卖。本教程将帮助零基础用户快速搭建MEV-Geth节点,开启以太坊MEV探索之旅。 ## 一、MEV-Geth简介 MEV(Maximal Extractable Value)即最大可提取价值,是指通过优化交易顺序和打包方式从区块链中提取的额外价值。MEV-Geth作为以太坊MEV拍卖的实现

gitblog_00673的博客 858

BT下载慢?批量导入 20+ 个 Tracker 的完整实战教程

BT 下载慢的元凶,往往不是带宽不够,而是 Tracker 列表太薄。trackerslist 是一个每天自动更新公共 BitTorrent Tracker 列表的开源项目,收录 104 个 Tracker,脚本每日巡检并自动剔除失效地址。想给 BT 下载加速,给客户端补一份精选的 Tracker 列表,通常比调整上下限更有效。 ## 为什么 Tracker 的数量与质量决定 BT 下载速度

gitblog_00185的博客 545

节点守望:Geth监控全解析 ==> 区块链geth后端监控

区块链Geth后端监控可以帮助及时发现节点是否按预期运行,比如是否与网络同步,是否参与新区块的挖掘等。通过监控资源使用情况,比如CPU和内存占用,可以对系统进行调优,确保节点运行在最佳状态。当系统出现故障时,监控系统可以提供必要的信息,帮助快速定位问题源头,加快故障恢复。

m0_73727719的博客 1777

在 python-sdk 中为 MCP 客户端接入 OAuth 2.0:OAuthClientProvider 与客户端凭据认证实战

本指南围绕官方 Python SDK(python-sdk)的 OAuth 客户端侧能力展开,讲解如何用 `OAuthClientProvider` 为受保护的 MCP Server 自动完成授权码(Authorization Code + PKCE)流程,以及用 `ClientCredentialsOAuthProvider` / `PrivateKeyJWTOAuthProvider` 实现机

gitblog_00080的博客 351

以太坊源码解读(3)以太坊启动流程简析

启动命令: geth --identity "TestNode1" --datadir "data0" --rpc --rpcapi "db,eth,net,web3" --port "30303" --networkid "29382" --ws --wsorigins="*" --rpccorsdomain="*" console 启动后,我们可以从日志来分

lj900911的专栏 3421

第10篇 geth命令参数详解

本文环境: 操作系统:windows 64; 节点版本:Geth1.9.14;

wonderBlock的博客 3732

以太坊节点同步机制:Geth如何高效同步区块链数据

以太坊节点同步是区块链网络正常运行的核心环节,Geth作为最流行的以太坊客户端之一,其同步机制直接影响节点的启动速度和数据一致性。本文将深入解析Geth节点如何高效同步区块链数据,帮助开发者和普通用户理解这一复杂过程背后的工作原理。 ## 为什么区块链同步如此重要? 区块链本质上是一个分布式账本,每个节点都需要维护完整的交易历史。当新节点加入网络或现有节点重启时,必须通过同步机制获取最新的区块

gitblog_00954的博客 947

39.以太坊源码分析(39)geth启动流程分析

geth是我们的go-ethereum最主要的一个命令行工具。 也是我们的各种网络的接入点(主网络main-net 测试网络test-net 和私有网络)。支持运行在全节点模式或者轻量级节点模式。 其他程序可以通过它暴露的JSON RPC调用来访问以太坊网络的功能。 如果什么命令都不输入直接运行geth。 就会默认启动一个全节点模式的节点。 连接到主网络。 我们看看启动的主要流程是什么,涉及到了那...

boss2967的博客 372

Centos7安装geth,搭建部署以太坊私有链

该目录下保存了各个账户的私钥的加密后的文件, 与我们学习过的。提示,因为 geth1.11以上版本,当执行personal等指令时会出现异常,这是因为新版本的geth已经取消的personal等模块。在我们现在的环境下,需要启动一个 Geth 节点来接入私链网络(实际上也是这个私链网络的唯一一个节点),负责在创世块后挖出第一个块。创世配置就是供以太坊私有链第一次启动时所使用,创世区块是最特殊的区块,它没有前一个区块,因为它自己是第一个块。上面的内容是保存在genesis.json文件中的。

qq_46573670的博客 1977
上一篇: 以太坊源码目录结构分析
下一篇: cmd中Geth重要衔接包代码简析
The_Web3_社区
博客等级 码龄10年 388粉丝 154原创
评论 1
成就一亿技术人!
拼手气红包6.0元
还能输入1000个字符
 
 条评论被折叠 查看
添加红包

请填写红包祝福语或标题

个

红包个数最小为10个

元

红包金额最低5元

当前余额3.43元 前往充值 >
需支付:10.00元
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包

打赏作者

The_Web3_社区

你的鼓励将是我创作的最大动力

¥1 ¥2 ¥4 ¥6 ¥10 ¥20
扫码支付:¥1
获取中
扫码支付

您的余额不足,请更换扫码支付或充值

打赏作者

实付元
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值