Skip to content

Commit ed539c9

Browse files
trantienduchnlightclientMariusVanDerWijdenfjlrjl493456442
authored
core/txpool: support SetCode tx 7702 (#49)
* pick up ethereum/go-ethereum#31073 Co-authored-by: lightclient <[email protected]> Co-authored-by: Marius van der Wijden <[email protected]> Co-authored-by: Felix Lange <[email protected]> * pick up ethereum/go-ethereum#31206 Co-authored-by: Marius van der Wijden <[email protected]> * pick up ethereum/go-ethereum#31209 Co-authored-by: rjl493456442 <[email protected]> Co-authored-by: lightclient <[email protected]> * pick up ethereum/go-ethereum#31249 Co-authored-by: buddho <[email protected]> Co-authored-by: lightclient <[email protected]> * fixup! pick up ethereum/go-ethereum#31249 * fixup! pick up ethereum/go-ethereum#31249 --------- Co-authored-by: lightclient <[email protected]> Co-authored-by: Marius van der Wijden <[email protected]> Co-authored-by: Felix Lange <[email protected]> Co-authored-by: rjl493456442 <[email protected]> Co-authored-by: lightclient <[email protected]> Co-authored-by: buddho <[email protected]>
1 parent 28af9f0 commit ed539c9

File tree

7 files changed

+596
-45
lines changed

7 files changed

+596
-45
lines changed

core/txpool/errors.go

Lines changed: 0 additions & 7 deletions
Original file line numberDiff line numberDiff line change
@@ -17,10 +17,6 @@ var (
1717
// configured for the transaction pool.
1818
ErrUnderpriced = errors.New("transaction underpriced")
1919

20-
// ErrTxPoolOverflow is returned if the transaction pool is full and can't accpet
21-
// another remote transaction.
22-
ErrTxPoolOverflow = errors.New("txpool is full")
23-
2420
// ErrReplaceUnderpriced is returned if a transaction is attempted to be replaced
2521
// with a different one without the required price bump.
2622
ErrReplaceUnderpriced = errors.New("replacement transaction underpriced")
@@ -47,9 +43,6 @@ var (
4743

4844
// ErrAddressBlacklisted is returned if a transaction is sent to blacklisted address
4945
ErrAddressBlacklisted = errors.New("address is blacklisted")
50-
// ErrFutureReplacePending is returned if a future transaction replaces a pending
51-
// transaction. Future transactions should only be able to replace other future transactions.
52-
ErrFutureReplacePending = errors.New("future transaction tries to replace pending")
5346

5447
// ErrAlreadyReserved is returned if the sender address has a pending transaction
5548
// in a different subpool. For example, this error is returned in response to any

core/txpool/legacypool/legacypool.go

Lines changed: 178 additions & 27 deletions
Original file line numberDiff line numberDiff line change
@@ -18,8 +18,10 @@ package legacypool
1818

1919
import (
2020
"container/heap"
21+
"errors"
2122
"math"
2223
"math/big"
24+
"slices"
2325
"sort"
2426
"sync"
2527
"sync/atomic"
@@ -53,6 +55,25 @@ const (
5355
txMaxSize = 4 * txSlotSize // 128KB
5456
)
5557

58+
var (
59+
// ErrTxPoolOverflow is returned if the transaction pool is full and can't accept
60+
// another remote transaction.
61+
ErrTxPoolOverflow = errors.New("txpool is full")
62+
63+
// ErrInflightTxLimitReached is returned when the maximum number of in-flight
64+
// transactions is reached for specific accounts.
65+
ErrInflightTxLimitReached = errors.New("in-flight transaction limit reached for delegated accounts")
66+
67+
// ErrAuthorityReserved is returned if a transaction has an authorization
68+
// signed by an address which already has in-flight transactions known to the
69+
// pool.
70+
ErrAuthorityReserved = errors.New("authority already reserved")
71+
72+
// ErrFutureReplacePending is returned if a future transaction replaces a pending
73+
// one. Future transactions should only be able to replace other future transactions.
74+
ErrFutureReplacePending = errors.New("future transaction tries to replace pending")
75+
)
76+
5677
var (
5778
evictionInterval = time.Minute // Time interval to check for evictable transactions
5879
statsReportInterval = 8 * time.Second // Time interval to report transaction pool stats
@@ -186,6 +207,20 @@ func (config *Config) sanitize() Config {
186207
// The pool separates processable transactions (which can be applied to the
187208
// current state) and future transactions. Transactions move between those
188209
// two states over time as they are received and processed.
210+
//
211+
// In addition to tracking transactions, the pool also tracks a set of pending SetCode
212+
// authorizations (EIP7702). This helps minimize number of transactions that can be
213+
// trivially churned in the pool. As a standard rule, any account with a deployed
214+
// delegation or an in-flight authorization to deploy a delegation will only be allowed a
215+
// single transaction slot instead of the standard number. This is due to the possibility
216+
// of the account being sweeped by an unrelated account.
217+
//
218+
// Because SetCode transactions can have many authorizations included, we avoid explicitly
219+
// checking their validity to save the state lookup. So long as the encompassing
220+
// transaction is valid, the authorization will be accepted and tracked by the pool. In
221+
// case the pool is tracking a pending / queued transaction from a specific account, it
222+
// will reject new transactions with delegations from that account with standard in-flight
223+
// transactions.
189224
type LegacyPool struct {
190225
config Config
191226
chainconfig *params.ChainConfig
@@ -270,7 +305,7 @@ func New(config Config, chainconfig *params.ChainConfig, chain blockChain) *Lega
270305
// pool, specifically, whether it is a Legacy, AccessList, Dynamic or Sponsored transaction.
271306
func (pool *LegacyPool) Filter(tx *types.Transaction) bool {
272307
switch tx.Type() {
273-
case types.LegacyTxType, types.AccessListTxType, types.DynamicFeeTxType, types.SponsoredTxType:
308+
case types.LegacyTxType, types.AccessListTxType, types.DynamicFeeTxType, types.SponsoredTxType, types.SetCodeTxType:
274309
return true
275310
default:
276311
return false
@@ -594,9 +629,10 @@ func (pool *LegacyPool) validateTxBasics(tx *types.Transaction, local bool) erro
594629
opts := &txpool.ValidationOptions{
595630
Config: pool.chainconfig,
596631
Accept: 0 |
597-
1<<types.LegacyTxType |
598-
1<<types.AccessListTxType |
599-
1<<types.DynamicFeeTxType,
632+
1<<types.LegacyTxType |
633+
1<<types.AccessListTxType |
634+
1<<types.DynamicFeeTxType |
635+
1<<types.SetCodeTxType,
600636
MaxSize: txMaxSize,
601637
MinTip: pool.gasTip.Load(),
602638
AcceptSponsoredTx: true,
@@ -614,21 +650,11 @@ func (pool *LegacyPool) validateTxBasics(tx *types.Transaction, local bool) erro
614650
// rules and adheres to some heuristic limits of the local node (price and size).
615651
func (pool *LegacyPool) validateTx(tx *types.Transaction, local bool) error {
616652
opts := &txpool.ValidationOptionsWithState{
617-
Config: pool.chainconfig,
618-
State: pool.currentState,
619-
Head: pool.currentHead.Load(),
620-
FirstNonceGap: nil, // Pool allows arbitrary arrival order, don't invalidate nonce gaps
621-
// The global and account slot and queue are checked later
622-
UsedAndLeftSlots: func(addr common.Address) (int, int) {
623-
var have int
624-
if list := pool.pending[addr]; list != nil {
625-
have += list.Len()
626-
}
627-
if list := pool.queue[addr]; list != nil {
628-
have += list.Len()
629-
}
630-
return have, math.MaxInt
631-
},
653+
Config: pool.chainconfig,
654+
State: pool.currentState,
655+
Head: pool.currentHead.Load(),
656+
FirstNonceGap: nil, // Pool allows arbitrary arrival order, don't invalidate nonce gaps
657+
UsedAndLeftSlots: nil, // Pool has own mechanism to limit the number of transactions
632658
ExistingExpenditure: func(addr common.Address) *big.Int {
633659
return pool.getAccountPendingCost(addr)
634660
},
@@ -645,6 +671,56 @@ func (pool *LegacyPool) validateTx(tx *types.Transaction, local bool) error {
645671
return err
646672
}
647673

674+
return pool.validateAuth(tx)
675+
}
676+
677+
// validateAuth verifies that the transaction complies with code authorization
678+
// restrictions brought by SetCode transaction type.
679+
func (pool *LegacyPool) validateAuth(tx *types.Transaction) error {
680+
from, _ := types.Sender(pool.signer, tx) // validated
681+
682+
// Allow at most one in-flight tx for delegated accounts or those with a
683+
// pending authorization.
684+
if pool.currentState.GetCodeHash(from) != types.EmptyCodeHash || len(pool.all.auths[from]) != 0 {
685+
var (
686+
count int
687+
exists bool
688+
)
689+
pending := pool.pending[from]
690+
if pending != nil {
691+
count += pending.Len()
692+
exists = pending.Contains(tx.Nonce())
693+
}
694+
queue := pool.queue[from]
695+
if queue != nil {
696+
count += queue.Len()
697+
exists = exists || queue.Contains(tx.Nonce())
698+
}
699+
// Replace the existing in-flight transaction for delegated accounts
700+
// are still supported
701+
if count >= 1 && !exists {
702+
return ErrInflightTxLimitReached
703+
}
704+
}
705+
// Allow at most one in-flight tx for delegated accounts or those with a
706+
// pending authorization in case of sponsor tx.
707+
if tx.Type() == types.SponsoredTxType {
708+
payer, err := types.Payer(pool.signer, tx)
709+
if err != nil {
710+
return err
711+
}
712+
if pool.currentState.GetCodeHash(payer) != types.EmptyCodeHash || len(pool.all.auths[payer]) != 0 {
713+
return ErrInflightTxLimitReached
714+
}
715+
}
716+
// Authorities cannot conflict with any pending or queued transactions.
717+
if auths := tx.SetCodeAuthorities(); len(auths) > 0 {
718+
for _, auth := range auths {
719+
if pool.pending[auth] != nil || pool.queue[auth] != nil || pool.totalPendingPayerCost[auth] != nil {
720+
return ErrAuthorityReserved
721+
}
722+
}
723+
}
648724
return nil
649725
}
650726

@@ -712,7 +788,7 @@ func (pool *LegacyPool) add(tx *types.Transaction, local bool) (replaced bool, e
712788
// replacements to 25% of the slots
713789
if pool.changesSinceReorg > int(pool.config.GlobalSlots/4) {
714790
throttleTxMeter.Mark(1)
715-
return false, txpool.ErrTxPoolOverflow
791+
return false, ErrTxPoolOverflow
716792
}
717793

718794
// New transaction is better than our worse ones, make room for it.
@@ -724,7 +800,7 @@ func (pool *LegacyPool) add(tx *types.Transaction, local bool) (replaced bool, e
724800
if !local && !success {
725801
log.Trace("Discarding overflown transaction", "hash", hash)
726802
overflowedTxMeter.Mark(1)
727-
return false, txpool.ErrTxPoolOverflow
803+
return false, ErrTxPoolOverflow
728804
}
729805

730806
// If the new transaction is a future transaction it should never churn pending transactions
@@ -743,7 +819,7 @@ func (pool *LegacyPool) add(tx *types.Transaction, local bool) (replaced bool, e
743819
heap.Push(&pool.priced.urgent, dropTx)
744820
}
745821
log.Trace("Discarding future transaction replacing pending tx", "hash", hash)
746-
return false, txpool.ErrFutureReplacePending
822+
return false, ErrFutureReplacePending
747823
}
748824
}
749825

@@ -1415,8 +1491,7 @@ func (pool *LegacyPool) promoteExecutables(accounts []common.Address) []*types.T
14151491
// Drop all transactions that are deemed too old (low nonce)
14161492
forwards := list.Forward(pool.currentState.GetNonce(addr))
14171493
for _, tx := range forwards {
1418-
hash := tx.Hash()
1419-
pool.all.Remove(hash)
1494+
pool.all.Remove(tx.Hash())
14201495
}
14211496
log.Trace("Removed old queued transactions", "count", len(forwards))
14221497
payers := list.Payers()
@@ -1430,8 +1505,7 @@ func (pool *LegacyPool) promoteExecutables(accounts []common.Address) []*types.T
14301505
maxGas := txpool.CurrentBlockMaxGas(pool.chainconfig, head)
14311506
drops, _ := list.Filter(pool.currentState.GetBalance(addr), maxGas, payerCostLimit, head.Time)
14321507
for _, tx := range drops {
1433-
hash := tx.Hash()
1434-
pool.all.Remove(hash)
1508+
pool.all.Remove(tx.Hash())
14351509
}
14361510
log.Trace("Removed unpayable queued transactions", "count", len(drops))
14371511
queuedNofundsMeter.Mark(int64(len(drops)))
@@ -1639,8 +1713,8 @@ func (pool *LegacyPool) demoteUnexecutables() {
16391713
drops, invalids := list.Filter(pool.currentState.GetBalance(addr), maxGas, payerCostLimit, head.Time)
16401714
for _, tx := range drops {
16411715
hash := tx.Hash()
1642-
log.Trace("Removed unpayable pending transaction", "hash", hash)
16431716
pool.all.Remove(hash)
1717+
log.Trace("Removed unpayable pending transaction", "hash", hash)
16441718
}
16451719
pendingNofundsMeter.Mark(int64(len(drops)))
16461720

@@ -1677,6 +1751,41 @@ func (pool *LegacyPool) demoteUnexecutables() {
16771751
}
16781752
}
16791753

1754+
// Clear removing all tracked txs from the pool
1755+
// and rotating the journal.
1756+
func (pool *LegacyPool) Clear() {
1757+
pool.mu.Lock()
1758+
defer pool.mu.Unlock()
1759+
1760+
// unreserve each tracked account. Ideally, we could just clear the
1761+
// reservation map in the parent txpool context. However, if we clear in
1762+
// parent context, to avoid exposing the subpool lock, we have to lock the
1763+
// reservations and then lock each subpool.
1764+
//
1765+
// This creates the potential for a deadlock situation:
1766+
//
1767+
// * TxPool.Clear locks the reservations
1768+
// * a new transaction is received which locks the subpool mutex
1769+
// * TxPool.Clear attempts to lock subpool mutex
1770+
//
1771+
// The transaction addition may attempt to reserve the sender addr which
1772+
// can't happen until Clear releases the reservation lock. Clear cannot
1773+
// acquire the subpool lock until the transaction addition is completed.
1774+
for _, tx := range pool.all.locals {
1775+
senderAddr, _ := types.Sender(pool.signer, tx)
1776+
pool.reserve(senderAddr, false)
1777+
}
1778+
for _, tx := range pool.all.remotes {
1779+
senderAddr, _ := types.Sender(pool.signer, tx)
1780+
pool.reserve(senderAddr, false)
1781+
}
1782+
pool.all = newLookup()
1783+
pool.priced = newPricedList(pool.all)
1784+
pool.pending = make(map[common.Address]*list)
1785+
pool.queue = make(map[common.Address]*list)
1786+
pool.pendingNonces = newNoncer(pool.currentState)
1787+
}
1788+
16801789
// addressByHeartbeat is an account address tagged with its last activity timestamp.
16811790
type addressByHeartbeat struct {
16821791
address common.Address
@@ -1776,13 +1885,15 @@ type lookup struct {
17761885
lock sync.RWMutex
17771886
locals map[common.Hash]*types.Transaction
17781887
remotes map[common.Hash]*types.Transaction
1888+
auths map[common.Address][]common.Hash // All accounts with a pooled authorization
17791889
}
17801890

17811891
// newLookup returns a new lookup structure.
17821892
func newLookup() *lookup {
17831893
return &lookup{
17841894
locals: make(map[common.Hash]*types.Transaction),
17851895
remotes: make(map[common.Hash]*types.Transaction),
1896+
auths: make(map[common.Address][]common.Hash),
17861897
}
17871898
}
17881899

@@ -1873,6 +1984,7 @@ func (t *lookup) Add(tx *types.Transaction, local bool) {
18731984
t.lock.Lock()
18741985
defer t.lock.Unlock()
18751986

1987+
t.addAuthorities(tx)
18761988
t.slots += numSlots(tx)
18771989
slotsGauge.Update(int64(t.slots))
18781990

@@ -1896,6 +2008,7 @@ func (t *lookup) Remove(hash common.Hash) {
18962008
log.Error("No transaction found to be deleted", "hash", hash)
18972009
return
18982010
}
2011+
t.removeAuthorities(tx)
18992012
t.slots -= numSlots(tx)
19002013
slotsGauge.Update(int64(t.slots))
19012014

@@ -1939,6 +2052,44 @@ func (t *lookup) RemotesBelowTip(threshold *big.Int, isVenoki bool) types.Transa
19392052
return found
19402053
}
19412054

2055+
// addAuthorities tracks the supplied tx in relation to each authority it
2056+
// specifies.
2057+
func (t *lookup) addAuthorities(tx *types.Transaction) {
2058+
for _, addr := range tx.SetCodeAuthorities() {
2059+
list, ok := t.auths[addr]
2060+
if !ok {
2061+
list = []common.Hash{}
2062+
}
2063+
if slices.Contains(list, tx.Hash()) {
2064+
// Don't add duplicates.
2065+
continue
2066+
}
2067+
list = append(list, tx.Hash())
2068+
t.auths[addr] = list
2069+
}
2070+
}
2071+
2072+
// removeAuthorities stops tracking the supplied tx in relation to its
2073+
// authorities.
2074+
func (t *lookup) removeAuthorities(tx *types.Transaction) {
2075+
hash := tx.Hash()
2076+
for _, addr := range tx.SetCodeAuthorities() {
2077+
list := t.auths[addr]
2078+
// Remove tx from tracker.
2079+
if i := slices.Index(list, hash); i >= 0 {
2080+
list = append(list[:i], list[i+1:]...)
2081+
} else {
2082+
log.Error("Authority with untracked tx", "addr", addr, "hash", hash)
2083+
}
2084+
if len(list) == 0 {
2085+
// If list is newly empty, delete it entirely.
2086+
delete(t.auths, addr)
2087+
continue
2088+
}
2089+
t.auths[addr] = list
2090+
}
2091+
}
2092+
19422093
// numSlots calculates the number of slots needed for a single transaction.
19432094
func numSlots(tx *types.Transaction) int {
19442095
return int((tx.Size() + txSlotSize - 1) / txSlotSize)

0 commit comments

Comments
 (0)