Skip to content

Commit

Permalink
TxPool: added check dynamic transactions gas fee (#13630)
Browse files Browse the repository at this point in the history
Update `SendRawTransaction` method with additional check for transaction
fee.

* Introduced `checkDynamicTxFee` to validate the fee cap against the
base fee for dynamic fee transactions.
* Check dynamic fee for transactions with types: `types.BlobTxType`,
`types.DynamicFeeTxType`, `types.SetCodeTxType` as dynamic fees applies
to these types of transactions
 
Closes
https://github.com/orgs/erigontech/projects/18/views/2?pane=issue&itemId=95054679&issue=erigontech%7Csecurity%7C16
  • Loading branch information
dvovk authored Jan 31, 2025
1 parent 07fec89 commit 39a712a
Show file tree
Hide file tree
Showing 4 changed files with 157 additions and 4 deletions.
1 change: 1 addition & 0 deletions core/types/dynamic_fee_tx.go
Original file line number Diff line number Diff line change
Expand Up @@ -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)
}
Expand Down
42 changes: 38 additions & 4 deletions turbo/jsonrpc/send_transaction.go
Original file line number Diff line number Diff line change
Expand Up @@ -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"
Expand All @@ -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")
}
Expand Down Expand Up @@ -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
}
102 changes: 102 additions & 0 deletions turbo/jsonrpc/send_transaction_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -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")
}
}
16 changes: 16 additions & 0 deletions turbo/stages/mock/mock_sentry.go
Original file line number Diff line number Diff line change
Expand Up @@ -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")
Expand Down

0 comments on commit 39a712a

Please sign in to comment.