Skip to content

Commit 2e739fc

Browse files
core/txpool: add 7702 protection to blobpool (#31526)
This pull request introduces two constraints in the blobPool: (a) If the sender has a pending authorization or delegation, only one in-flight executable transaction can be cached. (b) If the authority address in a SetCode transaction is already reserved by the blobPool, the transaction will be rejected. These constraints mitigate an attack where an attacker spams the pool with numerous blob transactions, evicts other transactions, and then cancels all pending blob transactions by draining the sender’s funds if they have a delegation. Note, because there is no exclusive lock held between different subpools when processing transactions, it's totally possible the SetCode transaction and blob transactions with conflict sender and authorities are accepted simultaneously. I think it's acceptable as it's very hard to be exploited. --------- Co-authored-by: lightclient <[email protected]>
1 parent ec6d104 commit 2e739fc

File tree

11 files changed

+286
-213
lines changed

11 files changed

+286
-213
lines changed

core/txpool/blobpool/blobpool.go

Lines changed: 52 additions & 14 deletions
Original file line numberDiff line numberDiff line change
@@ -298,8 +298,9 @@ func newBlobTxMeta(id uint64, size uint64, storageSize uint32, tx *types.Transac
298298
// minimums will need to be done only starting at the swapped in/out nonce
299299
// and leading up to the first no-change.
300300
type BlobPool struct {
301-
config Config // Pool configuration
302-
reserve txpool.AddressReserver // Address reserver to ensure exclusivity across subpools
301+
config Config // Pool configuration
302+
reserver *txpool.Reserver // Address reserver to ensure exclusivity across subpools
303+
hasPendingAuth func(common.Address) bool // Determine whether the specified address has a pending 7702-auth
303304

304305
store billy.Database // Persistent data store for the tx metadata and blobs
305306
stored uint64 // Useful data size of all transactions on disk
@@ -329,13 +330,14 @@ type BlobPool struct {
329330

330331
// New creates a new blob transaction pool to gather, sort and filter inbound
331332
// blob transactions from the network.
332-
func New(config Config, chain BlockChain) *BlobPool {
333+
func New(config Config, chain BlockChain, hasPendingAuth func(common.Address) bool) *BlobPool {
333334
// Sanitize the input to ensure no vulnerable gas prices are set
334335
config = (&config).sanitize()
335336

336337
// Create the transaction pool with its initial settings
337338
return &BlobPool{
338339
config: config,
340+
hasPendingAuth: hasPendingAuth,
339341
signer: types.LatestSigner(chain.Config()),
340342
chain: chain,
341343
lookup: newLookup(),
@@ -353,8 +355,8 @@ func (p *BlobPool) Filter(tx *types.Transaction) bool {
353355
// Init sets the gas price needed to keep a transaction in the pool and the chain
354356
// head to allow balance / nonce checks. The transaction journal will be loaded
355357
// from disk and filtered based on the provided starting settings.
356-
func (p *BlobPool) Init(gasTip uint64, head *types.Header, reserve txpool.AddressReserver) error {
357-
p.reserve = reserve
358+
func (p *BlobPool) Init(gasTip uint64, head *types.Header, reserver *txpool.Reserver) error {
359+
p.reserver = reserver
358360

359361
var (
360362
queuedir string
@@ -499,7 +501,7 @@ func (p *BlobPool) parseTransaction(id uint64, size uint32, blob []byte) error {
499501
return err
500502
}
501503
if _, ok := p.index[sender]; !ok {
502-
if err := p.reserve(sender, true); err != nil {
504+
if err := p.reserver.Hold(sender); err != nil {
503505
return err
504506
}
505507
p.index[sender] = []*blobTxMeta{}
@@ -554,7 +556,7 @@ func (p *BlobPool) recheck(addr common.Address, inclusions map[common.Hash]uint6
554556
if inclusions != nil { // only during reorgs will the heap be initialized
555557
heap.Remove(p.evict, p.evict.index[addr])
556558
}
557-
p.reserve(addr, false)
559+
p.reserver.Release(addr)
558560

559561
if gapped {
560562
log.Warn("Dropping dangling blob transactions", "from", addr, "missing", next, "drop", nonces, "ids", ids)
@@ -707,7 +709,7 @@ func (p *BlobPool) recheck(addr common.Address, inclusions map[common.Hash]uint6
707709
if inclusions != nil { // only during reorgs will the heap be initialized
708710
heap.Remove(p.evict, p.evict.index[addr])
709711
}
710-
p.reserve(addr, false)
712+
p.reserver.Release(addr)
711713
} else {
712714
p.index[addr] = txs
713715
}
@@ -1006,7 +1008,7 @@ func (p *BlobPool) reinject(addr common.Address, txhash common.Hash) error {
10061008
// Update the indices and metrics
10071009
meta := newBlobTxMeta(id, tx.Size(), p.store.Size(id), tx)
10081010
if _, ok := p.index[addr]; !ok {
1009-
if err := p.reserve(addr, true); err != nil {
1011+
if err := p.reserver.Hold(addr); err != nil {
10101012
log.Warn("Failed to reserve account for blob pool", "tx", tx.Hash(), "from", addr, "err", err)
10111013
return err
10121014
}
@@ -1066,7 +1068,7 @@ func (p *BlobPool) SetGasTip(tip *big.Int) {
10661068
delete(p.spent, addr)
10671069

10681070
heap.Remove(p.evict, p.evict.index[addr])
1069-
p.reserve(addr, false)
1071+
p.reserver.Release(addr)
10701072
}
10711073
// Clear out the transactions from the data store
10721074
log.Warn("Dropping underpriced blob transaction", "from", addr, "rejected", tx.nonce, "tip", tx.execTipCap, "want", tip, "drop", nonces, "ids", ids)
@@ -1101,6 +1103,39 @@ func (p *BlobPool) ValidateTxBasics(tx *types.Transaction) error {
11011103
return txpool.ValidateTransaction(tx, p.head, p.signer, opts)
11021104
}
11031105

1106+
// checkDelegationLimit determines if the tx sender is delegated or has a
1107+
// pending delegation, and if so, ensures they have at most one in-flight
1108+
// **executable** transaction, e.g. disallow stacked and gapped transactions
1109+
// from the account.
1110+
func (p *BlobPool) checkDelegationLimit(tx *types.Transaction) error {
1111+
from, _ := types.Sender(p.signer, tx) // validated
1112+
1113+
// Short circuit if the sender has neither delegation nor pending delegation.
1114+
if p.state.GetCodeHash(from) == types.EmptyCodeHash {
1115+
// Because there is no exclusive lock held between different subpools
1116+
// when processing transactions, a blob transaction may be accepted
1117+
// while other SetCode transactions with pending authorities from the
1118+
// same address are also accepted simultaneously.
1119+
//
1120+
// This scenario is considered acceptable, as the rule primarily ensures
1121+
// that attackers cannot easily and endlessly stack blob transactions
1122+
// with a delegated or pending delegated sender.
1123+
if p.hasPendingAuth == nil || !p.hasPendingAuth(from) {
1124+
return nil
1125+
}
1126+
}
1127+
// Allow a single in-flight pending transaction.
1128+
pending := p.index[from]
1129+
if len(pending) == 0 {
1130+
return nil
1131+
}
1132+
// If account already has a pending transaction, allow replacement only.
1133+
if len(pending) == 1 && pending[0].nonce == tx.Nonce() {
1134+
return nil
1135+
}
1136+
return txpool.ErrInflightTxLimitReached
1137+
}
1138+
11041139
// validateTx checks whether a transaction is valid according to the consensus
11051140
// rules and adheres to some heuristic limits of the local node (price and size).
11061141
func (p *BlobPool) validateTx(tx *types.Transaction) error {
@@ -1141,6 +1176,9 @@ func (p *BlobPool) validateTx(tx *types.Transaction) error {
11411176
if err := txpool.ValidateTransactionWithState(tx, p.signer, stateOpts); err != nil {
11421177
return err
11431178
}
1179+
if err := p.checkDelegationLimit(tx); err != nil {
1180+
return err
1181+
}
11441182
// If the transaction replaces an existing one, ensure that price bumps are
11451183
// adhered to.
11461184
var (
@@ -1369,7 +1407,7 @@ func (p *BlobPool) add(tx *types.Transaction) (err error) {
13691407
// only by this subpool until all transactions are evicted
13701408
from, _ := types.Sender(p.signer, tx) // already validated above
13711409
if _, ok := p.index[from]; !ok {
1372-
if err := p.reserve(from, true); err != nil {
1410+
if err := p.reserver.Hold(from); err != nil {
13731411
addNonExclusiveMeter.Mark(1)
13741412
return err
13751413
}
@@ -1381,7 +1419,7 @@ func (p *BlobPool) add(tx *types.Transaction) (err error) {
13811419
// by a return statement before running deferred methods. Take care with
13821420
// removing or subscoping err as it will break this clause.
13831421
if err != nil {
1384-
p.reserve(from, false)
1422+
p.reserver.Release(from)
13851423
}
13861424
}()
13871425
}
@@ -1513,7 +1551,7 @@ func (p *BlobPool) drop() {
15131551
if last {
15141552
delete(p.index, from)
15151553
delete(p.spent, from)
1516-
p.reserve(from, false)
1554+
p.reserver.Release(from)
15171555
} else {
15181556
txs[len(txs)-1] = nil
15191557
txs = txs[:len(txs)-1]
@@ -1789,7 +1827,7 @@ func (p *BlobPool) Clear() {
17891827
// can't happen until Clear releases the reservation lock. Clear cannot
17901828
// acquire the subpool lock until the transaction addition is completed.
17911829
for acct := range p.index {
1792-
p.reserve(acct, false)
1830+
p.reserver.Release(acct)
17931831
}
17941832
p.lookup = newLookup()
17951833
p.index = make(map[common.Address][]*blobTxMeta)

core/txpool/blobpool/blobpool_test.go

Lines changed: 18 additions & 42 deletions
Original file line numberDiff line numberDiff line change
@@ -26,7 +26,6 @@ import (
2626
"math/big"
2727
"os"
2828
"path/filepath"
29-
"sync"
3029
"testing"
3130

3231
"github.com/ethereum/go-ethereum/common"
@@ -168,33 +167,6 @@ func (bc *testBlockChain) StateAt(common.Hash) (*state.StateDB, error) {
168167
return bc.statedb, nil
169168
}
170169

171-
// makeAddressReserver is a utility method to sanity check that accounts are
172-
// properly reserved by the blobpool (no duplicate reserves or unreserves).
173-
func makeAddressReserver() txpool.AddressReserver {
174-
var (
175-
reserved = make(map[common.Address]struct{})
176-
lock sync.Mutex
177-
)
178-
return func(addr common.Address, reserve bool) error {
179-
lock.Lock()
180-
defer lock.Unlock()
181-
182-
_, exists := reserved[addr]
183-
if reserve {
184-
if exists {
185-
panic("already reserved")
186-
}
187-
reserved[addr] = struct{}{}
188-
return nil
189-
}
190-
if !exists {
191-
panic("not reserved")
192-
}
193-
delete(reserved, addr)
194-
return nil
195-
}
196-
}
197-
198170
// makeTx is a utility method to construct a random blob transaction and sign it
199171
// with a valid key, only setting the interesting fields from the perspective of
200172
// the blob pool.
@@ -433,6 +405,10 @@ func verifyBlobRetrievals(t *testing.T, pool *BlobPool) {
433405
}
434406
}
435407

408+
func newReserver() *txpool.Reserver {
409+
return txpool.NewReservationTracker().NewHandle(42)
410+
}
411+
436412
// Tests that transactions can be loaded from disk on startup and that they are
437413
// correctly discarded if invalid.
438414
//
@@ -699,8 +675,8 @@ func TestOpenDrops(t *testing.T) {
699675
blobfee: uint256.NewInt(params.BlobTxMinBlobGasprice),
700676
statedb: statedb,
701677
}
702-
pool := New(Config{Datadir: storage}, chain)
703-
if err := pool.Init(1, chain.CurrentBlock(), makeAddressReserver()); err != nil {
678+
pool := New(Config{Datadir: storage}, chain, nil)
679+
if err := pool.Init(1, chain.CurrentBlock(), newReserver()); err != nil {
704680
t.Fatalf("failed to create blob pool: %v", err)
705681
}
706682
defer pool.Close()
@@ -817,8 +793,8 @@ func TestOpenIndex(t *testing.T) {
817793
blobfee: uint256.NewInt(params.BlobTxMinBlobGasprice),
818794
statedb: statedb,
819795
}
820-
pool := New(Config{Datadir: storage}, chain)
821-
if err := pool.Init(1, chain.CurrentBlock(), makeAddressReserver()); err != nil {
796+
pool := New(Config{Datadir: storage}, chain, nil)
797+
if err := pool.Init(1, chain.CurrentBlock(), newReserver()); err != nil {
822798
t.Fatalf("failed to create blob pool: %v", err)
823799
}
824800
defer pool.Close()
@@ -918,8 +894,8 @@ func TestOpenHeap(t *testing.T) {
918894
blobfee: uint256.NewInt(105),
919895
statedb: statedb,
920896
}
921-
pool := New(Config{Datadir: storage}, chain)
922-
if err := pool.Init(1, chain.CurrentBlock(), makeAddressReserver()); err != nil {
897+
pool := New(Config{Datadir: storage}, chain, nil)
898+
if err := pool.Init(1, chain.CurrentBlock(), newReserver()); err != nil {
923899
t.Fatalf("failed to create blob pool: %v", err)
924900
}
925901
defer pool.Close()
@@ -997,8 +973,8 @@ func TestOpenCap(t *testing.T) {
997973
blobfee: uint256.NewInt(105),
998974
statedb: statedb,
999975
}
1000-
pool := New(Config{Datadir: storage, Datacap: datacap}, chain)
1001-
if err := pool.Init(1, chain.CurrentBlock(), makeAddressReserver()); err != nil {
976+
pool := New(Config{Datadir: storage, Datacap: datacap}, chain, nil)
977+
if err := pool.Init(1, chain.CurrentBlock(), newReserver()); err != nil {
1002978
t.Fatalf("failed to create blob pool: %v", err)
1003979
}
1004980
// Verify that enough transactions have been dropped to get the pool's size
@@ -1098,8 +1074,8 @@ func TestChangingSlotterSize(t *testing.T) {
10981074
blobfee: uint256.NewInt(105),
10991075
statedb: statedb,
11001076
}
1101-
pool := New(Config{Datadir: storage}, chain)
1102-
if err := pool.Init(1, chain.CurrentBlock(), makeAddressReserver()); err != nil {
1077+
pool := New(Config{Datadir: storage}, chain, nil)
1078+
if err := pool.Init(1, chain.CurrentBlock(), newReserver()); err != nil {
11031079
t.Fatalf("failed to create blob pool: %v", err)
11041080
}
11051081

@@ -1541,8 +1517,8 @@ func TestAdd(t *testing.T) {
15411517
blobfee: uint256.NewInt(105),
15421518
statedb: statedb,
15431519
}
1544-
pool := New(Config{Datadir: storage}, chain)
1545-
if err := pool.Init(1, chain.CurrentBlock(), makeAddressReserver()); err != nil {
1520+
pool := New(Config{Datadir: storage}, chain, nil)
1521+
if err := pool.Init(1, chain.CurrentBlock(), newReserver()); err != nil {
15461522
t.Fatalf("test %d: failed to create blob pool: %v", i, err)
15471523
}
15481524
verifyPoolInternals(t, pool)
@@ -1638,10 +1614,10 @@ func benchmarkPoolPending(b *testing.B, datacap uint64) {
16381614
blobfee: uint256.NewInt(blobfee),
16391615
statedb: statedb,
16401616
}
1641-
pool = New(Config{Datadir: ""}, chain)
1617+
pool = New(Config{Datadir: ""}, chain, nil)
16421618
)
16431619

1644-
if err := pool.Init(1, chain.CurrentBlock(), makeAddressReserver()); err != nil {
1620+
if err := pool.Init(1, chain.CurrentBlock(), newReserver()); err != nil {
16451621
b.Fatalf("failed to create blob pool: %v", err)
16461622
}
16471623
// Make the pool not use disk (just drop everything). This test never reads

core/txpool/errors.go

Lines changed: 4 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -56,4 +56,8 @@ var (
5656
// input transaction of non-blob type when a blob transaction from this sender
5757
// remains pending (and vice-versa).
5858
ErrAlreadyReserved = errors.New("address already reserved")
59+
60+
// ErrInflightTxLimitReached is returned when the maximum number of in-flight
61+
// transactions is reached for specific accounts.
62+
ErrInflightTxLimitReached = errors.New("in-flight transaction limit reached for delegated accounts")
5963
)

0 commit comments

Comments
 (0)