以太坊源码之交易池启动过程分析

从以太坊的main函数入手,该源码在项目工程中的 cmd/geth/main.go 中,代码如下:
func main() {
	if err := app.Run(os.Args); err != nil {
		fmt.Fprintln(os.Stderr, err)
		os.Exit(1)
	}
}
在默认情况下, app.Run(os.Args)调用geth,其代码如下:
func geth(ctx *cli.Context) error {
	node := makeFullNode(ctx)
	startNode(ctx, node)
	node.Wait()
	return nil
}

在geth函数主要实现了一下几点功能:
1、配置全节点Node的配置文件和创建相关服务
2、启动一个Node节点
3、等待节点的退出信号
下面主要对功能1进行解析,makeFullNode的代码如下:

func makeFullNode(ctx *cli.Context) *node.Node {
	stack, cfg := makeConfigNode(ctx)  //配置节点相关信息

	utils.RegisterEthService(stack, &cfg.Eth)  //注册相关服务

	if ctx.GlobalBool(utils.DashboardEnabledFlag.Name) {
		utils.RegisterDashboardService(stack, &cfg.Dashboard, gitCommit)
	}
	// Whisper must be explicitly enabled by specifying at least 1 whisper flag or in dev mode
	shhEnabled := enableWhisper(ctx)
	shhAutoEnabled := !ctx.GlobalIsSet(utils.WhisperEnabledFlag.Name) && ctx.GlobalIsSet(utils.DeveloperFlag.Name)
	if shhEnabled || shhAutoEnabled {
		if ctx.GlobalIsSet(utils.WhisperMaxMessageSizeFlag.Name) {
			cfg.Shh.MaxMessageSize = uint32(ctx.Int(utils.WhisperMaxMessageSizeFlag.Name))
		}
		if ctx.GlobalIsSet(utils.WhisperMinPOWFlag.Name) {
			cfg.Shh.MinimumAcceptedPOW = ctx.Float64(utils.WhisperMinPOWFlag.Name)
		}
		utils.RegisterShhService(stack, &cfg.Shh)
	}

	// Add the Ethereum Stats daemon if requested.
	if cfg.Ethstats.URL != "" {
		utils.RegisterEthStatsService(stack, cfg.Ethstats.URL)
	}
	return stack
}

在utils.RegisterEthService(stack, &cfg.Eth)注册服务中会创建一个 eth.New 实例并初始化相关模块,下面进入 utils.RegisterEthService 函数看代码:

func RegisterEthService(stack *node.Node, cfg *eth.Config) {
	var err error
	if cfg.SyncMode == downloader.LightSync {  //
		err = stack.Register(func(ctx *node.ServiceContext) (node.Service, error) {
			return les.New(ctx, cfg)
		})
	} else { // 
		err = stack.Register(func(ctx *node.ServiceContext) (node.Service, error) { 
			fullNode, err := eth.New(ctx, cfg) //创建一个node服务实例
			if fullNode != nil && cfg.LightServ > 0 {
				ls, _ := les.NewLesServer(fullNode, cfg)
				fullNode.AddLesServer(ls)
			}
			return fullNode, err
		})
	}
	if err != nil {
		Fatalf("Failed to register the Ethereum service: %v", err)
	}
}
注:以太坊在 eth/downloader/modes.go 定义了数据的三种同步模式:
			const (
		    FullSync  SyncMode = iota   // 同步完整的区块信息
		    FastSync                              // 快速同步header,然后再跟进header同步全部内容
		    LightSync                            // 只下载header并在之后终止,轻节点
		)

一般不使用轻节点去同步数据,接下来就是 eth.New(ctx, cfg) 来创建一个服务实例,代码如下:

