diff --git a/eth/backend.go b/eth/backend.go index 98b45ea63e..6e7a3895ff 100644 --- a/eth/backend.go +++ b/eth/backend.go @@ -390,7 +390,7 @@ func (s *Ethereum) APIs() []rpc.API { if s.config.RollupSequencerTxConditionalEnabled { log.Info("Enabling eth_sendRawTransactionConditional endpoint support") costRateLimit := rate.Limit(s.config.RollupSequencerTxConditionalCostRateLimit) - apis = append(apis, sequencerapi.GetSendRawTxConditionalAPI(s.APIBackend, s.seqRPCService, costRateLimit)) + apis = append(apis, sequencerapi.GetSendRawTxConditionalAPI(celoBackend, s.seqRPCService, costRateLimit)) } // Append all the local APIs and return diff --git a/internal/ethapi/api.go b/internal/ethapi/api.go index 9912784119..386f0d9b0f 100644 --- a/internal/ethapi/api.go +++ b/internal/ethapi/api.go @@ -32,6 +32,7 @@ import ( "github.com/ethereum/go-ethereum/accounts/keystore" "github.com/ethereum/go-ethereum/accounts/scwallet" "github.com/ethereum/go-ethereum/common" + "github.com/ethereum/go-ethereum/common/exchange" "github.com/ethereum/go-ethereum/common/hexutil" "github.com/ethereum/go-ethereum/common/math" "github.com/ethereum/go-ethereum/consensus" @@ -527,7 +528,7 @@ func (api *PersonalAccountAPI) SignTransaction(ctx context.Context, args Transac } // Before actually signing the transaction, ensure the transaction fee is reasonable. tx := args.ToTransaction(types.LegacyTxType) - if err := checkTxFee(tx.GasPrice(), tx.Gas(), api.b.RPCTxFeeCap()); err != nil { + if err := CheckTxFee(ctx, api.b, tx.GasPrice(), tx.Gas(), tx.FeeCurrency()); err != nil { return nil, err } signed, err := api.signTransaction(ctx, &args, passwd) @@ -2218,10 +2219,10 @@ func (api *TransactionAPI) sign(addr common.Address, tx *types.Transaction) (*ty } // SubmitTransaction is a helper function that submits tx to txPool and logs a message. -func SubmitTransaction(ctx context.Context, b Backend, tx *types.Transaction) (common.Hash, error) { +func SubmitTransaction(ctx context.Context, b CeloBackend, tx *types.Transaction) (common.Hash, error) { // If the transaction fee cap is already specified, ensure the // fee of the given transaction is _reasonable_. - if err := checkTxFee(tx.GasPrice(), tx.Gas(), b.RPCTxFeeCap()); err != nil { + if err := CheckTxFee(ctx, b, tx.GasPrice(), tx.Gas(), tx.FeeCurrency()); err != nil { return common.Hash{}, err } if !b.UnprotectedAllowed() && !tx.Protected() { @@ -2366,7 +2367,7 @@ func (api *TransactionAPI) SignTransaction(ctx context.Context, args Transaction } // Before actually sign the transaction, ensure the transaction fee is reasonable. tx := args.ToTransaction(types.LegacyTxType) - if err := checkTxFee(tx.GasPrice(), tx.Gas(), api.b.RPCTxFeeCap()); err != nil { + if err := CheckTxFee(ctx, api.b, tx.GasPrice(), tx.Gas(), tx.FeeCurrency()); err != nil { return nil, err } signed, err := api.sign(args.from(), tx) @@ -2434,7 +2435,7 @@ func (api *TransactionAPI) Resend(ctx context.Context, sendArgs TransactionArgs, if gasLimit != nil { gas = uint64(*gasLimit) } - if err := checkTxFee(price, gas, api.b.RPCTxFeeCap()); err != nil { + if err := CheckTxFee(ctx, api.b, price, gas, matchTx.FeeCurrency()); err != nil { return common.Hash{}, err } // Iterate the pending list for replacement @@ -2644,6 +2645,21 @@ func checkTxFee(gasPrice *big.Int, gas uint64, cap float64) error { } // CheckTxFee exports a helper function used to check whether the fee is reasonable -func CheckTxFee(gasPrice *big.Int, gas uint64, cap float64) error { - return checkTxFee(gasPrice, gas, cap) +func CheckTxFee(ctx context.Context, backend CeloBackend, gasPrice *big.Int, gas uint64, feeCurrency *common.Address) error { + // This early return prevents unnecessary API calls and ensures tests that don't involve fee currency conversion pass + if feeCurrency == nil { + return checkTxFee(gasPrice, gas, backend.RPCTxFeeCap()) + } + + rates, err := backend.GetExchangeRates(ctx, rpc.BlockNumberOrHashWithNumber(rpc.LatestBlockNumber)) + if err != nil { + return err + } + + gasPriceInCelo, err := exchange.ConvertCurrencyToCelo(rates, feeCurrency, gasPrice) + if err != nil { + return err + } + + return checkTxFee(gasPriceInCelo, gas, backend.RPCTxFeeCap()) } diff --git a/internal/ethapi/celo_api_test.go b/internal/ethapi/celo_api_test.go index 136190580d..f3721a893b 100644 --- a/internal/ethapi/celo_api_test.go +++ b/internal/ethapi/celo_api_test.go @@ -2,11 +2,14 @@ package ethapi import ( "context" + "fmt" "math/big" "testing" "github.com/ethereum/go-ethereum/common" + "github.com/ethereum/go-ethereum/common/exchange" "github.com/ethereum/go-ethereum/common/hexutil" + "github.com/ethereum/go-ethereum/core" "github.com/ethereum/go-ethereum/core/types" "github.com/ethereum/go-ethereum/crypto" "github.com/ethereum/go-ethereum/internal/blocktest" @@ -613,3 +616,50 @@ func TestRPCMarshalBlock_Celo1TotalDifficulty(t *testing.T) { assert.Equal(t, nil, res["totalDifficulty"]) }) } + +// TestCheckTxFee ensures CheckTxFee validates whether the product of GasPrice and Gas +// does not exceed a predefined cap. Additionally, if FeeCurrency +// is provided, it ensures that GasPrice is correctly converted to +// the native currency before validation +func TestCheckTxFee(t *testing.T) { + t.Parallel() + + var ( + txFeeCap = float64(1.5) + rates = common.ExchangeRates{ + core.DevFeeCurrencyAddr: big.NewRat(2, 1), + } + config = allEnabledChainConfig() + ) + + backend := newCeloBackendMock(config) + backend.SetExchangeRates(rates) + backend.SetRPCTxFeeCap(txFeeCap) + + t.Run("should allow transaction if fee does not exceed 1.5 Ether", func(t *testing.T) { + err := CheckTxFee(context.Background(), backend, big.NewInt(params.Ether), 1, nil) // 1 Ether + assert.NoError(t, err) + }) + + t.Run("should reject transaction if fee exceeds 1.5 Ether", func(t *testing.T) { + err := CheckTxFee(context.Background(), backend, big.NewInt(params.Ether), 2, nil) // 2 Ether + expected := fmt.Sprintf("tx fee (%.2f ether) exceeds the configured cap (%.2f ether)", 2.0, 1.5) + assert.ErrorContains(t, err, expected) + }) + + t.Run("should allow transaction if fee currency is given and converted fee does not exceed 1.5 Ether", func(t *testing.T) { + err := CheckTxFee(context.Background(), backend, big.NewInt(params.Ether), 2, &core.DevFeeCurrencyAddr) // 1 Ether + assert.NoError(t, err) + }) + + t.Run("should reject transaction if fee currency is given and converted fee exceeds 1.5 Ether", func(t *testing.T) { + err := CheckTxFee(context.Background(), backend, big.NewInt(params.Ether), 4, &core.DevFeeCurrencyAddr) // 2 Ether + expected := fmt.Sprintf("tx fee (%.2f ether) exceeds the configured cap (%.2f ether)", 2.0, 1.5) + assert.ErrorContains(t, err, expected) + }) + + t.Run("should reject transaction if fee currency is not registered", func(t *testing.T) { + err := CheckTxFee(context.Background(), backend, big.NewInt(params.Ether), 4, &core.DevFeeCurrencyAddr2) + assert.ErrorIs(t, err, exchange.ErrUnregisteredFeeCurrency) + }) +} diff --git a/internal/ethapi/transaction_args_test.go b/internal/ethapi/transaction_args_test.go index b7dd4d1fc2..600f71d4dc 100644 --- a/internal/ethapi/transaction_args_test.go +++ b/internal/ethapi/transaction_args_test.go @@ -318,6 +318,8 @@ type celoBackendMock struct { chainDb ethdb.Database blockByNumber map[int64]*types.Block receiptsByHash map[common.Hash]types.Receipts + rates common.ExchangeRates + rpcTxFeeCap float64 } func newCeloBackendMock(config *params.ChainConfig) *celoBackendMock { @@ -329,15 +331,21 @@ func newCeloBackendMock(config *params.ChainConfig) *celoBackendMock { } } +func (c *celoBackendMock) SetExchangeRates(rates common.ExchangeRates) { + c.rates = rates +} + +func (c *celoBackendMock) SetRPCTxFeeCap(txFeeCap float64) { + c.rpcTxFeeCap = txFeeCap +} + func (c *celoBackendMock) GetFeeBalance(ctx context.Context, blockNumOrHash rpc.BlockNumberOrHash, account common.Address, feeCurrency *common.Address) (*big.Int, error) { // Celo specific backend features are currently not tested return nil, errCeloNotImplemented } func (c *celoBackendMock) GetExchangeRates(ctx context.Context, blockNumOrHash rpc.BlockNumberOrHash) (common.ExchangeRates, error) { - var er common.ExchangeRates - // This Celo specific backend features are currently not tested - return er, errCeloNotImplemented + return c.rates, nil } func (c *celoBackendMock) ConvertToCurrency(ctx context.Context, blockNumOrHash rpc.BlockNumberOrHash, value *big.Int, fromFeeCurrency *common.Address) (*big.Int, error) { @@ -374,6 +382,8 @@ func (c *celoBackendMock) setReceipts(hash common.Hash, receipts types.Receipts) c.receiptsByHash[hash] = receipts } +func (c *celoBackendMock) RPCTxFeeCap() float64 { return c.rpcTxFeeCap } + type backendMock struct { current *types.Header config *params.ChainConfig diff --git a/internal/sequencerapi/api.go b/internal/sequencerapi/api.go index 9a64258948..d1ae174db0 100644 --- a/internal/sequencerapi/api.go +++ b/internal/sequencerapi/api.go @@ -23,12 +23,12 @@ var ( ) type sendRawTxCond struct { - b ethapi.Backend + b ethapi.CeloBackend seqRPC *rpc.Client costLimiter *rate.Limiter } -func GetSendRawTxConditionalAPI(b ethapi.Backend, seqRPC *rpc.Client, costRateLimit rate.Limit) rpc.API { +func GetSendRawTxConditionalAPI(b ethapi.CeloBackend, seqRPC *rpc.Client, costRateLimit rate.Limit) rpc.API { // Applying a manual bump to the burst to allow conditional txs to queue. Metrics will // will inform of adjustments that may need to be made here. costLimiter := rate.NewLimiter(costRateLimit, 3*params.TransactionConditionalMaxCost) @@ -104,7 +104,7 @@ func (s *sendRawTxCond) SendRawTransactionConditional(ctx context.Context, txByt // forward if seqRPC is set, otherwise submit the tx if s.seqRPC != nil { // Some precondition checks done by `ethapi.SubmitTransaction` that are good to also check here - if err := ethapi.CheckTxFee(tx.GasPrice(), tx.Gas(), s.b.RPCTxFeeCap()); err != nil { + if err := ethapi.CheckTxFee(ctx, s.b, tx.GasPrice(), tx.Gas(), tx.FeeCurrency()); err != nil { return common.Hash{}, err } if !s.b.UnprotectedAllowed() && !tx.Protected() { @@ -113,7 +113,7 @@ func (s *sendRawTxCond) SendRawTransactionConditional(ctx context.Context, txByt } var hash common.Hash - err := s.seqRPC.CallContext(ctx, &hash, "eth_sendRawTransactionConditional", txBytes, cond) + err = s.seqRPC.CallContext(ctx, &hash, "eth_sendRawTransactionConditional", txBytes, cond) return hash, err } else { // Set out-of-consensus internal tx fields