diff --git a/Makefile b/Makefile index a2748358..16f1457f 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/api/api.go b/api/api.go index 78db1c6a..0e1f163a 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, @@ -180,7 +183,13 @@ func (b *BlockChainAPI) SendRawTransaction( return common.Hash{}, err } - id, err := b.evm.SendRawTransaction(ctx, input) + feeParams, err := b.feeParameters.Get() + if err != nil { + 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) if err != nil { return handleError[common.Hash](err, l, b.collector) } @@ -850,8 +859,17 @@ 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)(b.config.GasPrice) + blockRewards[i] = (*hexutil.Big)(gasPrice) } for i := maxCount; i >= uint64(1); i-- { @@ -1050,7 +1068,13 @@ 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 { + 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 } // GetUncleCountByBlockHash returns number of uncles in the block for the given block hash @@ -1091,7 +1115,13 @@ 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 { + 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 } // Mining returns true if client is actively mining new blocks. diff --git a/bootstrap/bootstrap.go b/bootstrap/bootstrap.go index f8c672b8..9213178d 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,13 +52,19 @@ 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 - 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 +199,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, @@ -314,6 +323,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, @@ -645,6 +655,15 @@ func setupStorage( // // TODO(JanezP): verify storage account owner is correct // } + feeParameters := pebble.NewFeeParameters(store) + 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 { err = batch.Commit(pebbleDB.Sync) if err != nil { @@ -653,12 +672,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 } @@ -781,3 +801,35 @@ 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 + surgeFactor, ok := val.(cadence.UFix64) + if !ok { + return nil, fmt.Errorf("failed to convert surgeFactor %v to UFix64, got type: %T", val, val) + } + + 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 00000000..5e24819e --- /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/events.go b/models/events.go index 2d199f9f..12cab63a 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 00000000..86656c26 --- /dev/null +++ b/models/fee_parameters.go @@ -0,0 +1,64 @@ +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) + +func DefaultFeeParameters() *FeeParameters { + return &FeeParameters{ + SurgeFactor: cadence.UFix64(feeParamsPrecision), + InclusionEffortCost: cadence.UFix64(feeParamsPrecision), + ExecutionEffortCost: cadence.UFix64(feeParamsPrecision), + } +} + +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 (f *FeeParameters) CalculateGasPrice(currentGasPrice *big.Int) *big.Int { + 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) { + 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 04d012a0..5f012007 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 1e4e5692..e9986833 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 ed86cd27..9f8bb86a 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/services/requester/batch_tx_pool.go b/services/requester/batch_tx_pool.go index d9810491..2ed95076 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 4b5dc6f4..70870afe 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 72374991..cf5c1706 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,9 @@ 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) + 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 { @@ -444,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 d71954cd..aac6be62 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 d3ee4d7f..f6f453ce 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/index.go b/storage/index.go index 4b6083e3..897733ba 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 00000000..c201ae5b --- /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 00000000..2182bcdf --- /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 %v: %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 aa46b61a..e1b59172 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) diff --git a/tests/e2e_web3js_test.go b/tests/e2e_web3js_test.go index 86cfe8bd..a7ef8481 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" @@ -122,6 +123,45 @@ 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) { + 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) + } + }) + }) + 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 00000000..1f9e4271 --- /dev/null +++ b/tests/web3js/eth_gas_price_surge_test.js @@ -0,0 +1,106 @@ +const utils = require('web3-utils') +const { assert } = require('chai') +const conf = require('./config') +const helpers = require('./helpers') +const web3 = conf.web3 + +it('should update the value of eth_gasPrice', async () => { + let gasPrice = await web3.eth.getGasPrice() + // The surge factor was last set to 100.0 + assert.equal(gasPrice, 100n * conf.minGasPrice) +}) + +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) + // 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) +}) + +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 gasPrice = await web3.eth.getGasPrice() + // 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 + try { + await helpers.signAndSend({ + from: conf.eoa.address, + to: receiver.address, + value: transferValue, + gasPrice: gasPrice - 10n, // provide a lower gas price + 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}` + ) + } +}) + +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 last set to 100.0 + assert.equal(gasPrice, 100n * conf.minGasPrice) + + let transfer = await helpers.signAndSend({ + from: conf.eoa.address, + to: receiver.address, + value: transferValue, + gasPrice: gasPrice, // provide the updated gas price + 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) +}) + +it('should update gas price for eth_feeFistory', async () => { + let response = await web3.eth.getFeeHistory(10, 'latest', [20]) + + 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] + } + ) +})