剖析go-ethereum源码--->2. ethereum node如何工作

  • 0xdoomxy
  • 更新于 2024-12-10 15:37
  • 阅读 223

以上为作者从零开始对Ethereum源码进行剖析,在目前绝大多数的资料中,只是对ethereum的架构做了一个科普性的介绍,没有对ethereum的实现做过多阐述。作为开发者而言,了解web3的前提是必须熟悉一条区块链的底层实现,因为他代表了一种P2P的新范式,这与以往web2的开发模式大相径庭,

以上为作者从零开始对Ethereum源码进行剖析,在目前绝大多数的资料中,只是对ethereum的架构做了一个科普性的介绍,没有对ethereum的实现做过多阐述。作为开发者而言,了解web3的前提是必须熟悉一条区块链的底层实现,因为他代表了一种P2P的新范式,这与以往web2的开发模式大相径庭,这更多的是一种理念的创新。不管是Ethereum还是Bitcoin或者说是其他的Layer2以及Solana,其最终都会遵守一个最基本的开发理念:去中心化。作者想通过对Ethereum源码的分析来对当前的所有链的底层原理有一个通俗的理解,同时想借用这系列的文章与更多web3的工作者交流技术。源码版本下载

剖析go-ethereum源码--->1. geth console会执行什么 讲到geth console最终会执行localConsole这个函数,并且附带用户参数的上下文cCtx