func New(ctx *node.ServiceContext, config *Config) (*Ethereum, error) {
	if config.SyncMode == downloader.LightSync {
		return nil, errors.New("can't run eth.Ethereum in light sync mode, use les.LightEthereum")
	}
	if !config.SyncMode.IsValid() {
		return nil, fmt.Errorf("invalid sync mode %d", config.SyncMode)
	}
	chainDb, err := CreateDB(ctx, config, "chaindata")
	if err != nil {
		return nil, err
	}
	chainConfig, genesisHash, genesisErr := core.SetupGenesisBlock(chainDb, config.Genesis)
	if _, ok := genesisErr.(*params.ConfigCompatError); genesisErr != nil && !ok {
		return nil, genesisErr
	}
	log.Info("Initialised chain configuration", "config", chainConfig)
	
	。。。。。。

	if config.TxPool.Journal != "" {
		config.TxPool.Journal = ctx.ResolvePath(config.TxPool.Journal)
	}
	eth.txPool = core.NewTxPool(config.TxPool, eth.chainConfig, eth.blockchain)  //初始化交易池

	if eth.protocolManager, err = NewProtocolManager(eth.chainConfig, config.SyncMode, config.NetworkId, eth.eventMux, eth.txPool, eth.engine, eth.blockchain, chainDb); err != nil {
		return nil, err
	}
	eth.miner = miner.New(eth, eth.chainConfig, eth.EventMux(), eth.engine)
	eth.miner.SetExtra(makeExtraData(config.ExtraData))

	eth.APIBackend = &EthAPIBackend{eth, nil}
	gpoParams := config.GPO
	if gpoParams.Default == nil {
		gpoParams.Default = config.GasPrice
	}
	eth.APIBackend.gpo = gasprice.NewOracle(eth.APIBackend, gpoParams)

	return eth, nil
}
中间代码太多就省略部分,其主要是创建DB实例、创建创世块及链配置、创建共识引擎,创建Bloom服务,加载完整链,初始化交易池等操作

通过上面的源码分析,我们已经在整个程序启动加载时找到了 初始化交易池 的入口 core.NewTxPool,也算见到庐山真面目,工欲利其事必先利其器,接下来先分析一下跟 TxPool 紧密相关的几个数据结构:

type TxPoolConfig struct {
	NoLocals  bool          // 是否应该禁用本地事务处理
	Journal   string         // 本地事务日志,保存相关交易数据的文件名;系统每隔一定时间将交易数据回写到本地文件
	Rejournal time.Duration  // 重新生成本地事务日志的时间间隔

	PriceLimit uint64 // 最低gas价格,以接受入池
	PriceBump  uint64 // 更换//现有交易的最低价格增幅 

	AccountSlots uint64 // 每个帐户最多在pending队列的交易数
	GlobalSlots  uint64 //penging队列最大支持的交易数
	AccountQueue uint64 // 每个帐户最多在queue队列的交易数
	GlobalQueue  uint64 // queue队列最大支持的交易数

	Lifetime time.Duration //queue 队列超过多少小时交易不处理,剔除
}
针对TxPoolConfig系统的默认数值如下:
var DefaultTxPoolConfig = TxPoolConfig{
	Journal:   "transactions.rlp",
	Rejournal: time.Hour,
	PriceLimit: 1,
	PriceBump:  10,
	AccountSlots: 16,
	GlobalSlots:  4096,
	AccountQueue: 64,
	GlobalQueue:  1024,
	Lifetime: 3 * time.Hour,
}

type TxPool struct {
	config       TxPoolConfig                      //配置信息
	chainconfig  *params.ChainConfig     //链配置
	chain        blockChain                        //当前的链
	gasPrice     *big.Int                            //最低的gas价格
	txFeed       event.Feed                      //通过txFedd订阅TxPool的消息
	scope        event.SubscriptionScope  //提供了同时取消多个订阅的功能
	chainHeadCh  chan ChainHeadEvent  //当有了新的区块的产生会收到消息,订阅区块头消息
	chainHeadSub event.Subscription       //区块头消息订阅器
	signer       types.Signer                        //对事物进行签名处理
	mu           sync.RWMutex                    //读写互斥锁

	currentState  *state.StateDB              //当前区块链头部的状态
	pendingState  *state.ManagedState  //挂起状态跟踪虚拟nonces
	currentMaxGas uint64                       // 目前交易的费用上限   

	locals  *accountSet                          //一套豁免驱逐规则的本地交易
	journal *txJournal                            //本地事务日志备份到磁盘

	pending map[common.Address]*txList             //等待队列
	queue   map[common.Address]*txList             //排队但不可处理的事务
	beats   map[common.Address]time.Time        //每个已知帐户的最后一次心跳
	all     map[common.Hash]*types.Transaction  //所有允许查询的事务
	priced  *txPricedList                                        //所有按价格排序的交易       
	wg sync.WaitGroup // for shutdown sync        //关闭同步

	homestead bool                                              //家园版本判断
} 

