@@ -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"
@@ -55,6 +57,25 @@ const (
55
57
txMaxSize = 4 * txSlotSize // 128KB
56
58
)
57
59
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
+
58
79
var (
59
80
evictionInterval = time .Minute // Time interval to check for evictable transactions
60
81
statsReportInterval = 8 * time .Second // Time interval to report transaction pool stats
@@ -188,6 +209,20 @@ func (config *Config) sanitize() Config {
188
209
// The pool separates processable transactions (which can be applied to the
189
210
// current state) and future transactions. Transactions move between those
190
211
// 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.
191
226
type LegacyPool struct {
192
227
config Config
193
228
chainconfig * params.ChainConfig
@@ -272,7 +307,7 @@ func New(config Config, chainconfig *params.ChainConfig, chain blockChain) *Lega
272
307
// pool, specifically, whether it is a Legacy, AccessList, Dynamic or Sponsored transaction.
273
308
func (pool * LegacyPool ) Filter (tx * types.Transaction ) bool {
274
309
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 :
276
311
return true
277
312
default :
278
313
return false
@@ -596,9 +631,10 @@ func (pool *LegacyPool) validateTxBasics(tx *types.Transaction, local bool) erro
596
631
opts := & txpool.ValidationOptions {
597
632
Config : pool .chainconfig ,
598
633
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 ,
602
638
MaxSize : txMaxSize ,
603
639
MinTip : pool .gasTip .Load (),
604
640
AcceptSponsoredTx : true ,
@@ -616,21 +652,11 @@ func (pool *LegacyPool) validateTxBasics(tx *types.Transaction, local bool) erro
616
652
// rules and adheres to some heuristic limits of the local node (price and size).
617
653
func (pool * LegacyPool ) validateTx (tx * types.Transaction ) error {
618
654
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
634
660
ExistingExpenditure : func (addr common.Address ) * big.Int {
635
661
return pool .getAccountPendingCost (addr )
636
662
},
@@ -647,6 +673,56 @@ func (pool *LegacyPool) validateTx(tx *types.Transaction) error {
647
673
return err
648
674
}
649
675
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
+ }
650
726
return nil
651
727
}
652
728
@@ -714,7 +790,7 @@ func (pool *LegacyPool) add(tx *types.Transaction, local bool) (replaced bool, e
714
790
// replacements to 25% of the slots
715
791
if pool .changesSinceReorg > int (pool .config .GlobalSlots / 4 ) {
716
792
throttleTxMeter .Mark (1 )
717
- return false , txpool . ErrTxPoolOverflow
793
+ return false , ErrTxPoolOverflow
718
794
}
719
795
720
796
// 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
726
802
if ! local && ! success {
727
803
log .Trace ("Discarding overflown transaction" , "hash" , hash )
728
804
overflowedTxMeter .Mark (1 )
729
- return false , txpool . ErrTxPoolOverflow
805
+ return false , ErrTxPoolOverflow
730
806
}
731
807
732
808
// 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
745
821
heap .Push (& pool .priced .urgent , dropTx )
746
822
}
747
823
log .Trace ("Discarding future transaction replacing pending tx" , "hash" , hash )
748
- return false , txpool . ErrFutureReplacePending
824
+ return false , ErrFutureReplacePending
749
825
}
750
826
}
751
827
@@ -1438,8 +1514,7 @@ func (pool *LegacyPool) promoteExecutables(accounts []common.Address) []*types.T
1438
1514
// Drop all transactions that are deemed too old (low nonce)
1439
1515
forwards := list .Forward (pool .currentState .GetNonce (addr ))
1440
1516
for _ , tx := range forwards {
1441
- hash := tx .Hash ()
1442
- pool .all .Remove (hash )
1517
+ pool .all .Remove (tx .Hash ())
1443
1518
}
1444
1519
log .Trace ("Removed old queued transactions" , "count" , len (forwards ))
1445
1520
payers := list .Payers ()
@@ -1453,8 +1528,7 @@ func (pool *LegacyPool) promoteExecutables(accounts []common.Address) []*types.T
1453
1528
maxGas := txpool .CurrentBlockMaxGas (pool .chainconfig , head )
1454
1529
drops , _ := list .Filter (pool .currentState .GetBalance (addr ), maxGas , payerCostLimit , head .Time )
1455
1530
for _ , tx := range drops {
1456
- hash := tx .Hash ()
1457
- pool .all .Remove (hash )
1531
+ pool .all .Remove (tx .Hash ())
1458
1532
}
1459
1533
log .Trace ("Removed unpayable queued transactions" , "count" , len (drops ))
1460
1534
queuedNofundsMeter .Mark (int64 (len (drops )))
@@ -1662,8 +1736,8 @@ func (pool *LegacyPool) demoteUnexecutables() {
1662
1736
drops , invalids := list .Filter (pool .currentState .GetBalance (addr ), maxGas , payerCostLimit , head .Time )
1663
1737
for _ , tx := range drops {
1664
1738
hash := tx .Hash ()
1665
- log .Trace ("Removed unpayable pending transaction" , "hash" , hash )
1666
1739
pool .all .Remove (hash )
1740
+ log .Trace ("Removed unpayable pending transaction" , "hash" , hash )
1667
1741
}
1668
1742
pendingNofundsMeter .Mark (int64 (len (drops )))
1669
1743
@@ -1700,6 +1774,41 @@ func (pool *LegacyPool) demoteUnexecutables() {
1700
1774
}
1701
1775
}
1702
1776
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
+
1703
1812
// addressByHeartbeat is an account address tagged with its last activity timestamp.
1704
1813
type addressByHeartbeat struct {
1705
1814
address common.Address
@@ -1799,13 +1908,15 @@ type lookup struct {
1799
1908
lock sync.RWMutex
1800
1909
locals map [common.Hash ]* types.Transaction
1801
1910
remotes map [common.Hash ]* types.Transaction
1911
+ auths map [common.Address ][]common.Hash // All accounts with a pooled authorization
1802
1912
}
1803
1913
1804
1914
// newLookup returns a new lookup structure.
1805
1915
func newLookup () * lookup {
1806
1916
return & lookup {
1807
1917
locals : make (map [common.Hash ]* types.Transaction ),
1808
1918
remotes : make (map [common.Hash ]* types.Transaction ),
1919
+ auths : make (map [common.Address ][]common.Hash ),
1809
1920
}
1810
1921
}
1811
1922
@@ -1896,6 +2007,7 @@ func (t *lookup) Add(tx *types.Transaction, local bool) {
1896
2007
t .lock .Lock ()
1897
2008
defer t .lock .Unlock ()
1898
2009
2010
+ t .addAuthorities (tx )
1899
2011
t .slots += numSlots (tx )
1900
2012
slotsGauge .Update (int64 (t .slots ))
1901
2013
@@ -1919,6 +2031,7 @@ func (t *lookup) Remove(hash common.Hash) {
1919
2031
log .Error ("No transaction found to be deleted" , "hash" , hash )
1920
2032
return
1921
2033
}
2034
+ t .removeAuthorities (tx )
1922
2035
t .slots -= numSlots (tx )
1923
2036
slotsGauge .Update (int64 (t .slots ))
1924
2037
@@ -1962,6 +2075,44 @@ func (t *lookup) RemotesBelowTip(threshold *big.Int, isVenoki bool) types.Transa
1962
2075
return found
1963
2076
}
1964
2077
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
+
1965
2116
// numSlots calculates the number of slots needed for a single transaction.
1966
2117
func numSlots (tx * types.Transaction ) int {
1967
2118
return int ((tx .Size () + txSlotSize - 1 ) / txSlotSize )
0 commit comments