Skip to content
Draft
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
2 changes: 1 addition & 1 deletion internal/ethapi/api.go
Original file line number Diff line number Diff line change
Expand Up @@ -1645,7 +1645,7 @@ func SubmitTransaction(ctx context.Context, b Backend, tx *types.Transaction) (c
return common.Hash{}, errors.New("only replay-protected (EIP-155) transactions allowed over RPC")
}
if err := b.SendTx(ctx, tx); err != nil {
return common.Hash{}, err
return common.Hash{}, txSubmitError(err)
}
// Print a log with full tx details for manual investigations and interventions
head := b.CurrentBlock()
Expand Down
76 changes: 76 additions & 0 deletions internal/ethapi/errors.go
Original file line number Diff line number Diff line change
Expand Up @@ -24,6 +24,7 @@ import (
"github.com/ethereum/go-ethereum/common"
"github.com/ethereum/go-ethereum/common/hexutil"
"github.com/ethereum/go-ethereum/core"
"github.com/ethereum/go-ethereum/core/txpool"
"github.com/ethereum/go-ethereum/core/vm"
)

Expand Down Expand Up @@ -151,6 +152,81 @@ func txValidationError(err error) *invalidTxError {
}
}

// Standardized JSON-RPC error codes for transaction submission, shared across
// EVM clients via the execution-apis error-code catalog (src/error-groups):
// ExecutionErrors (1-199), GasErrors (800-999) and TxPoolErrors (1000-1199).
// See https://github.com/ethereum/execution-apis/tree/master/src/error-groups.
const (
errCodeStdNonceTooLow = 1
errCodeStdNonceTooHigh = 2
errCodeStdIntrinsicGas = 800
errCodeStdGasPriceTooLow = 802
errCodeStdGasExceedsBlockLimit = 803
errCodeStdTipAboveFeeCap = 804
errCodeStdGasUintOverflow = 805
errCodeStdFeeCapTooLow = 806
errCodeStdTipVeryHigh = 807
errCodeStdFeeCapVeryHigh = 808
errCodeStdInsufficientFunds = 809
errCodeStdAlreadyKnown = 1000
errCodeStdInvalidSender = 1001
errCodeStdReplaceUnderpriced = 1002
)

// txSubmitError maps an error returned while submitting a transaction to the
// pool (eth_sendTransaction / eth_sendRawTransaction) to its standardized
// execution-apis JSON-RPC error code, preserving the original error message.
// Errors without a catalog code are returned unchanged, so they keep geth's
// default behavior (the generic -32000 code).
func txSubmitError(err error) error {
if err == nil {
return nil
}
if code, ok := txSubmitErrorCode(err); ok {
return &invalidTxError{Message: err.Error(), Code: code}
}
return err
}

// txSubmitErrorCode returns the standardized error code for a transaction
// submission error, and whether a code is defined for it.
func txSubmitErrorCode(err error) (int, bool) {
switch {
// TxPoolErrors (1000-1199)
case errors.Is(err, txpool.ErrAlreadyKnown):
return errCodeStdAlreadyKnown, true
case errors.Is(err, txpool.ErrInvalidSender):
return errCodeStdInvalidSender, true
case errors.Is(err, txpool.ErrReplaceUnderpriced):
return errCodeStdReplaceUnderpriced, true
// GasErrors (800-999)
case errors.Is(err, core.ErrIntrinsicGas):
return errCodeStdIntrinsicGas, true
case errors.Is(err, txpool.ErrTxGasPriceTooLow):
return errCodeStdGasPriceTooLow, true
case errors.Is(err, txpool.ErrGasLimit):
return errCodeStdGasExceedsBlockLimit, true
case errors.Is(err, core.ErrTipAboveFeeCap):
return errCodeStdTipAboveFeeCap, true
case errors.Is(err, core.ErrGasUintOverflow):
return errCodeStdGasUintOverflow, true
case errors.Is(err, core.ErrFeeCapTooLow):
return errCodeStdFeeCapTooLow, true
case errors.Is(err, core.ErrTipVeryHigh):
return errCodeStdTipVeryHigh, true
case errors.Is(err, core.ErrFeeCapVeryHigh):
return errCodeStdFeeCapVeryHigh, true
case errors.Is(err, core.ErrInsufficientFunds), errors.Is(err, core.ErrInsufficientFundsForTransfer):
return errCodeStdInsufficientFunds, true
// ExecutionErrors (1-199)
case errors.Is(err, core.ErrNonceTooLow):
return errCodeStdNonceTooLow, true
case errors.Is(err, core.ErrNonceTooHigh):
return errCodeStdNonceTooHigh, true
}
return 0, false
}

