diff --git a/internal/ethapi/transaction_args.go b/internal/ethapi/transaction_args.go index 1032d067f1..3ecc384210 100644 --- a/internal/ethapi/transaction_args.go +++ b/internal/ethapi/transaction_args.go @@ -42,6 +42,7 @@ import ( type TransactionArgs struct { From *common.Address `json:"from"` To *common.Address `json:"to"` + Type *hexutil.Uint64 `json:"type,omitempty"` Gas *hexutil.Uint64 `json:"gas"` GasPrice *hexutil.Big `json:"gasPrice"` MaxFeePerGas *hexutil.Big `json:"maxFeePerGas"` @@ -101,6 +102,9 @@ type sidecarConfig struct { // setDefaults fills in default values for unspecified tx fields. func (args *TransactionArgs) setDefaults(ctx context.Context, b Backend, config sidecarConfig) error { + if err := args.validateTxTypeSupported(); err != nil { + return err + } if err := args.setBlobTxSidecar(ctx, config); err != nil { return err } @@ -176,6 +180,9 @@ func (args *TransactionArgs) setDefaults(ctx context.Context, b Backend, config } else { args.ChainID = (*hexutil.Big)(want) } + if err := args.validateTxTypeMatch(types.LegacyTxType); err != nil { + return err + } return nil } @@ -390,6 +397,9 @@ func (args *TransactionArgs) setBlobTxSidecar(ctx context.Context, config sideca // CallDefaults sanitizes the transaction arguments, often filling in zero values, // for the purpose of eth_call class of RPC methods. func (args *TransactionArgs) CallDefaults(globalGasCap uint64, baseFee *big.Int, chainID *big.Int) error { + if err := args.validateTxTypeSupported(); err != nil { + return err + } // Reject invalid combinations of pre- and post-1559 fee styles if args.GasPrice != nil && (args.MaxFeePerGas != nil || args.MaxPriorityFeePerGas != nil) { return errors.New("both gasPrice and (maxFeePerGas or maxPriorityFeePerGas) specified") @@ -436,10 +446,40 @@ func (args *TransactionArgs) CallDefaults(globalGasCap uint64, baseFee *big.Int, if args.BlobFeeCap == nil && args.BlobHashes != nil { args.BlobFeeCap = new(hexutil.Big) } + if err := args.validateTxTypeMatch(types.LegacyTxType); err != nil { + return err + } return nil } +func (args *TransactionArgs) validateTxTypeSupported() error { + if args.Type == nil { + return nil + } + switch uint64(*args.Type) { + case types.LegacyTxType, types.AccessListTxType, types.DynamicFeeTxType, types.BlobTxType, types.SetCodeTxType: + return nil + default: + return fmt.Errorf("unsupported transaction type: %d", uint64(*args.Type)) + } +} + +func (args *TransactionArgs) validateTxTypeMatch(defaultType int) error { + if args.Type == nil { + return nil + } + // Blob txs cannot be contract creations. ToTransaction assumes this invariant. + if args.BlobHashes != nil && args.To == nil { + return errors.New(`missing "to" in blob transaction`) + } + inferred := args.ToTransaction(defaultType).Type() + if uint64(*args.Type) != uint64(inferred) { + return fmt.Errorf("transaction type mismatch (requested=%d inferred=%d)", uint64(*args.Type), inferred) + } + return nil +} + // ToMessage converts the transaction arguments to the Message type used by the // core evm. This method is used in calls and traces that do not require a real // live transaction. diff --git a/internal/ethapi/transaction_args_test.go b/internal/ethapi/transaction_args_test.go index ccb46a810d..20ff039368 100644 --- a/internal/ethapi/transaction_args_test.go +++ b/internal/ethapi/transaction_args_test.go @@ -256,6 +256,79 @@ func TestSetFeeDefaults(t *testing.T) { } } +func TestTransactionArgsRejectUnsupportedTypeInCallDefaults(t *testing.T) { + t.Parallel() + + badType := hexutil.Uint64(0x5) + args := &TransactionArgs{Type: &badType} + err := args.CallDefaults(0, big.NewInt(1), big.NewInt(1)) + if err == nil { + t.Fatal("expected error for unsupported transaction type") + } + if err.Error() != "unsupported transaction type: 5" { + t.Fatalf("unexpected error: %v", err) + } +} + +func TestTransactionArgsRejectUnsupportedTypeInSetDefaults(t *testing.T) { + t.Parallel() + + badType := hexutil.Uint64(0x5) + gas := hexutil.Uint64(21000) + to := common.Address{0x1} + args := &TransactionArgs{ + To: &to, + Gas: &gas, + Type: &badType, + } + err := args.setDefaults(context.Background(), newBackendMock(), sidecarConfig{}) + if err == nil { + t.Fatal("expected error for unsupported transaction type") + } + if err.Error() != "unsupported transaction type: 5" { + t.Fatalf("unexpected error: %v", err) + } +} + +func TestTransactionArgsRejectTypeMismatchInCallDefaults(t *testing.T) { + t.Parallel() + + requestedLegacy := hexutil.Uint64(types.LegacyTxType) + args := &TransactionArgs{ + Type: &requestedLegacy, + MaxFeePerGas: (*hexutil.Big)(big.NewInt(2)), + } + err := args.CallDefaults(0, big.NewInt(1), big.NewInt(1)) + if err == nil { + t.Fatal("expected type mismatch error") + } + if err.Error() != "transaction type mismatch (requested=0 inferred=2)" { + t.Fatalf("unexpected error: %v", err) + } +} + +func TestTransactionArgsRejectTypeMismatchInSetDefaults(t *testing.T) { + t.Parallel() + + requestedDynamic := hexutil.Uint64(types.DynamicFeeTxType) + gas := hexutil.Uint64(21000) + gasPrice := (*hexutil.Big)(big.NewInt(1)) + to := common.Address{0x1} + args := &TransactionArgs{ + To: &to, + Gas: &gas, + GasPrice: gasPrice, + Type: &requestedDynamic, + } + err := args.setDefaults(context.Background(), newBackendMock(), sidecarConfig{}) + if err == nil { + t.Fatal("expected type mismatch error") + } + if err.Error() != "transaction type mismatch (requested=2 inferred=0)" { + t.Fatalf("unexpected error: %v", err) + } +} + type backendMock struct { current *types.Header config *params.ChainConfig