From ad3d14ff9cc71da1f51dfd5b00786d6c7952d133 Mon Sep 17 00:00:00 2001 From: Ardit Marku Date: Fri, 22 Aug 2025 11:11:15 +0300 Subject: [PATCH 01/12] Track and index the latest Flow fees surge factor --- Makefile | 2 +- bootstrap/bootstrap.go | 34 ++++++++---- models/events.go | 29 +++++++++- models/fee_parameters.go | 46 +++++++++++++++ services/ingestion/engine.go | 13 +++++ services/ingestion/engine_test.go | 12 ++++ services/ingestion/event_subscriber.go | 40 ++++++++++--- storage/index.go | 6 ++ storage/mocks/FeeParametersIndexer.go | 77 ++++++++++++++++++++++++++ storage/pebble/fee_parameters.go | 56 +++++++++++++++++++ storage/pebble/keys.go | 3 + 11 files changed, 296 insertions(+), 22 deletions(-) create mode 100644 models/fee_parameters.go create mode 100644 storage/mocks/FeeParametersIndexer.go create mode 100644 storage/pebble/fee_parameters.go diff --git a/Makefile b/Makefile index a27483582..16f1457f7 100644 --- a/Makefile +++ b/Makefile @@ -99,8 +99,8 @@ generate: mockery --dir=storage --name=BlockIndexer --output=storage/mocks mockery --dir=storage --name=ReceiptIndexer --output=storage/mocks mockery --dir=storage --name=TransactionIndexer --output=storage/mocks - mockery --dir=storage --name=AccountIndexer --output=storage/mocks mockery --dir=storage --name=TraceIndexer --output=storage/mocks + mockery --dir=storage --name=FeeParametersIndexer --output=storage/mocks mockery --all --dir=services/traces --output=services/traces/mocks mockery --all --dir=services/ingestion --output=services/ingestion/mocks mockery --dir=models --name=Engine --output=models/mocks diff --git a/bootstrap/bootstrap.go b/bootstrap/bootstrap.go index f8c672b8b..96863392d 100644 --- a/bootstrap/bootstrap.go +++ b/bootstrap/bootstrap.go @@ -51,12 +51,13 @@ const ( ) type Storages struct { - Storage *pebble.Storage - Registers *pebble.RegisterStorage - Blocks storage.BlockIndexer - Transactions storage.TransactionIndexer - Receipts storage.ReceiptIndexer - Traces storage.TraceIndexer + Storage *pebble.Storage + Registers *pebble.RegisterStorage + Blocks storage.BlockIndexer + Transactions storage.TransactionIndexer + Receipts storage.ReceiptIndexer + Traces storage.TraceIndexer + FeeParameters storage.FeeParametersIndexer } type Publishers struct { @@ -191,6 +192,7 @@ func (b *Bootstrap) StartEventIngestion(ctx context.Context) error { b.storages.Receipts, b.storages.Transactions, b.storages.Traces, + b.storages.FeeParameters, b.publishers.Block, b.publishers.Logs, b.logger, @@ -645,6 +647,13 @@ func setupStorage( // // TODO(JanezP): verify storage account owner is correct // } + feeParameters := pebble.NewFeeParameters(store) + if _, err = feeParameters.Get(); errors.Is(err, errs.ErrEntityNotFound) { + if err := feeParameters.Store(models.DefaultFeeParameters, batch); err != nil { + return nil, nil, fmt.Errorf("failed to bootstrap fee parameters: %w", err) + } + } + if batch.Count() > 0 { err = batch.Commit(pebbleDB.Sync) if err != nil { @@ -653,12 +662,13 @@ func setupStorage( } return db, &Storages{ - Storage: store, - Blocks: blocks, - Registers: registerStore, - Transactions: pebble.NewTransactions(store), - Receipts: pebble.NewReceipts(store), - Traces: pebble.NewTraces(store), + Storage: store, + Blocks: blocks, + Registers: registerStore, + Transactions: pebble.NewTransactions(store), + Receipts: pebble.NewReceipts(store), + Traces: pebble.NewTraces(store), + FeeParameters: feeParameters, }, nil } diff --git a/models/events.go b/models/events.go index 2d199f9f1..12cab63a5 100644 --- a/models/events.go +++ b/models/events.go @@ -13,8 +13,9 @@ import ( ) const ( - BlockExecutedQualifiedIdentifier = string(events.EventTypeBlockExecuted) - TransactionExecutedQualifiedIdentifier = string(events.EventTypeTransactionExecuted) + BlockExecutedQualifiedIdentifier = string(events.EventTypeBlockExecuted) + TransactionExecutedQualifiedIdentifier = string(events.EventTypeTransactionExecuted) + FeeParametersChangedQualifiedIdentifier = "FlowFees.FeeParametersChanged" ) // isBlockExecutedEvent checks whether the given event contains block executed data. @@ -33,6 +34,15 @@ func isTransactionExecutedEvent(event cadence.Event) bool { return event.EventType.QualifiedIdentifier == TransactionExecutedQualifiedIdentifier } +// isFeeParametersChangedEvent checks whether the given event contains updates +// to Flow fees parameters. +func isFeeParametersChangedEvent(event cadence.Event) bool { + if event.EventType == nil { + return false + } + return event.EventType.QualifiedIdentifier == FeeParametersChangedQualifiedIdentifier +} + // CadenceEvents contains Flow emitted events containing one or zero evm block executed event, // and multiple or zero evm transaction events. type CadenceEvents struct { @@ -42,6 +52,7 @@ type CadenceEvents struct { transactions []Transaction // transactions in the EVM block txEventPayloads []events.TransactionEventPayload // EVM.TransactionExecuted event payloads receipts []*Receipt // receipts for transactions + feeParameters *FeeParameters // updates to Flow fees parameters } // NewCadenceEvents decodes the events into evm types. @@ -124,6 +135,15 @@ func decodeCadenceEvents(events flow.BlockEvents) (*CadenceEvents, error) { e.txEventPayloads = append(e.txEventPayloads, *txEventPayload) e.receipts = append(e.receipts, receipt) } + + if isFeeParametersChangedEvent(val) { + feeParameters, err := decodeFeeParametersChangedEvent(val) + if err != nil { + return nil, err + } + + e.feeParameters = feeParameters + } } // safety check, we have a missing block in the events @@ -182,6 +202,11 @@ func (c *CadenceEvents) Receipts() []*Receipt { return c.receipts } +// FeeParameters returns any updates to the Flow fees parameters. +func (c *CadenceEvents) FeeParameters() *FeeParameters { + return c.feeParameters +} + // Empty checks if there is an EVM block included in the events. // If there are no evm block or transactions events this is a heartbeat event. func (c *CadenceEvents) Empty() bool { diff --git a/models/fee_parameters.go b/models/fee_parameters.go new file mode 100644 index 000000000..f30077fd3 --- /dev/null +++ b/models/fee_parameters.go @@ -0,0 +1,46 @@ +package models + +import ( + "fmt" + + "github.com/ethereum/go-ethereum/rlp" + "github.com/onflow/cadence" +) + +var DefaultFeeParameters = &FeeParameters{ + SurgeFactor: cadence.UFix64(100_000_000), + InclusionEffortCost: cadence.UFix64(100_000_000), + ExecutionEffortCost: cadence.UFix64(100_000_000), +} + +type FeeParameters struct { + SurgeFactor cadence.UFix64 `cadence:"surgeFactor"` + InclusionEffortCost cadence.UFix64 `cadence:"inclusionEffortCost"` + ExecutionEffortCost cadence.UFix64 `cadence:"executionEffortCost"` +} + +func (f *FeeParameters) ToBytes() ([]byte, error) { + return rlp.EncodeToBytes(f) +} + +func NewFeeParametersFromBytes(data []byte) (*FeeParameters, error) { + feeParameters := &FeeParameters{} + if err := rlp.DecodeBytes(data, feeParameters); err != nil { + return nil, err + } + + return feeParameters, nil +} + +func decodeFeeParametersChangedEvent(event cadence.Event) (*FeeParameters, error) { + feeParameters := &FeeParameters{} + if err := cadence.DecodeFields(event, feeParameters); err != nil { + return nil, fmt.Errorf( + "failed to Cadence-decode FlowFees.FeeParametersChanged event [%s]: %w", + event.String(), + err, + ) + } + + return feeParameters, nil +} diff --git a/services/ingestion/engine.go b/services/ingestion/engine.go index 04d012a07..5f0120078 100644 --- a/services/ingestion/engine.go +++ b/services/ingestion/engine.go @@ -48,6 +48,7 @@ type Engine struct { receipts storage.ReceiptIndexer transactions storage.TransactionIndexer traces storage.TraceIndexer + feeParameters storage.FeeParametersIndexer log zerolog.Logger evmLastHeight *models.SequentialHeight blocksPublisher *models.Publisher[*models.Block] @@ -65,6 +66,7 @@ func NewEventIngestionEngine( receipts storage.ReceiptIndexer, transactions storage.TransactionIndexer, traces storage.TraceIndexer, + feeParameters storage.FeeParametersIndexer, blocksPublisher *models.Publisher[*models.Block], logsPublisher *models.Publisher[[]*gethTypes.Log], log zerolog.Logger, @@ -84,6 +86,7 @@ func NewEventIngestionEngine( receipts: receipts, transactions: transactions, traces: traces, + feeParameters: feeParameters, log: log, blocksPublisher: blocksPublisher, logsPublisher: logsPublisher, @@ -217,6 +220,16 @@ func (e *Engine) processEvents(events *models.CadenceEvents) error { // indexEvents will replay the evm transactions using the block events and index all results. func (e *Engine) indexEvents(events *models.CadenceEvents, batch *pebbleDB.Batch) error { + if events.FeeParameters() != nil { + if err := e.feeParameters.Store(events.FeeParameters(), batch); err != nil { + return fmt.Errorf( + "failed to update fee parameters for height: %d, during events ingestion: %w", + events.CadenceHeight(), + err, + ) + } + } + // if heartbeat interval with no data still update the cadence height if events.Empty() { if err := e.blocks.SetLatestCadenceHeight(events.CadenceHeight(), batch); err != nil { diff --git a/services/ingestion/engine_test.go b/services/ingestion/engine_test.go index 1e4e56921..e99868331 100644 --- a/services/ingestion/engine_test.go +++ b/services/ingestion/engine_test.go @@ -54,6 +54,7 @@ func TestSerialBlockIngestion(t *testing.T) { Once() // make sure this isn't called multiple times traces := &storageMock.TraceIndexer{} + feeParams := &storageMock.FeeParametersIndexer{} eventsChan := make(chan models.BlockEvents) @@ -73,6 +74,7 @@ func TestSerialBlockIngestion(t *testing.T) { receipts, transactions, traces, + feeParams, models.NewPublisher[*models.Block](), models.NewPublisher[[]*gethTypes.Log](), zerolog.Nop(), @@ -134,6 +136,7 @@ func TestSerialBlockIngestion(t *testing.T) { Once() // make sure this isn't called multiple times traces := &storageMock.TraceIndexer{} + feeParams := &storageMock.FeeParametersIndexer{} eventsChan := make(chan models.BlockEvents) subscriber := &mocks.EventSubscriber{} @@ -152,6 +155,7 @@ func TestSerialBlockIngestion(t *testing.T) { receipts, transactions, traces, + feeParams, models.NewPublisher[*models.Block](), models.NewPublisher[[]*gethTypes.Log](), zerolog.Nop(), @@ -264,6 +268,8 @@ func TestBlockAndTransactionIngestion(t *testing.T) { return nil }) + feeParams := &storageMock.FeeParametersIndexer{} + engine := NewEventIngestionEngine( subscriber, replayer.NewBlocksProvider(blocks, flowGo.Emulator, nil), @@ -273,6 +279,7 @@ func TestBlockAndTransactionIngestion(t *testing.T) { receipts, transactions, traces, + feeParams, models.NewPublisher[*models.Block](), models.NewPublisher[[]*gethTypes.Log](), zerolog.Nop(), @@ -372,6 +379,8 @@ func TestBlockAndTransactionIngestion(t *testing.T) { return nil }) + feeParams := &storageMock.FeeParametersIndexer{} + engine := NewEventIngestionEngine( subscriber, replayer.NewBlocksProvider(blocks, flowGo.Emulator, nil), @@ -381,6 +390,7 @@ func TestBlockAndTransactionIngestion(t *testing.T) { receipts, transactions, traces, + feeParams, models.NewPublisher[*models.Block](), models.NewPublisher[[]*gethTypes.Log](), zerolog.Nop(), @@ -456,6 +466,7 @@ func TestBlockAndTransactionIngestion(t *testing.T) { Once() // make sure this isn't called multiple times traces := &storageMock.TraceIndexer{} + feeParams := &storageMock.FeeParametersIndexer{} eventsChan := make(chan models.BlockEvents) subscriber := &mocks.EventSubscriber{} @@ -475,6 +486,7 @@ func TestBlockAndTransactionIngestion(t *testing.T) { receipts, transactions, traces, + feeParams, models.NewPublisher[*models.Block](), models.NewPublisher[[]*gethTypes.Log](), zerolog.Nop(), diff --git a/services/ingestion/event_subscriber.go b/services/ingestion/event_subscriber.go index ed86cd273..9f8bb86af 100644 --- a/services/ingestion/event_subscriber.go +++ b/services/ingestion/event_subscriber.go @@ -146,7 +146,7 @@ func (r *RPCEventSubscriber) subscribe(ctx context.Context, height uint64) <-cha blockEventsStream, errChan, err = r.client.SubscribeEventsByBlockHeight( ctx, height, - blocksFilter(r.chain), + blocksEventFilter(r.chain), access.WithHeartbeatInterval(1), ) @@ -480,7 +480,7 @@ func (r *RPCEventSubscriber) fetchMissingData( // remove existing events blockEvents.Events = nil - for _, eventType := range blocksFilter(r.chain).EventTypes { + for _, eventType := range evmEventFilter(r.chain).EventTypes { recoveredEvents, err := r.client.GetEventsForHeightRange( ctx, eventType, @@ -561,11 +561,37 @@ func (r *RPCEventSubscriber) recover( return models.NewBlockEventsError(err) } -// blockFilter define events we subscribe to: -// A.{evm}.EVM.BlockExecuted and A.{evm}.EVM.TransactionExecuted, -// where {evm} is EVM deployed contract address, which depends on the chain ID we configure. -func blocksFilter(chainId flowGo.ChainID) flow.EventFilter { - evmAddress := common.Address(systemcontracts.SystemContractsForChain(chainId).EVMContract.Address) +// blocksEventFilter defines the full set of events we subscribe to: +// - A.{evm}.EVM.BlockExecuted +// - A.{evm}.EVM.TransactionExecuted, +// - A.{flow_fees}.FlowFees.FeeParametersChanged, +// where {evm} is the EVM deployed contract address, which depends on the +// configured chain ID and {flow_fees} is the FlowFees deployed contract +// address for the configured chain ID. +func blocksEventFilter(chainID flowGo.ChainID) flow.EventFilter { + contracts := systemcontracts.SystemContractsForChain(chainID) + flowFeesAddress := common.Address(contracts.FlowFees.Address) + eventFilter := evmEventFilter(chainID) + + feeParametersChangedEvent := common.NewAddressLocation( + nil, + flowFeesAddress, + models.FeeParametersChangedQualifiedIdentifier, + ).ID() + + eventFilter.EventTypes = append(eventFilter.EventTypes, feeParametersChangedEvent) + + return eventFilter +} + +// evmEventFilter defines the EVM-related events we subscribe to: +// - A.{evm}.EVM.BlockExecuted, +// - A.{evm}.EVM.TransactionExecuted, +// where {evm} is the EVM deployed contract address, which depends on the +// configured chain ID. +func evmEventFilter(chainID flowGo.ChainID) flow.EventFilter { + contracts := systemcontracts.SystemContractsForChain(chainID) + evmAddress := common.Address(contracts.EVMContract.Address) blockExecutedEvent := common.NewAddressLocation( nil, diff --git a/storage/index.go b/storage/index.go index 4b6083e3e..897733bab 100644 --- a/storage/index.go +++ b/storage/index.go @@ -102,3 +102,9 @@ type TraceIndexer interface { // GetTransaction will retrieve transaction trace by the transaction ID. GetTransaction(ID common.Hash) (json.RawMessage, error) } + +type FeeParametersIndexer interface { + Store(feeParameters *models.FeeParameters, batch *pebble.Batch) error + + Get() (*models.FeeParameters, error) +} diff --git a/storage/mocks/FeeParametersIndexer.go b/storage/mocks/FeeParametersIndexer.go new file mode 100644 index 000000000..c201ae5bc --- /dev/null +++ b/storage/mocks/FeeParametersIndexer.go @@ -0,0 +1,77 @@ +// Code generated by mockery v2.43.2. DO NOT EDIT. + +package mocks + +import ( + models "github.com/onflow/flow-evm-gateway/models" + mock "github.com/stretchr/testify/mock" + + pebble "github.com/cockroachdb/pebble" +) + +// FeeParametersIndexer is an autogenerated mock type for the FeeParametersIndexer type +type FeeParametersIndexer struct { + mock.Mock +} + +// Get provides a mock function with given fields: +func (_m *FeeParametersIndexer) Get() (*models.FeeParameters, error) { + ret := _m.Called() + + if len(ret) == 0 { + panic("no return value specified for Get") + } + + var r0 *models.FeeParameters + var r1 error + if rf, ok := ret.Get(0).(func() (*models.FeeParameters, error)); ok { + return rf() + } + if rf, ok := ret.Get(0).(func() *models.FeeParameters); ok { + r0 = rf() + } else { + if ret.Get(0) != nil { + r0 = ret.Get(0).(*models.FeeParameters) + } + } + + if rf, ok := ret.Get(1).(func() error); ok { + r1 = rf() + } else { + r1 = ret.Error(1) + } + + return r0, r1 +} + +// Store provides a mock function with given fields: feeParameters, batch +func (_m *FeeParametersIndexer) Store(feeParameters *models.FeeParameters, batch *pebble.Batch) error { + ret := _m.Called(feeParameters, batch) + + if len(ret) == 0 { + panic("no return value specified for Store") + } + + var r0 error + if rf, ok := ret.Get(0).(func(*models.FeeParameters, *pebble.Batch) error); ok { + r0 = rf(feeParameters, batch) + } else { + r0 = ret.Error(0) + } + + return r0 +} + +// NewFeeParametersIndexer creates a new instance of FeeParametersIndexer. It also registers a testing interface on the mock and a cleanup function to assert the mocks expectations. +// The first argument is typically a *testing.T value. +func NewFeeParametersIndexer(t interface { + mock.TestingT + Cleanup(func()) +}) *FeeParametersIndexer { + mock := &FeeParametersIndexer{} + mock.Mock.Test(t) + + t.Cleanup(func() { mock.AssertExpectations(t) }) + + return mock +} diff --git a/storage/pebble/fee_parameters.go b/storage/pebble/fee_parameters.go new file mode 100644 index 000000000..547803ba1 --- /dev/null +++ b/storage/pebble/fee_parameters.go @@ -0,0 +1,56 @@ +package pebble + +import ( + "fmt" + "sync" + + "github.com/cockroachdb/pebble" + "github.com/onflow/flow-evm-gateway/models" + "github.com/onflow/flow-evm-gateway/storage" +) + +var _ storage.FeeParametersIndexer = &FeeParameters{} + +type FeeParameters struct { + store *Storage + mu sync.Mutex +} + +func NewFeeParameters(store *Storage) *FeeParameters { + return &FeeParameters{ + store: store, + } +} + +func (f *FeeParameters) Store(feeParameters *models.FeeParameters, batch *pebble.Batch) error { + f.mu.Lock() + defer f.mu.Unlock() + + val, err := feeParameters.ToBytes() + if err != nil { + return err + } + + if err := f.store.set(feeParametersKey, nil, val, batch); err != nil { + return fmt.Errorf("failed to store fee parameters %s: %w", feeParameters, err) + } + + return nil +} + +func (f *FeeParameters) Get() (*models.FeeParameters, error) { + f.mu.Lock() + defer f.mu.Unlock() + + data, err := f.store.get(feeParametersKey, nil) + if err != nil { + return nil, fmt.Errorf("failed to get fee parameters: %w", err) + } + + feeParameters, err := models.NewFeeParametersFromBytes(data) + if err != nil { + return nil, err + } + + return feeParameters, nil +} diff --git a/storage/pebble/keys.go b/storage/pebble/keys.go index aa46b61a3..e1b591724 100644 --- a/storage/pebble/keys.go +++ b/storage/pebble/keys.go @@ -27,6 +27,9 @@ const ( // registers registerKeyMarker = byte(50) + // fee parameters keys + feeParametersKey = byte(60) + // special keys latestEVMHeightKey = byte(100) latestCadenceHeightKey = byte(102) From d23625513564663050200d7a517b9c89134f5523 Mon Sep 17 00:00:00 2001 From: Ardit Marku Date: Fri, 22 Aug 2025 11:12:07 +0300 Subject: [PATCH 02/12] Multiply eth_gasPrice with the latest surge factor --- api/api.go | 14 +++++++++++++- bootstrap/bootstrap.go | 1 + 2 files changed, 14 insertions(+), 1 deletion(-) diff --git a/api/api.go b/api/api.go index 78db1c6a0..765da4342 100644 --- a/api/api.go +++ b/api/api.go @@ -86,6 +86,7 @@ type BlockChainAPI struct { blocks storage.BlockIndexer transactions storage.TransactionIndexer receipts storage.ReceiptIndexer + feeParameters storage.FeeParametersIndexer indexingResumedHeight uint64 rateLimiter RateLimiter collector metrics.Collector @@ -98,6 +99,7 @@ func NewBlockChainAPI( blocks storage.BlockIndexer, transactions storage.TransactionIndexer, receipts storage.ReceiptIndexer, + feeParameters storage.FeeParametersIndexer, rateLimiter RateLimiter, collector metrics.Collector, indexingResumedHeight uint64, @@ -109,6 +111,7 @@ func NewBlockChainAPI( blocks: blocks, transactions: transactions, receipts: receipts, + feeParameters: feeParameters, indexingResumedHeight: indexingResumedHeight, rateLimiter: rateLimiter, collector: collector, @@ -1050,7 +1053,16 @@ func (b *BlockChainAPI) Coinbase(ctx context.Context) (common.Address, error) { // GasPrice returns a suggestion for a gas price for legacy transactions. func (b *BlockChainAPI) GasPrice(ctx context.Context) (*hexutil.Big, error) { - return (*hexutil.Big)(b.config.GasPrice), nil + feeParams, err := b.feeParameters.Get() + if err != nil { + return nil, err + } + + surgeFactor := uint64(feeParams.SurgeFactor) + multiplier := new(big.Int).Exp(big.NewInt(10), big.NewInt(int64(8)), nil) + gp := b.config.GasPrice.Uint64() + gasPrice := new(big.Int).SetUint64(uint64(gp * surgeFactor)) + return (*hexutil.Big)(new(big.Int).Div(gasPrice, multiplier)), nil } // GetUncleCountByBlockHash returns number of uncles in the block for the given block hash diff --git a/bootstrap/bootstrap.go b/bootstrap/bootstrap.go index 96863392d..7c6a97a9a 100644 --- a/bootstrap/bootstrap.go +++ b/bootstrap/bootstrap.go @@ -316,6 +316,7 @@ func (b *Bootstrap) StartAPIServer(ctx context.Context) error { b.storages.Blocks, b.storages.Transactions, b.storages.Receipts, + b.storages.FeeParameters, rateLimiter, b.collector, indexingResumedHeight, From c9168e2689ee186e21b358200bd9adc8fcc1afaa Mon Sep 17 00:00:00 2001 From: Ardit Marku Date: Mon, 25 Aug 2025 10:54:01 +0300 Subject: [PATCH 03/12] Update tx submission logic to take into account the surge factor changes --- api/api.go | 7 ++- services/requester/requester.go | 22 +++++++-- tests/e2e_web3js_test.go | 34 +++++++++++++ tests/web3js/eth_gas_price_surge_test.js | 63 ++++++++++++++++++++++++ 4 files changed, 121 insertions(+), 5 deletions(-) create mode 100644 tests/web3js/eth_gas_price_surge_test.js diff --git a/api/api.go b/api/api.go index 765da4342..b7674fc06 100644 --- a/api/api.go +++ b/api/api.go @@ -183,7 +183,12 @@ func (b *BlockChainAPI) SendRawTransaction( return common.Hash{}, err } - id, err := b.evm.SendRawTransaction(ctx, input) + feeParams, err := b.feeParameters.Get() + if err != nil { + return common.Hash{}, err + } + + id, err := b.evm.SendRawTransaction(ctx, input, feeParams) if err != nil { return handleError[common.Hash](err, l, b.collector) } diff --git a/services/requester/requester.go b/services/requester/requester.go index 723749912..e05a56a47 100644 --- a/services/requester/requester.go +++ b/services/requester/requester.go @@ -53,7 +53,11 @@ const estimateGasErrorRatio = 0.015 type Requester interface { // SendRawTransaction will submit signed transaction data to the network. // The submitted EVM transaction hash is returned. - SendRawTransaction(ctx context.Context, data []byte) (common.Hash, error) + SendRawTransaction( + ctx context.Context, + data []byte, + feeParams *models.FeeParameters, + ) (common.Hash, error) // GetBalance returns the amount of wei for the given address in the state of the // given EVM block height. @@ -164,7 +168,11 @@ func NewEVM( }, nil } -func (e *EVM) SendRawTransaction(ctx context.Context, data []byte) (common.Hash, error) { +func (e *EVM) SendRawTransaction( + ctx context.Context, + data []byte, + feeParams *models.FeeParameters, +) (common.Hash, error) { tx := &types.Transaction{} if err := tx.UnmarshalBinary(data); err != nil { return common.Hash{}, err @@ -225,8 +233,14 @@ func (e *EVM) SendRawTransaction(ctx context.Context, data []byte) (common.Hash, } } - if tx.GasPrice().Cmp(e.config.GasPrice) < 0 && e.config.EnforceGasPrice { - return common.Hash{}, errs.NewTxGasPriceTooLowError(e.config.GasPrice) + surgeFactor := uint64(feeParams.SurgeFactor) + multiplier := new(big.Int).Exp(big.NewInt(10), big.NewInt(int64(8)), nil) + gp := e.config.GasPrice.Uint64() + gasPrice := new(big.Int).SetUint64(uint64(gp * surgeFactor)) + newGasPrice := new(big.Int).Div(gasPrice, multiplier) + + if tx.GasPrice().Cmp(newGasPrice) < 0 && e.config.EnforceGasPrice { + return common.Hash{}, errs.NewTxGasPriceTooLowError(newGasPrice) } if e.config.TxStateValidation == config.LocalIndexValidation { diff --git a/tests/e2e_web3js_test.go b/tests/e2e_web3js_test.go index 86cfe8bda..ed51c356a 100644 --- a/tests/e2e_web3js_test.go +++ b/tests/e2e_web3js_test.go @@ -122,6 +122,40 @@ func TestWeb3_E2E(t *testing.T) { }) }) + t.Run("gas price with surge factor multipler", func(t *testing.T) { + runWeb3TestWithSetup(t, "eth_gas_price_surge_test", func(emu emulator.Emulator) { + res, err := flowSendTransaction( + emu, + ` + import FlowFees from 0xe5a8b7f23e8b548f + + // This transaction sets the FlowFees parameters + transaction() { + let flowFeesAccountAdmin: &FlowFees.Administrator + + prepare(signer: auth(BorrowValue) &Account) { + self.flowFeesAccountAdmin = signer.storage.borrow<&FlowFees.Administrator>( + from: /storage/flowFeesAdmin + ) + ?? panic("Unable to borrow reference to administrator resource") + } + + execute { + self.flowFeesAccountAdmin.setFeeParameters( + surgeFactor: 2.0, + inclusionEffortCost: 1.0, + executionEffortCost: 1.0 + ) + } + } + + `, + ) + require.NoError(t, err) + require.NoError(t, res.Error) + }) + }) + t.Run("test filter-related endpoints", func(t *testing.T) { runWeb3Test(t, "eth_filter_endpoints_test") }) diff --git a/tests/web3js/eth_gas_price_surge_test.js b/tests/web3js/eth_gas_price_surge_test.js new file mode 100644 index 000000000..8d6acc0f8 --- /dev/null +++ b/tests/web3js/eth_gas_price_surge_test.js @@ -0,0 +1,63 @@ +const utils = require('web3-utils') +const { assert } = require('chai') +const conf = require('./config') +const helpers = require('./helpers') +const web3 = conf.web3 + +it('updates the gas price', async () => { + let gasPrice = await web3.eth.getGasPrice() + assert.equal(gasPrice, 2n * conf.minGasPrice) + + let receiver = web3.eth.accounts.create() + + // make sure receiver balance is initially 0 + let receiverWei = await web3.eth.getBalance(receiver.address) + assert.equal(receiverWei, 0n) + + // get sender balance + let senderBalance = await web3.eth.getBalance(conf.eoa.address) + assert.equal(senderBalance, utils.toWei(conf.fundedAmount, 'ether')) + + let txCount = await web3.eth.getTransactionCount(conf.eoa.address) + assert.equal(0n, txCount) + + let transferValue = utils.toWei('2.5', 'ether') + // assert that the minimum acceptable gas price has been multiplied by the surge factor + try { + let transfer = await helpers.signAndSend({ + from: conf.eoa.address, + to: receiver.address, + value: transferValue, + gasPrice: gasPrice - 10n, + gasLimit: 55_000, + }) + assert.fail('should not have gotten here') + } catch (e) { + assert.include( + e.message, + `the minimum accepted gas price for transactions is: ${gasPrice}` + ) + } + + let transfer = await helpers.signAndSend({ + from: conf.eoa.address, + to: receiver.address, + value: transferValue, + gasPrice: gasPrice, + gasLimit: 55_000, + }) + assert.equal(transfer.receipt.status, conf.successStatus) + assert.equal(transfer.receipt.from, conf.eoa.address) + assert.equal(transfer.receipt.to, receiver.address) + + let latestBlockNumber = await web3.eth.getBlockNumber() + let latestBlock = await web3.eth.getBlock(latestBlockNumber) + assert.equal(latestBlock.transactions.length, 2) + + let transferTx = await web3.eth.getTransactionFromBlock(latestBlockNumber, 0) + let transferTxReceipt = await web3.eth.getTransactionReceipt(transferTx.hash) + assert.equal(transferTxReceipt.effectiveGasPrice, gasPrice) + + let coinbaseFeesTx = await web3.eth.getTransactionFromBlock(latestBlockNumber, 1) + assert.equal(coinbaseFeesTx.value, transferTxReceipt.gasUsed * gasPrice) +}) From 2a03e82144a57d5b1386848d1787911d4de3bf07 Mon Sep 17 00:00:00 2001 From: Ardit Marku Date: Mon, 25 Aug 2025 11:42:03 +0300 Subject: [PATCH 04/12] Multiply eth_maxPriorityFeePerGas with the latest surge factor --- api/api.go | 11 +++++- tests/e2e_web3js_test.go | 2 +- tests/web3js/eth_gas_price_surge_test.js | 50 ++++++++++++++++-------- 3 files changed, 45 insertions(+), 18 deletions(-) diff --git a/api/api.go b/api/api.go index b7674fc06..d645e4d2f 100644 --- a/api/api.go +++ b/api/api.go @@ -1108,7 +1108,16 @@ func (b *BlockChainAPI) GetUncleByBlockNumberAndIndex( // MaxPriorityFeePerGas returns a suggestion for a gas tip cap for dynamic fee transactions. func (b *BlockChainAPI) MaxPriorityFeePerGas(ctx context.Context) (*hexutil.Big, error) { - return (*hexutil.Big)(b.config.GasPrice), nil + feeParams, err := b.feeParameters.Get() + if err != nil { + return nil, err + } + + surgeFactor := uint64(feeParams.SurgeFactor) + multiplier := new(big.Int).Exp(big.NewInt(10), big.NewInt(int64(8)), nil) + gp := b.config.GasPrice.Uint64() + gasPrice := new(big.Int).SetUint64(uint64(gp * surgeFactor)) + return (*hexutil.Big)(new(big.Int).Div(gasPrice, multiplier)), nil } // Mining returns true if client is actively mining new blocks. diff --git a/tests/e2e_web3js_test.go b/tests/e2e_web3js_test.go index ed51c356a..d0a2248b8 100644 --- a/tests/e2e_web3js_test.go +++ b/tests/e2e_web3js_test.go @@ -122,7 +122,7 @@ func TestWeb3_E2E(t *testing.T) { }) }) - t.Run("gas price with surge factor multipler", func(t *testing.T) { + t.Run("gas price updated with surge factor multipler", func(t *testing.T) { runWeb3TestWithSetup(t, "eth_gas_price_surge_test", func(emu emulator.Emulator) { res, err := flowSendTransaction( emu, diff --git a/tests/web3js/eth_gas_price_surge_test.js b/tests/web3js/eth_gas_price_surge_test.js index 8d6acc0f8..a4184f0f2 100644 --- a/tests/web3js/eth_gas_price_surge_test.js +++ b/tests/web3js/eth_gas_price_surge_test.js @@ -4,31 +4,40 @@ const conf = require('./config') const helpers = require('./helpers') const web3 = conf.web3 -it('updates the gas price', async () => { +it('should update the value of eth_gasPrice', async () => { let gasPrice = await web3.eth.getGasPrice() + // The surge factor was set to 2.0 assert.equal(gasPrice, 2n * conf.minGasPrice) +}) - let receiver = web3.eth.accounts.create() - - // make sure receiver balance is initially 0 - let receiverWei = await web3.eth.getBalance(receiver.address) - assert.equal(receiverWei, 0n) +it('should update the value of eth_MaxPriorityFeePerGas', async () => { + let response = await helpers.callRPCMethod( + 'eth_maxPriorityFeePerGas', + [] + ) + assert.equal(response.status, 200) + assert.isDefined(response.body.result) + let maxPriorityFeePerGas = utils.hexToNumber(response.body.result) + // The surge factor was set to 2.0 + assert.equal(maxPriorityFeePerGas, 2n * conf.minGasPrice) +}) - // get sender balance - let senderBalance = await web3.eth.getBalance(conf.eoa.address) - assert.equal(senderBalance, utils.toWei(conf.fundedAmount, 'ether')) +it('should reject transactions with gas price lower than the updated value', async () => { + let receiver = web3.eth.accounts.create() + let transferValue = utils.toWei('2.5', 'ether') - let txCount = await web3.eth.getTransactionCount(conf.eoa.address) - assert.equal(0n, txCount) + let gasPrice = await web3.eth.getGasPrice() + // The surge factor was set to 2.0 + assert.equal(gasPrice, 2n * conf.minGasPrice) - let transferValue = utils.toWei('2.5', 'ether') - // assert that the minimum acceptable gas price has been multiplied by the surge factor + // assert that the minimum acceptable gas price + // has been multiplied by the surge factor try { - let transfer = await helpers.signAndSend({ + await helpers.signAndSend({ from: conf.eoa.address, to: receiver.address, value: transferValue, - gasPrice: gasPrice - 10n, + gasPrice: gasPrice - 10n, // provide a lower gas price gasLimit: 55_000, }) assert.fail('should not have gotten here') @@ -38,12 +47,21 @@ it('updates the gas price', async () => { `the minimum accepted gas price for transactions is: ${gasPrice}` ) } +}) + +it('should accept transactions with the updated gas price', async () => { + let receiver = web3.eth.accounts.create() + let transferValue = utils.toWei('2.5', 'ether') + + let gasPrice = await web3.eth.getGasPrice() + // The surge factor was set to 2.0 + assert.equal(gasPrice, 2n * conf.minGasPrice) let transfer = await helpers.signAndSend({ from: conf.eoa.address, to: receiver.address, value: transferValue, - gasPrice: gasPrice, + gasPrice: gasPrice, // provide the updated gas price gasLimit: 55_000, }) assert.equal(transfer.receipt.status, conf.successStatus) From 2e61b5b8389c6975c0e3500c8ea1fa7485efd019 Mon Sep 17 00:00:00 2001 From: Ardit Marku Date: Mon, 25 Aug 2025 12:31:06 +0300 Subject: [PATCH 05/12] Move gas price calculation to FeeParameters --- api/api.go | 14 ++++---------- models/fee_parameters.go | 16 +++++++++++++--- services/requester/requester.go | 11 +++-------- 3 files changed, 20 insertions(+), 21 deletions(-) diff --git a/api/api.go b/api/api.go index d645e4d2f..f846561d8 100644 --- a/api/api.go +++ b/api/api.go @@ -1063,11 +1063,8 @@ func (b *BlockChainAPI) GasPrice(ctx context.Context) (*hexutil.Big, error) { return nil, err } - surgeFactor := uint64(feeParams.SurgeFactor) - multiplier := new(big.Int).Exp(big.NewInt(10), big.NewInt(int64(8)), nil) - gp := b.config.GasPrice.Uint64() - gasPrice := new(big.Int).SetUint64(uint64(gp * surgeFactor)) - return (*hexutil.Big)(new(big.Int).Div(gasPrice, multiplier)), nil + gasPrice := feeParams.CalculateGasPrice(b.config.GasPrice) + return (*hexutil.Big)(gasPrice), nil } // GetUncleCountByBlockHash returns number of uncles in the block for the given block hash @@ -1113,11 +1110,8 @@ func (b *BlockChainAPI) MaxPriorityFeePerGas(ctx context.Context) (*hexutil.Big, return nil, err } - surgeFactor := uint64(feeParams.SurgeFactor) - multiplier := new(big.Int).Exp(big.NewInt(10), big.NewInt(int64(8)), nil) - gp := b.config.GasPrice.Uint64() - gasPrice := new(big.Int).SetUint64(uint64(gp * surgeFactor)) - return (*hexutil.Big)(new(big.Int).Div(gasPrice, multiplier)), nil + gasPrice := feeParams.CalculateGasPrice(b.config.GasPrice) + return (*hexutil.Big)(gasPrice), nil } // Mining returns true if client is actively mining new blocks. diff --git a/models/fee_parameters.go b/models/fee_parameters.go index f30077fd3..f087cdb4f 100644 --- a/models/fee_parameters.go +++ b/models/fee_parameters.go @@ -2,15 +2,20 @@ package models import ( "fmt" + "math/big" "github.com/ethereum/go-ethereum/rlp" "github.com/onflow/cadence" ) +const feeParamsPrecision = 100_000_000 + +var surgeFactorScale = big.NewInt(feeParamsPrecision) + var DefaultFeeParameters = &FeeParameters{ - SurgeFactor: cadence.UFix64(100_000_000), - InclusionEffortCost: cadence.UFix64(100_000_000), - ExecutionEffortCost: cadence.UFix64(100_000_000), + SurgeFactor: cadence.UFix64(feeParamsPrecision), + InclusionEffortCost: cadence.UFix64(feeParamsPrecision), + ExecutionEffortCost: cadence.UFix64(feeParamsPrecision), } type FeeParameters struct { @@ -23,6 +28,11 @@ func (f *FeeParameters) ToBytes() ([]byte, error) { return rlp.EncodeToBytes(f) } +func (f *FeeParameters) CalculateGasPrice(currentGasPrice *big.Int) *big.Int { + gasPrice := new(big.Int).SetUint64(currentGasPrice.Uint64() * uint64(f.SurgeFactor)) + return new(big.Int).Div(gasPrice, surgeFactorScale) +} + func NewFeeParametersFromBytes(data []byte) (*FeeParameters, error) { feeParameters := &FeeParameters{} if err := rlp.DecodeBytes(data, feeParameters); err != nil { diff --git a/services/requester/requester.go b/services/requester/requester.go index e05a56a47..bd6270bf5 100644 --- a/services/requester/requester.go +++ b/services/requester/requester.go @@ -233,14 +233,9 @@ func (e *EVM) SendRawTransaction( } } - surgeFactor := uint64(feeParams.SurgeFactor) - multiplier := new(big.Int).Exp(big.NewInt(10), big.NewInt(int64(8)), nil) - gp := e.config.GasPrice.Uint64() - gasPrice := new(big.Int).SetUint64(uint64(gp * surgeFactor)) - newGasPrice := new(big.Int).Div(gasPrice, multiplier) - - if tx.GasPrice().Cmp(newGasPrice) < 0 && e.config.EnforceGasPrice { - return common.Hash{}, errs.NewTxGasPriceTooLowError(newGasPrice) + gasPrice := feeParams.CalculateGasPrice(e.config.GasPrice) + if tx.GasPrice().Cmp(gasPrice) < 0 && e.config.EnforceGasPrice { + return common.Hash{}, errs.NewTxGasPriceTooLowError(gasPrice) } if e.config.TxStateValidation == config.LocalIndexValidation { From 36b1a36545e05242fbd9666c6c99650431859034 Mon Sep 17 00:00:00 2001 From: Ardit Marku Date: Tue, 26 Aug 2025 10:40:46 +0300 Subject: [PATCH 06/12] Update eth_feeHistory to take into account changes to Flow fees surge factor --- api/api.go | 16 +++--- tests/e2e_web3js_test.go | 64 +++++++++++++----------- tests/web3js/eth_gas_price_surge_test.js | 41 ++++++++++++--- 3 files changed, 78 insertions(+), 43 deletions(-) diff --git a/api/api.go b/api/api.go index f846561d8..0985eaf48 100644 --- a/api/api.go +++ b/api/api.go @@ -856,12 +856,6 @@ func (b *BlockChainAPI) FeeHistory( ) maxCount := min(uint64(blockCount), lastBlockNumber) - - blockRewards := make([]*hexutil.Big, len(rewardPercentiles)) - for i := range rewardPercentiles { - blockRewards[i] = (*hexutil.Big)(b.config.GasPrice) - } - for i := maxCount; i >= uint64(1); i-- { // If the requested block count is 5, and the last block number // is 20, then we need the blocks [16, 17, 18, 19, 20] in this @@ -878,6 +872,16 @@ func (b *BlockChainAPI) FeeHistory( baseFees = append(baseFees, (*hexutil.Big)(models.BaseFeePerGas)) + blockRewards := make([]*hexutil.Big, len(rewardPercentiles)) + feeParams, err := b.feeParameters.Get() + if err != nil { + continue + } + gasPrice := feeParams.CalculateGasPrice(b.config.GasPrice) + for i := range rewardPercentiles { + blockRewards[i] = (*hexutil.Big)(gasPrice) + } + rewards = append(rewards, blockRewards) gasUsedRatio := float64(block.TotalGasUsed) / float64(BlockGasLimit) diff --git a/tests/e2e_web3js_test.go b/tests/e2e_web3js_test.go index d0a2248b8..a7ef8481a 100644 --- a/tests/e2e_web3js_test.go +++ b/tests/e2e_web3js_test.go @@ -3,6 +3,7 @@ package tests import ( _ "embed" "encoding/hex" + "fmt" "math/big" "testing" "time" @@ -124,35 +125,40 @@ func TestWeb3_E2E(t *testing.T) { t.Run("gas price updated with surge factor multipler", func(t *testing.T) { runWeb3TestWithSetup(t, "eth_gas_price_surge_test", func(emu emulator.Emulator) { - res, err := flowSendTransaction( - emu, - ` - import FlowFees from 0xe5a8b7f23e8b548f - - // This transaction sets the FlowFees parameters - transaction() { - let flowFeesAccountAdmin: &FlowFees.Administrator - - prepare(signer: auth(BorrowValue) &Account) { - self.flowFeesAccountAdmin = signer.storage.borrow<&FlowFees.Administrator>( - from: /storage/flowFeesAdmin - ) - ?? panic("Unable to borrow reference to administrator resource") - } - - execute { - self.flowFeesAccountAdmin.setFeeParameters( - surgeFactor: 2.0, - inclusionEffortCost: 1.0, - executionEffortCost: 1.0 - ) - } - } - - `, - ) - require.NoError(t, err) - require.NoError(t, res.Error) + surgeFactorValues := []string{"1.1", "2.0", "4.0", "10.0", "100.0"} + for _, surgeFactor := range surgeFactorValues { + res, err := flowSendTransaction( + emu, + fmt.Sprintf( + ` + import FlowFees from 0xe5a8b7f23e8b548f + + // This transaction sets the FlowFees parameters + transaction() { + let flowFeesAccountAdmin: &FlowFees.Administrator + + prepare(signer: auth(BorrowValue) &Account) { + self.flowFeesAccountAdmin = signer.storage.borrow<&FlowFees.Administrator>( + from: /storage/flowFeesAdmin + ) ?? panic("Unable to borrow reference to administrator resource") + } + + execute { + self.flowFeesAccountAdmin.setFeeParameters( + surgeFactor: %s, + inclusionEffortCost: 1.0, + executionEffortCost: 1.0 + ) + } + } + + `, + surgeFactor, + ), + ) + require.NoError(t, err) + require.NoError(t, res.Error) + } }) }) diff --git a/tests/web3js/eth_gas_price_surge_test.js b/tests/web3js/eth_gas_price_surge_test.js index a4184f0f2..269decd28 100644 --- a/tests/web3js/eth_gas_price_surge_test.js +++ b/tests/web3js/eth_gas_price_surge_test.js @@ -6,8 +6,8 @@ const web3 = conf.web3 it('should update the value of eth_gasPrice', async () => { let gasPrice = await web3.eth.getGasPrice() - // The surge factor was set to 2.0 - assert.equal(gasPrice, 2n * conf.minGasPrice) + // The surge factor was last set to 100.0 + assert.equal(gasPrice, 100n * conf.minGasPrice) }) it('should update the value of eth_MaxPriorityFeePerGas', async () => { @@ -18,8 +18,8 @@ it('should update the value of eth_MaxPriorityFeePerGas', async () => { assert.equal(response.status, 200) assert.isDefined(response.body.result) let maxPriorityFeePerGas = utils.hexToNumber(response.body.result) - // The surge factor was set to 2.0 - assert.equal(maxPriorityFeePerGas, 2n * conf.minGasPrice) + // The surge factor was last set to 100.0 + assert.equal(maxPriorityFeePerGas, 100n * conf.minGasPrice) }) it('should reject transactions with gas price lower than the updated value', async () => { @@ -27,8 +27,8 @@ it('should reject transactions with gas price lower than the updated value', asy let transferValue = utils.toWei('2.5', 'ether') let gasPrice = await web3.eth.getGasPrice() - // The surge factor was set to 2.0 - assert.equal(gasPrice, 2n * conf.minGasPrice) + // The surge factor was last set to 100.0 + assert.equal(gasPrice, 100n * conf.minGasPrice) // assert that the minimum acceptable gas price // has been multiplied by the surge factor @@ -54,8 +54,8 @@ it('should accept transactions with the updated gas price', async () => { let transferValue = utils.toWei('2.5', 'ether') let gasPrice = await web3.eth.getGasPrice() - // The surge factor was set to 2.0 - assert.equal(gasPrice, 2n * conf.minGasPrice) + // The surge factor was last set to 100.0 + assert.equal(gasPrice, 100n * conf.minGasPrice) let transfer = await helpers.signAndSend({ from: conf.eoa.address, @@ -79,3 +79,28 @@ it('should accept transactions with the updated gas price', async () => { let coinbaseFeesTx = await web3.eth.getTransactionFromBlock(latestBlockNumber, 1) assert.equal(coinbaseFeesTx.value, transferTxReceipt.gasUsed * gasPrice) }) + +it('should update gas price for eth_feeFistory', async () => { + let response = await web3.eth.getFeeHistory(10, 'latest', [20]) + console.log('Response: ', response) + + assert.deepEqual( + response, + { + oldestBlock: 1n, + reward: [ + ['0x3a98'], // 100 * gas price = 15000 + ['0x3a98'], // 100 * gas price = 15000 + ['0x3a98'], // 100 * gas price = 15000 + ['0x3a98'], // 100 * gas price = 15000 + ['0x3a98'], // 100 * gas price = 15000 + ['0x3a98'], // 100 * gas price = 15000 + ['0x3a98'], // 100 * gas price = 15000 + ['0x3a98'], // 100 * gas price = 15000 + ['0x3a98'], // 100 * gas price = 15000 + ], + baseFeePerGas: [1n, 1n, 1n, 1n, 1n, 1n, 1n, 1n, 1n], + gasUsedRatio: [0, 0.006205458333333334, 0, 0, 0, 0, 0, 0, 0.00035] + } + ) +}) From 08fafda4f6958b8f964e076286502729d2ae3b63 Mon Sep 17 00:00:00 2001 From: Ardit Marku Date: Tue, 26 Aug 2025 11:08:23 +0300 Subject: [PATCH 07/12] Logging and gas calculation improvements --- api/api.go | 13 +++++++++---- bootstrap/bootstrap.go | 2 ++ models/fee_parameters.go | 10 ++++++++-- tests/web3js/eth_gas_price_surge_test.js | 3 ++- 4 files changed, 21 insertions(+), 7 deletions(-) diff --git a/api/api.go b/api/api.go index 0985eaf48..02aab02ca 100644 --- a/api/api.go +++ b/api/api.go @@ -875,6 +875,11 @@ func (b *BlockChainAPI) FeeHistory( blockRewards := make([]*hexutil.Big, len(rewardPercentiles)) feeParams, err := b.feeParameters.Get() if err != nil { + b.logger.Warn(). + Uint64("height", blockHeight). + Err(err). + Msg("failed to get fee parameters for block in fee history") + continue } gasPrice := feeParams.CalculateGasPrice(b.config.GasPrice) @@ -1064,9 +1069,9 @@ func (b *BlockChainAPI) Coinbase(ctx context.Context) (common.Address, error) { func (b *BlockChainAPI) GasPrice(ctx context.Context) (*hexutil.Big, error) { feeParams, err := b.feeParameters.Get() if err != nil { - return nil, err + b.logger.Warn().Err(err).Msg("fee parameters unavailable; falling back to base gas price") + return (*hexutil.Big)(b.config.GasPrice), nil } - gasPrice := feeParams.CalculateGasPrice(b.config.GasPrice) return (*hexutil.Big)(gasPrice), nil } @@ -1111,9 +1116,9 @@ func (b *BlockChainAPI) GetUncleByBlockNumberAndIndex( func (b *BlockChainAPI) MaxPriorityFeePerGas(ctx context.Context) (*hexutil.Big, error) { feeParams, err := b.feeParameters.Get() if err != nil { - return nil, err + b.logger.Warn().Err(err).Msg("fee parameters unavailable; falling back to base gas price") + return (*hexutil.Big)(b.config.GasPrice), nil } - gasPrice := feeParams.CalculateGasPrice(b.config.GasPrice) return (*hexutil.Big)(gasPrice), nil } diff --git a/bootstrap/bootstrap.go b/bootstrap/bootstrap.go index 7c6a97a9a..632fa0a5d 100644 --- a/bootstrap/bootstrap.go +++ b/bootstrap/bootstrap.go @@ -653,6 +653,8 @@ func setupStorage( if err := feeParameters.Store(models.DefaultFeeParameters, batch); err != nil { return nil, nil, fmt.Errorf("failed to bootstrap fee parameters: %w", err) } + } else if err != nil { + return nil, nil, fmt.Errorf("failed to load latest fee parameters: %w", err) } if batch.Count() > 0 { diff --git a/models/fee_parameters.go b/models/fee_parameters.go index f087cdb4f..134d520dd 100644 --- a/models/fee_parameters.go +++ b/models/fee_parameters.go @@ -29,8 +29,14 @@ func (f *FeeParameters) ToBytes() ([]byte, error) { } func (f *FeeParameters) CalculateGasPrice(currentGasPrice *big.Int) *big.Int { - gasPrice := new(big.Int).SetUint64(currentGasPrice.Uint64() * uint64(f.SurgeFactor)) - return new(big.Int).Div(gasPrice, surgeFactorScale) + if currentGasPrice == nil { + return new(big.Int) // zero + } + + // gasPrice = (currentGasPrice * surgeFactor) / feeParamsPrecision + surgeFactor := new(big.Int).SetUint64(uint64(f.SurgeFactor)) + gasPrice := new(big.Int).Mul(currentGasPrice, surgeFactor) + return new(big.Int).Quo(gasPrice, surgeFactorScale) } func NewFeeParametersFromBytes(data []byte) (*FeeParameters, error) { diff --git a/tests/web3js/eth_gas_price_surge_test.js b/tests/web3js/eth_gas_price_surge_test.js index 269decd28..ce47f8d96 100644 --- a/tests/web3js/eth_gas_price_surge_test.js +++ b/tests/web3js/eth_gas_price_surge_test.js @@ -17,7 +17,8 @@ it('should update the value of eth_MaxPriorityFeePerGas', async () => { ) assert.equal(response.status, 200) assert.isDefined(response.body.result) - let maxPriorityFeePerGas = utils.hexToNumber(response.body.result) + // Convert hex quantity to BigInt (e.g., "0x3a98" -> 15000n) + const maxPriorityFeePerGas = BigInt(response.body.result) // The surge factor was last set to 100.0 assert.equal(maxPriorityFeePerGas, 100n * conf.minGasPrice) }) From 86d82c14904e0c67f65d188dd144aebefe0f9887 Mon Sep 17 00:00:00 2001 From: Ardit Marku Date: Wed, 10 Sep 2025 11:00:36 +0300 Subject: [PATCH 08/12] Bootstrap surge factor using the network's current value --- bootstrap/bootstrap.go | 52 ++++++++++++++++++--- bootstrap/cadence/get_fees_surge_factor.cdc | 5 ++ models/fee_parameters.go | 10 ++-- services/requester/batch_tx_pool.go | 4 +- services/requester/remote_cadence_arch.go | 2 +- services/requester/requester.go | 2 +- services/requester/single_tx_pool.go | 2 +- services/requester/utils.go | 5 +- 8 files changed, 65 insertions(+), 17 deletions(-) create mode 100644 bootstrap/cadence/get_fees_surge_factor.cdc diff --git a/bootstrap/bootstrap.go b/bootstrap/bootstrap.go index 632fa0a5d..d8aca2ee5 100644 --- a/bootstrap/bootstrap.go +++ b/bootstrap/bootstrap.go @@ -2,6 +2,7 @@ package bootstrap import ( "context" + _ "embed" "errors" "fmt" "math" @@ -9,6 +10,7 @@ import ( pebbleDB "github.com/cockroachdb/pebble" gethTypes "github.com/ethereum/go-ethereum/core/types" + "github.com/onflow/cadence" "github.com/onflow/flow-go-sdk/access" "github.com/onflow/flow-go-sdk/access/grpc" "github.com/onflow/flow-go/fvm/environment" @@ -50,6 +52,11 @@ const ( DefaultResourceExhaustedMaxRetryDelay = 30 * time.Second ) +var ( + //go:embed cadence/get_fees_surge_factor.cdc + getFeesSurgeFactor []byte +) + type Storages struct { Storage *pebble.Storage Registers *pebble.RegisterStorage @@ -649,12 +656,12 @@ func setupStorage( // } feeParameters := pebble.NewFeeParameters(store) - if _, err = feeParameters.Get(); errors.Is(err, errs.ErrEntityNotFound) { - if err := feeParameters.Store(models.DefaultFeeParameters, batch); err != nil { - return nil, nil, fmt.Errorf("failed to bootstrap fee parameters: %w", err) - } - } else if err != nil { - return nil, nil, fmt.Errorf("failed to load latest fee parameters: %w", err) + currentFeeParams, err := getNetworkFeeParams(context.Background(), config, client, logger) + if err != nil { + return nil, nil, fmt.Errorf("failed to fetch current fees surge factor: %w", err) + } + if err := feeParameters.Store(currentFeeParams, batch); err != nil { + return nil, nil, fmt.Errorf("failed to bootstrap fee parameters: %w", err) } if batch.Count() > 0 { @@ -794,3 +801,36 @@ func (m *metricsWrapper) Stop() { m.stopFN() <-m.Done() } + +// getNetworkFeeParams returns the network's current Flow fees parameters +func getNetworkFeeParams( + ctx context.Context, + config config.Config, + client *requester.CrossSporkClient, + logger zerolog.Logger, +) (*models.FeeParameters, error) { + val, err := client.ExecuteScriptAtLatestBlock( + ctx, + requester.ReplaceAddresses(getFeesSurgeFactor, config.FlowNetworkID), + nil, + ) + if err != nil { + return nil, err + } + + // sanity check, should never occur + if _, ok := val.(cadence.UFix64); !ok { + return nil, fmt.Errorf("failed to convert surgeFactor %v to UFix64, got type: %T", val, val) + } + + surgeFactor := val.(cadence.UFix64) + + logger.Debug(). + Uint64("surge-factor", uint64(surgeFactor)). + Msg("get current surge factor executed") + + feeParameters := models.DefaultFeeParameters() + feeParameters.SurgeFactor = surgeFactor + + return feeParameters, nil +} diff --git a/bootstrap/cadence/get_fees_surge_factor.cdc b/bootstrap/cadence/get_fees_surge_factor.cdc new file mode 100644 index 000000000..a66c6bc50 --- /dev/null +++ b/bootstrap/cadence/get_fees_surge_factor.cdc @@ -0,0 +1,5 @@ +import FlowFees + +access(all) fun main(): UFix64 { + return FlowFees.getFeeParameters().surgeFactor +} \ No newline at end of file diff --git a/models/fee_parameters.go b/models/fee_parameters.go index 134d520dd..86656c260 100644 --- a/models/fee_parameters.go +++ b/models/fee_parameters.go @@ -12,10 +12,12 @@ const feeParamsPrecision = 100_000_000 var surgeFactorScale = big.NewInt(feeParamsPrecision) -var DefaultFeeParameters = &FeeParameters{ - SurgeFactor: cadence.UFix64(feeParamsPrecision), - InclusionEffortCost: cadence.UFix64(feeParamsPrecision), - ExecutionEffortCost: cadence.UFix64(feeParamsPrecision), +func DefaultFeeParameters() *FeeParameters { + return &FeeParameters{ + SurgeFactor: cadence.UFix64(feeParamsPrecision), + InclusionEffortCost: cadence.UFix64(feeParamsPrecision), + ExecutionEffortCost: cadence.UFix64(feeParamsPrecision), + } } type FeeParameters struct { diff --git a/services/requester/batch_tx_pool.go b/services/requester/batch_tx_pool.go index d9810491a..2ed95076b 100644 --- a/services/requester/batch_tx_pool.go +++ b/services/requester/batch_tx_pool.go @@ -218,7 +218,7 @@ func (t *BatchTxPool) batchSubmitTransactionsForSameAddress( return err } - script := replaceAddresses(batchRunTxScript, t.config.FlowNetworkID) + script := ReplaceAddresses(batchRunTxScript, t.config.FlowNetworkID) flowTx, err := t.buildTransaction( latestBlock, account, @@ -254,7 +254,7 @@ func (t *BatchTxPool) submitSingleTransaction( return err } - script := replaceAddresses(runTxScript, t.config.FlowNetworkID) + script := ReplaceAddresses(runTxScript, t.config.FlowNetworkID) flowTx, err := t.buildTransaction( latestBlock, account, diff --git a/services/requester/remote_cadence_arch.go b/services/requester/remote_cadence_arch.go index 4b5dc6f48..70870afe8 100644 --- a/services/requester/remote_cadence_arch.go +++ b/services/requester/remote_cadence_arch.go @@ -109,7 +109,7 @@ func (rca *RemoteCadenceArch) runCall(input []byte) (*evmTypes.ResultSummary, er scriptResult, err := rca.client.ExecuteScriptAtBlockHeight( context.Background(), rca.blockHeight, - replaceAddresses(dryRunScript, rca.chainID), + ReplaceAddresses(dryRunScript, rca.chainID), []cadence.Value{hexEncodedTx, hexEncodedAddress}, ) if err != nil { diff --git a/services/requester/requester.go b/services/requester/requester.go index bd6270bf5..cf5c1706d 100644 --- a/services/requester/requester.go +++ b/services/requester/requester.go @@ -453,7 +453,7 @@ func (e *EVM) GetCode( func (e *EVM) GetLatestEVMHeight(ctx context.Context) (uint64, error) { val, err := e.client.ExecuteScriptAtLatestBlock( ctx, - replaceAddresses(getLatestEVMHeight, e.config.FlowNetworkID), + ReplaceAddresses(getLatestEVMHeight, e.config.FlowNetworkID), nil, ) if err != nil { diff --git a/services/requester/single_tx_pool.go b/services/requester/single_tx_pool.go index d71954cd9..aac6be62b 100644 --- a/services/requester/single_tx_pool.go +++ b/services/requester/single_tx_pool.go @@ -98,7 +98,7 @@ func (t *SingleTxPool) Add( return err } - script := replaceAddresses(runTxScript, t.config.FlowNetworkID) + script := ReplaceAddresses(runTxScript, t.config.FlowNetworkID) flowTx, err := t.buildTransaction( latestBlock, account, diff --git a/services/requester/utils.go b/services/requester/utils.go index d3ee4d7fc..f6f453ce6 100644 --- a/services/requester/utils.go +++ b/services/requester/utils.go @@ -8,14 +8,15 @@ import ( "github.com/onflow/flow-go/model/flow" ) -// replaceAddresses replace the addresses based on the network -func replaceAddresses(script []byte, chainID flow.ChainID) []byte { +// ReplaceAddresses replace the addresses based on the network +func ReplaceAddresses(script []byte, chainID flow.ChainID) []byte { // make the list of all contracts we should replace address for sc := systemcontracts.SystemContractsForChain(chainID) contracts := []systemcontracts.SystemContract{ sc.EVMContract, sc.FungibleToken, sc.FlowToken, + sc.FlowFees, } s := string(script) From 3b19086aff05fd095d74320102639314c6591580 Mon Sep 17 00:00:00 2001 From: Ardit Marku Date: Wed, 10 Sep 2025 11:15:01 +0300 Subject: [PATCH 09/12] Fix format verb for feeParameters argument --- storage/pebble/fee_parameters.go | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/storage/pebble/fee_parameters.go b/storage/pebble/fee_parameters.go index 547803ba1..2182bcdf7 100644 --- a/storage/pebble/fee_parameters.go +++ b/storage/pebble/fee_parameters.go @@ -32,7 +32,7 @@ func (f *FeeParameters) Store(feeParameters *models.FeeParameters, batch *pebble } if err := f.store.set(feeParametersKey, nil, val, batch); err != nil { - return fmt.Errorf("failed to store fee parameters %s: %w", feeParameters, err) + return fmt.Errorf("failed to store fee parameters %v: %w", feeParameters, err) } return nil From 16e67bdaedd62bc2d1dee1f7a5b47b83959fb62d Mon Sep 17 00:00:00 2001 From: Ardit Marku Date: Wed, 10 Sep 2025 11:55:05 +0300 Subject: [PATCH 10/12] Add newline at the end of Cadence script --- bootstrap/cadence/get_fees_surge_factor.cdc | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/bootstrap/cadence/get_fees_surge_factor.cdc b/bootstrap/cadence/get_fees_surge_factor.cdc index a66c6bc50..5e24819ee 100644 --- a/bootstrap/cadence/get_fees_surge_factor.cdc +++ b/bootstrap/cadence/get_fees_surge_factor.cdc @@ -2,4 +2,4 @@ import FlowFees access(all) fun main(): UFix64 { return FlowFees.getFeeParameters().surgeFactor -} \ No newline at end of file +} From 4df6ee53620bf8a4b408153a787c238bbbe55a7e Mon Sep 17 00:00:00 2001 From: Ardit Marku Date: Mon, 15 Sep 2025 10:11:34 +0300 Subject: [PATCH 11/12] Improve eth_feeHistory block reward calculation --- api/api.go | 30 ++++++++++++------------ bootstrap/bootstrap.go | 5 ++-- tests/web3js/eth_gas_price_surge_test.js | 1 - 3 files changed, 17 insertions(+), 19 deletions(-) diff --git a/api/api.go b/api/api.go index 02aab02ca..79756ff00 100644 --- a/api/api.go +++ b/api/api.go @@ -856,6 +856,21 @@ func (b *BlockChainAPI) FeeHistory( ) maxCount := min(uint64(blockCount), lastBlockNumber) + + blockRewards := make([]*hexutil.Big, len(rewardPercentiles)) + gasPrice := b.config.GasPrice + + feeParams, err := b.feeParameters.Get() + if err != nil { + b.logger.Warn().Err(err).Msg("fee parameters unavailable; falling back to base gas price") + } else { + gasPrice = feeParams.CalculateGasPrice(b.config.GasPrice) + } + + for i := range rewardPercentiles { + blockRewards[i] = (*hexutil.Big)(gasPrice) + } + for i := maxCount; i >= uint64(1); i-- { // If the requested block count is 5, and the last block number // is 20, then we need the blocks [16, 17, 18, 19, 20] in this @@ -872,21 +887,6 @@ func (b *BlockChainAPI) FeeHistory( baseFees = append(baseFees, (*hexutil.Big)(models.BaseFeePerGas)) - blockRewards := make([]*hexutil.Big, len(rewardPercentiles)) - feeParams, err := b.feeParameters.Get() - if err != nil { - b.logger.Warn(). - Uint64("height", blockHeight). - Err(err). - Msg("failed to get fee parameters for block in fee history") - - continue - } - gasPrice := feeParams.CalculateGasPrice(b.config.GasPrice) - for i := range rewardPercentiles { - blockRewards[i] = (*hexutil.Big)(gasPrice) - } - rewards = append(rewards, blockRewards) gasUsedRatio := float64(block.TotalGasUsed) / float64(BlockGasLimit) diff --git a/bootstrap/bootstrap.go b/bootstrap/bootstrap.go index d8aca2ee5..9213178dc 100644 --- a/bootstrap/bootstrap.go +++ b/bootstrap/bootstrap.go @@ -819,12 +819,11 @@ func getNetworkFeeParams( } // sanity check, should never occur - if _, ok := val.(cadence.UFix64); !ok { + surgeFactor, ok := val.(cadence.UFix64) + if !ok { return nil, fmt.Errorf("failed to convert surgeFactor %v to UFix64, got type: %T", val, val) } - surgeFactor := val.(cadence.UFix64) - logger.Debug(). Uint64("surge-factor", uint64(surgeFactor)). Msg("get current surge factor executed") diff --git a/tests/web3js/eth_gas_price_surge_test.js b/tests/web3js/eth_gas_price_surge_test.js index ce47f8d96..1f9e42715 100644 --- a/tests/web3js/eth_gas_price_surge_test.js +++ b/tests/web3js/eth_gas_price_surge_test.js @@ -83,7 +83,6 @@ it('should accept transactions with the updated gas price', async () => { it('should update gas price for eth_feeFistory', async () => { let response = await web3.eth.getFeeHistory(10, 'latest', [20]) - console.log('Response: ', response) assert.deepEqual( response, From b89311eaf87f92d2e54d4ecd15868c9c20ee1b2a Mon Sep 17 00:00:00 2001 From: Ardit Marku Date: Mon, 15 Sep 2025 11:56:33 +0300 Subject: [PATCH 12/12] Fallback to default fee parameters in case of error --- api/api.go | 3 ++- 1 file changed, 2 insertions(+), 1 deletion(-) diff --git a/api/api.go b/api/api.go index 79756ff00..0e1f163ae 100644 --- a/api/api.go +++ b/api/api.go @@ -185,7 +185,8 @@ func (b *BlockChainAPI) SendRawTransaction( feeParams, err := b.feeParameters.Get() if err != nil { - return common.Hash{}, err + b.logger.Warn().Err(err).Msg("fee parameters unavailable; falling back to base gas price") + feeParams = models.DefaultFeeParameters() } id, err := b.evm.SendRawTransaction(ctx, input, feeParams)