type invalidParamsError struct{ message string }

func (e *invalidParamsError) Error() string { return e.message }
Expand Down
94 changes: 94 additions & 0 deletions internal/ethapi/errors_test.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,94 @@
// Copyright 2026 The go-ethereum Authors
// This file is part of the go-ethereum library.
//
// The go-ethereum library is free software: you can redistribute it and/or modify
// it under the terms of the GNU Lesser General Public License as published by
// the Free Software Foundation, either version 3 of the License, or
// (at your option) any later version.
//
// The go-ethereum library is distributed in the hope that it will be useful,
// but WITHOUT ANY WARRANTY; without even the implied warranty of
// MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
// GNU Lesser General Public License for more details.
//
// You should have received a copy of the GNU Lesser General Public License
// along with the go-ethereum library. If not, see <http://www.gnu.org/licenses/>.

package ethapi

import (
"errors"
"fmt"
"testing"

"github.com/ethereum/go-ethereum/core"
"github.com/ethereum/go-ethereum/core/txpool"
)

// rpcErrorCoder mirrors the rpc package's interface for errors that carry a
// JSON-RPC error code.
type rpcErrorCoder interface {
error
ErrorCode() int
}

// TestTxSubmitError verifies that transaction-submission errors are mapped to
// the standardized execution-apis JSON-RPC error codes while preserving the
// original error message, and that unmapped errors are returned unchanged.
func TestTxSubmitError(t *testing.T) {
for _, tt := range []struct {
name string
err error
code int
}{
{"nonce too low", core.ErrNonceTooLow, errCodeStdNonceTooLow},
{"nonce too high", core.ErrNonceTooHigh, errCodeStdNonceTooHigh},
{"intrinsic gas", core.ErrIntrinsicGas, errCodeStdIntrinsicGas},
{"gas price too low", txpool.ErrTxGasPriceTooLow, errCodeStdGasPriceTooLow},
{"exceeds block gas limit", txpool.ErrGasLimit, errCodeStdGasExceedsBlockLimit},
{"tip above fee cap", core.ErrTipAboveFeeCap, errCodeStdTipAboveFeeCap},
{"gas uint overflow", core.ErrGasUintOverflow, errCodeStdGasUintOverflow},
{"fee cap too low", core.ErrFeeCapTooLow, errCodeStdFeeCapTooLow},
{"tip very high", core.ErrTipVeryHigh, errCodeStdTipVeryHigh},
{"fee cap very high", core.ErrFeeCapVeryHigh, errCodeStdFeeCapVeryHigh},
{"insufficient funds", core.ErrInsufficientFunds, errCodeStdInsufficientFunds},
{"insufficient funds for transfer", core.ErrInsufficientFundsForTransfer, errCodeStdInsufficientFunds},
{"already known", txpool.ErrAlreadyKnown, errCodeStdAlreadyKnown},
{"invalid sender", txpool.ErrInvalidSender, errCodeStdInvalidSender},
{"replacement underpriced", txpool.ErrReplaceUnderpriced, errCodeStdReplaceUnderpriced},
} {
// The raw sentinel error and a wrapped variant (as the pool/state
// actually return them, e.g. "nonce too low: next nonce 5, tx nonce 0")
// must both map to the same code via errors.Is.
for _, err := range []error{tt.err, fmt.Errorf("%w: extra context", tt.err)} {
got := txSubmitError(err)
coder, ok := got.(rpcErrorCoder)
if !ok {
t.Fatalf("%s: txSubmitError(%q) = %T, want an error with ErrorCode()", tt.name, err, got)
}
if coder.ErrorCode() != tt.code {
t.Errorf("%s: code = %d, want %d", tt.name, coder.ErrorCode(), tt.code)
}
if got.Error() != err.Error() {
t.Errorf("%s: message = %q, want it preserved as %q", tt.name, got.Error(), err.Error())
}
}
}
}

// TestTxSubmitErrorPassthrough verifies that errors without a catalog code are
// returned unchanged (so they keep geth's default -32000 behavior) and that a
// nil error stays nil.
func TestTxSubmitErrorPassthrough(t *testing.T) {
if got := txSubmitError(nil); got != nil {
t.Fatalf("txSubmitError(nil) = %v, want nil", got)
}
unmapped := errors.New("some unrelated failure")
got := txSubmitError(unmapped)
if got != unmapped {
t.Fatalf("txSubmitError(unmapped) = %v, want the original error returned unchanged", got)
}
if _, ok := got.(rpcErrorCoder); ok {
t.Fatalf("unmapped error should not carry an ErrorCode()")
}
}
Loading