internal/ethapi: return InvalidParams for malformed sendRawTransaction RLP

SendRawTransaction and SendRawTransactionSync currently return the bare
UnmarshalBinary error to the JSON-RPC layer. Because the underlying decode
errors don't implement rpc.Error.ErrorCode(), rpc/json.go:errorMessage falls
back to errcodeDefault = -32000. Per JSON-RPC 2.0, malformed method parameters
should use -32602 (InvalidParams), which is what Reth and Besu emit for the
same input.

internal/ethapi/errors.go already defines invalidParamsError with
ErrorCode() = errCodeInvalidParams. Wire it into the two decode paths and
add a parameterised test covering 5 distinct RLP-decode failure shapes
against both entry points.
This commit is contained in:
ManuelArto 2026-06-08 19:21:36 +02:00
parent 31d227ea83
commit 1e0855e5ae
2 changed files with 51 additions and 2 deletions

View file

@ -1735,7 +1735,7 @@ func (api *TransactionAPI) currentBlobSidecarVersion() byte {
func (api *TransactionAPI) SendRawTransaction(ctx context.Context, input hexutil.Bytes) (common.Hash, error) {
tx := new(types.Transaction)
if err := tx.UnmarshalBinary(input); err != nil {
return common.Hash{}, err
return common.Hash{}, &invalidParamsError{message: err.Error()}
}
// Convert legacy blob transaction proofs.
@ -1758,7 +1758,7 @@ func (api *TransactionAPI) SendRawTransaction(ctx context.Context, input hexutil
func (api *TransactionAPI) SendRawTransactionSync(ctx context.Context, input hexutil.Bytes, timeoutMs *uint64) (map[string]interface{}, error) {
tx := new(types.Transaction)
if err := tx.UnmarshalBinary(input); err != nil {
return nil, err
return nil, &invalidParamsError{message: err.Error()}
}
// Convert legacy blob transaction proofs.

View file

@ -4175,6 +4175,55 @@ func TestSendRawTransactionSync_Timeout(t *testing.T) {
}
}
// TestSendRawTransaction_InvalidParams_OnMalformedRLP verifies that malformed-RLP
// inputs are reported with JSON-RPC error code -32602 (InvalidParams), not the
// default -32000. Aligns with JSON-RPC 2.0 spec and the Reth/Besu implementations.
func TestSendRawTransaction_InvalidParams_OnMalformedRLP(t *testing.T) {
t.Parallel()
genesis := &core.Genesis{
Config: params.TestChainConfig,
Alloc: types.GenesisAlloc{},
}
b := newTestBackend(t, 0, genesis, ethash.NewFaker(), nil)
api := NewTransactionAPI(b, new(AddrLocker))
cases := []struct {
name string
raw string
}{
{"empty-list", "0xc0"},
{"truncated-short-list", "0xd4"},
{"unknown-tx-type", "0x09c0"},
{"eip4844-truncated-list", "0x03c0"},
{"eip7702-truncated-list", "0x04c0"},
}
for _, tc := range cases {
t.Run(tc.name+"/SendRawTransaction", func(t *testing.T) {
_, err := api.SendRawTransaction(context.Background(), hexutil.MustDecode(tc.raw))
assertInvalidParams(t, err)
})
t.Run(tc.name+"/SendRawTransactionSync", func(t *testing.T) {
_, err := api.SendRawTransactionSync(context.Background(), hexutil.MustDecode(tc.raw), nil)
assertInvalidParams(t, err)
})
}
}
func assertInvalidParams(t *testing.T, err error) {
t.Helper()
if err == nil {
t.Fatal("expected error, got nil")
}
var ec interface{ ErrorCode() int }
if !errors.As(err, &ec) {
t.Fatalf("expected error implementing ErrorCode(), got %T: %v", err, err)
}
if got, want := ec.ErrorCode(), -32602; got != want {
t.Fatalf("expected ErrorCode=%d (InvalidParams), got %d (%v)", want, got, err)
}
}
func TestGetStorageValues(t *testing.T) {
t.Parallel()