Skip to content

Commit 42d2f4a

Browse files
trantienduchnlightclientMariusVanDerWijdenfjlrjl493456442
authored andcommitted
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 c4f55d9 commit 42d2f4a

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"
@@ -55,6 +57,25 @@ const (
5557
txMaxSize = 4 * txSlotSize // 128KB
5658
)
5759

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

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

@@ -714,7 +790,7 @@ func (pool *LegacyPool) add(tx *types.Transaction, local bool) (replaced bool, e
714790
// replacements to 25% of the slots
715791
if pool.changesSinceReorg > int(pool.config.GlobalSlots/4) {
716792
throttleTxMeter.Mark(1)
717-
return false, txpool.ErrTxPoolOverflow
793+
return false, ErrTxPoolOverflow
718794
}
719795

720796
// New transaction is better than our worse ones, make room for it.
@@ -726,7 +802,7 @@ func (pool *LegacyPool) add(tx *types.Transaction, local bool) (replaced bool, e
726802
if !local && !success {
727803
log.Trace("Discarding overflown transaction", "hash", hash)
728804
overflowedTxMeter.Mark(1)
729-
return false, txpool.ErrTxPoolOverflow
805+
return false, ErrTxPoolOverflow
730806
}
731807

732808
// If the new transaction is a future transaction it should never churn pending transactions
@@ -745,7 +821,7 @@ func (pool *LegacyPool) add(tx *types.Transaction, local bool) (replaced bool, e
745821
heap.Push(&pool.priced.urgent, dropTx)
746822
}
747823
log.Trace("Discarding future transaction replacing pending tx", "hash", hash)
748-
return false, txpool.ErrFutureReplacePending
824+
return false, ErrFutureReplacePending
749825
}
750826
}
751827

@@ -1438,8 +1514,7 @@ func (pool *LegacyPool) promoteExecutables(accounts []common.Address) []*types.T
14381514
// Drop all transactions that are deemed too old (low nonce)
14391515
forwards := list.Forward(pool.currentState.GetNonce(addr))
14401516
for _, tx := range forwards {
1441-
hash := tx.Hash()
1442-
pool.all.Remove(hash)
1517+
pool.all.Remove(tx.Hash())
14431518
}
14441519
log.Trace("Removed old queued transactions", "count", len(forwards))
14451520
payers := list.Payers()
@@ -1453,8 +1528,7 @@ func (pool *LegacyPool) promoteExecutables(accounts []common.Address) []*types.T
14531528
maxGas := txpool.CurrentBlockMaxGas(pool.chainconfig, head)
14541529
drops, _ := list.Filter(pool.currentState.GetBalance(addr), maxGas, payerCostLimit, head.Time)
14551530
for _, tx := range drops {
1456-
hash := tx.Hash()
1457-
pool.all.Remove(hash)
1531+
pool.all.Remove(tx.Hash())
14581532
}
14591533
log.Trace("Removed unpayable queued transactions", "count", len(drops))
14601534
queuedNofundsMeter.Mark(int64(len(drops)))
@@ -1662,8 +1736,8 @@ func (pool *LegacyPool) demoteUnexecutables() {
16621736
drops, invalids := list.Filter(pool.currentState.GetBalance(addr), maxGas, payerCostLimit, head.Time)
16631737
for _, tx := range drops {
16641738
hash := tx.Hash()
1665-
log.Trace("Removed unpayable pending transaction", "hash", hash)
16661739
pool.all.Remove(hash)
1740+
log.Trace("Removed unpayable pending transaction", "hash", hash)
16671741
}
16681742
pendingNofundsMeter.Mark(int64(len(drops)))
16691743

@@ -1700,6 +1774,41 @@ func (pool *LegacyPool) demoteUnexecutables() {
17001774
}
17011775
}
17021776

