Go Ethereum源码学习笔记 001 Geth Start

Go Ethereum源码学习笔记

  • 前言
  • [Chapter_001] 万物的起点: Geth Start
    • 什么是 geth?
      • go-ethereum Codebase 结构
    • Geth Start
      • 前奏: Geth Console
      • geth 节点是如何启动的
      • Node
        • Node的关闭
      • Ethereum Backend
      • 附录

前言

首先读者需要具备Go语言基础,至少要通关菜鸟教程,知道Go语言的基本语法,这些基础教程网络上非常多,请大家自行学习。
具备语言基础了,还需要在开始这一章之前做一些准备工作:

  1. 安装Go SDK,即Go语言的开发环境;
  2. 安装GoLand,即Go语言的IDE,当然也可以选择VSCode等其他IDE;
  3. 克隆 Go Ethereum源码;
  4. 克隆Understanding-Ethereum-Go-version源码(可选);
  5. 以太坊基础知识,Ethereum 协议(黄皮书 )
    做好这些准备工作,就可以打开 Go Ethereum源码了,如下图所示:
    以太坊Go源码
    以太坊是以区块链作为基础的应用,所以我们必须具备区块链相关的基础知识,否则很难读懂源码究竟在做什么。

侵删声明:如果本文有侵犯到Understanding-Ethereum-Go-version原作者的地方,请告知删除相关内容,笔者已经向Understanding-Ethereum-Go-version作者发送了添加微信好友的请求,希望可以加到对方好友!
本文的宗旨还是学习和分享,并无商业目的,希望可以将自己的心得记录下来,对于引用的出处都会提前声明。
下面开始对Understanding-Ethereum-Go-version的增删改查工作,当然,主要是跟着作者思路去学习!

[Chapter_001] 万物的起点: Geth Start

本章概要:

  1. go-ethereum 代码库的主要目录结构。
  2. geth 客户端/节点是如何启动的。
  3. 如何修改/添加 geth 对外的APIs。

什么是 geth?

geth 是以太坊基金会基于 Go 语言开发以太坊的官方客户端,它实现了 Ethereum 协议(黄皮书 )中所有需要的实现的功能模块。我们可以通过启动 geth 来运行一个 Ethereum 的节点。在以太坊 Merge 之后,geth 作为节点的执行层继续在以太坊生态中发挥重要的作用。 go-ethereum是包含了 geth 客户端代码和以及编译 geth 所需要的其他代码在内的一个完整的代码库。在本系列中我们会通过深入 go-ethereum 代码库,从High-level 的 API 接口出发,沿着 Ethereum 主 Workflow,逐一的理解 Ethereum 具体实现的细节。

为了方便区分,在接下来的文章中,我们用 geth 来表示 Geth 客户端程序,用 go-ethereum (Geth)来表示 go-ethereum 的代码库。

总结的来说:

  1. 基于 go-ethereum 代码库中的代码,我们可以编译出 geth 客户端程序。
  2. 通过运行 geth 客户端程序我们可以启动一个 Ethereum 的节点。

go-ethereum Codebase 结构

为了更好的从整体工作流的角度来理解 Ethereum,根据主要的业务功能,我们可以把 go-ethereum 划分成如下几个模块。

  • Geth Client 模块(客户端)
  • Core 数据结构模块
  • State Management 模块(状态管理)
    • StateDB 模块(状态数据库)
    • Trie 数据结构模块
    • State Optimization (Pruning)减枝算法优化
  • Mining 模块(挖矿)
  • EVM 模块(以太坊虚拟机Ethereum Virtual Machine)
  • P2P 网络模块
    • 节点数据同步
      • 交易数据
      • 区块数据
      • 区块链数据
  • Storage 模块
    • 抽象数据库层
    • LevelDB 调用

目前,go-ethereum 代码库中的主要目录结构如下所示:

cmd/ 以太坊基金会官方开发的一些 Command-line 程序。该目录下的每个子目录都是一个单独运行的 CLI 程序。|── clef/ 以太坊官方推出的账户管理程序.|── geth/ 以太坊官方的节点客户端。
core/   以太坊核心模块,包括核心数据结构,statedb,EVM 等核心数据结构以及算法实现|── rawdb/ db 相关函数的高层封装(在 ethdb 和更底层的 leveldb 之上的封装)├──accessors_state.go 从 Disk Level 读取/写入与 State 相关的数据结构。|── state/├── statedb.go  StateDB 是管理以太坊 World State 最核心的代码,用于管理链上所有的 State 相关操作。├── state_object.go state_object 是以太坊账户(包括 EOA & Contract)在 StateDB 具体的实现。|── txpool        Transaction Pool 相关的代码。|── txpool.go  Transaction Pool 的具体实现。|── types/  以太坊中最核心的数据结构|── block.go   以太坊 Block 的的数据结构定义与相关函数实现|── bloom9.go  以太坊使用的一个 Bloom Filter 的实现|── transaction.go 以太坊 Transaction 的数据结构定义与相关函数实现。|── transaction_signing.go 用于对 Transaction 进行签名的函数的实现。|── receipt.go  以太坊交易收据的实现,用于记录以太坊 Transaction 执行的结果|── vm/            以太坊的核心中核心 EVM 相关的一些的数据结构的定义。|── evm.go            EVM 数据结构和方法的定义|── instructions.go   EVM 指令的具体的定义,核心中的核心中的核心文件。|── logger.go   用于追踪 EVM 执行交易过程的日志接口的定义。具体的实现在eth/tracers/logger/logger.go 文件中。|── opcode.go   EVM 指令和数值的对应关系。|── genesis.go     创世区块相关的函数。每个 geth 客户端/以太坊节点初始化的都需要调用这个模块。|── state_processor.go EVM 执行交易的核心代码模块。 
console/|── bridge.go|── console.go  Geth Web3 控制台的入口
eth/      Ethereum 节点/后端/客户端具体功能定义和实现。例如节点的启动关闭,P2P 网络中交易和区块的同步。
ethdb/    Ethereum 本地存储的相关实现, 包括 leveldb 的调用|── leveldb/   Go-Ethereum使用的与 Bitcoin Core version一样的Leveldb作为本机存储用的数据库
internal/ 一些内部使用的工具库的集合,比如在测试用例中模拟 cmd 的工具。在构建 Ethereum 生态相关的工具时值得注意这个文件夹。
miner/|── miner.go   矿工模块的实现。|── worker.go  Block generation 的实现,包括打包 transaction,计算合法的 Block
p2p/     Ethereum 的P2P模块|── params    Ethereum 的一些参数的配置,例如: bootnode 的 enode 地址|── bootnodes.go  bootnode 的 enode 地址 like: aws 的一些节点,azure 的一些节点,Ethereum Foundation 的节点和 Rinkeby 测试网的节点
rlp/     RLP的 Encode与 Decode的相关,RLP(Recursive Length Prefix)是以太坊中序列化数据的编码方式。
rpc/     Ethereum RPC客户端的实现,远程过程调用。
les/     Ethereum light client 轻节点的实现
trie/    Ethereum 中至关重要的数据结构 Merkle Patrica Trie(MPT) 的实现|── committer.go    Trie 向 Memory Database 提交数据的工具函数。|── database.go     Memory Database,是 Trie 数据和 Disk Database 提交的中间层。同时还实现了 Trie 剪枝的功能。**非常重要**|── node.go         MPT中的节点的定义以及相关的函数。|── secure_trie.go  基于 Trie 的封装的结构。与 trie 中的函数功能相同,不过secure_trie中的 key 是经过hashKey()函数hash过的,无法通过路径获得原始的 key值 |── stack_trie.go   Block 中使用的 Transaction/Receipt Trie 的实现|── trie.go         MPT 具体功能的函数实现。

