重要接口:
ITxsPool的实现:
type TxsPool struct {
config TxsPoolConfig
chainconfig *params.ChainConfig
bc common.IBlockChain
currentState *state.IntraBlockState
pendingNonces *txNoncer
currentMaxGas uint64
ctx context.Context //
cancel context.CancelFunc
wg sync.WaitGroup
mu sync.RWMutex // lock
istanbul bool // Fork indicator whether we are in the istanbul stage.
eip2718 bool // Fork indicator whether we are using EIP-2718 type transactions.
eip1559 bool // Fork indicator whether we are using EIP-1559 type transactions.
shanghai bool // Fork indicator whether we are in the Shanghai stage.
locals *accountSet
pending map[types.Address]*txsList
queue map[types.Address]*txsList
beats map[types.Address]time.Time
all *txLookup
priced *txPricedList
gasPrice *uint256.Int
// channel
reqResetCh chan *txspoolResetRequest
reqPromoteCh chan *accountSet
queueTxEventCh chan *transaction.Transaction
reorgDoneCh chan chan struct{}
reorgShutdownCh chan struct{}
changesSinceReorg int
isRun uint32
deposit *deposit.Deposit
}
1、Has
Has(hash types.Hash) bool
根据输入的hash值判断交易池中是否有该交易,返回bool值
input:hash
output:bool
2、Pending
Pending(enforceTips bool) map[types.Address][]*transaction.Transaction
函数遍历所有当前可处理的交易,按原始帐户分组并按随机数排序。
input:enforceTips 参数可用于对挂起的交易进行额外的过滤,只返回那些在下一个挂起的执行环境中有效提示足够大的交易。
return pending;返回的交易集是一个副本,可以通过调用代码自由修改。
input:enforceTips
output:pending(账户+交易)
txs := list.Flatten()
使用txpool_list中的Flatten()函数可以实现按照nonce排序,并将排序结果缓存下来,形成副本,用于保存。
pending[addr] = txs
这一行代码实现了按照账户将交易进行分组。
3、GetTransaction
GetTransaction() ([]*transaction.Transaction, error)
获取pending队列里面的交易,返回pending,且err:nil
4、GetTx
GetTx(hash types.Hash) *transaction.Transaction
通过pool对象中的txLookup类型的all,通过其中的pool.all.Get(hash)方法Get(hash types.Hash) *transaction.Transaction
可以根据hash值获取对应的交易。
5、AddRemotes
AddRemotes(txs []*transaction.Transaction) []error
将远程交易加入交易池中,调用pool对象中的addTxs()方法func (pool *TxsPool) addTxs(txs []*transaction.Transaction, local bool, sync bool) []error
,其中bool local为false,bool sync为false,sync是同步的意思
5.1 addTxs
addTxs(txs []*transaction.Transaction, local bool, sync bool) []error
无需池锁
遍历整个交易列表:
- 1 交易池中已经有的交易不能要:通过**get(hash)**函数
- 2 发送者验证不通过的交易不能要:通过pool.validatesender()函数
上锁之后通过addTxsLocked得到一些err和地址集
如果是本地的,可以把交易通过event.GlobalEvent.Send事件
通过pool.requestPromoteExecutables获得请求的结果。
5.1.1 addTxsLocked
addTxsLocked(txs []*transaction.Transaction, local bool) ([]error, *accountSet)
addTxsLocked 尝试对一批有效的交易进行排队。
必须持有交易池锁。
dirty的定义是调用tx_list中的**newAccountSet()方法实现的。
遍历交易列表,通过调用pool.add()方法进行处理。如果没有被替换的且没有错误信息,就可以调用addTx(tx)**将交易加入交易池中
5.1.1.1 newAccountSet
func newAccountSet(addrs ...types.Address) *accountSet
newAccountSet 创建一个新地址集,其中包含用于发送者派生的关联签名者。
5.1.1.2 add
func (pool *TxsPool) add(tx *transaction.Transaction, local bool) (replaced bool, err error)
add 验证事务并将其插入到不可执行队列queue中以供稍后挂起的提升和执行。 如果该交易是已待处理或排队交易的替代交易,则如果其价格较高,它将覆盖前一交易。
如果新添加的交易被标记为本地local,则其发送帐户将被添加到白名单中,以防止任何关联交易因定价限制而被从池中删除。
- 1 通过**pool.all.Get(hash)**判断是否在交易池中,如果已经存在,返回replaced:false和错误信息。
- 2 local本地的判断
isLocal := local || pool.locals.containsTx(tx)
如果它来自本地源或来自网络但发送者之前被标记为本地,则将其视为本地事务。
- 3 通过pool.validateTx来进行验证
- 4 如果交易池已经满了,价格过低的交易可以丢弃
- 5 非本地的新交易的价格过低,不接受
- 6 回滚重组的个数应该不大于总GlobalSlots的25%,否则有错误信息
- 7 新的交易更好的话,为其腾出空间;如果是本地事务,则强制丢弃所有可用事务;否则,如果我们无法为新的空间腾出足够的空间,则中止操作。通过txs_list.go里面的**Discard()**函数实现。
- 8 通过pool.removeTx淘汰定价过低的远程交易
- 9 452-468行实现如果交易是已待处理交易,则覆盖前一交易
- 10 通过pool.enqueueTx加入到queue中
- 11 480-484实现本地local将会被添加到白名单中
5.1.1.2.1 validateTx
func (pool *TxsPool) validateTx(tx *transaction.Transaction, local bool) error
完成对交易的验证
- 1 eip-2718/2930和eip-1559不支持
- 2 交易大小大于最大限制txMaxSize,这可以限制DOS攻击
- 3 交易数量不为负值
- 4 gas过大
- 5 确认类似gasprice、gastipcap这样的数据的位数是正确的
- 6 GasFeeCap要大于等于GasTipCap
GasFeeCap是指消息发送者针对每个GasUnit愿意支付的最高价格,以特定单位衡量。同时,GasFeeCap与另一个参数GasLimit一起,确定了发送者为消息支付的最大费用金额。这保证了发送者发送消息的费用不会超过GasLimit和GasFeeCap的乘积值。
GasTipCap是交易设置中的一个参数,对应于交易中的最大 小费,决定了在网络拥堵时,用户愿意支付的最高小费价格。同时,可以通过调用某些API函数,例如SuggestGasTipCap来获取到系统建议的GasTipCap值。这个参数的设置对于交易的执行速度有一定影响,因为如果设置了较高的GasTipCap,那么在网络拥堵的时候,你的交易可能会优先得到处理。不过需要注意的是,这并不意味着你可以无限制地提高GasTipCap,因为过高的费用可能会导致交易失败。 - 7 非本地交易不能降低到我们自己接受的最低汽油价格或小费之下
- 8 随机数nonce要排序
- 9 要有足够的余额来支付
- 10 交易的gas要比基本的汽油费要大
5.1.1.2.2 removeTx
func (pool *TxsPool) removeTx(hash types.Hash, outofbound bool)
removeTx 从队列中删除单个事务,将所有后续事务移回未来队列。
- 1 获取要删除的交易
- 2 通过
pool.all.Remove(hash)
从已知队列中删除
- 从pending队列中删除并且重置nonce,通过txs_list.go里面的
pending.Remove(tx)
进行删除 - 如果队列中没有剩余的交易,就删除list
6、AddLocal
AddLocal(tx *transaction.Transaction) error
通过调用pool.AddLocals([]*transaction.Transaction{tx})
将本地交易加入交易池
6.1 AddLocals
func (pool *TxsPool) AddLocals(txs []*transaction.Transaction) []error
这里调用pool.addTxs(txs, !pool.config.NoLocals, true)
,其中的sync是异步。参考5.1
7、Stats
Stats() (int, int, int, int)
通过遍历pending和queue中的交易获得输出
主要有四个输出:
pendingAddresses:待处理的地址数量
pendingTxs:待处理的交易数量
queuedAddresses:排队等待处理的地址数量
queuedTxs:排队等待处理的交易数量
8、Nonce
Nonce(addr types.Address) uint64
获取地址的pending随机数标识符列表
pool.mu.RLock()
是一个用于获取读锁的方法,通常用于在多线程环境中保护共享资源。当一个线程需要读取共享资源时,它会先获取读锁,其他线程在此期间无法修改共享资源。当线程完成读取操作后,它会释放读锁,以便其他线程可以访问共享资源。
defer
关键字用于延迟执行一个函数或方法,直到包含它的函数返回。因此,defer pool.mu.RUnlock()
表示在当前函数返回之前,pool.mu.RUnlock()
方法来释放读锁。这通常用于处理资源池、互斥锁等需要手动释放的资源。通过使用defer
关键字,可以确保即使出现异常情况,也能正确地释放资源。
pool.pendingNonces.get(addr)
是一个用于获取指定地址的待处理随机数的方法。在以太坊智能合约中,随机数生成器通常使用一个待处理的随机数集合来存储待处理的随机数。当合约需要生成一个新的随机数时,它会将该随机数添加到待处理集合中,并返回一个唯一的随机数标识符。当合约完成随机数的使用后,它会从待处理集合中删除该随机数标识符。
通过调用pool对象中的 pool.pendingNonces.get(addr)
方法,可以获取指定地址的待处理随机数标识符列表。这有助于确保每个地址只生成一次随机数,并且可以防止重复生成相同的随机数。
9、Content
Content() (map[types.Address][]*transaction.Transaction, map[types.Address][]*transaction.Transaction)
获取pending和queue,其中调用了tx_List的对象list,对其中的数据进行了排序,Flatten 根据松散排序的内部表示创建随机数排序的事务切片。 排序结果将被缓存,以备在对内容进行任何修改之前再次请求时使用。其中也需要进行读取锁和释放锁。
10、SetDeposit
SetDeposit(deposit *deposit.Deposit)
这是一个名为SetDeposit
的函数,它接受一个指向deposit.Deposit
类型的指针作为参数。这个函数的作用是将传入的存款对象设置到交易池中。
pool.deposit = deposit
11、promoteTx
func (pool *TxsPool) promoteTx(addr types.Address, hash types.Hash, tx *transaction.Transaction) bool
PromotionTx 将交易添加到待处理(可处理)的交易列表中,并返回该交易是否已插入或较旧的更好。
注意,此方法假设持有池锁!
- 1 如果
pending[addr]==nil
,创建交易列表 - 2 调用txs_list中的list对象的add方法
inserted, old := list.Add(tx, pool.config.PriceBump)
。Add 尝试将新交易插入列表中,返回该交易是否被接受,如果是,则返回它替换的任何先前交易。如果新交易被接受到列表中,列表的成本和气体阈值也可能会更新。
if old.GasFeeCapCmp(tx) >= 1 || old.GasTipCapCmp(tx) >= 1 {return false, nil}
这种情况下,交易不被接受。
- 3 如果不被接受,则说明old更好,就需要通过txs_list中的**pool.all.Remove()和pool.priced.Removed()**丢弃该交易,并返回false。
- 4 如果被接受,就要丢弃原来的旧交易
- 5 设置新nonce
- 6 记录心跳
12、promoteExecutables
func (pool *TxsPool) promoteExecutables(accounts []types.Address) []*transaction.Transaction
PromotionExecutables 将已变得可处理的事务从未来队列移动到待处理事务集pending。 在此过程中,所有无效交易(低随机数、低余额)都将被删除。
- 1 操作遍历整个queue
- 2 删除过旧的交易(nonce过小),其中调用list.Foward进行筛选和**remove(hash)**进行删除
- 3 删除昂贵的交易(balance不足或者gas耗尽),通过list.Filter进行筛选和**remove(hash)**进行删除
- 4 将所有可执行交易通过promoteTx(参考11)添加到可处理的交易列表中
- 5 删除超出了硬性limit的queue交易,通过list.Cap进行筛选(得到超出了limit的所有交易)和**remove(hash)**进行删除
- 6 对于空队列,可以将其delete
13、truncatePending
func (pool *TxsPool) truncatePending()
如果池高于挂起限制,truncatePending 将从挂起队列中删除事务。 对于具有许多待处理交易的所有账户,该算法尝试将交易计数减少大约相同的数量。 该函数的主要目的是在交易池中对待处理的交易进行修剪,以确保交易池中的交易数量不超过全局插槽数。如果某个地址的待处理交易数量超过了阈值,那么将该地址的所有待处理交易减少到阈值以下。。
- 1 首先计算所有待处理事务的总数量,并将其存储在变量pending中。然后,它检查pending是否小于等于全局插槽数
pool.config.GlobalSlots
,如果是,则直接返回,不执行任何操作。 - 2 调用github.com/prometheus/common/prque中的函数prque.New创建一个优先级队列,用于存储需要惩罚的大交易者。
- 3 遍历所有的pending,将满足条件的地址及其待处理交易数量添加到
spammers
队列中。(每个地址是否包含在本地地址列表中(!pool.locals.contains(addr)
),以及该地址对应的待处理事务数量是否大于账户插槽数(uint64(list.Len()) > pool.config.AccountSlots
) - 4 从spammers队列中取出下一个违规者,并将其添加到offenders列表中,直到所有待处理交易的数量都不超过全局插槽数或spammers队列为空。
for pending > pool.config.GlobalSlots && !spammers.Empty()
- 5 检查是否有多个违规者
if len(offenders) > 1
(即待处理交易数量超过全局插槽数的地址)。如果有多个违规者,那么计算所有违规者的阈值,即他们的待处理交易数量。 - 6 迭代地减少所有违规者的待处理交易,直到他们的待处理交易数量都低于阈值或低于全局插槽数。
在每次迭代中,代码会遍历所有违规者的待处理交易,通过list.Cap进行筛选,并将它们从全局交易池中通过remove移除。同时,还会更新违规者的nonce,以确保其后续的交易不会受到干扰。 - 7 记录已移除的交易数量,并更新价格池的状态。
pool.priced.Removed(len(caps))
14、truncateQueue
func (pool *TxsPool) truncateQueue()
如果池高于全局队列限制,则 truncate Queue 会删除队列中最旧的事务。
- 1 计算所有queue的总数量,并存在变量queued中。检查是否小于全局队列限制
pool.config.GlobalQueue
,如果是则直接返回,不执行任何操作。 - 2 创建一个
adddressByHeart
的对象,其中有len、less、swap三种方法 - 3 按照heartbeat进行排序,其中本地accounts不需要被丢弃
if !pool.locals.contains(addr)
- 4 放弃交易,直到queue总数低于限制
pool.config.GlobalQueue
或仅保留本地
for drop := queued - pool.config.GlobalQueue; drop > 0 && len(addresses) > 0;
- 5 如果交易少于溢出drop,就通过pool.removeTx删除所有的交易
if size := uint64(list.Len()); size <= drop {
for _, tx := range list.Flatten() {
hash := tx.Hash()
pool.removeTx(hash, true)
}
drop -= size
continue
}
- 6 否则,通过list.Flatten排序之后通过pool.removeTx就删除最新的几个交易
15、demoteUnexecutables
func (pool *TxsPool) demoteUnexecutables()
demoteUnexecutables 从池可执行/待处理队列中删除无效和已处理的事务,并且任何变得不可执行的后续事务都将移回到未来队列中。
注意:交易不会在价目表中标记为已删除,因为重新堆总是由 SetBaseFee 显式触发,并且此函数触发重新堆是不必要且浪费的
这段代码是以太坊中的一部分,它定义了一个名为demoteUnexecutables
的函数。这个函数的主要目的是处理待处理交易池中的不可执行交易。这个函数主要用于确保交易池中的交易都是可执行的,并且保持了交易的有效性和顺序。
-
- 遍历所有账户和pending待处理交易列表。
-
- 对于每个账户,通过
pool.currentState.GetNonce(addr)
获取其当前的nonce值。
- 对于每个账户,通过
-
- 删除所有被视为太旧的交易(即nonce值过低的交易),通过list.Forward筛选,通过pool.all.remove进行删除。
-
- 删除所有无法支付的交易(即余额过低或gas不足的交易),通过list.Filter筛选,通过pool.all.remove进行删除。
-
- 通过pool.enqueueTx将无效的交易排队以供后续处理。
-
- 如果本地账户包含某个地址,则不进行任何操作。
-
- 如果在某个地址前面存在间隙(这种情况不应该发生),则将所有交易推迟处理。
-
- 如果某个地址的待处理交易列表变为空,则从待处理交易池中删除该地址的所有条目。
16、blockChangeLoop
func (pool *TxsPool) blockChangeLoop()
作用是监听区块高度的变化,并在新的区块被插入时重置交易池。
-
- 使用
defer pool.wg.Done()
确保在函数退出时调用pool.wg.Done()
来标记工作完成。
- 使用
-
- 创建一个名为
highestBlockCh
的通道,用于接收最新的区块高度信息。
- 创建一个名为
highestBlockCh := make(chan common.ChainHighestBlock)
-
- 使用
defer close(highestBlockCh)
确保在函数退出时关闭通道。
- 使用
-
- 订阅全局事件
event.GlobalEvent
,并将highestBlockCh
作为回调函数传递给它。
- 订阅全局事件
highestSub := event.GlobalEvent.Subscribe(highestBlockCh)
-
- 使用
defer highestSub.Unsubscribe()
确保在函数退出时取消订阅。
- 使用
-
- 获取当前区块的高度,并将其存储在变量
oldBlock
中。oldBlock := pool.bc.CurrentBlock()
- 获取当前区块的高度,并将其存储在变量
- 进入一个无限循环,使用
select
语句监听以下事件:highestSub.Err()
:如果发生错误,则返回并结束函数执行。pool.ctx.Done()
:如果上下文被取消,则返回并结束函数执行。highestBlockCh
:如果从通道中接收到新的区块高度信息,则进行以下操作:- 如果新的高度有效且已插入,则调用
pool.requestReset(oldBlock, pool.bc.CurrentBlock())
方法重置交易池,并更新oldBlock
为当前区块的高度。
- 如果新的高度有效且已插入,则调用
case highestBlock, ok := <-highestBlockCh:
if ok && highestBlock.Inserted {
pool.requestReset(oldBlock, pool.bc.CurrentBlock())
oldBlock = pool.bc.CurrentBlock()
}
16.1 requestReset
func (pool *TxsPool) requestReset(oldBlock block.IBlock, newBlock block.IBlock) <-chan struct{}
该方法接收两个参数:oldBlock
和newBlock
,它们都是block.IBlock
类型的接口。这个方法的作用是向交易池发送一个重置请求,并等待重置操作完成或发生异常情况。
方法内部使用了select
语句来处理不同的事件。
-
- 它将一个
txspoolResetRequest
类型的请求对象发送到pool.reqResetCh
通道中,该请求对象包含了旧区块和新区块的信息。然后,它等待从pool.reorgDoneCh
通道中接收一个信号,表示重置操作已经完成。
- 它将一个
-
- 如果上下文
pool.ctx
被取消,则返回上下文的完成信号。
- 如果上下文
17、scheduleLoop
func (pool *TxsPool) scheduleLoop()
-
- 使用
defer pool.wg.Done()
确保在函数退出时调用pool.wg.Done()
来标记工作完成。
- 使用
-
curDone:一个非空的通道struct chan,当runReorg处于活动状态时,该通道不为空。
nextDone:一个空的通道,用于等待下一个操作完成。
launchNextRun:一个布尔值,表示是否启动下一个运行。
reset:一个指向txspoolResetRequest类型的指针,用于存储重置请求的信息。
dirtyAccounts:一个指向accountSet类型的指针,用于存储脏账户信息。
queuedEvents:一个映射,将地址映射到指向txsSortedMap类型的指针,用于存储排队的事件。
// Launch next background reorg if needed
if curDone == nil && launchNextRun {
// Run the background reorg and announcements
go pool.runReorg(nextDone, reset, dirtyAccounts, queuedEvents)
curDone, nextDone = nextDone, make(chan struct{})
launchNextRun = false
reset, dirtyAccounts = nil, nil
queuedEvents = make(map[types.Address]*txsSortedMap)
}
这段代码是用于在需要时启动下一个后台重组(background reorg)的。如果当前没有正在进行的重组(curDone == nil),并且需要启动下一个运行(launchNextRun),则执行以下操作:
- 使用go关键字启动一个新的协程来运行后台重组和公告(
pool.runReorg
)。 - 将nextDone赋值给curDone,并创建一个新的空通道(make(chan struct{}))赋值给nextDone。
- 将launchNextRun设置为false,表示不再需要启动下一个运行。
- 将reset、dirtyAccounts和queuedEvents重置为nil或空值,以便下一次使用。
对于select中的每一个case
-
req := <-pool.reqResetCh
: 从pool.reqResetCh
通道中接收重置请求。如果当前没有正在进行的重置(reset == nil
),则将请求赋值给reset
变量;否则,将新块更新到reset
变量中。然后将nextDone
发送到pool.reorgDoneCh
通道,表示重置已完成。
-
req := <-pool.reqPromoteCh
: 从pool.reqPromoteCh
通道中接收提升请求。如果当前没有正在进行的提升(dirtyAccounts == nil
),则将请求赋值给dirtyAccounts
变量;否则,将请求合并到dirtyAccounts
变量中。然后将nextDone
发送到pool.reorgDoneCh
通道,表示提升已完成。
-
tx := <-pool.queueTxEventCh
: 从pool.queueTxEventCh
通道中接收交易事件。获取交易的发送地址,并将交易添加到与该地址关联的队列中。如果该地址尚未在队列中,则创建一个新的排序映射并将其分配给该地址。
-
<-curDone
: 等待当前运行完成。如果curDone
不为空,则等待其完成。
-
case <-pool.ctx.Done():
: 等待当前运行完成。如果curDone
不为空,则等待其完成。然后关闭nextDone
通道并返回。
这段代码的主要目的是处理不同类型的请求,并根据请求类型执行相应的操作。