1777+
// Clear removing all tracked txs from the pool
1778+
// and rotating the journal.
1779+
func (pool *LegacyPool) Clear() {
1780+
pool.mu.Lock()
1781+
defer pool.mu.Unlock()
1782+
1783+
// unreserve each tracked account. Ideally, we could just clear the
1784+
// reservation map in the parent txpool context. However, if we clear in
1785+
// parent context, to avoid exposing the subpool lock, we have to lock the
1786+
// reservations and then lock each subpool.
1787+
//
1788+
// This creates the potential for a deadlock situation:
1789+
//
1790+
// * TxPool.Clear locks the reservations
1791+
// * a new transaction is received which locks the subpool mutex
1792+
// * TxPool.Clear attempts to lock subpool mutex
1793+
//
1794+
// The transaction addition may attempt to reserve the sender addr which
1795+
// can't happen until Clear releases the reservation lock. Clear cannot
1796+
// acquire the subpool lock until the transaction addition is completed.
1797+
for _, tx := range pool.all.locals {
1798+
senderAddr, _ := types.Sender(pool.signer, tx)
1799+
pool.reserve(senderAddr, false)
1800+
}
1801+
for _, tx := range pool.all.remotes {
1802+
senderAddr, _ := types.Sender(pool.signer, tx)
1803+
pool.reserve(senderAddr, false)
1804+
}
1805+
pool.all = newLookup()
1806+
pool.priced = newPricedList(pool.all)
1807+
pool.pending = make(map[common.Address]*list)
1808+
pool.queue = make(map[common.Address]*list)
1809+
pool.pendingNonces = newNoncer(pool.currentState)
1810+
}
1811+
17031812
// addressByHeartbeat is an account address tagged with its last activity timestamp.
17041813
type addressByHeartbeat struct {
17051814
address common.Address
@@ -1799,13 +1908,15 @@ type lookup struct {
17991908
lock sync.RWMutex
18001909
locals map[common.Hash]*types.Transaction
18011910
remotes map[common.Hash]*types.Transaction
1911+
auths map[common.Address][]common.Hash // All accounts with a pooled authorization
18021912
}
18031913

18041914
// newLookup returns a new lookup structure.
18051915
func newLookup() *lookup {
18061916
return &lookup{
18071917
locals: make(map[common.Hash]*types.Transaction),
18081918
remotes: make(map[common.Hash]*types.Transaction),
1919+
auths: make(map[common.Address][]common.Hash),
18091920
}
18101921
}
18111922

@@ -1896,6 +2007,7 @@ func (t *lookup) Add(tx *types.Transaction, local bool) {
18962007
t.lock.Lock()
18972008
defer t.lock.Unlock()
18982009

2010+
t.addAuthorities(tx)
18992011
t.slots += numSlots(tx)
19002012
slotsGauge.Update(int64(t.slots))
19012013

@@ -1919,6 +2031,7 @@ func (t *lookup) Remove(hash common.Hash) {
19192031
log.Error("No transaction found to be deleted", "hash", hash)
19202032
return
19212033
}
2034+
t.removeAuthorities(tx)
19222035
t.slots -= numSlots(tx)
19232036
slotsGauge.Update(int64(t.slots))
19242037

@@ -1962,6 +2075,44 @@ func (t *lookup) RemotesBelowTip(threshold *big.Int, isVenoki bool) types.Transa
19622075
return found
19632076
}
19642077

2078+
// addAuthorities tracks the supplied tx in relation to each authority it
2079+
// specifies.
2080+
func (t *lookup) addAuthorities(tx *types.Transaction) {
2081+
for _, addr := range tx.SetCodeAuthorities() {
2082+
list, ok := t.auths[addr]
2083+
if !ok {
2084+
list = []common.Hash{}
2085+
}
2086+
if slices.Contains(list, tx.Hash()) {
2087+
// Don't add duplicates.
2088+
continue
2089+
}
2090+
list = append(list, tx.Hash())
2091+
t.auths[addr] = list
2092+
}
2093+
}
2094+
2095+
// removeAuthorities stops tracking the supplied tx in relation to its
2096+
// authorities.
2097+
func (t *lookup) removeAuthorities(tx *types.Transaction) {
2098+
hash := tx.Hash()
2099+
for _, addr := range tx.SetCodeAuthorities() {
2100+
list := t.auths[addr]
2101+
// Remove tx from tracker.
2102+
if i := slices.Index(list, hash); i >= 0 {
2103+
list = append(list[:i], list[i+1:]...)
2104+
} else {
2105+
log.Error("Authority with untracked tx", "addr", addr, "hash", hash)
2106+
}
2107+
if len(list) == 0 {
2108+
// If list is newly empty, delete it entirely.
2109+
delete(t.auths, addr)
2110+
continue
2111+
}
2112+
t.auths[addr] = list
2113+
}
2114+
}
2115+
19652116
// numSlots calculates the number of slots needed for a single transaction.
19662117
func numSlots(tx *types.Transaction) int {
19672118
return int((tx.Size() + txSlotSize - 1) / txSlotSize)

0 commit comments

Comments
 (0)