做网站服务器内存,留白的网站,idc数据中心排名,网络广告电话Go Ethereum源码学习笔记 前言[Chapter_001] 万物的起点: Geth Start什么是 geth#xff1f;go-ethereum Codebase 结构 Geth Start前奏: Geth Consolegeth 节点是如何启动的NodeNode的关闭 Ethereum Backend附录 前言
首先读者需要具备Go语言基础#xff0c;至少要通关菜鸟… Go Ethereum源码学习笔记 前言[Chapter_001] 万物的起点: Geth Start什么是 gethgo-ethereum Codebase 结构 Geth Start前奏: Geth Consolegeth 节点是如何启动的NodeNode的关闭 Ethereum Backend附录 前言
首先读者需要具备Go语言基础至少要通关菜鸟教程知道Go语言的基本语法这些基础教程网络上非常多请大家自行学习。 具备语言基础了还需要在开始这一章之前做一些准备工作
安装Go SDK即Go语言的开发环境安装GoLand即Go语言的IDE当然也可以选择VSCode等其他IDE克隆 Go Ethereum源码克隆Understanding-Ethereum-Go-version源码可选以太坊基础知识Ethereum 协议(黄皮书 ) 做好这些准备工作就可以打开 Go Ethereum源码了如下图所示 以太坊是以区块链作为基础的应用所以我们必须具备区块链相关的基础知识否则很难读懂源码究竟在做什么。
侵删声明如果本文有侵犯到Understanding-Ethereum-Go-version原作者的地方请告知删除相关内容笔者已经向Understanding-Ethereum-Go-version作者发送了添加微信好友的请求希望可以加到对方好友 本文的宗旨还是学习和分享并无商业目的希望可以将自己的心得记录下来对于引用的出处都会提前声明。 下面开始对Understanding-Ethereum-Go-version的增删改查工作当然主要是跟着作者思路去学习
[Chapter_001] 万物的起点: Geth Start
本章概要:
go-ethereum 代码库的主要目录结构。geth 客户端/节点是如何启动的。如何修改/添加 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 的代码库。
总结的来说:
基于 go-ethereum 代码库中的代码我们可以编译出 geth 客户端程序。通过运行 geth 客户端程序我们可以启动一个 Ethereum 的节点。
go-ethereum Codebase 结构
为了更好的从整体工作流的角度来理解 Ethereum根据主要的业务功能我们可以把 go-ethereum 划分成如下几个模块。
Geth Client 模块客户端Core 数据结构模块State Management 模块状态管理 StateDB 模块状态数据库Trie 数据结构模块State Optimization (Pruning)减枝算法优化 Mining 模块挖矿EVM 模块以太坊虚拟机Ethereum Virtual MachineP2P 网络模块 节点数据同步 交易数据区块数据区块链数据 Storage 模块 抽象数据库层LevelDB 调用 …
目前go-ethereum 代码库中的主要目录结构如下所示:
cmd/ 以太坊基金会官方开发的一些 Command-line 程序。该目录下的每个子目录都是一个单独运行的 CLI 程序。|── clef/ 以太坊官方推出的账户管理程序.|── geth/ 以太坊官方的节点客户端。
core/ 以太坊核心模块包括核心数据结构statedbEVM 等核心数据结构以及算法实现|── 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的相关RLPRecursive 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 Authorsapp.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栈之前被调用。devp2pdev就是开发的意思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 启动时的命令的上下文加载到配置中并生成 stack 和backend 这两个实例。其中 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 接口主要对外提供了:
General Ethereum APIs, 这些 General APIs 对外提供了查询区块链节点管理对象的接口例如 ChainDb() 返回当前节点的 DB 实例, AccountManager()返回账户管理对象;Blockchain 相关的 APIs, 例如链上数据的查询(Block Transaction), CurrentHeader(), BlockByNumber(), GetTransaction();Transaction Pool 交易缓存池相关的APIs, 例如发送交易到本节点的 Transaction Pool, 以及查询交易池中的 Transactions, GetPoolTransaction获取交易池。
目前 Geth 代码库中有两个 ethapi.Backend 接口的实现分别是:
位于 eth\api_backend 中的 EthAPIBackend全节点位于 les\api_backend 的 LesApiBackend轻节点
顾名思义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 类型在 geth 的生命周期性中属于顶级实例它负责作为与外部通信的高级抽象模块的管理员比如管理 rpc serverhttp serverWeb 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)它的含义是如果element是T类型的话那么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}}