diff --git a/core/types/dynamic_fee_tx.go b/core/types/dynamic_fee_tx.go index bcd1726dc7e..a165b0109c5 100644 --- a/core/types/dynamic_fee_tx.go +++ b/core/types/dynamic_fee_tx.go @@ -49,6 +49,7 @@ func (tx *DynamicFeeTransaction) GetEffectiveGasTip(baseFee *uint256.Int) *uint2 } gasFeeCap := tx.GetFeeCap() // return 0 because effectiveFee cant be < 0 + // transaction max fee is below base fee if gasFeeCap.Lt(baseFee) { return uint256.NewInt(0) } diff --git a/turbo/jsonrpc/send_transaction.go b/turbo/jsonrpc/send_transaction.go index 813b4b6b081..9ce098809d6 100644 --- a/turbo/jsonrpc/send_transaction.go +++ b/turbo/jsonrpc/send_transaction.go @@ -7,8 +7,10 @@ import ( "math/big" "github.com/erigontech/erigon-lib/common" + "github.com/erigontech/erigon-lib/common/hexutil" "github.com/erigontech/erigon-lib/common/hexutility" txPoolProto "github.com/erigontech/erigon-lib/gointerfaces/txpoolproto" + "github.com/holiman/uint256" "github.com/erigontech/erigon/core/types" "github.com/erigontech/erigon/params" @@ -21,11 +23,25 @@ func (api *APIImpl) SendRawTransaction(ctx context.Context, encodedTx hexutility return common.Hash{}, err } - // If the transaction fee cap is already specified, ensure the - // fee of the given transaction is _reasonable_. - if err := checkTxFee(txn.GetPrice().ToBig(), txn.GetGas(), api.FeeCap); err != nil { - return common.Hash{}, err + if txn.Type() == types.BlobTxType || txn.Type() == types.DynamicFeeTxType || txn.Type() == types.SetCodeTxType { + baseFeeBig, err := api.BaseFee(ctx) + if err != nil { + return common.Hash{}, err + } + + // If the transaction fee cap is already specified, ensure the + // effective gas fee is less than fee cap. + if err := checkDynamicTxFee(txn.GetFeeCap(), baseFeeBig); err != nil { + return common.Hash{}, err + } + } else { + // If the transaction fee cap is already specified, ensure the + // fee of the given transaction is _reasonable_. + if err := checkTxFee(txn.GetPrice().ToBig(), txn.GetGas(), api.FeeCap); err != nil { + return common.Hash{}, err + } } + if !txn.Protected() && !api.AllowUnprotectedTxs { return common.Hash{}, errors.New("only replay-protected (EIP-155) transactions allowed over RPC") } @@ -77,10 +93,28 @@ func checkTxFee(gasPrice *big.Int, gas uint64, gasCap float64) error { if gasCap == 0 { return nil } + feeEth := new(big.Float).Quo(new(big.Float).SetInt(new(big.Int).Mul(gasPrice, new(big.Int).SetUint64(gas))), new(big.Float).SetInt(big.NewInt(params.Ether))) feeFloat, _ := feeEth.Float64() if feeFloat > gasCap { return fmt.Errorf("tx fee (%.2f ether) exceeds the configured cap (%.2f ether)", feeFloat, gasCap) } + + return nil +} + +// checkTxFee is an internal function used to check whether the fee of +// the given transaction is _reasonable_(under the cap). +func checkDynamicTxFee(gasCap *uint256.Int, baseFeeBig *hexutil.Big) error { + baseFee := uint256.NewInt(0) + overflow := baseFee.SetFromBig(baseFeeBig.ToInt()) + if overflow { + return errors.New("opts.Value higher than 2^256-1") + } + + if gasCap.Lt(baseFee) { + return errors.New("fee cap is lower than the base fee") + } + return nil } diff --git a/turbo/jsonrpc/send_transaction_test.go b/turbo/jsonrpc/send_transaction_test.go index 16da6a44f58..92c5b473532 100644 --- a/turbo/jsonrpc/send_transaction_test.go +++ b/turbo/jsonrpc/send_transaction_test.go @@ -190,3 +190,105 @@ func pricedTransaction(nonce uint64, gaslimit uint64, gasprice *uint256.Int, key tx, _ := types.SignTx(types.NewTransaction(nonce, common.Address{}, uint256.NewInt(100), gaslimit, gasprice, nil), *types.LatestSignerForChainID(big.NewInt(1337)), key) return tx } + +func TestSendRawTransactionDynamicFee(t *testing.T) { + // Initialize a mock Ethereum node (Sentry) with protocol changes enabled + mockSentry := mock.MockWithAllProtocolChanges(t) + require := require.New(t) + logger := log.New() + + // Set up a single block step for the mock chain + oneBlockStep(mockSentry, require, t) + + // Create a test gRPC connection and initialize TxPool & API + ctx, conn := rpcdaemontest.CreateTestGrpcConn(t, mockSentry) + txPool := txpool.NewTxpoolClient(conn) + api := jsonrpc.NewEthAPI( + newBaseApiForTest(mockSentry), + mockSentry.DB, + nil, + txPool, + nil, + 5_000_000, // Gas limit + 1*params.GWei, + 100_000, + false, + 100_000, + 128, + logger, + ) + + // Get the current base fee + baseFee, err := api.BaseFee(ctx) + require.NoError(err) + baseFeeValue := baseFee.Uint64() + + // Define gas tip (priority fee) + gasTip := uint256.NewInt(5 * params.Wei) + + // --- Test Case 1: Transaction with valid gas fee cap --- + { + // Gas fee cap: 2x BaseFee + Tip + gasFeeCap := uint256.NewInt((2 * baseFeeValue) + gasTip.Uint64()) + + // Create and sign a transaction + txn, err := types.SignTx( + types.NewEIP1559Transaction( + uint256.Int{1337}, // Nonce + 0, // Gas price (not used in EIP-1559) + common.Address{1}, // Recipient + uint256.NewInt(1234), + params.TxGas, + uint256.NewInt(2_000_000), + gasTip, + gasFeeCap, + nil, + ), + *types.LatestSignerForChainID(mockSentry.ChainConfig.ChainID), + mockSentry.Key, + ) + require.NoError(err) + + // Serialize the transaction + buf := bytes.NewBuffer(nil) + err = txn.MarshalBinary(buf) + require.NoError(err) + + // Send the transaction + _, err = api.SendRawTransaction(ctx, buf.Bytes()) + require.NoError(err, "Transaction with sufficient gas fee cap should be accepted") + } + + // --- Test Case 2: Transaction with gas fee cap lower than base fee --- + { + // Gas fee cap: BaseFee - Tip (too low to be accepted) + gasFeeCap := uint256.NewInt(baseFeeValue - gasTip.Uint64()) + + // Create and sign a transaction + txn, err := types.SignTx( + types.NewEIP1559Transaction( + uint256.Int{1337}, // Nonce + 1, // Gas price (not used in EIP-1559) + common.Address{1}, // Recipient + uint256.NewInt(1234), + params.TxGas, + uint256.NewInt(2_000_000), + gasTip, + gasFeeCap, + nil, + ), + *types.LatestSignerForChainID(mockSentry.ChainConfig.ChainID), + mockSentry.Key, + ) + require.NoError(err) + + // Serialize the transaction + buf := bytes.NewBuffer(nil) + err = txn.MarshalBinary(buf) + require.NoError(err) + + // Send the transaction (should fail) + _, err = api.SendRawTransaction(ctx, buf.Bytes()) + require.Error(err, "Transaction with gas fee cap lower than base fee should be rejected") + } +} diff --git a/turbo/stages/mock/mock_sentry.go b/turbo/stages/mock/mock_sentry.go index db6203bd52a..bd4d376990f 100644 --- a/turbo/stages/mock/mock_sentry.go +++ b/turbo/stages/mock/mock_sentry.go @@ -637,6 +637,22 @@ func MockWithTxPool(t *testing.T) *MockSentry { return MockWithEverything(t, gspec, key, prune.DefaultMode, ethash.NewFaker(), blockBufferSize, true, false, checkStateRoot) } +func MockWithAllProtocolChanges(t *testing.T) *MockSentry { + funds := big.NewInt(1 * params.Ether) + key, _ := crypto.HexToECDSA("b71c71a67e1177ad4e901695e1b4b9ee17ae16c6668d313eac2f96dbcda3f291") + address := crypto.PubkeyToAddress(key.PublicKey) + chainConfig := params.AllProtocolChanges + gspec := &types.Genesis{ + Config: chainConfig, + Alloc: types.GenesisAlloc{ + address: {Balance: funds}, + }, + } + + checkStateRoot := true + return MockWithEverything(t, gspec, key, prune.DefaultMode, ethash.NewFaker(), blockBufferSize, true, false, checkStateRoot) +} + func MockWithZeroTTD(t *testing.T, withPosDownloader bool) *MockSentry { funds := big.NewInt(1 * params.Ether) key, _ := crypto.HexToECDSA("b71c71a67e1177ad4e901695e1b4b9ee17ae16c6668d313eac2f96dbcda3f291")