core/txpool: validate stateful options

This commit is contained in:
esorense 2026-03-16 22:49:21 +08:00
parent a7d09cc14f
commit 87164a8553
3 changed files with 101 additions and 0 deletions

View file

@ -28,6 +28,10 @@ var (
// ErrInvalidSender is returned if the transaction contains an invalid signature.
ErrInvalidSender = errors.New("invalid sender")
// ErrInvalidValidationOptions is returned if a public validation helper is
// invoked without the required state or callbacks.
ErrInvalidValidationOptions = errors.New("invalid validation options")
// ErrUnderpriced is returned if a transaction's gas price is too low to be
// included in the pool. If the gas price is lower than the minimum configured
// one for the transaction pool, use ErrTxGasPriceTooLow instead.

View file

@ -246,12 +246,30 @@ type ValidationOptionsWithState struct {
ExistingCost func(addr common.Address, nonce uint64) *big.Int
}
func validateStatefulOptions(opts *ValidationOptionsWithState) error {
switch {
case opts == nil:
return fmt.Errorf("%w: missing options", ErrInvalidValidationOptions)
case opts.State == nil:
return fmt.Errorf("%w: missing state", ErrInvalidValidationOptions)
case opts.ExistingExpenditure == nil:
return fmt.Errorf("%w: missing ExistingExpenditure callback", ErrInvalidValidationOptions)
case opts.ExistingCost == nil:
return fmt.Errorf("%w: missing ExistingCost callback", ErrInvalidValidationOptions)
default:
return nil
}
}
// ValidateTransactionWithState is a helper method to check whether a transaction
// is valid according to the pool's internal state checks (balance, nonce, gaps).
//
// This check is public to allow different transaction pools to check the stateful
// rules without duplicating code and running the risk of missed updates.
func ValidateTransactionWithState(tx *types.Transaction, signer types.Signer, opts *ValidationOptionsWithState) error {
if err := validateStatefulOptions(opts); err != nil {
return err
}
// Ensure the transaction adheres to nonce ordering
from, err := types.Sender(signer, tx) // already validated (and cached), but cleaner to check
if err != nil {
@ -280,6 +298,9 @@ func ValidateTransactionWithState(tx *types.Transaction, signer types.Signer, op
// Ensure the transactor has enough funds to cover for replacements or nonce
// expansions without overdrafts
spent := opts.ExistingExpenditure(from)
if spent == nil {
return fmt.Errorf("%w: ExistingExpenditure returned nil", ErrInvalidValidationOptions)
}
if prev := opts.ExistingCost(from, tx.Nonce()); prev != nil {
bump := new(big.Int).Sub(cost, prev)
need := new(big.Int).Add(spent, bump)

View file

@ -21,13 +21,17 @@ import (
"errors"
"math"
"math/big"
"strings"
"testing"
"github.com/ethereum/go-ethereum/common"
"github.com/ethereum/go-ethereum/core"
"github.com/ethereum/go-ethereum/core/state"
"github.com/ethereum/go-ethereum/core/tracing"
"github.com/ethereum/go-ethereum/core/types"
"github.com/ethereum/go-ethereum/crypto"
"github.com/ethereum/go-ethereum/params"
"github.com/holiman/uint256"
)
func TestValidateTransactionEIP2681(t *testing.T) {
@ -96,6 +100,78 @@ func TestValidateTransactionEIP2681(t *testing.T) {
}
}
func TestValidateTransactionWithStateRejectsInvalidOptions(t *testing.T) {
key, err := crypto.GenerateKey()
if err != nil {
t.Fatal(err)
}
tx := createTestTransaction(key, 0)
from := crypto.PubkeyToAddress(key.PublicKey)
statedb, err := state.New(types.EmptyRootHash, state.NewDatabaseForTesting())
if err != nil {
t.Fatal(err)
}
statedb.SetBalance(from, new(uint256.Int).SetUint64(params.Ether), tracing.BalanceChangeUnspecified)
tests := []struct {
name string
opts *ValidationOptionsWithState
wantErr string
}{
{
name: "missing options",
opts: nil,
wantErr: "missing options",
},
{
name: "missing state",
opts: &ValidationOptionsWithState{
ExistingExpenditure: func(common.Address) *big.Int { return new(big.Int) },
ExistingCost: func(common.Address, uint64) *big.Int { return nil },
},
wantErr: "missing state",
},
{
name: "missing existing expenditure callback",
opts: &ValidationOptionsWithState{
State: statedb,
ExistingCost: func(common.Address, uint64) *big.Int { return nil },
},
wantErr: "missing ExistingExpenditure callback",
},
{
name: "missing existing cost callback",
opts: &ValidationOptionsWithState{
State: statedb,
ExistingExpenditure: func(common.Address) *big.Int { return new(big.Int) },
},
wantErr: "missing ExistingCost callback",
},
{
name: "nil expenditure result",
opts: &ValidationOptionsWithState{
State: statedb,
ExistingExpenditure: func(common.Address) *big.Int { return nil },
ExistingCost: func(common.Address, uint64) *big.Int { return nil },
},
wantErr: "ExistingExpenditure returned nil",
},
}
for _, tt := range tests {
t.Run(tt.name, func(t *testing.T) {
err := ValidateTransactionWithState(tx, types.HomesteadSigner{}, tt.opts)
if !errors.Is(err, ErrInvalidValidationOptions) {
t.Fatalf("expected ErrInvalidValidationOptions, got %v", err)
}
if !strings.Contains(err.Error(), tt.wantErr) {
t.Fatalf("expected error containing %q, got %v", tt.wantErr, err)
}
})
}
}
// createTestTransaction creates a basic transaction for testing
func createTestTransaction(key *ecdsa.PrivateKey, nonce uint64) *types.Transaction {
to := common.HexToAddress("0x0000000000000000000000000000000000000001")