下面重点来介绍一下 TxPool 启动的代码:

if config.TxPool.Journal != "" {
		config.TxPool.Journal = ctx.ResolvePath(config.TxPool.Journal)
	}
	eth.txPool = core.NewTxPool(config.TxPool, eth.chainConfig, eth.blockchain)

ctx.ResolvePath(config.TxPool.Journal)
将Journal的绝对路径转换为 geth 运行节点时相对路径,一般该文件默认命名:transactions.rlp
本文件存储的是本地帐号pending队列交易数据和queue队列交易数据。

core.NewTxPool(config.TxPool, eth.chainConfig, eth.blockchain)
为真正加载交易池的代码,下面对代码做具体分析:

func NewTxPool(config TxPoolConfig, chainconfig *params.ChainConfig, chain blockChain) *TxPool {
	// Sanitize the input to ensure no vulnerable gas prices are set
	config = (&config).sanitize()

	// Create the transaction pool with its initial settings
	pool := &TxPool{
		config:      config,
		chainconfig: chainconfig,
		chain:       chain,
		signer:      types.NewEIP155Signer(chainconfig.ChainId),
		pending:     make(map[common.Address]*txList),
		queue:       make(map[common.Address]*txList),
		beats:       make(map[common.Address]time.Time),
		all:         make(map[common.Hash]*types.Transaction),
		chainHeadCh: make(chan ChainHeadEvent, chainHeadChanSize),
		gasPrice:    new(big.Int).SetUint64(config.PriceLimit),
	}
	pool.locals = newAccountSet(pool.signer)
	pool.priced = newTxPricedList(&pool.all)
	pool.reset(nil, chain.CurrentBlock().Header())

	// If local transactions and journaling is enabled, load from disk
	if !config.NoLocals && config.Journal != "" {
		pool.journal = newTxJournal(config.Journal)
       //本地文件中加载local交易
		if err := pool.journal.load(pool.AddLocal); err != nil {
			log.Warn("Failed to load transaction journal", "err", err)
		}
		if err := pool.journal.rotate(pool.local()); err != nil {
			log.Warn("Failed to rotate transaction journal", "err", err)
		}
	}
	// Subscribe events from blockchain
	pool.chainHeadSub = pool.chain.SubscribeChainHeadEvent(pool.chainHeadCh)

	// Start the event loop and return
	pool.wg.Add(1)
	go pool.loop()

	return pool
}

