From 8598a72dbab7df38ee521a1af39d988a207e579b Mon Sep 17 00:00:00 2001 From: Ardit Marku Date: Fri, 22 Aug 2025 11:11:15 +0300 Subject: [PATCH 01/14] 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 2fb170cd8..d3d13e027 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 90f206031..a31643f94 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), ) @@ -471,7 +471,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, @@ -552,11 +552,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 7484cbcacb15c005b56976f4f866c0f864c9d824 Mon Sep 17 00:00:00 2001 From: Ardit Marku Date: Fri, 22 Aug 2025 11:12:07 +0300 Subject: [PATCH 02/14] 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 22f3c51c6..fe9ab5912 100644 --- a/api/api.go +++ b/api/api.go @@ -85,6 +85,7 @@ type BlockChainAPI struct { blocks storage.BlockIndexer transactions storage.TransactionIndexer receipts storage.ReceiptIndexer + feeParameters storage.FeeParametersIndexer indexingResumedHeight uint64 rateLimiter RateLimiter collector metrics.Collector @@ -97,6 +98,7 @@ func NewBlockChainAPI( blocks storage.BlockIndexer, transactions storage.TransactionIndexer, receipts storage.ReceiptIndexer, + feeParameters storage.FeeParametersIndexer, rateLimiter RateLimiter, collector metrics.Collector, indexingResumedHeight uint64, @@ -108,6 +110,7 @@ func NewBlockChainAPI( blocks: blocks, transactions: transactions, receipts: receipts, + feeParameters: feeParameters, indexingResumedHeight: indexingResumedHeight, rateLimiter: rateLimiter, collector: collector, @@ -1007,7 +1010,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 d3d13e027..d49e40348 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 453c678ddbb12fcdb59590cd7e9669afc0302f20 Mon Sep 17 00:00:00 2001 From: Ardit Marku Date: Mon, 25 Aug 2025 10:54:01 +0300 Subject: [PATCH 03/14] 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 fe9ab5912..ec80b3bc0 100644 --- a/api/api.go +++ b/api/api.go @@ -182,7 +182,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 56350d18b..b1292d5a6 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. @@ -163,7 +167,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 @@ -224,8 +232,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 323a44045..4ff0de7ec 100644 --- a/tests/e2e_web3js_test.go +++ b/tests/e2e_web3js_test.go @@ -114,6 +114,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 60fc001e4286d2a80ded86706dc1b2c984912ea1 Mon Sep 17 00:00:00 2001 From: Ardit Marku Date: Mon, 25 Aug 2025 11:42:03 +0300 Subject: [PATCH 04/14] 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 ec80b3bc0..4dcfba4d7 100644 --- a/api/api.go +++ b/api/api.go @@ -1065,7 +1065,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 4ff0de7ec..51f703c53 100644 --- a/tests/e2e_web3js_test.go +++ b/tests/e2e_web3js_test.go @@ -114,7 +114,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 55612753759f7889655d78c674be2d9b145409ac Mon Sep 17 00:00:00 2001 From: Ardit Marku Date: Mon, 25 Aug 2025 12:31:06 +0300 Subject: [PATCH 05/14] 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 4dcfba4d7..6981d036a 100644 --- a/api/api.go +++ b/api/api.go @@ -1020,11 +1020,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 @@ -1070,11 +1067,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 b1292d5a6..0ec803d54 100644 --- a/services/requester/requester.go +++ b/services/requester/requester.go @@ -232,14 +232,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 557d5f41c83006380753940c91f62b24ba48b919 Mon Sep 17 00:00:00 2001 From: Ardit Marku Date: Tue, 26 Aug 2025 10:40:46 +0300 Subject: [PATCH 06/14] 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 6981d036a..74229d3f9 100644 --- a/api/api.go +++ b/api/api.go @@ -813,12 +813,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 @@ -835,6 +829,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 51f703c53..d4d5f5fe5 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" @@ -116,35 +117,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 a09baeb3587fd4f76aed6b96355818e9b25afe71 Mon Sep 17 00:00:00 2001 From: Ardit Marku Date: Tue, 26 Aug 2025 11:08:23 +0300 Subject: [PATCH 07/14] 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 74229d3f9..8cc121c5b 100644 --- a/api/api.go +++ b/api/api.go @@ -832,6 +832,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) @@ -1021,9 +1026,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 } @@ -1068,9 +1073,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 d49e40348..43bae2b34 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 407106e3520193cc7cf798262b9a338140807a9e Mon Sep 17 00:00:00 2001 From: Ardit Marku Date: Thu, 28 Aug 2025 12:49:20 +0300 Subject: [PATCH 08/14] Track Flow fees params updates in its own subscriber service --- bootstrap/bootstrap.go | 8 + models/events.go | 46 ++-- services/ingestion/engine.go | 105 +++++---- services/ingestion/engine_test.go | 35 +++ services/ingestion/event_subscriber.go | 25 +-- services/ingestion/fee_params_subscriber.go | 205 ++++++++++++++++++ .../ingestion/mocks/FeeParamsSubscriber.go | 50 +++++ 7 files changed, 396 insertions(+), 78 deletions(-) create mode 100644 services/ingestion/fee_params_subscriber.go create mode 100644 services/ingestion/mocks/FeeParamsSubscriber.go diff --git a/bootstrap/bootstrap.go b/bootstrap/bootstrap.go index 43bae2b34..b9c6ac95e 100644 --- a/bootstrap/bootstrap.go +++ b/bootstrap/bootstrap.go @@ -182,9 +182,17 @@ func (b *Bootstrap) StartEventIngestion(ctx context.Context) error { ValidateResults: true, } + feeParamsSubscriber := ingestion.NewFeeParamsEventSubscriber( + b.logger, + b.client, + chainID, + nextCadenceHeight, + ) + // initialize event ingestion engine b.events = ingestion.NewEventIngestionEngine( subscriber, + feeParamsSubscriber, blocksProvider, b.storages.Storage, b.storages.Registers, diff --git a/models/events.go b/models/events.go index 12cab63a5..adc0f534f 100644 --- a/models/events.go +++ b/models/events.go @@ -52,7 +52,6 @@ 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. @@ -135,15 +134,6 @@ 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 @@ -202,11 +192,6 @@ 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 { @@ -279,3 +264,34 @@ func NewBlockEventsError(err error) BlockEvents { Err: err, } } + +type FeeParamsEvents struct { + FeeParameters *FeeParameters // updates to Flow fees parameters + Err error +} + +func NewFeeParamsEvents(events flow.BlockEvents) *FeeParamsEvents { + for _, event := range events.Events { + val := event.Value + if isFeeParametersChangedEvent(val) { + feeParameters, err := decodeFeeParametersChangedEvent(val) + return &FeeParamsEvents{ + FeeParameters: feeParameters, + Err: err, + } + } + } + + return &FeeParamsEvents{ + Err: fmt.Errorf( + "could not find any %s events", + FeeParametersChangedQualifiedIdentifier, + ), + } +} + +func NewFeeParamsEventsError(err error) *FeeParamsEvents { + return &FeeParamsEvents{ + Err: err, + } +} diff --git a/services/ingestion/engine.go b/services/ingestion/engine.go index 5f0120078..039d9d547 100644 --- a/services/ingestion/engine.go +++ b/services/ingestion/engine.go @@ -40,25 +40,27 @@ var _ models.Engine = &Engine{} type Engine struct { *models.EngineStatus - subscriber EventSubscriber - blocksProvider *replayer.BlocksProvider - store *pebble.Storage - registerStore *pebble.RegisterStorage - blocks storage.BlockIndexer - receipts storage.ReceiptIndexer - transactions storage.TransactionIndexer - traces storage.TraceIndexer - feeParameters storage.FeeParametersIndexer - log zerolog.Logger - evmLastHeight *models.SequentialHeight - blocksPublisher *models.Publisher[*models.Block] - logsPublisher *models.Publisher[[]*gethTypes.Log] - collector metrics.Collector - replayerConfig replayer.Config + subscriber EventSubscriber + feeParamsSubscriber FeeParamsSubscriber + blocksProvider *replayer.BlocksProvider + store *pebble.Storage + registerStore *pebble.RegisterStorage + blocks storage.BlockIndexer + receipts storage.ReceiptIndexer + transactions storage.TransactionIndexer + traces storage.TraceIndexer + feeParameters storage.FeeParametersIndexer + log zerolog.Logger + evmLastHeight *models.SequentialHeight + blocksPublisher *models.Publisher[*models.Block] + logsPublisher *models.Publisher[[]*gethTypes.Log] + collector metrics.Collector + replayerConfig replayer.Config } func NewEventIngestionEngine( subscriber EventSubscriber, + feeParamsSubscriber FeeParamsSubscriber, blocksProvider *replayer.BlocksProvider, store *pebble.Storage, registerStore *pebble.RegisterStorage, @@ -78,20 +80,21 @@ func NewEventIngestionEngine( return &Engine{ EngineStatus: models.NewEngineStatus(), - subscriber: subscriber, - blocksProvider: blocksProvider, - store: store, - registerStore: registerStore, - blocks: blocks, - receipts: receipts, - transactions: transactions, - traces: traces, - feeParameters: feeParameters, - log: log, - blocksPublisher: blocksPublisher, - logsPublisher: logsPublisher, - collector: collector, - replayerConfig: replayerConfig, + subscriber: subscriber, + feeParamsSubscriber: feeParamsSubscriber, + blocksProvider: blocksProvider, + store: store, + registerStore: registerStore, + blocks: blocks, + receipts: receipts, + transactions: transactions, + traces: traces, + feeParameters: feeParameters, + log: log, + blocksPublisher: blocksPublisher, + logsPublisher: logsPublisher, + collector: collector, + replayerConfig: replayerConfig, } } @@ -123,6 +126,7 @@ func (e *Engine) Run(ctx context.Context) error { defer e.MarkStopped() events := e.subscriber.Subscribe(ctx) + feeParamsEvents := e.feeParamsSubscriber.Subscribe(ctx) for { select { @@ -145,6 +149,20 @@ func (e *Engine) Run(ctx context.Context) error { e.log.Error().Err(err).Msg("failed to process EVM events") return err } + case feeParams, ok := <-feeParamsEvents: + if !ok { + return nil + } + if feeParams.Err != nil { + return fmt.Errorf( + "failure in FeeParametersChanged event subscription with: %w", + feeParams.Err, + ) + } + if err := e.processFeeParamsEvents(feeParams); err != nil { + e.log.Error().Err(err).Msg("failed to process FeeParametersChanged events") + return err + } } } } @@ -172,6 +190,25 @@ func (e *Engine) withBatch(f func(batch *pebbleDB.Batch) error) error { return nil } +func (e *Engine) processFeeParamsEvents(events *models.FeeParamsEvents) error { + if events == nil || events.FeeParameters == nil { + return nil + } + + err := e.withBatch( + func(batch *pebbleDB.Batch) error { + return e.feeParameters.Store(events.FeeParameters, batch) + }, + ) + if err != nil { + return fmt.Errorf("failed to update fee parameters during events ingestion: %w", err) + } + + e.log.Info().Msg("updated fee parameters") + + return nil +} + // processEvents converts the events to block and transactions and indexes them. // // BlockEvents are received by the access node API and contain Cadence height (always a single Flow block), @@ -220,16 +257,6 @@ 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 e99868331..5482531fb 100644 --- a/services/ingestion/engine_test.go +++ b/services/ingestion/engine_test.go @@ -65,8 +65,15 @@ func TestSerialBlockIngestion(t *testing.T) { return eventsChan }) + feeParamsSubscriber := &mocks.FeeParamsSubscriber{} + feeParamsSubscriber.On("Subscribe", mock.Anything). + Return(func(ctx context.Context) <-chan *models.FeeParamsEvents { + return make(chan *models.FeeParamsEvents) + }) + engine := NewEventIngestionEngine( subscriber, + feeParamsSubscriber, replayer.NewBlocksProvider(blocks, flowGo.Emulator, nil), store, registerStore, @@ -146,8 +153,15 @@ func TestSerialBlockIngestion(t *testing.T) { return eventsChan }) + feeParamsSubscriber := &mocks.FeeParamsSubscriber{} + feeParamsSubscriber.On("Subscribe", mock.Anything). + Return(func(ctx context.Context) <-chan *models.FeeParamsEvents { + return make(chan *models.FeeParamsEvents) + }) + engine := NewEventIngestionEngine( subscriber, + feeParamsSubscriber, replayer.NewBlocksProvider(blocks, flowGo.Emulator, nil), store, registerStore, @@ -270,8 +284,15 @@ func TestBlockAndTransactionIngestion(t *testing.T) { feeParams := &storageMock.FeeParametersIndexer{} + feeParamsSubscriber := &mocks.FeeParamsSubscriber{} + feeParamsSubscriber.On("Subscribe", mock.Anything). + Return(func(ctx context.Context) <-chan *models.FeeParamsEvents { + return make(chan *models.FeeParamsEvents) + }) + engine := NewEventIngestionEngine( subscriber, + feeParamsSubscriber, replayer.NewBlocksProvider(blocks, flowGo.Emulator, nil), store, registerStore, @@ -366,6 +387,12 @@ func TestBlockAndTransactionIngestion(t *testing.T) { return eventsChan }) + feeParamsSubscriber := &mocks.FeeParamsSubscriber{} + feeParamsSubscriber.On("Subscribe", mock.Anything). + Return(func(ctx context.Context) <-chan *models.FeeParamsEvents { + return make(chan *models.FeeParamsEvents) + }) + txCdc, txEvent, transaction, res, err := newTransaction(nextHeight) require.NoError(t, err) blockCdc, _, blockEvent, err := newBlock(nextHeight, []gethCommon.Hash{res.TxHash}) @@ -383,6 +410,7 @@ func TestBlockAndTransactionIngestion(t *testing.T) { engine := NewEventIngestionEngine( subscriber, + feeParamsSubscriber, replayer.NewBlocksProvider(blocks, flowGo.Emulator, nil), store, registerStore, @@ -477,8 +505,15 @@ func TestBlockAndTransactionIngestion(t *testing.T) { }). Once() + feeParamsSubscriber := &mocks.FeeParamsSubscriber{} + feeParamsSubscriber.On("Subscribe", mock.Anything). + Return(func(ctx context.Context) <-chan *models.FeeParamsEvents { + return make(chan *models.FeeParamsEvents) + }) + engine := NewEventIngestionEngine( subscriber, + feeParamsSubscriber, replayer.NewBlocksProvider(blocks, flowGo.Emulator, nil), store, registerStore, diff --git a/services/ingestion/event_subscriber.go b/services/ingestion/event_subscriber.go index a31643f94..0db1a2de5 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, - blocksEventFilter(r.chain), + evmEventFilter(r.chain), access.WithHeartbeatInterval(1), ) @@ -552,29 +552,6 @@ func (r *RPCEventSubscriber) recover( return models.NewBlockEventsError(err) } -// 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, diff --git a/services/ingestion/fee_params_subscriber.go b/services/ingestion/fee_params_subscriber.go new file mode 100644 index 000000000..e3e09838e --- /dev/null +++ b/services/ingestion/fee_params_subscriber.go @@ -0,0 +1,205 @@ +package ingestion + +import ( + "context" + "fmt" + "time" + + "google.golang.org/grpc/codes" + "google.golang.org/grpc/status" + + "github.com/onflow/cadence/common" + "github.com/onflow/flow-evm-gateway/models" + errs "github.com/onflow/flow-evm-gateway/models/errors" + "github.com/onflow/flow-evm-gateway/services/requester" + + "github.com/onflow/flow-go-sdk" + "github.com/onflow/flow-go-sdk/access" + "github.com/onflow/flow-go/fvm/systemcontracts" + flowGo "github.com/onflow/flow-go/model/flow" + "github.com/rs/zerolog" +) + +type FeeParamsSubscriber interface { + Subscribe(ctx context.Context) <-chan *models.FeeParamsEvents +} + +var _ FeeParamsSubscriber = &FeeParamsEventSubscriber{} + +type FeeParamsEventSubscriber struct { + logger zerolog.Logger + client *requester.CrossSporkClient + chain flowGo.ChainID + height uint64 +} + +func NewFeeParamsEventSubscriber( + logger zerolog.Logger, + client *requester.CrossSporkClient, + chainID flowGo.ChainID, + startHeight uint64, +) *FeeParamsEventSubscriber { + logger = logger.With().Str("component", "fee_params_subscriber").Logger() + return &FeeParamsEventSubscriber{ + logger: logger, + client: client, + chain: chainID, + height: startHeight, + } +} + +func (r *FeeParamsEventSubscriber) Subscribe(ctx context.Context) <-chan *models.FeeParamsEvents { + // buffered channel so that the decoding of the events can happen in parallel to other operations + eventsChan := make(chan *models.FeeParamsEvents, 1000) + + go func() { + defer func() { + close(eventsChan) + }() + + for evt := range r.subscribe(ctx, r.height) { + eventsChan <- evt + } + + r.logger.Warn().Msg("ended subscription for fee parameters changed events") + }() + + return eventsChan +} + +// subscribe to events by the provided height and handle any errors. +// +// Subscribing to EVM specific events and handle any disconnection errors +// as well as context cancellations. +func (r *FeeParamsEventSubscriber) subscribe(ctx context.Context, height uint64) <-chan *models.FeeParamsEvents { + // create the channel with a buffer size of 1, + // to avoid blocking on the two error cases below + eventsChan := make(chan *models.FeeParamsEvents, 1) + + _, err := r.client.GetBlockHeaderByHeight(ctx, height) + if err != nil { + err = fmt.Errorf("failed to subscribe for FeeParametersChanged events, the block height %d doesn't exist: %w", height, err) + eventsChan <- models.NewFeeParamsEventsError(err) + close(eventsChan) + return eventsChan + } + + var blockEventsStream <-chan flow.BlockEvents + var errChan <-chan error + + lastReceivedHeight := height + connect := func(height uint64) error { + var err error + + // we always use heartbeat interval of 1 to have the + // least amount of delay from the access node + blockEventsStream, errChan, err = r.client.SubscribeEventsByBlockHeight( + ctx, + height, + feeParamsEventFilter(r.chain), + access.WithHeartbeatInterval(1), + ) + + return err + } + + if err := connect(lastReceivedHeight); err != nil { + eventsChan <- models.NewFeeParamsEventsError( + fmt.Errorf("failed to subscribe for FeeParametersChanged events by block height: %d, with: %w", height, err), + ) + close(eventsChan) + return eventsChan + } + + go func() { + defer func() { + close(eventsChan) + }() + + for ctx.Err() == nil { + select { + case <-ctx.Done(): + r.logger.Info().Msg("event ingestion received done signal") + return + + case blockEvents, ok := <-blockEventsStream: + if !ok { + // typically we receive an error in the errChan before the channels are closed + var err error + err = errs.ErrDisconnected + if ctx.Err() != nil { + err = ctx.Err() + } + eventsChan <- models.NewFeeParamsEventsError(err) + return + } + + lastReceivedHeight = blockEvents.Height + if len(blockEvents.Events) == 0 { + continue + } + feeParamsEvents := models.NewFeeParamsEvents(blockEvents) + eventsChan <- feeParamsEvents + + case err, ok := <-errChan: + if !ok { + // typically we receive an error in the errChan before the channels are closed + var err error + err = errs.ErrDisconnected + if ctx.Err() != nil { + err = ctx.Err() + } + eventsChan <- models.NewFeeParamsEventsError(err) + return + } + + switch status.Code(err) { + case codes.NotFound: + // we can get not found when reconnecting after a disconnect/restart before the + // next block is finalized. just wait briefly and try again + time.Sleep(200 * time.Millisecond) + case codes.DeadlineExceeded, codes.Internal: + // these are sometimes returned when the stream is disconnected by a middleware or the server + default: + // skip reconnect on all other errors + eventsChan <- models.NewFeeParamsEventsError(fmt.Errorf("%w: %w", errs.ErrDisconnected, err)) + return + } + + if err := connect(lastReceivedHeight + 1); err != nil { + eventsChan <- models.NewFeeParamsEventsError( + fmt.Errorf( + "failed to resubscribe for events on height: %d, with: %w", + lastReceivedHeight+1, + err, + ), + ) + return + } + } + } + }() + + return eventsChan +} + +// feeParamsEventFilter defines the FlowFees set of events we subscribe to: +// - A.{flow_fees}.FlowFees.FeeParametersChanged, +// where {flow_fees} is the FlowFees deployed contract address for the +// configured chain ID. +func feeParamsEventFilter(chainID flowGo.ChainID) flow.EventFilter { + contracts := systemcontracts.SystemContractsForChain(chainID) + flowFeesAddress := common.Address(contracts.FlowFees.Address) + + feeParametersChangedEvent := common.NewAddressLocation( + nil, + flowFeesAddress, + models.FeeParametersChangedQualifiedIdentifier, + ).ID() + + return flow.EventFilter{ + EventTypes: []string{ + feeParametersChangedEvent, + }, + } +} diff --git a/services/ingestion/mocks/FeeParamsSubscriber.go b/services/ingestion/mocks/FeeParamsSubscriber.go new file mode 100644 index 000000000..6e6e19833 --- /dev/null +++ b/services/ingestion/mocks/FeeParamsSubscriber.go @@ -0,0 +1,50 @@ +// Code generated by mockery v2.43.2. DO NOT EDIT. + +package mocks + +import ( + context "context" + + mock "github.com/stretchr/testify/mock" + + models "github.com/onflow/flow-evm-gateway/models" +) + +// FeeParamsSubscriber is an autogenerated mock type for the FeeParamsSubscriber type +type FeeParamsSubscriber struct { + mock.Mock +} + +// Subscribe provides a mock function with given fields: ctx +func (_m *FeeParamsSubscriber) Subscribe(ctx context.Context) <-chan *models.FeeParamsEvents { + ret := _m.Called(ctx) + + if len(ret) == 0 { + panic("no return value specified for Subscribe") + } + + var r0 <-chan *models.FeeParamsEvents + if rf, ok := ret.Get(0).(func(context.Context) <-chan *models.FeeParamsEvents); ok { + r0 = rf(ctx) + } else { + if ret.Get(0) != nil { + r0 = ret.Get(0).(<-chan *models.FeeParamsEvents) + } + } + + return r0 +} + +// NewFeeParamsSubscriber creates a new instance of FeeParamsSubscriber. 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 NewFeeParamsSubscriber(t interface { + mock.TestingT + Cleanup(func()) +}) *FeeParamsSubscriber { + mock := &FeeParamsSubscriber{} + mock.Mock.Test(t) + + t.Cleanup(func() { mock.AssertExpectations(t) }) + + return mock +} From d395389d66e55a8a1d42905ded9d3439cbc0cc4b Mon Sep 17 00:00:00 2001 From: Ardit Marku Date: Fri, 29 Aug 2025 10:50:22 +0300 Subject: [PATCH 09/14] Use evmEventFilter method instead of blocksFilter for EVM event subscription --- services/ingestion/block_tracking_subscriber.go | 2 +- services/ingestion/sealing_verifier.go | 2 +- 2 files changed, 2 insertions(+), 2 deletions(-) diff --git a/services/ingestion/block_tracking_subscriber.go b/services/ingestion/block_tracking_subscriber.go index 27e95e150..27d641d1c 100644 --- a/services/ingestion/block_tracking_subscriber.go +++ b/services/ingestion/block_tracking_subscriber.go @@ -287,7 +287,7 @@ func (r *RPCBlockTrackingSubscriber) evmEventsForBlock( ctx context.Context, blockHeader *flow.BlockHeader, ) (flow.BlockEvents, error) { - eventTypes := blocksFilter(r.chain).EventTypes + eventTypes := evmEventFilter(r.chain).EventTypes // evm Block events blockEvents, err := r.getEventsByType(ctx, blockHeader, eventTypes[0]) diff --git a/services/ingestion/sealing_verifier.go b/services/ingestion/sealing_verifier.go index 13284f926..faae89aa2 100644 --- a/services/ingestion/sealing_verifier.go +++ b/services/ingestion/sealing_verifier.go @@ -134,7 +134,7 @@ func (v *SealingVerifier) Run(ctx context.Context) error { eventsChan, errChan, err = v.client.SubscribeEventsByBlockHeight( subscriptionCtx, height, - blocksFilter(v.chain), + evmEventFilter(v.chain), access.WithHeartbeatInterval(1), ) From 0f26f75e2157bf9a51c887d2769c18f0f2d90470 Mon Sep 17 00:00:00 2001 From: Ardit Marku Date: Wed, 10 Sep 2025 11:31:35 +0300 Subject: [PATCH 10/14] 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 +- storage/pebble/fee_parameters.go | 2 +- 9 files changed, 66 insertions(+), 18 deletions(-) create mode 100644 bootstrap/cadence/get_fees_surge_factor.cdc diff --git a/bootstrap/bootstrap.go b/bootstrap/bootstrap.go index 909fa5c13..d598125eb 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 @@ -692,12 +699,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 { @@ -838,3 +845,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..5e24819ee --- /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 +} 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 06f0fb5b3..223dfc06f 100644 --- a/services/requester/remote_cadence_arch.go +++ b/services/requester/remote_cadence_arch.go @@ -103,7 +103,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 0ec803d54..9c149e6b5 100644 --- a/services/requester/requester.go +++ b/services/requester/requester.go @@ -451,7 +451,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) 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 16d7682d70f0fa22fe1ccee99d3ac9875d9b9f47 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Bastian=20M=C3=BCller?= Date: Thu, 11 Sep 2025 15:59:09 -0700 Subject: [PATCH 11/14] Allow manual image build --- .github/workflows/build.yml | 7 ++++++- 1 file changed, 6 insertions(+), 1 deletion(-) diff --git a/.github/workflows/build.yml b/.github/workflows/build.yml index f0b63eac6..9833bfa1f 100644 --- a/.github/workflows/build.yml +++ b/.github/workflows/build.yml @@ -4,6 +4,11 @@ on: push: tags: - '*' + workflow_dispatch: + inputs: + version: + required: true + type: string env: DOCKER_IMAGE_URL: ${{ vars.REPO_DOCKER_IMAGE_URL }} @@ -22,7 +27,7 @@ jobs: - name: Set Gateway Version id: set_version - run: echo "GATEWAY_VERSION=$(git describe --tags --abbrev=0 2>/dev/null || echo 'unknown')" >> $GITHUB_OUTPUT + run: echo "GATEWAY_VERSION=$(echo '${{ inputs.version }}' || git describe --tags --abbrev=0 2>/dev/null || echo 'unknown')" >> $GITHUB_OUTPUT - name: Google auth id: auth From 2f59024f7a6249f7afc0bdb5a309f84097270209 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Bastian=20M=C3=BCller?= Date: Fri, 12 Sep 2025 09:48:10 -0700 Subject: [PATCH 12/14] remove input --- .github/workflows/build.yml | 6 +----- 1 file changed, 1 insertion(+), 5 deletions(-) diff --git a/.github/workflows/build.yml b/.github/workflows/build.yml index 9833bfa1f..7cfe4e4c7 100644 --- a/.github/workflows/build.yml +++ b/.github/workflows/build.yml @@ -5,10 +5,6 @@ on: tags: - '*' workflow_dispatch: - inputs: - version: - required: true - type: string env: DOCKER_IMAGE_URL: ${{ vars.REPO_DOCKER_IMAGE_URL }} @@ -27,7 +23,7 @@ jobs: - name: Set Gateway Version id: set_version - run: echo "GATEWAY_VERSION=$(echo '${{ inputs.version }}' || git describe --tags --abbrev=0 2>/dev/null || echo 'unknown')" >> $GITHUB_OUTPUT + run: echo "GATEWAY_VERSION=$(git describe --tags --abbrev=0 2>/dev/null || echo 'unknown')" >> $GITHUB_OUTPUT - name: Google auth id: auth From e163682450c7f4a3b1d26bc4f264af05f1e7109b Mon Sep 17 00:00:00 2001 From: Ardit Marku Date: Mon, 15 Sep 2025 12:19:48 +0300 Subject: [PATCH 13/14] 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 a5500cb02..f6bb0d5b8 100644 --- a/api/api.go +++ b/api/api.go @@ -820,6 +820,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 @@ -836,21 +851,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 e53510608..5ca2b4d3a 100644 --- a/bootstrap/bootstrap.go +++ b/bootstrap/bootstrap.go @@ -863,12 +863,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 4cb6bb79d40739b2233f884ac88e3ceddee09699 Mon Sep 17 00:00:00 2001 From: Ardit Marku Date: Mon, 15 Sep 2025 12:20:23 +0300 Subject: [PATCH 14/14] 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 f6bb0d5b8..2a5008ac3 100644 --- a/api/api.go +++ b/api/api.go @@ -184,7 +184,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)