Tokenpocket钱包官方网站:理解以太坊: Go-Ethereum 源码剖析(0): Geth Start)
本文作者:hsyodyssey[1]
Author: Siyuan Han
Github: https://github.com/hsyodyssey
什么是 Geth?
Geth[2] 是基于 Go 语言开发以太坊的客户端,它实现了 Ethereum 协议(黄皮书)中所有需要的实现的功能模块,包括状态管理,挖矿,P2P 网络通信,密码学,数据库,EVM 解释器等。我们可以通过启动 Geth 来运行一个 Ethereum 的节点。Go-ethereum 是包含了 Geth 在内的一个代码库,它包含了 Geth,以及编译 Geth 所需要的其他代码。在本系列中,我们会深入 Go-ethereum 代码库,从 High-level 的 API 接口出发,沿着 Ethereum 主 Workflow,从而理解 Ethereum 具体实现的细节。
Go-ethereum Codebase 结构
为了更好的从整体工作流的角度来理解 Ethereum,根据主要的业务功能,我们将 go-ethereum 划分成如下几个模块来分析。
Geth Client 模块Core 数据结构模块State Management 模块StateDB 模块Trie 模块State Optimization (Pruning)Mining 模块EVM 模块P2P 网络模块节点数据同步...目前,go-ethereum 项目的主要目录结构如下所示:
cmd/ ethereum相关的Command-line程序。该目录下的每个子目录都包含一个可运行的main.go。
|── clef/ Ethereum官方推出的Account管理程序.
|── geth/ Geth的本体。
core/ 以太坊核心模块,包括核心数据结构,statedb,EVM等算法实现
|── rawdb/ db相关函数的高层封装(在ethdb和更底层的leveldb之上的封装)
|── state/
├──statedb.go StateDB结构用于存储所有的与Merkle trie相关的存储, 包括一些循环state结构
|── types/ 包括Block在内的以太坊核心数据结构
|── block.go 以太坊block
|── bloom9.go 一个Bloom Filter的实现
|── transaction.go 以太坊transaction的数据结构与实现
|── transaction_signing.go 用于对transaction进行签名的函数的实现
|── receipt.go 以太坊收据的实现,用于说明以太坊交易的结果
|── vm/
|── genesis.go 创世区块相关的函数,在每个geth初始化的都需要调用这个模块
|── tx_pool.go Ethereum Transaction Pool的实现
consensus/
|── consensus.go 共识相关的参数设定,包括Block Reward的数量
console/
|── bridge.go
|── console.go Geth Web3 控制台的入口
ethdb/ Ethereum 本地存储的相关实现, 包括leveldb的调用
|── leveldb/ Go-Ethereum使用的与Bitcoin Core version一样的Leveldb作为本机存储用的数据库
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的相关
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结构。与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中找到它的定义。
// Console is a JavaScript interpreted runtime environment. It is a fully fledged// JavaScript console attached to a running node via an external or in-process RPC// client.type Console struct{
client *rpc.Client // RPC client to execute Ethereum requests throughjsre *jsre.JSRE // JavaScript runtime environment running the interpreterprompt string // Input prompt prefix stringprompter prompt.UserPrompter // Input prompter to allow interactive user feedbackhistPath string // Absolute path to the console scrollback historyhistory []string // Scroll history maintained by the consoleprinter io.Writer // Output writer to serialize any display strings to}
启动
了解 Ethereum,我们首先要了解 Ethereum 客户端 Geth 是怎么运行的。
Geth 的启动点位于cmd/geth/main.go/main()函数处,如下所示。
func main(){
if err := app.Run(os.Args); err != nil{
fmt.Fprintln(os.Stderr, err)
os.Exit(1)
}
}
我们可以看到main()函数非常的简短,其主要功能就是启动一个解析 command line 命令的工具: gopkg.in/urfave/cli.v1。我们会发现在 cli app 初始化的时候会调用app.Action = geth,来调用geth()函数。geth()函数就是用于启动 Ethereum 节点的顶层函数,其代码如下所示。
func geth(ctx *cli.Context) error{
if args := ctx.Args(); len(args) > 0{
return fmt.Errorf("invalid command: %q", args[0])
}
prepare(ctx)
stack, backend := makeFullNode(ctx)
deferstack.Close()
startNode(ctx, stack, backend, false)
stack.Wait()
return nil}
在geth()函数,我们可以看到有三个比较重要的函数调用,分别是:prepare(),makeFullNode(),以及startNode()。
prepare() 函数的实现就在当前的main.go文件中。它主要用于设置一些节点初始化需要的配置。比如,我们在节点启动时看到的这句话: Starting Geth on Ethereum mainnet... 就是在prepare()函数中被打印出来的。
makeFullNode()函数的实现位于cmd/geth/config.go文件中。它会将 Geth 启动时的命令的上下文加载到配置中,并生成stack和backend这两个实例。其中stack是一个 Node 类型的实例,它是通过makeFullNode()函数调用makeConfigNode()函数来生成。Node 是 Geth 生命周期中最顶级的实例,它的开启和关闭与 Geth 的启动和关闭直接对应。关于 Node 类型的定义位于node/node.go文件中。
backend实例是指的是具体 Ethereum Client 的功能性实例。它是一个 Ethereum 类型的实例,负责提供更为具体的以太坊的功能性 Service,比如管理 Blockchain,共识算法等具体模块。它根据上下文的配置信息在调用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实例中。
eth := &Ethereum{
config: config,
merger: merger,
chainDb: chainDb,
eventMux: stack.EventMux(),
accountManager: stack.AccountManager(),
engine: ethconfig.CreateConsensusEngine(stack, chainConfig, ðashConfig, config.Miner.Notify, config.Miner.Noverify, chainDb),
closeBloomHandler: make(chan struct{}),
networkID: config.NetworkId,
gasPrice: config.Miner.GasPrice,
etherbase: config.Miner.Etherbase,
bloomRequests: make(chan chan*bloombits.Retrieval),
bloomIndexer: core.NewBloomIndexer(chainDb, params.BloomBitsBlocks, params.BloomConfirms),
p2pServer: stack.Server(),
shutdownTracker: shutdowncheck.NewShutdownTracker(chainDb),
}
startNode()函数的作用是正式的启动一个 Ethereum Node。它通过调用utils.StartNode()函数来触发Node.Start()函数来启动Stack实例(Node)。在Node.Start()函数中,会遍历Node.lifecycles中注册的后端实例,并在启动它们。此外,在startNode()函数中,还是调用了unlockAccounts()函数,并将解锁的钱包注册到stack中,以及通过stack.Attach()函数创建了与 local Geth 交互的 RPClient 模块。
在geth()函数的最后,函数通过执行stack.Wait(),使得主线程进入了监听状态,其他的功能模块的服务被分散到其他的子协程中进行维护。
Node
正如我们前面提到的,Node 类型在 Geth 的生命周期性中属于顶级实例,它负责作为与外部通信的外部接口,比如管理 rpc server,http server,Web Socket,以及 P2P Server 外部接口。同时,Node 中维护了节点运行所需要的后端的实例和服务(lifecycles []Lifecycle)。
// Node is a container on which services can be registered.type Node struct{
eventmux *event.TypeMux
config *Config
accman *accounts.Manager
log log.Logger
keyDir string // key store directory keyDirTemp bool // If true, key directory will be removed by Stop dirLock fileutil.Releaser // prevents concurrent use of instance directory stop chan struct{} // Channel to wait for termination notifications server *p2p.Server // Currently running P2P networking layer startStopLock sync.Mutex // Start/Stop are protected by an additional lock state int // Tracks state of node lifecyclelock sync.Mutex
lifecycles []Lifecycle // All registered backends, services, and auxiliary services that have a lifecycle rpcAPIs []rpc.API // List of APIs currently provided by the node http *httpServer // ws *httpServer // httpAuth *httpServer // wsAuth *httpServer // ipc *ipcServer // Stores information about the ipc http server inprocHandler *rpc.Server // In-process RPC request handler to process the API requests databases map[*closeTrackingDB]struct{} // All open databases}
Ethereum API Backend
我们可以在eth/backend.go中找到Ethereum这个结构体的定义。这个结构体包含的成员变量以及接收的方法实现了一个 Ethereum full node 所需要的全部功能和数据结构。我们可以在下面的代码定义中看到,Ethereum 结构体中包含了TxPool,Blockchain,consensus.Engine,miner等最核心的几个数据结构作为成员变量,我们会在后面的章节中详细的讲述这些核心数据结构的主要功能,以及它们的实现的方法。
// Ethereum implements the Ethereum full node service.type Ethereum struct{
config *ethconfig.Config
// HandlerstxPool *core.TxPool
blockchain *core.BlockChain
handler *handler
ethDialCandidates enode.Iterator
snapDialCandidates enode.Iterator
merger *consensus.Merger
// DB interfaces chainDb ethdb.Database // Block chain databaseeventMux *event.TypeMux
engine consensus.Engine
accountManager *accounts.Manager
bloomRequests chan chan *bloombits.Retrieval // Channel receiving bloom data retrieval requests bloomIndexer *core.ChainIndexer // Bloom indexer operating during block imports closeBloomHandler chan struct{}
APIBackend *EthAPIBackend
miner *miner.Miner
gasPrice *big.Int
etherbase common.Address
networkID uint64netRPCService *ethapi.PublicNetAPI
p2pServer *p2p.Server
lock sync.RWMutex // Protects the variadic fields (e.g. gas price and etherbase) shutdownTracker *shutdowncheck.ShutdownTracker // Tracks if and when the node has shutdown ungracefully}
节点启动和停止 Mining 的就是通过调用Ethereum.StartMining()和Ethereum.StopMining()实现的。设置 Mining 的收益账户是通过调用Ethereum.SetEtherbase()实现的。
// StartMining starts the miner with the given number of CPU threads. If mining// is already running, this method adjust the number of threads allowed to use// and updates the minimum price required by the transaction pool.func (s *Ethereum) StartMining(threads int) error{
...
// If the miner was not running, initialize it if!s.IsMining() {
...
// Start Mining gos.miner.Start(eb)
}
return nil}
这里我们额外关注一下handler这个成员变量。handler的定义在eth/handler.go中。
我们从从宏观角度来看,一个节点的主工作流需要: 1.从网络中获取/同步 Transaction 和 Block 的数据 2. 将网络中获取到 Block 添加到 Blockchain 中。而handler就维护了 backend 中同步/请求数据的实例,比如downloader.Downloader,fetcher.TxFetcher。关于这些成员变量的具体实现,我们会在后续的文章中详细介绍。
type handler struct{
networkID uint64 forkFilter forkid.Filter // Fork ID filter, constant across the lifetime of the node snapSync uint32 // Flag whether snap sync is enabled (gets disabled if we already have blocks) acceptTxs uint32 // Flag whether were considered synchronised (enables transaction processing) checkpointNumber uint64 // Block number for the sync progress validator to cross reference checkpointHash common.Hash // Block hash for the sync progress validator to cross referencedatabase ethdb.Database
txpool txPool
chain *core.BlockChain
maxPeers intdownloader *downloader.Downloader
blockFetcher *fetcher.BlockFetcher
txFetcher *fetcher.TxFetcher
peers *peerSet
merger *consensus.Merger
eventMux *event.TypeMux
txsCh chancore.NewTxsEvent
txsSub event.Subscription
minedBlockSub *event.TypeMuxSubscription
peerRequiredBlocks map[uint64]common.Hash
// channels for fetcher, syncer, txsyncLoop quitSync chan struct{}
chainSync *chainSyncer
wg sync.WaitGroup
peerWG sync.WaitGroup
}
这样,我们就介绍了 Geth 及其所需要的基本模块是如何启动的。我们在接下来将视角转入到各个模块中,从更细粒度的角度深入 Ethereum 的实现。
Appendix
这里补充一个 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类型。
varcli *clique.Clique
ifc, ok := s.engine.(*clique.Clique); ok {
cli = c
} else ifcl, ok := s.engine.(*beacon.Beacon); ok {
ifc, ok := cl.InnerEngine().(*clique.Clique); ok {
cli = c
}
}
参考资料
[1]hsyodyssey: https://learnblockchain.cn/people/6574
[2]Geth: https://learnblockchain.cn/2017/12/01/geth_cmd_short