Skip to content

Commit 0c2ad07

Browse files
LuisPH3lightclient
andauthored
core/txpool: allow tx and authority regardless of admission order (#31373)
This PR proposes a change to the authorizations' validation introduced in commit cdb66c8. These changes make the expected behavior independent of the order of admission of authorizations, improving the predictability of the resulting state and the usability of the system with it. The current implementation behavior is dependent on the transaction submission order: This issue is related to authorities and the sender of a transaction, and can be reproduced respecting the normal nonce rules. The issue can be reproduced by the two following cases: **First case** - Given an empty pool. - Submit transaction `{ from: B, auths [ A ] }`: is accepted. - Submit transaction `{ from: A }`: Is accepted: it becomes the one in-flight transaction allowed. **Second case** - Given an empty pool. - Submit transaction `{ from: A }`: is accepted - Submit transaction `{ from: B, auths [ A ] }`: is rejected since there is already a queued/pending transaction from A. The expected behavior is that both sequences of events would lead to the same sets of accepted and rejected transactions. **Proposed changes** The queued/pending transactions issued from any authority of the transaction being validated have to be counted, allowing one transaction from accounts submitting an authorization. - Notice that the expected behavior was explicitly forbidden in the case "reject-delegation-from-pending-account", I believe that this behavior conflicts to the definition of the limitation, and it is removed in this PR. The expected behavior is tested in "accept-authorization-from-sender-of-one-inflight-tx". - Replacement tests have been separated to improve readability of the acceptance test. - The test "allow-more-than-one-tx-from-replaced-authority" has been extended with one extra transaction, since the system would always have accepted one transaction (but not two). - The test "accept-one-inflight-tx-of-delegated-account" is extended to clean-up state, avoiding leaking the delegation used into the other tests. Additionally, replacement check is removed to be tested in its own test case. **Expected behavior** The expected behavior of the authorizations' validation shall be as follows: ![image](https://github.com/user-attachments/assets/dbde7a1f-9679-4691-94eb-c197a0cbb823) Notice that replacement shall be allowed, and behavior shall remain coherent with the table, according to the replaced transaction. --------- Co-authored-by: lightclient <[email protected]>
1 parent 60b922f commit 0c2ad07

File tree

6 files changed

+238
-71
lines changed

6 files changed

+238
-71
lines changed

core/txpool/blobpool/blobpool.go

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -299,7 +299,7 @@ func newBlobTxMeta(id uint64, size uint64, storageSize uint32, tx *types.Transac
299299
// and leading up to the first no-change.
300300
type BlobPool struct {
301301
config Config // Pool configuration
302-
reserver *txpool.Reserver // Address reserver to ensure exclusivity across subpools
302+
reserver txpool.Reserver // Address reserver to ensure exclusivity across subpools
303303
hasPendingAuth func(common.Address) bool // Determine whether the specified address has a pending 7702-auth
304304

305305
store billy.Database // Persistent data store for the tx metadata and blobs
@@ -355,7 +355,7 @@ func (p *BlobPool) Filter(tx *types.Transaction) bool {
355355
// Init sets the gas price needed to keep a transaction in the pool and the chain
356356
// head to allow balance / nonce checks. The transaction journal will be loaded
357357
// from disk and filtered based on the provided starting settings.
358-
func (p *BlobPool) Init(gasTip uint64, head *types.Header, reserver *txpool.Reserver) error {
358+
func (p *BlobPool) Init(gasTip uint64, head *types.Header, reserver txpool.Reserver) error {
359359
p.reserver = reserver
360360

361361
var (

core/txpool/blobpool/blobpool_test.go

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

3132
"github.com/ethereum/go-ethereum/common"
@@ -167,6 +168,44 @@ func (bc *testBlockChain) StateAt(common.Hash) (*state.StateDB, error) {
167168
return bc.statedb, nil
168169
}
169170

171+
// reserver is a utility struct to sanity check that accounts are
172+
// properly reserved by the blobpool (no duplicate reserves or unreserves).
173+
type reserver struct {
174+
accounts map[common.Address]struct{}
175+
lock sync.RWMutex
176+
}
177+
178+
func newReserver() txpool.Reserver {
179+
return &reserver{accounts: make(map[common.Address]struct{})}
180+
}
181+
182+
func (r *reserver) Hold(addr common.Address) error {
183+
r.lock.Lock()
184+
defer r.lock.Unlock()
185+
if _, exists := r.accounts[addr]; exists {
186+
panic("already reserved")
187+
}
188+
r.accounts[addr] = struct{}{}
189+
return nil
190+
}
191+
192+
func (r *reserver) Release(addr common.Address) error {
193+
r.lock.Lock()
194+
defer r.lock.Unlock()
195+
if _, exists := r.accounts[addr]; !exists {
196+
panic("not reserved")
197+
}
198+
delete(r.accounts, addr)
199+
return nil
200+
}
201+
202+
func (r *reserver) Has(address common.Address) bool {
203+
r.lock.RLock()
204+
defer r.lock.RUnlock()
205+
_, exists := r.accounts[address]
206+
return exists
207+
}
208+
170209
// makeTx is a utility method to construct a random blob transaction and sign it
171210
// with a valid key, only setting the interesting fields from the perspective of
172211
// the blob pool.
@@ -405,10 +444,6 @@ func verifyBlobRetrievals(t *testing.T, pool *BlobPool) {
405444
}
406445
}
407446

408-
func newReserver() *txpool.Reserver {
409-
return txpool.NewReservationTracker().NewHandle(42)
410-
}
411-
412447
// Tests that transactions can be loaded from disk on startup and that they are
413448
// correctly discarded if invalid.
414449
//

core/txpool/legacypool/legacypool.go

Lines changed: 20 additions & 8 deletions
Original file line numberDiff line numberDiff line change
@@ -237,7 +237,7 @@ type LegacyPool struct {
237237
currentHead atomic.Pointer[types.Header] // Current head of the blockchain
238238
currentState *state.StateDB // Current state in the blockchain head
239239
pendingNonces *noncer // Pending state tracking virtual nonces
240-
reserver *txpool.Reserver // Address reserver to ensure exclusivity across subpools
240+
reserver txpool.Reserver // Address reserver to ensure exclusivity across subpools
241241

242242
pending map[common.Address]*list // All currently processable transactions
243243
queue map[common.Address]*list // Queued but non-processable transactions
@@ -302,7 +302,7 @@ func (pool *LegacyPool) Filter(tx *types.Transaction) bool {
302302
// Init sets the gas price needed to keep a transaction in the pool and the chain
303303
// head to allow balance / nonce checks. The internal
304304
// goroutines will be spun up and the pool deemed operational afterwards.
305-
func (pool *LegacyPool) Init(gasTip uint64, head *types.Header, reserver *txpool.Reserver) error {
305+
func (pool *LegacyPool) Init(gasTip uint64, head *types.Header, reserver txpool.Reserver) error {
306306
// Set the address reserver to request exclusive access to pooled accounts
307307
pool.reserver = reserver
308308

@@ -640,11 +640,18 @@ func (pool *LegacyPool) validateAuth(tx *types.Transaction) error {
640640
if err := pool.checkDelegationLimit(tx); err != nil {
641641
return err
642642
}
643-
// Authorities must not conflict with any pending or queued transactions,
644-
// nor with addresses that have already been reserved.
643+
// For symmetry, allow at most one in-flight tx for any authority with a
644+
// pending transaction.
645645
if auths := tx.SetCodeAuthorities(); len(auths) > 0 {
646646
for _, auth := range auths {
647-
if pool.pending[auth] != nil || pool.queue[auth] != nil {
647+
var count int
648+
if pending := pool.pending[auth]; pending != nil {
649+
count += pending.Len()
650+
}
651+
if queue := pool.queue[auth]; queue != nil {
652+
count += queue.Len()
653+
}
654+
if count > 1 {
648655
return ErrAuthorityReserved
649656
}
650657
// Because there is no exclusive lock held between different subpools
@@ -1907,9 +1914,14 @@ func (pool *LegacyPool) Clear() {
19071914
// The transaction addition may attempt to reserve the sender addr which
19081915
// can't happen until Clear releases the reservation lock. Clear cannot
19091916
// acquire the subpool lock until the transaction addition is completed.
1910-
for _, tx := range pool.all.txs {
1911-
senderAddr, _ := types.Sender(pool.signer, tx)
1912-
pool.reserver.Release(senderAddr)
1917+
1918+
for addr := range pool.pending {
1919+
if _, ok := pool.queue[addr]; !ok {
1920+
pool.reserver.Release(addr)
1921+
}
1922+
}
1923+
for addr := range pool.queue {
1924+
pool.reserver.Release(addr)
19131925
}
19141926
pool.all = newLookup()
19151927
pool.priced = newPricedList(pool.all)

0 commit comments

Comments
 (0)