Geth Start

前奏: Geth Console

当我们想要部署一个 Ethereum 节点的时候,最直接的方式就是下载官方提供的发行版的 geth 客户端程序。geth是一个基于 CLI (命令行)的应用,启动geth和调用 geth 的功能性 API 需要使用对应的指令来操作。geth 提供了一个相对友好的 console 来方便用户调用各种指令。当我第一次阅读 Ethereum 的文档的时候,我曾经有过这样的疑问,为什么geth是由 Go 语言编写的,但是在官方文档中的 Web3 的API却是基于 Javascript 的调用?

这是因为 geth 内置了一个 Javascript 的解释器: Goja (interpreter),来作为用户与 geth 交互的 CLI Console。我们可以在console/console.go 中找到它的定义。

< !-- /Goja is an implementation of ECMAScript 5.1 in Pure GO/ -->

//控制台是一个JavaScript解释的运行时环境。它是一个完全成熟的JavaScript控制台,通过外部或进程内RPC客户端连接到正在运行的节点。
type Console struct {client   *rpc.Client         // 通过RPC客户端执行以太坊请求jsre     *jsre.JSRE          // 运行解释器的JavaScript运行时环境prompt   string              // 输入提示前缀字符串prompter prompt.UserPrompter // 输入提示器,通过它来允许交互式用户反馈histPath string              // 控制台回滚历史记录的绝对路径history  []string            // 由控制台维护的滚动历史记录字符串数组printer  io.Writer           // 输出写入器,通过它来序列化任何显示字符串interactiveStopped chan struct{}stopInteractiveCh  chan struct{}signalReceived     chan struct{}stopped            chan struct{}wg                 sync.WaitGroupstopOnce           sync.Once
}

笔者对引用的源代码做了更新,并且对注解做了中文翻译,大家阅读时可以参照源代码。

geth 节点是如何启动的

了解 Ethereum,我们首先要了解 Ethereum 客户端 Geth 是怎么运行的。 geth 程序的启动点位于 cmd/geth/main.go/main() 函数处,如下所示。

func main() {
//笔者注:运行app,如果有错误就打印出来,然后退出if err := app.Run(os.Args); err != nil {fmt.Fprintln(os.Stderr, err)os.Exit(1)}
}

笔者这里是补充一下main.go中对app的定义

var app = flags.NewApp("the go-ethereum command line interface")func init() {// Initialize the CLI app and start Gethapp.Action = gethapp.HideVersion = true // we have a command to print the versionapp.Copyright = "Copyright 2013-2022 The go-ethereum Authors"app.Commands = []*cli.Command{// See chaincmd.go:initCommand,importCommand,exportCommand,importPreimagesCommand,exportPreimagesCommand,removedbCommand,dumpCommand,dumpGenesisCommand,// See accountcmd.go:accountCommand,walletCommand,// See consolecmd.go:consoleCommand,attachCommand,javascriptCommand,// See misccmd.go:makecacheCommand,makedagCommand,versionCommand,versionCheckCommand,licenseCommand,// See config.godumpConfigCommand,// see dbcmd.godbCommand,// See cmd/utils/flags_legacy.goutils.ShowDeprecated,// See snapshot.gosnapshotCommand,// See verkle.goverkleCommand,}sort.Sort(cli.CommandsByName(app.Commands))app.Flags = flags.Merge(nodeFlags,rpcFlags,consoleFlags,debug.Flags,metricsFlags,)app.Before = func(ctx *cli.Context) error {flags.MigrateGlobalFlags(ctx)return debug.Setup(ctx)}app.After = func(ctx *cli.Context) error {debug.Exit()prompt.Stdin.Close() // Resets terminal mode.return nil}
}

大家从app的定义中知道了app就是go-ethereum命令行接口,那么才会进一步有下面的逻辑,所以大家在阅读的时候一定要参照源代码,否则很难跟上作者的节奏。

我们可以看到 main() 函数非常的简短,其主要功能就是启动一个解析 command line命令的工具: gopkg.in/urfave/cli.v1。继续深入,我们会发现在 cli app 初始化的时候会调用 app.Action = geth ,来调用 geth() 函数。而 geth() 函数就是用于启动 Ethereum 节点的顶层函数,其代码如下所示:

// 如果没有运行特殊的子命令,Geth是进入系统的主要入口点。
// 它根据命令行参数创建一个默认节点,并以阻塞模式运行它,直到它关闭才解除阻塞。
func geth(ctx *cli.Context) error {if args := ctx.Args().Slice(); len(args) > 0 {return fmt.Errorf("invalid command: %q", args[0])}prepare(ctx)stack, backend := makeFullNode(ctx)defer stack.Close()startNode(ctx, stack, backend, false)stack.Wait()return nil
}

geth() 函数中,有三个比较重要的函数调用,分别是:prepare()makeFullNode(),以及 startNode()

