Skip to content

Commit cdb66c8

Browse files
lightclientMariusVanDerWijdenfjl
authored
core/txpool/legacypool: add support for SetCode transactions (#31073)
The new SetCode transaction type introduces some additional complexity when handling the transaction pool. This complexity stems from two new account behaviors: 1. The balance and nonce of an account can change during regular transaction execution *when they have a deployed delegation*. 2. The nonce and code of an account can change without any EVM execution at all. This is the "set code" mechanism introduced by EIP-7702. The first issue has already been considered extensively during the design of ERC-4337, and we're relatively confident in the solution of simply limiting the number of in-flight pending transactions an account can have to one. This puts a reasonable bound on transaction cancellation. Normally to cancel, you would need to spend 21,000 gas. Now it's possible to cancel for around the cost of warming the account and sending value (`2,600+9,000=11,600`). So 50% cheaper. The second issue is more novel and needs further consideration. Since authorizations are not bound to a specific transaction, we cannot drop transactions with conflicting authorizations. Otherwise, it might be possible to cherry-pick authorizations from txs and front run them with different txs at much lower fee amounts, effectively DoSing the authority. Fortunately, conflicting authorizations do not affect the underlying validity of the transaction so we can just accept both. --------- Co-authored-by: Marius van der Wijden <[email protected]> Co-authored-by: Felix Lange <[email protected]>
1 parent 22b9354 commit cdb66c8

File tree

6 files changed

+368
-17
lines changed

6 files changed

+368
-17
lines changed

core/txpool/blobpool/blobpool_test.go

Lines changed: 4 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -142,6 +142,7 @@ func (bc *testBlockChain) CurrentBlock() *types.Header {
142142
GasLimit: gasLimit,
143143
BaseFee: baseFee,
144144
ExcessBlobGas: &excessBlobGas,
145+
Difficulty: common.Big0,
145146
}
146147
}
147148