1.config = (&config).sanitize()
对TxPoolConfig的相关配置信息进行校验,配置不正确恢复为代码默认配置,代码很简单,自行查看
2.pool := &TxPool{

}
实例化一个TxPool对象,具体字段的定义请参照上文
3.pool.locals = newAccountSet(pool.signer)
设置本地的白名单帐号存储数据结构,即本地的帐号;在后续交易池中排队具有优先特权
4.pool.priced = newTxPricedList(&pool.all)
设置交易池所有按价格排序的交易的存储数据结构
5.pool.reset(nil, chain.CurrentBlock().Header())
重置交易池,后面单独进行分析
6.pool.journal.load()和pool.journal.rotate()
本地文件中加载local交易,函数实现将在后面单独进行分析
7.pool.chain.SubscribeChainHeadEvent(pool.chainHeadCh)
订阅规范链更新事件
8.pool.wg.Add(1)
开启数据同步
9.pool.loop()
启动启动事件监听,下面对.pool.loop()源码进行解析:
```

	func (pool *TxPool) loop() {
				// 代码退出计数器减一,关闭数据同步
				defer pool.wg.Done()
			
				// 定义启动统计报表和交易退出提示符
				var prevPending, prevQueued, prevStales int
				//启用定时器
				report := time.NewTicker(statsReportInterval)  //默认8秒
				defer report.Stop()
			   
				evict := time.NewTicker(evictionInterval)   //默认1分钟
				defer evict.Stop()
			   
				journal := time.NewTicker(pool.config.Rejournal)  //默认3小时
				defer journal.Stop()
			
				// 获取当前区块头
				head := pool.chain.CurrentBlock()
			
				//死循环
				for {
					select {
					// 处理链头事件
					case ev := <-pool.chainHeadCh:
						if ev.Block != nil {
							pool.mu.Lock()
							if pool.chainconfig.IsHomestead(ev.Block.Number()) {
								pool.homestead = true
							}
							pool.reset(head.Header(), ev.Block.Header())
							head = ev.Block
			
							pool.mu.Unlock()
						}
					// 由于系统停止而取消订阅
					case <-pool.chainHeadSub.Err():
						return
			
					// 处理统计报表刻度
					case <-report.C:
						pool.mu.RLock()
						pending, queued := pool.stats()
						stales := pool.priced.stales
						pool.mu.RUnlock()
			
						if pending != prevPending || queued != prevQueued || stales != prevStales {
							log.Debug("Transaction pool status report", "executable", pending, "queued", queued, "stales", stales)
							prevPending, prevQueued, prevStales = pending, queued, stales
						}
			
					// 处理非活动帐户事务退出
					case <-evict.C:
						pool.mu.Lock()
						for addr := range pool.queue {
							// 从退出机制中跳过本地事务,本地账户
							if pool.locals.contains(addr) {
								continue
							}
							//长时间未被处理的交易将清除
							if time.Since(pool.beats[addr]) > pool.config.Lifetime {
								for _, tx := range pool.queue[addr].Flatten() {
									pool.removeTx(tx.Hash(), true)
								}
							}
						}
						pool.mu.Unlock()
			
					// 处理本地交易日志的轮换
					case <-journal.C:
						if pool.journal != nil {
							pool.mu.Lock()
							if err := pool.journal.rotate(pool.local()); err != nil {
								log.Warn("Failed to rotate local tx journal", "err", err)
							}
							pool.mu.Unlock()
						}
					}
				}
			}
	```

重置交易池代码进行源码分析:

