Geth是如何启动的?
Author: Siyuan Han Github: https://github.com/hsyodyssey
Geth是基于Go语言开发以太坊的客户端,它实现了Ethereum协议(黄皮书)中所有需要的实现的功能模块,包括状态管理,挖矿,P2P网络通信,密码学,数据库,EVM解释器等。我们可以通过启动Geth来运行一个Ethereum的节点。Go-ethereum是包含了Geth在内的一个代码库,它包含了Geth,以及编译Geth所需要的其他代码。在本系列中,我们会深入Go-ethereum代码库,从High-level的API接口出发,沿着Ethereum主Workflow,从而理解Ethereum具体实现的细节。
为了更好的从整体工作流的角度来理解Ethereum,根据主要的业务功能,我们将go-ethereum划分成如下几个模块来分析。
目前,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具体功能的函数实现
当我们想要部署一个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/ -->
// 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 through
jsre *jsre.JSRE // JavaScript runtime environment running the interpreter
prompt string // Input prompt prefix string
prompter prompt.UserPrompter // Input prompter to allow interactive user feedback
histPath string // Absolute path to the console scrollback history
history []string // Scroll history maintained by the console
printer io.Writer // Output writer to serialize any display strings to
}
了解Ethereum,我们首先要了解Ethereum客户端Geth是怎么运行的。
<!-- geth console 2
-->
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)
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()
函数中被打印出来的。
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类型在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 lifecycle
lock 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
}
我们可以在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
// Handlers
txPool *core.TxPool
blockchain *core.BlockChain
handler *handler
ethDialCandidates enode.Iterator
snapDialCandidates enode.Iterator
merger *consensus.Merger
// DB interfaces
chainDb ethdb.Database // Block chain database
eventMux *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 uint64
netRPCService *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
go s.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 we're 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 reference
database ethdb.Database
txpool txPool
chain *core.BlockChain
maxPeers int
downloader *downloader.Downloader
blockFetcher *fetcher.BlockFetcher
txFetcher *fetcher.TxFetcher
peers *peerSet
merger *consensus.Merger
eventMux *event.TypeMux
txsCh chan core.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的实现。
这里补充一个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.Clique
if 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
}
}
如果觉得我的文章对您有用,请随意打赏。你的支持将鼓励我继续创作!