@@ -1565,8 +1566,9 @@ func TestAdd(t *testing.T) {
15651566
if tt.block != nil {
15661567
// Fake a header for the new set of transactions
15671568
header := &types.Header{
1568-
Number: big.NewInt(int64(chain.CurrentBlock().Number.Uint64() + 1)),
1569-
BaseFee: chain.CurrentBlock().BaseFee, // invalid, but nothing checks it, yolo
1569+
Number: big.NewInt(int64(chain.CurrentBlock().Number.Uint64() + 1)),
1570+
Difficulty: common.Big0,
1571+
BaseFee: chain.CurrentBlock().BaseFee, // invalid, but nothing checks it, yolo
15701572
}
15711573
// Inject the fake block into the chain
15721574
txs := make([]*types.Transaction, len(tt.block))

core/txpool/errors.go

Lines changed: 9 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -60,4 +60,13 @@ var (
6060
// input transaction of non-blob type when a blob transaction from this sender
6161
// remains pending (and vice-versa).
6262
ErrAlreadyReserved = errors.New("address already reserved")
63+
64+
// ErrAuthorityReserved is returned if a transaction has an authorization
65+
// signed by an address which already has in-flight transactions known to the
66+
// pool.
67+
ErrAuthorityReserved = errors.New("authority already reserved")
68+
69+
// ErrAuthorityNonce is returned if a transaction has an authorization with
70+
// a nonce that is not currently valid for the authority.
71+
ErrAuthorityNonceTooLow = errors.New("authority nonce too low")
6372
)

core/txpool/legacypool/legacypool.go

Lines changed: 81 additions & 8 deletions
Original file line numberDiff line numberDiff line change
@@ -21,6 +21,7 @@ import (
2121
"errors"
2222
"math"
2323
"math/big"
24+
"slices"
2425
"sort"
2526
"sync"
2627
"sync/atomic"
@@ -196,6 +197,20 @@ func (config *Config) sanitize() Config {
196197
// The pool separates processable transactions (which can be applied to the
197198
// current state) and future transactions. Transactions move between those
198199
// two states over time as they are received and processed.
200+
//
201+
// In addition to tracking transactions, the pool also tracks a set of pending SetCode
202+
// authorizations (EIP7702). This helps minimize number of transactions that can be
203+
// trivially churned in the pool. As a standard rule, any account with a deployed
204+
// delegation or an in-flight authorization to deploy a delegation will only be allowed a
205+
// single transaction slot instead of the standard number. This is due to the possibility
206+
// of the account being sweeped by an unrelated account.
207+
//
208+
// Because SetCode transactions can have many authorizations included, we avoid explicitly
209+
// checking their validity to save the state lookup. So long as the encompassing
210+
// transaction is valid, the authorization will be accepted and tracked by the pool. In
211+
// case the pool is tracking a pending / queued transaction from a specific account, it
212+
// will reject new transactions with delegations from that account with standard in-flight
213+
// transactions.
199214
type LegacyPool struct {
200215
config Config
201216
chainconfig *params.ChainConfig
@@ -263,7 +278,7 @@ func New(config Config, chain BlockChain) *LegacyPool {
263278
// pool, specifically, whether it is a Legacy, AccessList or Dynamic transaction.
264279
func (pool *LegacyPool) Filter(tx *types.Transaction) bool {
265280
switch tx.Type() {
266-
case types.LegacyTxType, types.AccessListTxType, types.DynamicFeeTxType:
281+
case types.LegacyTxType, types.AccessListTxType, types.DynamicFeeTxType, types.SetCodeTxType:
267282
return true
268283
default:
269284
return false
@@ -540,7 +555,8 @@ func (pool *LegacyPool) validateTxBasics(tx *types.Transaction) error {
540555
Accept: 0 |
541556
1<<types.LegacyTxType |
542557
1<<types.AccessListTxType |
543-
1<<types.DynamicFeeTxType,
558+
1<<types.DynamicFeeTxType |
559+
1<<types.SetCodeTxType,
544560
MaxSize: txMaxSize,
545561
MinTip: pool.gasTip.Load().ToBig(),
546562
}
@@ -565,6 +581,11 @@ func (pool *LegacyPool) validateTx(tx *types.Transaction) error {
565581
if list := pool.queue[addr]; list != nil {
566582
have += list.Len()
567583
}
584+
if pool.currentState.GetCodeHash(addr) != types.EmptyCodeHash || len(pool.all.auths[addr]) != 0 {
585+
// Allow at most one in-flight tx for delegated accounts or those with
586+
// a pending authorization.
587+
return have, max(0, 1-have)
588+
}
568589
return have, math.MaxInt
569590
},
570591
ExistingExpenditure: func(addr common.Address) *big.Int {
@@ -581,6 +602,18 @@ func (pool *LegacyPool) validateTx(tx *types.Transaction) error {
581602
}
582603
return nil
583604
},
605+
KnownConflicts: func(from common.Address, auths []common.Address) []common.Address {
606+
var conflicts []common.Address
607+
// Authorities cannot conflict with any pending or queued transactions.
608+
for _, addr := range auths {
609+
if list := pool.pending[addr]; list != nil {
610+
conflicts = append(conflicts, addr)
611+
} else if list := pool.queue[addr]; list != nil {
612+
conflicts = append(conflicts, addr)
613+
}
614+
}
615+
return conflicts
616+
},
584617
}
585618
if err := txpool.ValidateTransactionWithState(tx, pool.signer, opts); err != nil {
586619
return err
@@ -1334,15 +1367,13 @@ func (pool *LegacyPool) promoteExecutables(accounts []common.Address) []*types.T
13341367
// Drop all transactions that are deemed too old (low nonce)
13351368
forwards := list.Forward(pool.currentState.GetNonce(addr))
13361369
for _, tx := range forwards {
1337-
hash := tx.Hash()
1338-
pool.all.Remove(hash)
1370+
pool.all.Remove(tx.Hash())
13391371
}
13401372
log.Trace("Removed old queued transactions", "count", len(forwards))
13411373
// Drop all transactions that are too costly (low balance or out of gas)
13421374
drops, _ := list.Filter(pool.currentState.GetBalance(addr), gasLimit)
13431375
for _, tx := range drops {
1344-
hash := tx.Hash()
1345-
pool.all.Remove(hash)
1376+
pool.all.Remove(tx.Hash())
13461377
}
13471378
log.Trace("Removed unpayable queued transactions", "count", len(drops))
13481379
queuedNofundsMeter.Mark(int64(len(drops)))
@@ -1531,8 +1562,8 @@ func (pool *LegacyPool) demoteUnexecutables() {
15311562
drops, invalids := list.Filter(pool.currentState.GetBalance(addr), gasLimit)
15321563
for _, tx := range drops {
15331564
hash := tx.Hash()
1534-
log.Trace("Removed unpayable pending transaction", "hash", hash)
15351565
pool.all.Remove(hash)
1566+
log.Trace("Removed unpayable pending transaction", "hash", hash)
15361567
}
15371568
pendingNofundsMeter.Mark(int64(len(drops)))
15381569

@@ -1641,12 +1672,15 @@ type lookup struct {
16411672
slots int
16421673
lock sync.RWMutex
16431674
txs map[common.Hash]*types.Transaction
1675+
1676+
auths map[common.Address][]common.Hash // All accounts with a pooled authorization
16441677
}
16451678

16461679
// newLookup returns a new lookup structure.
16471680
func newLookup() *lookup {
16481681
return &lookup{
1649-
txs: make(map[common.Hash]*types.Transaction),
1682+
txs: make(map[common.Hash]*types.Transaction),
1683+
auths: make(map[common.Address][]common.Hash),
16501684
}
16511685
}
16521686

@@ -1697,13 +1731,15 @@ func (t *lookup) Add(tx *types.Transaction) {
16971731
slotsGauge.Update(int64(t.slots))
16981732

16991733
t.txs[tx.Hash()] = tx
1734+
t.addAuthorities(tx)
17001735
}
17011736

17021737
// Remove removes a transaction from the lookup.
17031738
func (t *lookup) Remove(hash common.Hash) {
17041739
t.lock.Lock()
17051740
defer t.lock.Unlock()
17061741

1742+
t.removeAuthorities(hash)
17071743
tx, ok := t.txs[hash]
17081744
if !ok {
17091745
log.Error("No transaction found to be deleted", "hash", hash)
@@ -1727,6 +1763,43 @@ func (t *lookup) TxsBelowTip(threshold *big.Int) types.Transactions {
17271763
return found
17281764
}
17291765

1766+
// addAuthorities tracks the supplied tx in relation to each authority it
1767+
// specifies.
1768+
func (t *lookup) addAuthorities(tx *types.Transaction) {
1769+
for _, addr := range tx.SetCodeAuthorities() {
1770+
list, ok := t.auths[addr]
1771+
if !ok {
1772+
list = []common.Hash{}
1773+
}
1774+
if slices.Contains(list, tx.Hash()) {
1775+
// Don't add duplicates.
1776+
continue
1777+
}
1778+
list = append(list, tx.Hash())
1779+
t.auths[addr] = list
1780+
}
1781+
}
1782+
1783+
// removeAuthorities stops tracking the supplied tx in relation to its
1784+
// authorities.
1785+
func (t *lookup) removeAuthorities(hash common.Hash) {
1786+
for addr := range t.auths {
1787+
list := t.auths[addr]
1788+
// Remove tx from tracker.
1789+
if i := slices.Index(list, hash); i >= 0 {
1790+
list = append(list[:i], list[i+1:]...)
1791+
} else {
1792+
log.Error("Authority with untracked tx", "addr", addr, "hash", hash)
1793+
}
1794+
if len(list) == 0 {
1795+
// If list is newly empty, delete it entirely.
1796+
delete(t.auths, addr)
1797+
continue
1798+
}
1799+
t.auths[addr] = list
1800+
}
1801+
}
1802+
17301803
// numSlots calculates the number of slots needed for a single transaction.
17311804
func numSlots(tx *types.Transaction) int {
17321805
return int((tx.Size() + txSlotSize - 1) / txSlotSize)

0 commit comments

Comments
 (0)