func (pool *TxPool) reset(oldHead, newHead *types.Header) {
	var reinject types.Transactions

	if oldHead != nil && oldHead.Hash() != newHead.ParentHash {
		// 新区快头的父区块不等于老区块,说明新老区块不在同一条链
		oldNum := oldHead.Number.Uint64()
		newNum := newHead.Number.Uint64()

		if depth := uint64(math.Abs(float64(oldNum) - float64(newNum))); depth > 64 {
			// 如果新头区块和旧头区块相差大于64,则所有交易不必回退到交易池
			log.Debug("Skipping deep transaction reorg", "depth", depth)
		} else {
			var discarded, included types.Transactions

			var (
				rem = pool.chain.GetBlock(oldHead.Hash(), oldHead.Number.Uint64())
				add = pool.chain.GetBlock(newHead.Hash(), newHead.Number.Uint64())
			)
			// 如果旧链的头区块大于新链的头区块高度,旧链向后退并回收所有回退的交易
			for rem.NumberU64() > add.NumberU64() {
				discarded = append(discarded, rem.Transactions()...)
				if rem = pool.chain.GetBlock(rem.ParentHash(), rem.NumberU64()-1); rem == nil {
					log.Error("Unrooted old chain seen by tx pool", "block", oldHead.Number, "hash", oldHead.Hash())
					return
				}
			}
			//如果新链的头区块大于旧链的头区块,新链后退并回收交易
			for add.NumberU64() > rem.NumberU64() {
				included = append(included, add.Transactions()...)
				if add = pool.chain.GetBlock(add.ParentHash(), add.NumberU64()-1); add == nil {
					log.Error("Unrooted new chain seen by tx pool", "block", newHead.Number, "hash", newHead.Hash())
					return
				}
			}
			// 当新旧链到达同一高度的时候同时回退,知道找到共同的父节点
			for rem.Hash() != add.Hash() {
				discarded = append(discarded, rem.Transactions()...)
				if rem = pool.chain.GetBlock(rem.ParentHash(), rem.NumberU64()-1); rem == nil {
					log.Error("Unrooted old chain seen by tx pool", "block", oldHead.Number, "hash", oldHead.Hash())
					return
				}
				included = append(included, add.Transactions()...)
				if add = pool.chain.GetBlock(add.ParentHash(), add.NumberU64()-1); add == nil {
					log.Error("Unrooted new chain seen by tx pool", "block", newHead.Number, "hash", newHead.Hash())
					return
				}
			}
			// 找到discarded中那些不存在于included中的交易
			reinject = types.TxDifference(discarded, included)
		}
	}
	//初始化当前区块头
	if newHead == nil {
		newHead = pool.chain.CurrentBlock().Header() // Special case during testing
	}
	//依据新区块的Root设置交易池最新的世界状态
	statedb, err := pool.chain.StateAt(newHead.Root)
	if err != nil {
		log.Error("Failed to reset txpool state", "err", err)
		return
	}
	//设置新链头区块的状态
	pool.currentState = statedb
	pool.pendingState = state.ManageState(statedb)
	pool.currentMaxGas = newHead.GasLimit

	log.Debug("Reinjecting stale transactions", "count", len(reinject))
	// 把旧链回退的交易放入交易池
	pool.addTxsLocked(reinject, false)
    
    //交易降级,将pending转换为queue
	pool.demoteUnexecutables()
   
   // 把所有的accounts的nonce更新到最新的pending Nouce
	for addr, list := range pool.pending {
		txs := list.Flatten() // Heavy but will be cached and is needed by the miner anyway
		pool.pendingState.SetNonce(addr, txs[len(txs)-1].Nonce()+1)
	}
	
	//交易升级(queue状态 提升为 pending状态)
	pool.promoteExecutables(nil)
}

针对重置交易池的源码其主要的工作就是判断新老区块是否在同一条链上,如果不存在倒退到共同的父节点,将不同的交易数据重新放回交易池并设置相关状态,其中涉及到交易添加交易池(pool.addTxsLocked --> pool.add)和交易升级pool.promoteExecutables,这部分源码将在后续的 《交易池交易数据维护》文章中讲述。下面对重置函数中的交易状态有pending转换为queue状态函数进行讲解,下面看代码:

func (pool *TxPool) demoteUnexecutables() {
	// 从交易的list中获取地址信息
	for addr, list := range pool.pending {
	   //通过地址获取当前的交易序号Nonce
		nonce := pool.currentState.GetNonce(addr)

		// 剔除掉比当前Nonce小的交易(该交易已被打包存在主链上)
		for _, tx := range list.Forward(nonce) {
			hash := tx.Hash()
			log.Trace("Removed old pending transaction", "hash", hash)
			delete(pool.all, hash)
			pool.priced.Removed()
		}
		// 提出余额不足或没有汽油的交易
		drops, invalids := list.Filter(pool.currentState.GetBalance(addr), pool.currentMaxGas)
		for _, tx := range drops {
			hash := tx.Hash()
			log.Trace("Removed unpayable pending transaction", "hash", hash)
			delete(pool.all, hash)
			pool.priced.Removed()
			pendingNofundsCounter.Inc(1)
		}
		//将符合条件的交易进行降级处理,由pending ----->>> queue
		for _, tx := range invalids {
			hash := tx.Hash()
			log.Trace("Demoting pending transaction", "hash", hash)
			pool.enqueueTx(hash, tx)
		}
		// 上述条件均不满足,符合交易要求进行交易降级处理
		if list.Len() > 0 && list.txs.Get(nonce) == nil {
			for _, tx := range list.Cap(0) {
				hash := tx.Hash()
				log.Error("Demoting invalidated transaction", "hash", hash)
				pool.enqueueTx(hash, tx)
			}
		}
		
		// 清除pending为空的地址
		if list.Empty() {
			delete(pool.pending, addr)
			delete(pool.beats, addr)
		}
	}
}

