diff --git a/core/txpool/legacypool/legacypool.go b/core/txpool/legacypool/legacypool.go index 76ba6067e96..1a05e32d196 100644 --- a/core/txpool/legacypool/legacypool.go +++ b/core/txpool/legacypool/legacypool.go @@ -68,6 +68,10 @@ var ( // another remote transaction. ErrTxPoolOverflow = errors.New("txpool is full") + // ErrOutOfOrderTxFromDelegated is returned when the transaction with gapped + // nonce received from the accounts with delegation or pending delegation. + ErrOutOfOrderTxFromDelegated = errors.New("gapped-nonce tx from delegated accounts") + // ErrInflightTxLimitReached is returned when the maximum number of in-flight // transactions is reached for specific accounts. ErrInflightTxLimitReached = errors.New("in-flight transaction limit reached for delegated accounts") @@ -230,11 +234,11 @@ func (config *Config) sanitize() Config { // two states over time as they are received and processed. // // In addition to tracking transactions, the pool also tracks a set of pending SetCode -// authorizations (EIP7702). This helps minimize number of transactions that can be +// authorizations (EIP7702). This helps minimize the number of transactions that can be // trivially churned in the pool. As a standard rule, any account with a deployed // delegation or an in-flight authorization to deploy a delegation will only be allowed a // single transaction slot instead of the standard number. This is due to the possibility -// of the account being sweeped by an unrelated account. +// of the account being swept by an unrelated account. // // Because SetCode transactions can have many authorizations included, we avoid explicitly // checking their validity to save the state lookup. So long as the encompassing @@ -723,33 +727,39 @@ func (pool *LegacyPool) validateTx(tx *types.Transaction, local bool) error { return pool.validateAuth(tx) } +// checkDelegationLimit determines if the tx sender is delegated or has a +// pending delegation, and if so, ensures they have at most one in-flight +// **executable** transaction, e.g. disallow stacked and gapped transactions +// from the account. +func (pool *LegacyPool) checkDelegationLimit(tx *types.Transaction) error { + from, _ := types.Sender(pool.signer, tx) // validated + + // Short circuit if the sender has neither delegation nor pending delegation. + if pool.currentState.GetCodeHash(from) == types.EmptyCodeHash && !pool.all.hasAuth(from) { + return nil + } + pending := pool.pending[from] + if pending == nil { + // Transaction with gapped nonce is not supported for delegated accounts + if pool.pendingNonces.get(from) != tx.Nonce() { + return ErrOutOfOrderTxFromDelegated + } + return nil + } + // Transaction replacement is supported + if pending.Contains(tx.Nonce()) { + return nil + } + return ErrInflightTxLimitReached +} + // validateAuth verifies that the transaction complies with code authorization // restrictions brought by SetCode transaction type. func (pool *LegacyPool) validateAuth(tx *types.Transaction) error { - from, _ := types.Sender(pool.signer, tx) // validated - // Allow at most one in-flight tx for delegated accounts or those with a // pending authorization. - if pool.currentState.GetCodeHash(from) != types.EmptyCodeHash || len(pool.all.auths[from]) != 0 { - var ( - count int - exists bool - ) - pending := pool.pending[from] - if pending != nil { - count += pending.Len() - exists = pending.Contains(tx.Nonce()) - } - queue := pool.queue[from] - if queue != nil { - count += queue.Len() - exists = exists || queue.Contains(tx.Nonce()) - } - // Replacing the existing in-flight transaction for delegated accounts - // is still supported. - if count >= 1 && !exists { - return ErrInflightTxLimitReached - } + if err := pool.checkDelegationLimit(tx); err != nil { + return err } // Authorities cannot conflict with any pending or queued transactions. if auths := tx.SetCodeAuthorities(); len(auths) > 0 { @@ -1236,7 +1246,7 @@ func (pool *LegacyPool) Has(hash common.Hash) bool { // removeTx removes a single transaction from the queue, moving all subsequent // transactions back to the future queue. // -// In unreserve is false, the account will not be relinquished to the main txpool +// If unreserve is false, the account will not be relinquished to the main txpool // even if there are no more references to it. This is used to handle a race when // a tx being added, and it evicts a previously scheduled tx from the same account, // which could lead to a premature release of the lock. @@ -2173,6 +2183,15 @@ func (t *lookup) removeAuthorities(hash common.Hash) { } } +// hasAuth returns a flag indicating whether there are pending authorizations +// from the specified address. +func (t *lookup) hasAuth(addr common.Address) bool { + t.lock.RLock() + defer t.lock.RUnlock() + + return len(t.auths[addr]) > 0 +} + // numSlots calculates the number of slots needed for a single transaction. func numSlots(tx *types.Transaction) int { return int((tx.Size() + txSlotSize - 1) / txSlotSize) diff --git a/core/txpool/legacypool/legacypool_test.go b/core/txpool/legacypool/legacypool_test.go index ebb49b74da9..f3ecd734585 100644 --- a/core/txpool/legacypool/legacypool_test.go +++ b/core/txpool/legacypool/legacypool_test.go @@ -2658,7 +2658,7 @@ func TestSetCodeTransactions(t *testing.T) { minGasFee := uint256.MustFromBig(minGasPrice) doubleGasFee := new(uint256.Int).Mul(new(uint256.Int).Set(minGasFee), uint256.NewInt(2)) tripleGasFee := new(uint256.Int).Mul(new(uint256.Int).Set(minGasFee), uint256.NewInt(3)) - legacyReplacePrice := new(big.Int).Mul(new(big.Int).Set(minGasPrice), big.NewInt(10)) + legacyReplacePrice := new(big.Int).Mul(minGasPrice, big.NewInt(10)) for _, tt := range []struct { name string @@ -2676,15 +2676,20 @@ func TestSetCodeTransactions(t *testing.T) { aa := common.Address{0xaa, 0xaa} statedb.SetCode(addrA, append(types.DelegationPrefix, aa.Bytes()...)) statedb.SetCode(aa, []byte{byte(vm.ADDRESS), byte(vm.PUSH0), byte(vm.SSTORE)}) + + // Send gapped transaction, it should be rejected. + if err := pool.addRemoteSync(pricedTransaction(2, 100000, minGasPrice, keyA)); !errors.Is(err, ErrOutOfOrderTxFromDelegated) { + t.Fatalf("%s: error mismatch: want %v, have %v", name, ErrOutOfOrderTxFromDelegated, err) + } // Send transactions. First is accepted, second is rejected. if err := pool.addRemoteSync(pricedTransaction(0, 100000, minGasPrice, keyA)); err != nil { t.Fatalf("%s: failed to add remote transaction: %v", name, err) } - if err := pool.addRemoteSync(pricedTransaction(1, 100000, new(big.Int).Set(minGasPrice), keyA)); !errors.Is(err, ErrInflightTxLimitReached) { + if err := pool.addRemoteSync(pricedTransaction(1, 100000, minGasPrice, keyA)); !errors.Is(err, ErrInflightTxLimitReached) { t.Fatalf("%s: error mismatch: want %v, have %v", name, ErrInflightTxLimitReached, err) } - // Also check gapped transaction. - if err := pool.addRemoteSync(pricedTransaction(2, 100000, new(big.Int).Set(minGasPrice), keyA)); !errors.Is(err, ErrInflightTxLimitReached) { + // Check gapped transaction again. + if err := pool.addRemoteSync(pricedTransaction(2, 100000, minGasPrice, keyA)); !errors.Is(err, ErrInflightTxLimitReached) { t.Fatalf("%s: error mismatch: want %v, have %v", name, ErrInflightTxLimitReached, err) } // Replace by fee. @@ -2715,11 +2720,11 @@ func TestSetCodeTransactions(t *testing.T) { if err := pool.addRemoteSync(setCodeTx(0, keyA, []unsignedAuth{{0, keyC}})); err != nil { t.Fatalf("%s: failed to add with remote setcode transaction: %v", name, err) } - if err := pool.addRemoteSync(pricedTransaction(0, 100000, new(big.Int).Set(minGasPrice), keyC)); err != nil { + if err := pool.addRemoteSync(pricedTransaction(0, 100000, minGasPrice, keyC)); err != nil { t.Fatalf("%s: failed to add with pending delegatio: %v", name, err) } // Also check gapped transaction is rejected. - if err := pool.addRemoteSync(pricedTransaction(1, 100000, new(big.Int).Set(minGasPrice), keyC)); !errors.Is(err, ErrInflightTxLimitReached) { + if err := pool.addRemoteSync(pricedTransaction(1, 100000, minGasPrice, keyC)); !errors.Is(err, ErrInflightTxLimitReached) { t.Fatalf("%s: error mismatch: want %v, have %v", name, ErrInflightTxLimitReached, err) } }, @@ -2749,7 +2754,7 @@ func TestSetCodeTransactions(t *testing.T) { t.Fatalf("%s: failed to add with remote setcode transaction: %v", name, err) } // Now send a regular tx from B. - if err := pool.addRemoteSync(pricedTransaction(0, 100000, new(big.Int).Set(minGasPrice), keyB)); err != nil { + if err := pool.addRemoteSync(pricedTransaction(0, 100000, minGasPrice, keyB)); err != nil { t.Fatalf("%s: failed to replace with remote transaction: %v", name, err) } }, @@ -2770,7 +2775,7 @@ func TestSetCodeTransactions(t *testing.T) { t.Fatalf("%s: failed to replace with remote transaction: %v", name, err) } // Make sure we can still send from keyB. - if err := pool.addRemoteSync(pricedTransaction(0, 100000, new(big.Int).Set(minGasPrice), keyB)); err != nil { + if err := pool.addRemoteSync(pricedTransaction(0, 100000, minGasPrice, keyB)); err != nil { t.Fatalf("%s: failed to replace with remote transaction: %v", name, err) } }, @@ -2792,10 +2797,10 @@ func TestSetCodeTransactions(t *testing.T) { } // Make sure we can only pool one tx from keyC since it is still a // pending authority. - if err := pool.addRemoteSync(pricedTransaction(0, 100000, new(big.Int).Set(minGasPrice), keyC)); err != nil { + if err := pool.addRemoteSync(pricedTransaction(0, 100000, minGasPrice, keyC)); err != nil { t.Fatalf("%s: failed to added single pooled for account with pending delegation: %v", name, err) } - if err, want := pool.addRemoteSync(pricedTransaction(1, 100000, new(big.Int).Set(minGasPrice), keyC)), ErrInflightTxLimitReached; !errors.Is(err, want) { + if err, want := pool.addRemoteSync(pricedTransaction(1, 100000, minGasPrice, keyC)), ErrInflightTxLimitReached; !errors.Is(err, want) { t.Fatalf("%s: error mismatch: want %v, have %v", name, want, err) } }, @@ -2805,7 +2810,7 @@ func TestSetCodeTransactions(t *testing.T) { pending: 1, run: func(name string) { // Attempt to submit a delegation from an account with a pending tx. - if err := pool.addRemoteSync(pricedTransaction(0, 100000, new(big.Int).Set(minGasPrice), keyC)); err != nil { + if err := pool.addRemoteSync(pricedTransaction(0, 100000, minGasPrice, keyC)); err != nil { t.Fatalf("%s: failed to add with remote setcode transaction: %v", name, err) } if err, want := pool.addRemoteSync(setCodeTx(0, keyA, []unsignedAuth{{1, keyC}})), ErrAuthorityReserved; !errors.Is(err, want) {