// localConsole starts a new geth node, attaching a JavaScript console to it at the
// same time.
func localConsole(ctx *cli.Context) error {
    // 如果是主网的话会填充默认的缓存信息,以及geth 程序的系统metrcis的注册以及收集
    prepare(ctx)
    //初始化ethereum节点,主要是对本地的区块链信息的构建(包括两类交易池blob pool和transaction pool,miner,gas price预言机,同时初始化该节点最新的区块数据和本地数据库)以及账户管理的服务注册(目前支持公私钥对、USB接口以及ID card接口)、geth对外暴露相关的服务接口(包括http、websocket、rpc),geth还会开启IPC通信(目前没有找到会用在什么地方),这是了解full node启动的重要函数之一。
    stack := makeFullNode(ctx)
        //启动由makeFullNode注册的相关服务。
    startNode(ctx, stack, true)
    defer stack.Close()

    // Attach to the newly started node and create the JavaScript console.
    client := stack.Attach()
    config := console.Config{
        DataDir: utils.MakeDataDir(ctx),
        DocRoot: ctx.String(utils.JSpathFlag.Name),
        Client:  client,
        Preload: utils.MakeConsolePreloads(ctx),
    }
    console, err := console.New(config)
    if err != nil {
        return fmt.Errorf("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.String(utils.ExecFlag.Name); script != "" {
        console.Evaluate(script)
        return nil
    }

    // Track node shutdown and stop the console when it goes down.
    // This happens when SIGTERM is sent to the process.
    go func() {
        stack.Wait()
        console.StopInteractive()
    }()

    // Print the welcome screen and enter interactive mode.
    console.Welcome()
    console.Interactive()
    return nil
}

对于localConsole函数来讲,要了解ethereum启动的时候具体干了什么,最重要的是把握makeFullNode和startNode函数,通过这个函数能够初始化ethereum节点,主要是对本地的区块链信息的构建(包括两类交易池blob pool和transaction pool,miner,gas price预言机,同时初始化该节点最新的区块数据和本地数据库)以及账户管理的服务注册(目前支持公私钥对、USB接口以及ID card接口)、geth对外暴露相关的服务接口(包括http、websocket、rpc),geth还会开启IPC通信(目前没有找到会用在什么地方)。

makeFullNode以及startNode都是对上述结构体进行操作,对于ethereum来说,我们常见的操作一般是发起交易、参与共识出块,访问当前区块链的整体信息,从底层来讲,他还会涉及到P2P节点的通信相关问题。但总体而言,Full Node都会对外界提供一种方式让用户进行上述过程,我们需要找到他具体为外界提供了哪些操作,这方便我们从接口出发去剖析底层区块链的运行原理,这是必要的。

为了逐步了解Full Node对于用户提供的所有可访问的接口,作者找到了ethereum的JSON-RPC文档作为参考去分析。在startNode阶段,geth会开启上述的相关服务以便为用户提供上述服务,startNode会操控stack这个数据结构达到上述目的,为了后面的源码分析,我们需要了解Node这个结构体,初步判断Node会通过什么样的方式去暴露上述接口

// Node is a container on which services can be registered.
type Node struct {
    eventmux      *event.TypeMux
        //这里的配置包括默认的配置+用户自定义的配置信息
    config        *Config
        // 管理用户账号的 manager
    accman        *accounts.Manager
    log           log.Logger
    keyDir        string        // key store directory
    keyDirTemp    bool          // If true, key directory will be removed by Stop
        //文件锁
    dirLock       *flock.Flock  // prevents concurrent use of instance directory
        //判断服务是否停止的信号量,这是很常见的用法,grpc的实现中也会利用到这一点
    stop          chan struct{} // Channel to wait for termination notifications
        // P2P服务
    server        *p2p.Server   // Currently running P2P networking layer
    startStopLock sync.Mutex    // Start/Stop are protected by an additional lock
        // node现在处于的阶段,总共有三个阶段,初始化,运行中,结束
    state         int           // Tracks state of node lifecycle
    lock          sync.Mutex
        // 下面的lifecycles以及rpcAPIS以及多个httpServer与full node对外界提供服务相关
    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
        // ethereum database的包装器,用于加强对database的关闭的方法
    databases map[*closeTrackingDB]struct{} // All open databases
}

在通过makeFullNode将geth所有相关的服务注册就绪后,startNode会统一启动这些注册的服务以暴露在http、websocket、rpc下,我们需要重点关注在makeFullNode中究竟注册了哪些服务。在此之前,我们需要关注在startNode方法下会将makeFullNode的哪些东西进行start,这是非常关键的,这有助于我们去反向推导我们在makeFullNode中应该关注哪些字段或者函数。

<!--EndFragment-->

image.png

<!--EndFragment-->

image.png <!--EndFragment-->

image.png

<!--EndFragment-->

image.png <!--EndFragment-->

image.png

<!--EndFragment-->

image.png

<!--StartFragment-->

1733739346273.png

从上面的图来看,在startNode中会将lifecycle数组中的ethereum.Backend调用start进行启动,以及对rpcAPIs的相关服务进行注册(我们应该了解到的是在startNode阶段,geth通过反射机制将rpc.API的继承结构体的对应方法注册到路由中)。在后续对makeFullNode的分析中,我们应该重要关注这两个字段的改变,下面我们先来具体看一下makeFullNode如何初始化ethereum full node。

// makeFullNode 需要核心关注两个点 makeConfigNode和utils.RegisterEthService,
// 前者主要是注册P2P节点相关的服务并以stack的形式进行返回
// 后端会注册各种ethereum相关的接口以便通过http、rpc、websocket、以及进程内部调用和IPC进行访问,
// 这些接口包括共识的相关接口,区块链历史数据同步相关接口、区块链相关数据访问接口
// 以及区块链管理的相关接口(通过导入的方式构建区块链等等)
func makeFullNode(ctx *cli.Context) *node.Node {
        //初始化P2P节点,,并初始化区块链中blob pool、transaction pool、gas price oracle,同时构建accoumt manager 
        //这里会尝试获取datadir文件夹下面的文件锁,保证同一时间对datadir下面的文件夹访问是互斥的
    stack, cfg := makeConfigNode(ctx)
    if ctx.IsSet(utils.OverrideCancun.Name) {
        v := ctx.Uint64(utils.OverrideCancun.Name)
        cfg.Eth.OverrideCancun = &v
    }
    if ctx.IsSet(utils.OverrideVerkle.Name) {
        v := ctx.Uint64(utils.OverrideVerkle.Name)
        cfg.Eth.OverrideVerkle = &v
    }
        //初始化以太坊节点,这里的初始化是具有真正意义上的初始化。他会通过节点已有的数据构建区块链,同时注册区块链相关的服务,也就是说,他是区块链运行的核心启动方法
    backend, eth := utils.RegisterEthService(stack, &cfg.Eth)

    // Create gauge with geth system and build information
    if eth != nil { // The 'eth' backend may be nil in light mode
        var protos []string
        for _, p := range eth.Protocols() {
            protos = append(protos, fmt.Sprintf("%v/%d", p.Name, p.Version))
        }
        metrics.NewRegisteredGaugeInfo("geth/info", nil).Update(metrics.GaugeInfoValue{
            "arch":      runtime.GOARCH,
            "os":        runtime.GOOS,
            "version":   cfg.Node.Version,
            "protocols": strings.Join(protos, ","),
        })
    }

    // 主要是用于过滤客户端订阅的日志,ethereum的日志不仅仅有通过智能合约的emit去输出的这一种方式,

    filterSystem := utils.RegisterFilterAPI(stack, backend, &cfg.Eth)

    // 是否让geth 提供 graphql的服务
    if ctx.IsSet(utils.GraphQLEnabledFlag.Name) {
        utils.RegisterGraphQLService(stack, backend, filterSystem, &cfg.Node)
    }
    // 暴露当前geth的blockchain的相关指标,包括gas、blockchain、连接的对等节点数量等等
    if cfg.Ethstats.URL != "" {
        utils.RegisterEthStatsService(stack, backend, cfg.Ethstats.URL)
    }
    // Configure full-sync tester service if requested
    if ctx.IsSet(utils.SyncTargetFlag.Name) {
        hex := hexutil.MustDecode(ctx.String(utils.SyncTargetFlag.Name))
        if len(hex) != common.HashLength {
            utils.Fatalf("invalid sync target length: have %d, want %d", len(hex), common.HashLength)
        }
        utils.RegisterFullSyncTester(stack, eth, common.BytesToHash(hex))
    }
        //注册POS共识机制,可以是由第三方提供的共识服务
    if ctx.IsSet(utils.DeveloperFlag.Name) {
        // Start dev mode.
        simBeacon, err := catalyst.NewSimulatedBeacon(ctx.Uint64(utils.DeveloperPeriodFlag.Name), eth)
        if err != nil {
            utils.Fatalf("failed to register dev mode catalyst service: %v", err)
        }
        catalyst.RegisterSimulatedBeaconAPIs(stack, simBeacon)
        stack.RegisterLifecycle(simBeacon)
    } else if ctx.IsSet(utils.BeaconApiFlag.Name) {
        // Start blsync mode.
        srv := rpc.NewServer()
        srv.RegisterName("engine", catalyst.NewConsensusAPI(eth))
        blsyncer := blsync.NewClient(utils.MakeBeaconLightConfig(ctx))
        blsyncer.SetEngineRPC(rpc.DialInProc(srv))
        stack.RegisterLifecycle(blsyncer)
    } else {
        // Launch the engine API for interacting with external consensus client.
        err := catalyst.Register(stack, eth)
        if err != nil {
            utils.Fatalf("failed to register catalyst service: %v", err)
        }
    }
    return stack
}

这里主要注意两点makeConfigNode和utils.RegisterEthService都会对ethereum full node 的服务进行构建,makeConfigNode主要是在于对P2P节点的功能进行构建,utils.RegisterEthService主要是对于ethereum blockchain相关的服务进行构建,其中包括初始化本地的database以便知晓当前的本节点的blockchain的情况以进行后续的同步和共识,同时还会对外界访问ethereum blockchain的相关接口进行注册(这个会在剖析utils.RegisterEthService这个方法的时候具体说明),

接下来让我们进入到stack, cfg := makeConfigNode(ctx)

通过utils.RegisterEthStatsService注册的服务暴露的相关指标详细可以参考官方给出的dashboard

// makeConfigNode loads geth configuration and creates a blank node instance.
func makeConfigNode(ctx *cli.Context) (*node.Node, gethConfig) {
        //加载ethereum的基本配置,包括ethereum的配置,以及P2P节点的配置甚至上述说到的EthStat和
        //普通Metrics的配置(ethereum通过配置文件的方式进行统一配置,支持toml格式)
    cfg := loadBaseConfig(ctx)
        //初始化一个P2P节点,此节点仅仅是提供P2P服务,不会操作上层的ethereum服务,但是会注册node.apis()
        //到rpcAPIs
    stack, err := node.New(&cfg.Node)
    if err != nil {
        utils.Fatalf("Failed to create the protocol stack: %v", err)
    }
    // 创建account manager 的backend
    if err := setAccountManagerBackends(stack.Config(), stack.AccountManager(), stack.KeyStoreDir()); err != nil {
        utils.Fatalf("Failed to set account manager backends: %v", err)
    }
        //将ethereum相关的配置加入到cfg.Eth中
    utils.SetEthConfig(ctx, stack, &cfg.Eth)
    if ctx.IsSet(utils.EthStatsURLFlag.Name) {
        cfg.Ethstats.URL = ctx.String(utils.EthStatsURLFlag.Name)
    }
        //在cfg中生效用户传入的metrics相关的参数配置
    applyMetricConfig(ctx, &cfg)

    return stack, cfg
}

从makeConfigNode来看,通过node.New()创建了后续startNode的对象stack,前面我们说过,startNode主要是对于stack的lifestyle和rpcAPIS进行操作。node.New()方法中,通过node.rpcAPIs = append(node.rpcAPIs, node.apis()...)将node对应的apis写入rpcAPIS。下一步我们来看一下node.apis()包含哪个元素。

/ apis returns the collection of built-in RPC APIs.
func (n *Node) apis() []rpc.API {
    return []rpc.API{
        {
            Namespace: "admin",
                        //与P2P节点加入有关
            Service:   &adminAPI{n},
        }, {
            Namespace: "debug",
                        //与profile的有关
            Service:   debug.Handler,
        }, {
            Namespace: "debug",
                        //提供版本信息和sha3接口
            Service:   &p2pDebugAPI{n},
        }, {
            Namespace: "web3",
                        //与节点发现有关
            Service:   &web3API{n},
        },
    }
}

想要了解上述四个元素未来会在startNode注册哪些接口,可以查看对应Service结构体所包含的方法即可,下面不做过多解释(这里rpc.API都只和P2P节点的服务相关)。

下面让我们接着来看一下utils.RegisterEthService方法。

当前对源码的剖析旨在于大体掌握geth的整体工作模式和设计,后续会针对各个重点模块具体剖析


// RegisterEthService adds an Ethereum client to the stack.
// The second return value is the full node instance.
func RegisterEthService(stack *node.Node, cfg *ethconfig.Config) (*eth.EthAPIBackend, *eth.Ethereum) {
        //构建ethereum所需要的各种服务
    backend, err := eth.New(stack, cfg)
    if err != nil {
        Fatalf("Failed to register the Ethereum service: %v", err)
    }
        //这里注册对智能合约进行调试的相关接口
    stack.RegisterAPIs(tracers.APIs(backend.APIBackend))
    return backend.APIBackend, backend
}
func New(stack *node.Node, config *ethconfig.Config) (*Ethereum, error) {
    // Ensure configuration values are compatible and sane
    if !config.SyncMode.IsValid() {
        return nil, fmt.Errorf("invalid sync mode %d", config.SyncMode)
    }
    if config.Miner.GasPrice == nil || config.Miner.GasPrice.Sign() &lt;= 0 {
        log.Warn("Sanitizing invalid miner gas price", "provided", config.Miner.GasPrice, "updated", ethconfig.Defaults.Miner.GasPrice)
        config.Miner.GasPrice = new(big.Int).Set(ethconfig.Defaults.Miner.GasPrice)
    }
    if config.NoPruning && config.TrieDirtyCache > 0 {
        if config.SnapshotCache > 0 {
            config.TrieCleanCache += config.TrieDirtyCache * 3 / 5
            config.SnapshotCache += config.TrieDirtyCache * 2 / 5
        } else {
            config.TrieCleanCache += config.TrieDirtyCache
        }
        config.TrieDirtyCache = 0
    }
    log.Info("Allocated trie memory caches", "clean", common.StorageSize(config.TrieCleanCache)*1024*1024, "dirty", common.StorageSize(config.TrieDirtyCache)*1024*1024)
//初始化ethereum数据库,这里需要注意的是,为了避免索引太大,以太坊用冷热数据这种类似的模式对数据进行持久化
//eth在底层建立了两种数据库,对于第一种基于LSM的KV的数据库,只保存前9w个区块的索引,剩下的会以另一种方式存储(通过idx文件以6字节为一个单位保存对内容的索引,
//对每个内容文件以最大为2GB进行切割,具体的内容包括区块信息(receipt、header和body),以及状态信息(account、history和storage))
    chainDb, err := stack.OpenDatabaseWithFreezer("chaindata", config.DatabaseCache, config.DatabaseHandles, config.DatabaseFreezer, "eth/db/chaindata/", false)
    if err != nil {
        return nil, err
    }
    scheme, err := rawdb.ParseStateScheme(config.StateScheme, chainDb)
    if err != nil {
        return nil, err
    }
    // Try to recover offline state pruning only in hash-based.
    if scheme == rawdb.HashScheme {
        if err := pruner.RecoverPruning(stack.ResolvePath(""), chainDb); err != nil {
            log.Error("Failed to recover state", "error", err)
        }
    }
    // 尝试在本地存储中获取区块链的信息(包括区块链的网络号、存款合约地址,以及各种重要提案开始影响的区块号(比如说The Dao事件,上海分叉,伊斯坦布尔分叉以及EIP150提案))
    chainConfig, err := core.LoadChainConfig(chainDb, config.Genesis)
    if err != nil {
        return nil, err
    }
        //这里的共识机制是设置POW而存在的,后面在2022年eth尝试将共识机制过渡为pos之后,此方法用作适配,并不会真正设置共识机制
    engine, err := ethconfig.CreateConsensusEngine(chainConfig, chainDb)
    if err != nil {
        return nil, err
    }
    networkID := config.NetworkId
    if networkID == 0 {
        networkID = chainConfig.ChainID.Uint64()
    }
    eth := &Ethereum{
        config:            config,
        chainDb:           chainDb,
        eventMux:          stack.EventMux(),
        accountManager:    stack.AccountManager(),
        engine:            engine,
        closeBloomHandler: make(chan struct{}),
        networkID:         networkID,
        gasPrice:          config.Miner.GasPrice,
        bloomRequests:     make(chan chan *bloombits.Retrieval),
        bloomIndexer:      core.NewBloomIndexer(chainDb, params.BloomBitsBlocks, params.BloomConfirms),
        p2pServer:         stack.Server(),
        discmix:           enode.NewFairMix(0),
        shutdownTracker:   shutdowncheck.NewShutdownTracker(chainDb),
    }
    bcVersion := rawdb.ReadDatabaseVersion(chainDb)
    var dbVer = "&lt;nil>"
    if bcVersion != nil {
        dbVer = fmt.Sprintf("%d", *bcVersion)
    }
    log.Info("Initialising Ethereum protocol", "network", networkID, "dbversion", dbVer)

    if !config.SkipBcVersionCheck {
        if bcVersion != nil && *bcVersion > core.BlockChainVersion {
            return nil, fmt.Errorf("database version is v%d, Geth %s only supports v%d", *bcVersion, version.WithMeta, core.BlockChainVersion)
        } else if bcVersion == nil || *bcVersion &lt; core.BlockChainVersion {
            if bcVersion != nil { // only print warning on upgrade, not on init
                log.Warn("Upgrade blockchain database version", "from", dbVer, "to", core.BlockChainVersion)
            }
            rawdb.WriteDatabaseVersion(chainDb, core.BlockChainVersion)
        }
    }
    var (
        vmConfig = vm.Config{
            EnablePreimageRecording: config.EnablePreimageRecording,
        }
                //用于缓存Merkle Patricia Trie以及snapshot相关配置
        cacheConfig = &core.CacheConfig{
            TrieCleanLimit:      config.TrieCleanCache,
            TrieCleanNoPrefetch: config.NoPrefetch,
            TrieDirtyLimit:      config.TrieDirtyCache,
            TrieDirtyDisabled:   config.NoPruning,
            TrieTimeLimit:       config.TrieTimeout,
            SnapshotLimit:       config.SnapshotCache,
            Preimages:           config.Preimages,
            StateHistory:        config.StateHistory,
            StateScheme:         scheme,
        }
    )
    if config.VMTrace != "" {
        traceConfig := json.RawMessage("{}")
        if config.VMTraceJsonConfig != "" {
            traceConfig = json.RawMessage(config.VMTraceJsonConfig)
        }
        t, err := tracers.LiveDirectory.New(config.VMTrace, traceConfig)
        if err != nil {
            return nil, fmt.Errorf("failed to create tracer %s: %v", config.VMTrace, err)
        }
        vmConfig.Tracer = t
    }
    // Override the chain config with provided settings.
    var overrides core.ChainOverrides
    if config.OverrideCancun != nil {
        overrides.OverrideCancun = config.OverrideCancun
    }
    if config.OverrideVerkle != nil {
        overrides.OverrideVerkle = config.OverrideVerkle
    }
        //初始化以太坊核心的数据结构,这里面会在此之上对当前节点的区块链状态进行确认和验证。
        //当currentHeader的Number为0时,代表区块链还未同步完成。
        //在区块链中,对于一个区块来讲存在两种状态,分别是safe和Final,前者代表区块最终安全,这代表不会被分叉(这个概念在区块链中非常有意义,上链的区块不一定最终安全,在P2P网络上来讲,最终安全理论上来讲包含两个要素,1.此区块已经被绝大多数P2P节点确认,2.此区块在链上已经经过一段时间(有子区块的产生)),对于Final区块来讲,表示他已经通过共识机制来写入区块链中。
        //这里还会加载transaction index来加速对区块链的查询,默认仅对最近235w个区块进行加速查询
    eth.blockchain, err = core.NewBlockChain(chainDb, cacheConfig, config.Genesis, &overrides, eth.engine, vmConfig, &config.TransactionHistory)
    if err != nil {
        return nil, err
    }
    eth.bloomIndexer.Start(eth.blockchain)
        // EIP4844通过了一种新的交易数据结构blob来扩大以太坊中的数据存储大小,这有利于layer2的gas费进一步降低,对于底层来讲,这被抽象成一种新的交易Pool给予txPool进行集成。
    if config.BlobPool.Datadir != "" {
        config.BlobPool.Datadir = stack.ResolvePath(config.BlobPool.Datadir)
    }
    blobPool := blobpool.New(config.BlobPool, eth.blockchain)

    if config.TxPool.Journal != "" {
        config.TxPool.Journal = stack.ResolvePath(config.TxPool.Journal)
    }
    legacyPool := legacypool.New(config.TxPool, eth.blockchain)
        //这里的txpool.New会开启一个协程对区块进行监控,防止被区块的交易依然保留在交易池里
    eth.txPool, err = txpool.New(config.TxPool.PriceLimit, eth.blockchain, []txpool.SubPool{legacyPool, blobPool})
    if err != nil {
        return nil, err
    }
    // Permit the downloader to use the trie cache allowance during fast sync
    cacheLimit := cacheConfig.TrieCleanLimit + cacheConfig.TrieDirtyLimit + cacheConfig.SnapshotLimit
        //开启downloader对区块链历史数据进行同步
    if eth.handler, err = newHandler(&handlerConfig{
        NodeID:         eth.p2pServer.Self().ID(),
        Database:       chainDb,
        Chain:          eth.blockchain,
        TxPool:         eth.txPool,
        Network:        networkID,
        Sync:           config.SyncMode,
        BloomCache:     uint64(cacheLimit),
        EventMux:       eth.eventMux,
        RequiredBlocks: config.RequiredBlocks,
    }); err != nil {
        return nil, err
    }

    eth.miner = miner.New(eth, config.Miner, eth.engine)
    eth.miner.SetExtra(makeExtraData(config.Miner.ExtraData))

    eth.APIBackend = &EthAPIBackend{stack.Config().ExtRPCEnabled(), stack.Config().AllowUnprotectedTxs, eth, nil}
    if eth.APIBackend.allowUnprotectedTxs {
        log.Info("Unprotected transactions allowed")
    }
    eth.APIBackend.gpo = gasprice.NewOracle(eth.APIBackend, config.GPO, config.Miner.GasPrice)

    // Start the RPC service
    eth.netRPCService = ethapi.NewNetAPI(eth.p2pServer, networkID)

    // 注册eth APIS方法里面的相关接口到rpcAPIS
    stack.RegisterAPIs(eth.APIs())
    stack.RegisterProtocols(eth.Protocols())
        //注册eth到lifecycle
    stack.RegisterLifecycle(eth)

    // Successful startup; push a marker and check previous unclean shutdowns.
    eth.shutdownTracker.MarkStartup()

    return eth, nil
}

上述会对本地节点的区块链数据进行构建,区块链节点为了降低索引数据过大,将整个区块链数据以类似冷热数据的方式分割成两种存储,对于第一种基于LSM的KV的数据库,只保存前9w个区块的索引,剩下的会以另一种freezer方式存储(通过idx文件以6字节为一个单位保存对内容的索引,对每个内容文件以最大为2GB进行切割,具体的内容包括区块信息(receipt、header和body),以及状态信息(account、history和storage)),如果想详细了解freezer这种存储特性请参考下面两篇文章。 ethereum-internals-geth-storage-model geth-freezer-files-block-data-done-fast

同时,geth会对当前节点的区块链数据进行验证和构建,构建出账户状态以及区块链的状态来为区块链的运行做准备。在后续geth会构建交易池来对用户的交易进行统一的管理,同时会开始异步协程对已经上链的交易进行监控并消除,与此同时,geth通过newHandler()对历史数据进行同步,这里有一个细节的点在于当bc.currentHeader的Number为0时,代表区块链还未同步完成,这不代表目前的block的高度为0。

对于ethereum来讲,对于状态的存储通常都以MPT树的方式进行,但是在后续的改进中,为了便于对于状态的快速验证,后续推出了verkle的数据结构来加速状态的验证,如果你对verkle感兴趣的话,下面有一些作者认为比较专业的文章对此进行描述,这里不会做过多剖析,后续会单独推出一篇文章对此进行详细的剖析。Verkle 树 ,PCS multiproofs using random evaluation,verkle_tree_eip

在以太坊中对于区块有多种的状态,这主要是为了在去中心化系统下保证交易的最终确定性,这在很多layer2的链里面也有类似的设计,详细可以参考pos共识

最后来到我们关注的点,这里会通过stack.RegisterAPIs(eth.APIs())、stack.RegisterLifecycle(eth)来对我们上述关注的两个点进行修改。

这里回顾一下,上述我们通过对startNode进行剖析,了解到ethereum会注册rpcAPIS的相关方法来为外部和内部提供接口式的服务,同时会遍历lifecycles,对每一个元素调用start方法

// APIs return the collection of RPC services the ethereum package offers.
// NOTE, some of these services probably need to be moved to somewhere else.
func (s *Ethereum) APIs() []rpc.API {
    apis := ethapi.GetAPIs(s.APIBackend)

    apis = append(apis, s.engine.APIs(s.BlockChain())...)

    // Append all the local APIs and return
    return append(apis, []rpc.API{
        {
            Namespace: "miner",
            Service:   NewMinerAPI(s),
        }, {
            Namespace: "eth",
            Service:   downloader.NewDownloaderAPI(s.handler.downloader, s.blockchain, s.eventMux),
        }, {
            Namespace: "admin",
            Service:   NewAdminAPI(s),
        }, {
            Namespace: "debug",
            Service:   NewDebugAPI(s),
        }, {
            Namespace: "net",
            Service:   s.netRPCService,
        },
    }...)
}
func GetAPIs(apiBackend Backend) []rpc.API {
    nonceLock := new(AddrLocker)
    return []rpc.API{
        {
            Namespace: "eth",
            Service:   NewEthereumAPI(apiBackend),
        }, {
            Namespace: "eth",
            Service:   NewBlockChainAPI(apiBackend),
        }, {
            Namespace: "eth",
            Service:   NewTransactionAPI(apiBackend, nonceLock),
        }, {
            Namespace: "txpool",
            Service:   NewTxPoolAPI(apiBackend),
        }, {
            Namespace: "debug",
            Service:   NewDebugAPI(apiBackend),
        }, {
            Namespace: "eth",
            Service:   NewEthereumAccountAPI(apiBackend.AccountManager()),
        },
    }
}

上述的接口主要是提供对ethereum blockchain的服务调用,包括发送交易,获取区块链信息等等,注册的这些方法都能在ethereum的JSON-RPC文档,这是一个好消息,意味着我们找到了外部访问以太坊的所有入口(这些方法都会后续以单独的文章为各位读者剖析)。

接下来让我们关注stack.RegisterLifecycle(eth)中eth的start方法

// Start implements node.Lifecycle, starting all internal goroutines needed by the
// Ethereum protocol implementation.
func (s *Ethereum) Start() error {
    s.setupDiscovery()

    // Start the bloom bits servicing goroutines
    s.startBloomHandlers(params.BloomBitsBlocks)

    // Regularly update shutdown marker
    s.shutdownTracker.Start()

    // Start the networking layer
    s.handler.Start(s.p2pServer.MaxPeers)
    return nil
}
func (h *handler) Start(maxPeers int) {
    h.maxPeers = maxPeers

    // broadcast and announce transactions (only new ones, not resurrected ones)
    h.wg.Add(1)
    h.txsCh = make(chan core.NewTxsEvent, txChanSize)
    h.txsSub = h.txpool.SubscribeTransactions(h.txsCh, false)
        //以太坊会对用户签名的交易进行广播以便让交易更快上链
    go h.txBroadcastLoop()

    // 对获取到的transaction进行处理,对于P2P系统来说,可能会收到重复的消息以及从未收到必要收到的消息,以太坊通过包装txFetcher对上述情况进行处理,简单来讲,它负责根据公告获取新的交易。整个过程可以分为三个阶段,并且确保了数据的完整性和避免资源的浪费。每笔交易在txFetcher中会经过三个阶段
       //交易等待阶段:
//新发现的交易会被移入一个等待列表(wait list)。这个阶段是交易还未完全获取到的数据状态。
      // 交易排队阶段:
//大约 500 毫秒后,如果等待列表中的交易没有完全从网络中广播给节点,它们将会被移入一个排队区域(queueing area)。这意味着在这段时间内,如果某个交易还没有完全获取到,系统会准备好进一步处理这些交易。
     //交易获取阶段:
//当一个连接的对等节点没有任何正在进行的获取请求时,排队区域中的交易会分配给该对等节点,进入获取状态(fetching)。接下来,交易会被发送请求获取,直到交易被成功获取或获取失败。
    h.txFetcher.Start()

    // start peer handler tracker
    h.wg.Add(1)
    go h.protoTracker()
}

总结

<!--StartFragment-->

1733814602748.png

总的来说,geth通过makeFullNode和startNode对上述的一些服务进行了初始化以及启动。主要包括p2pNode、blockchain这个复杂的数据结构,以及向外部和内部进程提供的服务servers(通过反射rpcAPIS的servers里面的公共方法实现注册),同时通过accountmanager对账户进行管理,后续包括了一个系统必不可少了profile、metrics、trace等相关信息。这里我们没有过多地讲述consensus engine,因为目前很多full node都是通过第三方的共识模块来实现POS的。

geth对各个组件进行初始化来完成这个复杂系统的协同性工作,这里面包含了许多复杂原理和数学知识,这篇文章粗略讲解full node的整体服务。我们在开篇讲过,我们从geth给外界提供的接口出发来深入剖析每一步的流程。

点赞 0
收藏 0
分享
本文参与登链社区写作激励计划 ,好文好收益,欢迎正在阅读的你也加入。

2 条评论

请先 登录 后评论
0xdoomxy
0xdoomxy
江湖只有他的大名,没有他的介绍。