@@ -18,8 +18,10 @@ package legacypool
18
18
19
19
import (
20
20
"container/heap"
21
+ "errors"
21
22
"math"
22
23
"math/big"
24
+ "slices"
23
25
"sort"
24
26
"sync"
25
27
"sync/atomic"
@@ -53,6 +55,25 @@ const (
53
55
txMaxSize = 4 * txSlotSize // 128KB
54
56
)
55
57
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
+
56
77
var (
57
78
evictionInterval = time .Minute // Time interval to check for evictable transactions
58
79
statsReportInterval = 8 * time .Second // Time interval to report transaction pool stats
@@ -186,6 +207,20 @@ func (config *Config) sanitize() Config {
186
207
// The pool separates processable transactions (which can be applied to the
187
208
// current state) and future transactions. Transactions move between those
188
209
// 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.
189
224
type LegacyPool struct {
190
225
config Config
191
226
chainconfig * params.ChainConfig
@@ -270,7 +305,7 @@ func New(config Config, chainconfig *params.ChainConfig, chain blockChain) *Lega
270
305
// pool, specifically, whether it is a Legacy, AccessList, Dynamic or Sponsored transaction.
271
306
func (pool * LegacyPool ) Filter (tx * types.Transaction ) bool {
272
307
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 :
274
309
return true
275
310
default :
276
311
return false
@@ -594,9 +629,10 @@ func (pool *LegacyPool) validateTxBasics(tx *types.Transaction, local bool) erro
594
629
opts := & txpool.ValidationOptions {
595
630
Config : pool .chainconfig ,
596
631
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 ,
600
636
MaxSize : txMaxSize ,
601
637
MinTip : pool .gasTip .Load (),
602
638
AcceptSponsoredTx : true ,
@@ -614,21 +650,11 @@ func (pool *LegacyPool) validateTxBasics(tx *types.Transaction, local bool) erro
614
650
// rules and adheres to some heuristic limits of the local node (price and size).
615
651
func (pool * LegacyPool ) validateTx (tx * types.Transaction , local bool ) error {
616
652
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
632
658
ExistingExpenditure : func (addr common.Address ) * big.Int {
633
659
return pool .getAccountPendingCost (addr )
634
660
},
@@ -645,6 +671,56 @@ func (pool *LegacyPool) validateTx(tx *types.Transaction, local bool) error {
645
671
return err
646
672
}
647
673
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
+ }
648
724
return nil
649
725
}
650
726
@@ -712,7 +788,7 @@ func (pool *LegacyPool) add(tx *types.Transaction, local bool) (replaced bool, e
712
788
// replacements to 25% of the slots
713
789
if pool .changesSinceReorg > int (pool .config .GlobalSlots / 4 ) {
714
790
throttleTxMeter .Mark (1 )
715
- return false , txpool . ErrTxPoolOverflow
791
+ return false , ErrTxPoolOverflow
716
792
}
717
793
718
794
// 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
724
800
if ! local && ! success {
725
801
log .Trace ("Discarding overflown transaction" , "hash" , hash )
726
802
overflowedTxMeter .Mark (1 )
727
- return false , txpool . ErrTxPoolOverflow
803
+ return false , ErrTxPoolOverflow
728
804
}
729
805
730
806
// 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
743
819
heap .Push (& pool .priced .urgent , dropTx )
744
820
}
745
821
log .Trace ("Discarding future transaction replacing pending tx" , "hash" , hash )
746
- return false , txpool . ErrFutureReplacePending
822
+ return false , ErrFutureReplacePending
747
823
}
748
824
}
749
825
@@ -1415,8 +1491,7 @@ func (pool *LegacyPool) promoteExecutables(accounts []common.Address) []*types.T
1415
1491
// Drop all transactions that are deemed too old (low nonce)
1416
1492
forwards := list .Forward (pool .currentState .GetNonce (addr ))
1417
1493
for _ , tx := range forwards {
1418
- hash := tx .Hash ()
1419
- pool .all .Remove (hash )
1494
+ pool .all .Remove (tx .Hash ())
1420
1495
}
1421
1496
log .Trace ("Removed old queued transactions" , "count" , len (forwards ))
1422
1497
payers := list .Payers ()
@@ -1430,8 +1505,7 @@ func (pool *LegacyPool) promoteExecutables(accounts []common.Address) []*types.T
1430
1505
maxGas := txpool .CurrentBlockMaxGas (pool .chainconfig , head )
1431
1506
drops , _ := list .Filter (pool .currentState .GetBalance (addr ), maxGas , payerCostLimit , head .Time )
1432
1507
for _ , tx := range drops {
1433
- hash := tx .Hash ()
1434
- pool .all .Remove (hash )
1508
+ pool .all .Remove (tx .Hash ())
1435
1509
}
1436
1510
log .Trace ("Removed unpayable queued transactions" , "count" , len (drops ))
1437
1511
queuedNofundsMeter .Mark (int64 (len (drops )))
@@ -1639,8 +1713,8 @@ func (pool *LegacyPool) demoteUnexecutables() {
1639
1713
drops , invalids := list .Filter (pool .currentState .GetBalance (addr ), maxGas , payerCostLimit , head .Time )
1640
1714
for _ , tx := range drops {
1641
1715
hash := tx .Hash ()
1642
- log .Trace ("Removed unpayable pending transaction" , "hash" , hash )
1643
1716
pool .all .Remove (hash )
1717
+ log .Trace ("Removed unpayable pending transaction" , "hash" , hash )
1644
1718
}
1645
1719
pendingNofundsMeter .Mark (int64 (len (drops )))
1646
1720
@@ -1677,6 +1751,41 @@ func (pool *LegacyPool) demoteUnexecutables() {
1677
1751
}
1678
1752
}
1679
1753
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
+
1680
1789
// addressByHeartbeat is an account address tagged with its last activity timestamp.
1681
1790
type addressByHeartbeat struct {
1682
1791
address common.Address
@@ -1776,13 +1885,15 @@ type lookup struct {
1776
1885
lock sync.RWMutex
1777
1886
locals map [common.Hash ]* types.Transaction
1778
1887
remotes map [common.Hash ]* types.Transaction
1888
+ auths map [common.Address ][]common.Hash // All accounts with a pooled authorization
1779
1889
}
1780
1890
1781
1891
// newLookup returns a new lookup structure.
1782
1892
func newLookup () * lookup {
1783
1893
return & lookup {
1784
1894
locals : make (map [common.Hash ]* types.Transaction ),
1785
1895
remotes : make (map [common.Hash ]* types.Transaction ),
1896
+ auths : make (map [common.Address ][]common.Hash ),
1786
1897
}
1787
1898
}
1788
1899
@@ -1873,6 +1984,7 @@ func (t *lookup) Add(tx *types.Transaction, local bool) {
1873
1984
t .lock .Lock ()
1874
1985
defer t .lock .Unlock ()
1875
1986
1987
+ t .addAuthorities (tx )
1876
1988
t .slots += numSlots (tx )
1877
1989
slotsGauge .Update (int64 (t .slots ))
1878
1990
@@ -1896,6 +2008,7 @@ func (t *lookup) Remove(hash common.Hash) {
1896
2008
log .Error ("No transaction found to be deleted" , "hash" , hash )
1897
2009
return
1898
2010
}
2011
+ t .removeAuthorities (tx )
1899
2012
t .slots -= numSlots (tx )
1900
2013
slotsGauge .Update (int64 (t .slots ))
1901
2014
@@ -1939,6 +2052,44 @@ func (t *lookup) RemotesBelowTip(threshold *big.Int, isVenoki bool) types.Transa
1939
2052
return found
1940
2053
}
1941
2054
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
+
1942
2093
// numSlots calculates the number of slots needed for a single transaction.
1943
2094
func numSlots (tx * types.Transaction ) int {
1944
2095
return int ((tx .Size () + txSlotSize - 1 ) / txSlotSize )
0 commit comments