prepare() 函数的实现就在当前的 main.go 文件中。它主要用于设置一些节点初始化需要的配置。比如,我们在节点启动时看到的这句话: Starting Geth on Ethereum mainnet… 就是在 prepare() 函数中被打印出来的。

// prepare函数操作内存缓存空间分配并设置矩阵系统。
// 这个函数应该在启动devp2p栈之前被调用。(devp2p,dev就是开发的意思,p2p就是点到点)
func prepare(ctx *cli.Context) {// 如果我们正在运行一个已知的预设,为了方便起见记录它。switch {case ctx.IsSet(utils.RopstenFlag.Name):log.Info("Starting Geth on Ropsten testnet...")case ctx.IsSet(utils.RinkebyFlag.Name):log.Info("Starting Geth on Rinkeby testnet...")case ctx.IsSet(utils.GoerliFlag.Name):log.Info("Starting Geth on Görli testnet...")case ctx.IsSet(utils.SepoliaFlag.Name):log.Info("Starting Geth on Sepolia testnet...")case ctx.IsSet(utils.KilnFlag.Name):log.Info("Starting Geth on Kiln testnet...")case ctx.IsSet(utils.DeveloperFlag.Name):log.Info("Starting Geth in ephemeral dev mode...")log.Warn(`You are running Geth in --dev mode. Please note the following:1. 此模式仅用于快速的迭代开发,没有安全性或持久性的考虑。2. 除非另有说明,否则数据库将在内存中创建。因此,关闭计算机或断电将擦除开发环境中的整个区块数据和链状态。3. 一个随机的、预先分配的开发者账户将可用并解锁为eth.Coinbase,可用于测试。随机的dev帐户是临时的,存储在一个ramdisk硬盘上,如果你的机器重新启动,这个帐户就会丢失。4. 默认开启挖掘。但是,只有在mempool(内存池)中有待处理的事务时,客户端才会密封块。该矿工接受的最低汽油价格是1。5. 禁用网络;没有listen-address(监听地址),最大对等体数设置为0,发现功能未开启。
`)case !ctx.IsSet(utils.NetworkIdFlag.Name):log.Info("Starting Geth on Ethereum mainnet...")}// 如果我们是主网上没有指定缓存的完整节点,则取消默认缓存配额if ctx.String(utils.SyncModeFlag.Name) != "light" && !ctx.IsSet(utils.CacheFlag.Name) && !ctx.IsSet(utils.NetworkIdFlag.Name) {// 确保我们也不在任何受支持的预配置testnet测试网络上if !ctx.IsSet(utils.RopstenFlag.Name) &&!ctx.IsSet(utils.SepoliaFlag.Name) &&!ctx.IsSet(utils.RinkebyFlag.Name) &&!ctx.IsSet(utils.GoerliFlag.Name) &&!ctx.IsSet(utils.KilnFlag.Name) &&!ctx.IsSet(utils.DeveloperFlag.Name) {// 不,我们真的在主网上。提升缓存!log.Info("Bumping default cache on mainnet", "provided", ctx.Int(utils.CacheFlag.Name), "updated", 4096)ctx.Set(utils.CacheFlag.Name, strconv.Itoa(4096))}}// 如果我们在任何网络上运行轻量客户端,请将缓存降低到某个有意义的值if ctx.String(utils.SyncModeFlag.Name) == "light" && !ctx.IsSet(utils.CacheFlag.Name) {log.Info("Dropping default light client cache", "provided", ctx.Int(utils.CacheFlag.Name), "updated", 128)ctx.Set(utils.CacheFlag.Name, strconv.Itoa(128))}// 如果有启用则启动矩阵导出utils.SetupMetrics(ctx)// 启动系统运行时矩阵收集go metrics.CollectProcessMetrics(3 * time.Second)
}

prepare函数源码在原文中没有给出,因为作者预设大家会看源代码,所以比较精简,笔者在这里给出源代码是为了方便大家流畅阅读,后续的源码中如果有“笔者附加源码”的字样,代表原文没有引用的源码,但是笔者为了方便大家阅读而附加上去的,类似这样的提示后文就不再说明了。

makeFullNode() 函数的实现位于 cmd/geth/config.go 文件中。它会将 Geth 启动时的命令的上下文加载到配置中,并生成 stackbackend 这两个实例。其中 stack 是一个 Node 类型的实例,它是通过 makeFullNode() 函数调用 makeConfigNode() 函数来初始化的。Node 是 geth 生命周期中最顶级的实例,它负责管理节点中的 P2P Server, Http Server, Database 等业务非直接相关的高级抽象。关于 Node 类型的定义位于node/node.go文件中。

// 笔者附加源码
// makeFullNode加载geth配置并创建以太坊后端。
func makeFullNode(ctx *cli.Context) (*node.Node, ethapi.Backend) {stack, cfg := makeConfigNode(ctx)//这里的stack就是返回的Node节点,cfg是config配置//读取配置if ctx.IsSet(utils.OverrideTerminalTotalDifficulty.Name) {cfg.Eth.OverrideTerminalTotalDifficulty = flags.GlobalBig(ctx, utils.OverrideTerminalTotalDifficulty.Name)}if ctx.IsSet(utils.OverrideTerminalTotalDifficultyPassed.Name) {override := ctx.Bool(utils.OverrideTerminalTotalDifficultyPassed.Name)cfg.Eth.OverrideTerminalTotalDifficultyPassed = &override}//根据节点和配置注册以太坊服务backend, eth := utils.RegisterEthService(stack, &cfg.Eth)// 警告用户迁移,如果他们有一个遗留的冷冻格式。if eth != nil && !ctx.IsSet(utils.IgnoreLegacyReceiptsFlag.Name) {firstIdx := uint64(0)// 侵入以加快对主网的检查,因为我们知道第一个非空块,创世区块46147,这个编号说明以太坊为自己至少提前挖掘了46147个区块。ghash := rawdb.ReadCanonicalHash(eth.ChainDb(), 0)if cfg.Eth.NetworkId == 1 && ghash == params.MainnetGenesisHash {firstIdx = 46147}isLegacy, firstLegacy, err := dbHasLegacyReceipts(eth.ChainDb(), firstIdx)if err != nil {log.Error("Failed to check db for legacy receipts", "err", err)} else if isLegacy {stack.Close()log.Error("Database has receipts with a legacy format", "firstLegacy", firstLegacy)utils.Fatalf("Aborting. Please run `geth db freezer-migrate`.")}}// 配置日志过滤器RPC API。filterSystem := utils.RegisterFilterAPI(stack, backend, &cfg.Eth)// 如果需要,配置GraphQL。if ctx.IsSet(utils.GraphQLEnabledFlag.Name) {utils.RegisterGraphQLService(stack, backend, filterSystem, &cfg.Node)}// 如果需要,添加以太坊统计守护进程。if cfg.Ethstats.URL != "" {utils.RegisterEthStatsService(stack, backend, cfg.Ethstats.URL)}return stack, backend
}

这里的 backend 是一个 ethapi.Backend 类型的接口,提供了获取以太坊执行层运行时,所需要的基本函数功能。它的定义位于 internal/ethapi/backend.go 中。 由于这个接口中函数较多,我们选取了其中的部分关键函数方便大家理解这个接口所提供的基本功能,如下所示。

// 笔者更新源码且附注中文注解
// 后端接口提供公共API服务(由完全客户端和轻量级客户端提供)以访问必要的功能。
type Backend interface {// 以太坊通用APISyncProgress() ethereum.SyncProgress//同步进度SuggestGasTipCap(ctx context.Context) (*big.Int, error)FeeHistory(ctx context.Context, blockCount int, lastBlock rpc.BlockNumber, rewardPercentiles []float64) (*big.Int, [][]*big.Int, []*big.Int, []float64, error)ChainDb() ethdb.DatabaseAccountManager() *accounts.ManagerExtRPCEnabled() boolRPCGasCap() uint64            // global gas cap for eth_call over rpc: DoS protectionRPCEVMTimeout() time.Duration // global timeout for eth_call over rpc: DoS protectionRPCTxFeeCap() float64         // global tx fee cap for all transaction related APIsUnprotectedAllowed() bool     // allows only for EIP155 transactions.// 区块链APISetHead(number uint64)HeaderByNumber(ctx context.Context, number rpc.BlockNumber) (*types.Header, error)HeaderByHash(ctx context.Context, hash common.Hash) (*types.Header, error)HeaderByNumberOrHash(ctx context.Context, blockNrOrHash rpc.BlockNumberOrHash) (*types.Header, error)CurrentHeader() *types.HeaderCurrentBlock() *types.BlockBlockByNumber(ctx context.Context, number rpc.BlockNumber) (*types.Block, error)BlockByHash(ctx context.Context, hash common.Hash) (*types.Block, error)BlockByNumberOrHash(ctx context.Context, blockNrOrHash rpc.BlockNumberOrHash) (*types.Block, error)StateAndHeaderByNumber(ctx context.Context, number rpc.BlockNumber) (*state.StateDB, *types.Header, error)StateAndHeaderByNumberOrHash(ctx context.Context, blockNrOrHash rpc.BlockNumberOrHash) (*state.StateDB, *types.Header, error)PendingBlockAndReceipts() (*types.Block, types.Receipts)GetReceipts(ctx context.Context, hash common.Hash) (types.Receipts, error)GetTd(ctx context.Context, hash common.Hash) *big.IntGetEVM(ctx context.Context, msg core.Message, state *state.StateDB, header *types.Header, vmConfig *vm.Config) (*vm.EVM, func() error, error)SubscribeChainEvent(ch chan<- core.ChainEvent) event.SubscriptionSubscribeChainHeadEvent(ch chan<- core.ChainHeadEvent) event.SubscriptionSubscribeChainSideEvent(ch chan<- core.ChainSideEvent) event.Subscription// 交易池APISendTx(ctx context.Context, signedTx *types.Transaction) errorGetTransaction(ctx context.Context, txHash common.Hash) (*types.Transaction, common.Hash, uint64, uint64, error)GetPoolTransactions() (types.Transactions, error)GetPoolTransaction(txHash common.Hash) *types.TransactionGetPoolNonce(ctx context.Context, addr common.Address) (uint64, error)Stats() (pending int, queued int)TxPoolContent() (map[common.Address]types.Transactions, map[common.Address]types.Transactions)TxPoolContentFrom(addr common.Address) (types.Transactions, types.Transactions)SubscribeNewTxsEvent(chan<- core.NewTxsEvent) event.SubscriptionChainConfig() *params.ChainConfigEngine() consensus.Engine// 这里是从filters.Backend复制的// eth/filters 需要从这个后端类型初始化,所以它所需的方法也必须包含在这里。GetLogs(ctx context.Context, blockHash common.Hash, number uint64) ([][]*types.Log, error)SubscribeRemovedLogsEvent(ch chan<- core.RemovedLogsEvent) event.SubscriptionSubscribeLogsEvent(ch chan<- []*types.Log) event.SubscriptionSubscribePendingLogsEvent(ch chan<- []*types.Log) event.SubscriptionBloomStatus() (uint64, uint64)ServiceFilter(ctx context.Context, session *bloombits.MatcherSession)
}

我们可以发现 ethapi.Backend 接口主要对外提供了:

  1. General Ethereum APIs, 这些 General APIs 对外提供了查询区块链节点管理对象的接口,例如 ChainDb() 返回当前节点的 DB 实例, AccountManager()返回账户管理对象;
  2. Blockchain 相关的 APIs, 例如链上数据的查询(Block & Transaction), CurrentHeader(), BlockByNumber(), GetTransaction();
  3. Transaction Pool (交易缓存池)相关的APIs, 例如发送交易到本节点的 Transaction Pool, 以及查询交易池中的 Transactions, GetPoolTransaction获取交易池。

目前 Geth 代码库中,有两个 ethapi.Backend 接口的实现,分别是:

  • 位于 eth\api_backend 中的 EthAPIBackend(全节点)
  • 位于 les\api_backendLesApiBackend(轻节点)

顾名思义,EthAPIBackend 提供了针对全节点的 Backend API 服务, 而 LesApiBackend 提供了轻节点的 Backend API 服务。总结的来说,如果读者想定制一些新的 RPC API(远程过程调用接口),可以在 ethapi.Backend 接口中定义函数,并给 EthAPIBackend 添加具体的实现。

读者可能会发现,ethapi.Backend 接口所提供的函数功能,主要读写本地的维护的数据结构(i.e. Transaction Pool, Blockchain)的为主。那么作为一个有网络连接的 Backend, 以太坊的 Backend 或者说 Node 是怎么管理以太坊执行层节点的网络连接,共识等功能模块的呢?

我们深入 makeFullNode() 函数可以发现,生成ethapi.Backend 接口的语句 backend, eth := utils.RegisterEthService(stack, &cfg.Eth), 还返回了另一个 Ethereum 类型的实例 eth。 这个 Ethereum 类型才是以太坊节点数结构中核心中的核心,它实现了以太坊全节点所需要的所有的 Service。它负责提供更为具体的以太坊的功能性 Service, 负责与以太坊业务直接相关的抽象,比如维护 Blockchain 的更新,共识算法,从 P2P 网络中同步区块,同步P2P节点远端的交易并放到交易池中,等业务功能。我们会在后续详细讲解 Ethereum 类型具体提供的服务。

// 笔者附加源码
// RegisterEthService将以太坊客户端添加到栈中。
// 第二个返回值是完整的node实例,如果节点作为轻量客户端运行,这个值可能是nil。
func RegisterEthService(stack *node.Node, cfg *ethconfig.Config) (ethapi.Backend, *eth.Ethereum) {//轻节点同步if cfg.SyncMode == downloader.LightSync {backend, err := les.New(stack, cfg)if err != nil {Fatalf("Failed to register the Ethereum service: %v", err)}stack.RegisterAPIs(tracers.APIs(backend.ApiBackend))if err := lescatalyst.Register(stack, backend); err != nil {Fatalf("Failed to register the Engine API service: %v", err)}return backend.ApiBackend, nil}backend, err := eth.New(stack, cfg)if err != nil {Fatalf("Failed to register the Ethereum service: %v", err)}if cfg.LightServ > 0 {_, err := les.NewLesServer(stack, backend, cfg)if err != nil {Fatalf("Failed to create the LES server: %v", err)}}if err := ethcatalyst.Register(stack, backend); err != nil {Fatalf("Failed to register the Engine API service: %v", err)}stack.RegisterAPIs(tracers.APIs(backend.APIBackend))// 在同步目标配置完成的情况下,注册辅助的全同步测试服务。if cfg.SyncTarget != nil && cfg.SyncMode == downloader.FullSync {ethcatalyst.RegisterFullSyncTester(stack, backend, cfg.SyncTarget)log.Info("Registered full-sync tester", "number", cfg.SyncTarget.NumberU64(), "hash", cfg.SyncTarget.Hash())}return backend.APIBackend, backend
}

Ethereum 实例根据上下文的配置信息在调用 utils.RegisterEthService() 函数生成。在utils.RegisterEthService()函数中,首先会根据当前的config来判断需要生成的Ethereum backend 的类型,是 light node backend 还是 full node backend。我们可以在 eth/backend/new() 函数和 les/client.go/new() 中找到这两种 Ethereum backend 的实例是如何初始化的。Ethereum backend 的实例定义了一些更底层的配置,比如chainid,链使用的共识算法的类型等。这两种后端服务的一个典型的区别是 light node backend 不能启动 Mining 服务。在 utils.RegisterEthService() 函数的最后,调用了 Nodes.RegisterAPIs() 函数,将刚刚生成的 backend 实例注册到 stack 实例中。

总结的说,api_backend 主要是用于对外提供查询,或者与后端功能性生命周期无关的函数,Ethereum 这类的节点层的后端,主要用于管理/控制节点后端的生命周期。

最后一个关键函数,startNode() 的作用是正式的启动一个以太坊执行层的节点。它通过调用 utils.StartNode() 函数来触发 Node.Start() 函数来启动Stack实例(Node)。在 Node.Start() 函数中,会遍历 Node.lifecycles 中注册的后端实例,并启动它们。此外,在 startNode() 函数中,还是调用了unlockAccounts() 函数,并将解锁的钱包注册到 stack 中,以及通过 stack.Attach() 函数创建了与 local Geth 交互的 RPClient 模块。

// 笔者附加源码
// startNode启动系统节点和所有注册的协议,之后它解锁任何请求的帐户,并启动RPC/IPC接口和矿工。
func startNode(ctx *cli.Context, stack *node.Node, backend ethapi.Backend, isConsole bool) {debug.Memsize.Add("node", stack)// 启动节点本身utils.StartNode(ctx, stack, isConsole)// 解锁任何特定要求的账户unlockAccounts(ctx, stack)// 注册钱包事件处理程序来打开和自动导出钱包events := make(chan accounts.WalletEvent, 16)stack.AccountManager().Subscribe(events)// 创建一个客户端与本地geth节点交互。rpcClient, err := stack.Attach()if err != nil {utils.Fatalf("Failed to attach to self: %v", err)}ethClient := ethclient.NewClient(rpcClient)go func() {// 打开任何已经连接的钱包for _, wallet := range stack.AccountManager().Wallets() {if err := wallet.Open(""); err != nil {log.Warn("Failed to open wallet", "url", wallet.URL(), "err", err)}}// 监听钱包事件直到终止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.DerivationPathif 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()}}}()// 生成一个独立的goroutine用于状态同步监控,如果用户需要,同步完成后关闭节点。if ctx.Bool(utils.ExitWhenSyncedFlag.Name) {go func() {sub := stack.EventMux().Subscribe(downloader.DoneEvent{})defer sub.Unsubscribe()for {event := <-sub.Chan()if event == nil {continue}done, ok := event.Data.(downloader.DoneEvent)if !ok {continue}if timestamp := time.Unix(int64(done.Latest.Time), 0); time.Since(timestamp) < 10*time.Minute {log.Info("Synchronisation completed", "latestnum", done.Latest.Number, "latesthash", done.Latest.Hash(),"age", common.PrettyAge(timestamp))stack.Close()}}}()}// 启动辅助服务(如果启用)if ctx.Bool(utils.MiningEnabledFlag.Name) || ctx.Bool(utils.DeveloperFlag.Name) {// 只有在完整的以太坊节点运行时,挖矿才有意义if ctx.String(utils.SyncModeFlag.Name) == "light" {utils.Fatalf("Light clients do not support mining")}ethBackend, ok := backend.(*eth.EthAPIBackend)if !ok {utils.Fatalf("Ethereum service not running")}// 通过CLI将汽油价格设置为限制,然后开始挖矿gasprice := flags.GlobalBig(ctx, utils.MinerGasPriceFlag.Name)ethBackend.TxPool().SetGasPrice(gasprice)// 开始挖矿threads := ctx.Int(utils.MinerThreadsFlag.Name)if err := ethBackend.StartMining(threads); err != nil {utils.Fatalf("Failed to start mining: %v", err)}}
}

geth() 函数的最后,函数通过执行 stack.Wait(),使得主线程进入了阻塞状态,其他的功能模块的服务被分散到其他的子协程中进行维护。

Node

node节点所在目录,以及相关函数的快速定位

正如我们前面提到的,Node 类型在 geth 的生命周期性中属于顶级实例,它负责作为与外部通信的高级抽象模块的管理员,比如管理 rpc server,http server,Web Socket,以及P2P Server外部接口。同时,Node中维护了节点运行所需要的后端的实例和服务 (lifecycles []Lifecycle),例如我们上面提到的负责具体 Service 的Ethereum 类型。

// Node节点是一个可以注册服务的容器。
type Node struct {eventmux      *event.TypeMuxconfig        *Configaccman        *accounts.Managerlog           log.LoggerkeyDir        string            // 密钥存储目录keyDirTemp    bool              // 如果为true,将通过Stop函数删除密钥目录,因为只是临时目录dirLock       fileutil.Releaser // 阻止并发使用实例目录stop          chan struct{}     // 等待终止通知的通道server        *p2p.Server       // 目前运行的P2P网络层startStopLock sync.Mutex        // Start/Stop函数由一个额外的互斥锁保护state         int               // 跟踪节点的生命周期状态lock          sync.Mutexlifecycles    []Lifecycle // 所有具有生命周期的注册后端、服务和辅助服务rpcAPIs       []rpc.API   // 节点当前提供的api列表http          *httpServer //ws            *httpServer //httpAuth      *httpServer //wsAuth        *httpServer //ipc           *ipcServer  // 存储ipc http服务器信息inprocHandler *rpc.Server // 进程内RPC请求处理程序来处理API请求databases map[*closeTrackingDB]struct{} // 所有开放数据库
}

Node的关闭

在前面我们提到,整个程序的主线程因为调用了 stack.Wait() 而进入了阻塞状态。我们可以看到 Node 结构中声明了一个叫做 stop 的 channel。由于这个 Channel 一直没有被赋值,所以整个 geth 的主进程才进入了阻塞状态,持续并发的执行其他的业务协程。

// Wait函数阻塞,直到节点关闭。
func (n *Node) Wait() {<-n.stop
}

n.stop 这个 Channel 被赋予值的时候,geth 主函数就会停止当前的阻塞状态,并开始执行相应的一系列的资源释放的操作。这个地方的写法还是非常有意思的,值得我们参考。

值得注意的是,在目前的 go-ethereum 的 codebase 中,并没有直接通过给 stop 这个 channel 赋值方式来结束主进程的阻塞状态,而是使用一种更简洁粗暴的方式: 调用 close() 函数直接关闭 Channel。我们可以在 node.doClose() 找到相关的实现。close() 是go语言的原生函数,用于关闭 Channel 时使用。

// doClose释放New()获取的资源,并且收集错误。
func (n *Node) doClose(errs []error) error {// 关闭数据库。这个操作需要锁,因为它需要与OpenDatabase*同步。n.lock.Lock()n.state = closedStateerrs = append(errs, n.closeDatabases()...)n.lock.Unlock()//关闭账户管理器if err := n.accman.Close(); err != nil {errs = append(errs, err)}//如果是临时目录,则全部删除if n.keyDirTemp {if err := os.RemoveAll(n.keyDir); err != nil {errs = append(errs, err)}}// 释放实例目录锁。n.closeDataDir()// 解锁n.Wait.这样就可以解除Wait造成的阻塞close(n.stop)// 报告可能发生的任何错误。switch len(errs) {case 0:return nilcase 1:return errs[0]default:return fmt.Errorf("%v", errs)}
}

Ethereum Backend

我们可以在 eth/backend.go 中找到 Ethereum 这个结构体的定义。这个结构体包含的成员变量以及接收的方法实现了一个 Ethereum full node 所需要的全部功能和数据结构。我们可以在下面的代码定义中看到,Ethereum结构体中包含 TxPoolBlockchainconsensus.Engineminer等最核心的几个数据结构作为成员变量,我们会在后面的章节中详细的讲述这些核心数据结构的主要功能,以及它们的实现的方法。

// Ethereum 实现了以太坊全节点服务.
type Ethereum struct {config *ethconfig.Config// 处理器txPool             *txpool.TxPoolblockchain         *core.BlockChainhandler            *handler // handler 是P2P 网络数据同步的核心实例,我们会在后续的网络同步模块仔细的讲解它的功能ethDialCandidates  enode.IteratorsnapDialCandidates enode.Iteratormerger             *consensus.Merger// 数据库接口chainDb ethdb.Database // 区块链数据库eventMux       *event.TypeMuxengine         consensus.EngineaccountManager *accounts.ManagerbloomRequests     chan chan *bloombits.Retrieval // 接收bloom data数据检索请求的通道bloomIndexer      *core.ChainIndexer             // Bloom索引器在块导入期间运行closeBloomHandler chan struct{}APIBackend *EthAPIBackendminer     *miner.MinergasPrice  *big.Intetherbase common.AddressnetworkID     uint64netRPCService *ethapi.NetAPIp2pServer *p2p.Serverlock sync.RWMutex // 读写互斥锁,保护可变字段(例如汽油价格和etherbase)shutdownTracker *shutdowncheck.ShutdownTracker // 跟踪节点是否以及何时非正常关闭
}

节点启动和停止 Mining 的就是通过调用 Ethereum.StartMining()Ethereum.StopMining() 实现的。设置 Mining 的收益账户是通过调用 Ethereum.SetEtherbase() 实现的。

// 笔者更新源码和注解
// StartMining使用给定的CPU线程数启动矿工。如果挖掘已经在运行,该方法会调整允许使用的线程数,并更新交易池所需的最低价格。
func (s *Ethereum) StartMining(threads int) error {// 更新共识引擎中的线程数type threaded interface {SetThreads(threads int)}if th, ok := s.engine.(threaded); ok {log.Info("Updated mining threads", "threads", threads)if threads == 0 {threads = -1 // 从内部禁用矿工}th.SetThreads(threads)}// 如果矿工没有运行,初始化它if !s.IsMining() {// 将初始价格点传播到交易池s.lock.RLock()price := s.gasPrices.lock.RUnlock()s.txPool.SetGasPrice(price)// 配置本地挖掘地址eb, err := s.Etherbase()if err != nil {log.Error("Cannot start mining without etherbase", "err", err)return fmt.Errorf("etherbase missing: %v", err)}var cli *clique.Cliqueif c, ok := s.engine.(*clique.Clique); ok {cli = c} else if cl, ok := s.engine.(*beacon.Beacon); ok {if c, ok := cl.InnerEngine().(*clique.Clique); ok {cli = c}}if cli != nil {wallet, err := s.accountManager.Find(accounts.Account{Address: eb})if wallet == nil || err != nil {log.Error("Etherbase account unavailable locally", "err", err)return fmt.Errorf("signer missing: %v", err)}cli.Authorize(eb, wallet.SignData)}// 如果开始挖掘,我们可以禁用为加快同步时间而引入的事务拒绝机制。atomic.StoreUint32(&s.handler.acceptTxs, 1)go s.miner.Start(eb)}return nil
}

这里我们额外关注一下 handler 这个成员变量。handler 的定义在 eth/handler.go 中。

我们从从宏观角度来看,一个节点的主工作流需要:
1.从网络中获取/同步 Transaction 和 Block 的数据
2. 将网络中获取到 Block 添加到 Blockchain 中。
handler 就负责提供其中同步区块和交易数据的功能,例如,downloader.Downloader 负责从网络中同步 Block ,fetcher.TxFetcher 负责从网络中同步交易。关于这些方法的具体实现,我们会在后续章节:数据同步中详细介绍。

type handler struct {networkID  uint64forkFilter forkid.Filter // Fork ID过滤器,在节点的生命周期中保持不变snapSync  uint32 // 标志是否启用snap sync(如果我们已经有数据块,则禁用)acceptTxs uint32 // 标志我们是否被认为是同步的(启用事务处理)checkpointNumber uint64      // 同步进度验证器要交叉引用的块号checkpointHash   common.Hash // 同步进度验证器用于交叉引用的块哈希值database ethdb.Databasetxpool   txPoolchain    *core.BlockChainmaxPeers intdownloader   *downloader.DownloaderblockFetcher *fetcher.BlockFetchertxFetcher    *fetcher.TxFetcherpeers        *peerSetmerger       *consensus.MergereventMux      *event.TypeMuxtxsCh         chan core.NewTxsEventtxsSub        event.SubscriptionminedBlockSub *event.TypeMuxSubscriptionrequiredBlocks map[uint64]common.Hash// 用于获取器,同步器,txsyncLoop的通道quitSync chan struct{}chainSync *chainSyncerwg        sync.WaitGrouppeerWG    sync.WaitGroup
}

到此,我们就介绍了 geth 及其所需要的基本模块是如何启动的和关闭的。我们在接下来将视角转入到各个模块中,从更细粒度的角度深入探索 Ethereum 的具体实现。

附录

这里补充一个Go语言的语法知识: 类型断言。在Ethereum.StartMining()函数中,出现了if c, ok := s.engine.(*clique.Clique); ok的写法。该写法是Golang中的语法糖,称为类型断言。具体的语法是value, ok := element.(T),它的含义是如果elementT类型的话,那么ok等于True, value等于element的值。在if c, ok := s.engine.(*clique.Clique); ok语句中,就是在判断s.engine的是否为*clique.Clique类型。

		var cli *clique.Cliqueif c, ok := s.engine.(*clique.Clique); ok {cli = c} else if cl, ok := s.engine.(*beacon.Beacon); ok {if c, ok := cl.InnerEngine().(*clique.Clique); ok {cli = c}}

本文来自互联网用户投稿,该文观点仅代表作者本人,不代表本站立场。本站仅提供信息存储空间服务,不拥有所有权,不承担相关法律责任。如若转载,请注明出处:http://www.mzph.cn/news/15202.shtml

如若内容造成侵权/违法违规/事实不符,请联系多彩编程网进行投诉反馈email:809451989@qq.com,一经查实,立即删除!

相关文章

【wsl-windows子系统】安装、启用、禁用以及同时支持docker-desktop和vmware方案

如果你要用docker桌面版&#xff0c;很可能会用到wsl&#xff0c;如果没配置好&#xff0c;很可能wsl镜像会占用C盘很多空间。 前提用管理员身份执行 wsl-windows子系统安装和启用 pushd "%~dp0" dir /b %SystemRoot%\servicing\Packages\*Hyper-V*.mum >hyper…

06. 管理Docker容器数据

目录 1、前言 2、Docker实现数据管理的方式 2.1、数据卷&#xff08;Data Volumes&#xff09; 2.2、数据卷容器&#xff08;Data Volume Containers&#xff09; 3、简单示例 3.1、数据卷示例 3.2、数据卷容器示例 1、前言 在生产环境中使用 Docker&#xff0c;一方面…

211. 添加与搜索单词 - 数据结构设计---------------字典树

211. 添加与搜索单词 - 数据结构设计 原题链接&#xff1a;完成情况&#xff1a;解题思路&#xff1a;参考代码&#xff1a; 原题链接&#xff1a; 211. 添加与搜索单词 - 数据结构设计 https://leetcode.cn/problems/design-add-and-search-words-data-structure/descriptio…

Exadata磁盘损坏导致磁盘组无法mount恢复(oracle一体机磁盘组异常恢复)---惜分飞

Oracle Exadata客户,在换盘过程中,cell节点又一块磁盘损坏,导致datac1磁盘组&#xff08;该磁盘组是normal方式冗余)无法mount Thu Jul 20 22:01:21 2023 SQL> alter diskgroup datac1 mount force NOTE: cache registered group DATAC1 number1 incarn0x0728ad12 NOTE: ca…

【iOS】Frame与Bounds的区别详解

iOS的坐标系 iOS特有的坐标是&#xff0c;是在iOS坐标系的左上角为坐标原点&#xff0c;往右为X正方向&#xff0c;向下为Y正方向。 bounds和frame都是属于CGRect类型的结构体&#xff0c;系统的定义如下&#xff0c;包含一个CGPoint&#xff08;起点&#xff09;和一个CGSiz…

windows使用多账户Git,多远程仓库版本管理

1 清除全局配置 git config --global --list // 看一下是否配置过user.name 和 user.email git config --global --unset user.name // 清除全局用户名 git config --global --unset user.email // 清除全局邮箱 2 本地仓库&#xff0c;每个远程对应的本地仓库目录下执行 $…

求三个球面交点的高效解法

文章目录 一、问题描述二、推导步骤代数法几何法 三、MATLAB代码 一、问题描述 如图&#xff0c;已知三个球面的球心坐标分别为 P 1 ( x 1 , y 1 , z 1 ) , P 2 ( x 2 , y 2 , z 2 ) , P 3 ( x 3 , y 3 , z 3 ) P_1(x_1,y_1,z_1),P_2(x_2,y_2,z_2),P_3(x_3,y_3,z_3) P1​(x1​,…

idea项目依赖全部找不到

目录 1&#xff0c;出错现象2&#xff0c;解决3&#xff0c;其他尝试 1&#xff0c;出错现象 很久没打开的Java项目&#xff0c;打开之后大部分依赖都找不到&#xff0c;出现了所有的含有import语句的文件都会报错和一些注解报红报错&#xff0c;但pom文件中改依赖是确实被引入…

深度学习实践——循环神经网络实践

系列实验 深度学习实践——卷积神经网络实践&#xff1a;裂缝识别 深度学习实践——循环神经网络实践 深度学习实践——模型部署优化实践 深度学习实践——模型推理优化练习 代码可见于&#xff1a; 深度学习实践——循环神经网络实践 0 概况1 架构实现1.1 RNN架构1.1.1 RNN架…

管理类联考——写作——论说文——实战篇——标题篇

角度3——4种材料类型、4个立意对象、5种写作态度 老吕的“1342”&#xff0c;一个标题&#xff0c;三句开头&#xff0c;四层结构&#xff0c;两句结尾。 经过审题立意后&#xff0c;我们要根据我们的立意&#xff0c;确定一个主题&#xff0c;这个主题必须通过文章的标题直接…

【手撕】list

系列文章目录 文章目录 系列文章目录前言list_node<T>&#xff08;节点&#xff09;_list_iterator<T, Ref, Ptr>&#xff08;迭代器&#xff09;成员变量构造函数运算符重载 ReverseIterator<Iterator, Ref, Ptr>&#xff08;反向迭代器&#xff09;List<…

python+django+mysql项目实践一(环境准备)

python项目实践 环境说明: Pycharm 开发环境 Django 前端 MySQL 数据库 Navicat 数据库管理 创建Pycharm项目 安装Django 在pycharm文件—设置进行安装 新建Django项目 注意项目创建目录 项目默认目录文件说明: __init__.py asgi.py 【异步接受网络…

机器学习--课后作业--hw1

机器学习(课后作业–hw1) 本篇文章全文参考这篇blog 网上找了很多教程&#xff0c;这个是相对来说清楚的&#xff0c;代码可能是一模一样&#xff0c;只是进行了一些微调&#xff0c;但是一定要理解这个模型具体的处理方法&#xff0c;这个模型我认为最巧妙的它对于数据的处理…

Linux新手小程序——进度条

前言 目录 前言 需要先了解 1.\r和\n 2.缓冲区 一.理解字符的含义&#xff1a; 学习c语言时&#xff0c;我们可以粗略把字符分为可显字符和控制字符. 在按回车换到下一行开始的操作时&#xff0c;实际上是进行了两个操作&#xff1a;1.让光标跳到下一行&#xff08;只…

Spring注解开发,bean的作用范围及生命周期、Spring注解开发依赖注入

&#x1f40c;个人主页&#xff1a; &#x1f40c; 叶落闲庭 &#x1f4a8;我的专栏&#xff1a;&#x1f4a8; c语言 数据结构 javaweb 石可破也&#xff0c;而不可夺坚&#xff1b;丹可磨也&#xff0c;而不可夺赤。 Spring注解开发 一、注解开发定义Bean二、纯注解开发Bean三…

常见的几种排序

&#x1f436;博主主页&#xff1a;ᰔᩚ. 一怀明月ꦿ ❤️‍&#x1f525;专栏系列&#xff1a;线性代数&#xff0c;C初学者入门训练&#xff0c;题解C&#xff0c;C的使用文章&#xff0c;「初学」C &#x1f525;座右铭&#xff1a;“不要等到什么都没有了&#xff0c;才下…

线程属性——线程分离应用

文章目录 相关函数初始化释放线程属性的资源获取线程分离的状态属性设置线程分离的状态属性获取线程的栈的大小线程分离应用 相关函数 可以通过man pthread_attr_然后按两次table键查询和属性相关的函数 初始化 释放线程属性的资源 获取线程分离的状态属性 设置线程分离的状…

RISCV - 4 ISA 扩展名命名约定

RISCV - 4 ISA 扩展名命名约定 1 Case Sensitivity2 Base Integer ISA3 Instruction-Set Extension Names4 Version Numbers5 Underscores6 Additional Standard Extension Names7 Supervisor-level Instruction-Set Extensions8 Hypervisor-level Instruction-Set Extensions9…

MD-MTSP:成长优化算法GO求解多仓库多旅行商问题MATLAB(可更改数据集,旅行商的数量和起点)

一、成长优化算法GO 成长优化算法&#xff08;Growth Optimizer&#xff0c;GO&#xff09;由Qingke Zhang等人于2023年提出&#xff0c;该算法的设计灵感来源于个人在成长过程中的学习和反思机制。学习是个人通过从外部世界获取知识而成长的过程&#xff0c;反思是检查个体自…

目标识别数据集互相转换——xml、txt、json数据格式互转

VOC数据格式与YOLO数据格式互转 1.VOC数据格式 VOC&#xff08;Visual Object Classes&#xff09;是一个常用的计算机视觉数据集&#xff0c;它主要用于对象检测、分类和分割任务。VOC的标注格式&#xff0c;也被许多其他的数据集采用&#xff0c;因此理解这个数据格式是很重…