从本地存储读取交易数据:pool.journal.load(pool.AddLocal)

func (journal *txJournal) load(add func(*types.Transaction) error) error {
	// 判断文件在本地是否存在,不存在直接跳过
	if _, err := os.Stat(journal.path); os.IsNotExist(err) {
		return nil
	}
	// 打开本地存储交易数据的文件
	input, err := os.Open(journal.path)
	if err != nil {
		return err
	}
	defer input.Close()

	journal.writer = new(devNull)
	defer func() { journal.writer = nil }()

	// Inject all transactions from the journal into the pool
	stream := rlp.NewStream(input, 0)
	total, dropped := 0, 0

	var failure error
	for {
		tx := new(types.Transaction)
		//读取交易数据并进行rlp解码
		if err = stream.Decode(tx); err != nil {
			if err != io.EOF {
				failure = err
			}
			break
		}
		//本机存储累计的交易总数
		total++
		if err = add(tx); err != nil {
			log.Debug("Failed to add journaled transaction", "err", err)
			dropped++
			//成功添加交易池的交易总数
			continue
		}
	}
	log.Info("Loaded local transaction journal", "transactions", total, "dropped", dropped)

	return failure
}
注: add(tx)为向交易池中添加一个交易数据,具体函数实现会在后续《交易池交易数据维护》的文章中讲解

交易数据写入本地存储:pool.journal.rotate(pool.local())

func (pool *TxPool) local() map[common.Address]types.Transactions {
	txs := make(map[common.Address]types.Transactions)
	for addr := range pool.locals.accounts {
		if pending := pool.pending[addr]; pending != nil {
			txs[addr] = append(txs[addr], pending.Flatten()...)
		}
		if queued := pool.queue[addr]; queued != nil {
			txs[addr] = append(txs[addr], queued.Flatten()...)
		}
	}
	return txs
}

该函数实现很简单,就是通过本地账户去获取pending和queue的交易数据集合,下面看看pool.journal.rotate代码实现:

func (journal *txJournal) rotate(all map[common.Address]types.Transactions) error {
	// 如果writer之前被打开,关闭
	if journal.writer != nil {
		if err := journal.writer.Close(); err != nil {
			return err
		}
		journal.writer = nil
	}
	// 打开一个文件
	replacement, err := os.OpenFile(journal.path+".new", os.O_WRONLY|os.O_CREATE|os.O_TRUNC, 0755)
	if err != nil {
		return err
	}
	
	journaled := 0
	for _, txs := range all {
		for _, tx := range txs {
		   //进行rlp编码并写入文件
			if err = rlp.Encode(replacement, tx); err != nil {
				replacement.Close()
				return err
			}
		}
		journaled += len(txs)
	}
	replacement.Close()

	// 文件重命名
	if err = os.Rename(journal.path+".new", journal.path); err != nil {
		return err
	}
	//打开该文件
	sink, err := os.OpenFile(journal.path, os.O_WRONLY|os.O_APPEND, 0755)
	if err != nil {
		return err
	}
	//句柄赋值
	journal.writer = sink
	log.Info("Regenerated local transaction journal", "transactions", journaled, "accounts", len(all))
	return nil
}

至此,交易池启动过程部分的源码讲解完毕。代码量有点大,整整撸了一天,该泡杯枸杞喝喝了。。。

  • 1
    点赞
  • 2
    收藏
    觉得还不错? 一键收藏
  • 0
    评论

“相关推荐”对你有帮助么?

  • 非常没帮助
  • 没帮助
  • 一般
  • 有帮助
  • 非常有帮助
提交
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值