diff --git a/core/txpool/errors.go b/core/txpool/errors.go index 8285cbf10e..ce741f3505 100644 --- a/core/txpool/errors.go +++ b/core/txpool/errors.go @@ -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. diff --git a/core/txpool/validation.go b/core/txpool/validation.go index 13b1bfa312..59bbbbda14 100644 --- a/core/txpool/validation.go +++ b/core/txpool/validation.go @@ -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) diff --git a/core/txpool/validation_test.go b/core/txpool/validation_test.go index 3945b548c1..7744e81ebe 100644 --- a/core/txpool/validation_test.go +++ b/core/txpool/validation_test.go @@ -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")