From a4f893f431851108748e5550a8a7baca8d3a99bd Mon Sep 17 00:00:00 2001 From: Ardit Marku Date: Mon, 2 Dec 2024 17:50:27 +0200 Subject: [PATCH] Support block overrides for eth_call & debug_traceCall endpoints --- api/api.go | 4 +- api/debug.go | 12 ++- bootstrap/bootstrap.go | 3 +- services/requester/blocks_provider.go | 108 ++++++++++++++++++++++++ services/requester/requester.go | 35 +++++--- tests/e2e_web3js_test.go | 4 + tests/web3js/eth_call_overrides_test.js | 98 +++++++++++++++++++++ 7 files changed, 245 insertions(+), 19 deletions(-) create mode 100644 services/requester/blocks_provider.go create mode 100644 tests/web3js/eth_call_overrides_test.js diff --git a/api/api.go b/api/api.go index ddac3371..2152ee89 100644 --- a/api/api.go +++ b/api/api.go @@ -607,7 +607,7 @@ func (b *BlockChainAPI) Call( args ethTypes.TransactionArgs, blockNumberOrHash *rpc.BlockNumberOrHash, stateOverrides *ethTypes.StateOverride, - _ *ethTypes.BlockOverrides, + blockOverrides *ethTypes.BlockOverrides, ) (hexutil.Bytes, error) { l := b.logger.With(). Str("endpoint", "call"). @@ -644,7 +644,7 @@ func (b *BlockChainAPI) Call( from = *args.From } - res, err := b.evm.Call(tx, from, height, stateOverrides) + res, err := b.evm.Call(tx, from, height, stateOverrides, blockOverrides) if err != nil { return handleError[hexutil.Bytes](err, l, b.collector) } diff --git a/api/debug.go b/api/debug.go index 962804d4..85d5fa47 100644 --- a/api/debug.go +++ b/api/debug.go @@ -291,11 +291,19 @@ func (d *DebugAPI) TraceCall( return nil, err } - blocksProvider := replayer.NewBlocksProvider( + blocksProvider := requester.NewBlocksProvider( d.blocks, d.config.FlowNetworkID, - tracer, ) + blocksProvider.SetTracer(tracer) + if config.BlockOverrides != nil { + blocksProvider.SetBlockOverrides(ðTypes.BlockOverrides{ + Number: config.BlockOverrides.Number, + Time: config.BlockOverrides.Time, + Coinbase: config.BlockOverrides.Coinbase, + Random: config.BlockOverrides.Random, + }) + } viewProvider := query.NewViewProvider( d.config.FlowNetworkID, flowEVM.StorageAccountAddress(d.config.FlowNetworkID), diff --git a/bootstrap/bootstrap.go b/bootstrap/bootstrap.go index f21f41af..3740c175 100644 --- a/bootstrap/bootstrap.go +++ b/bootstrap/bootstrap.go @@ -213,10 +213,9 @@ func (b *Bootstrap) StartAPIServer(ctx context.Context) error { b.logger, ) - blocksProvider := replayer.NewBlocksProvider( + blocksProvider := requester.NewBlocksProvider( b.storages.Blocks, b.config.FlowNetworkID, - nil, ) evm, err := requester.NewEVM( diff --git a/services/requester/blocks_provider.go b/services/requester/blocks_provider.go new file mode 100644 index 00000000..48d3948b --- /dev/null +++ b/services/requester/blocks_provider.go @@ -0,0 +1,108 @@ +package requester + +import ( + ethTypes "github.com/onflow/flow-evm-gateway/eth/types" + "github.com/onflow/flow-evm-gateway/models" + "github.com/onflow/flow-evm-gateway/storage" + "github.com/onflow/flow-go/fvm/evm/offchain/blocks" + evmTypes "github.com/onflow/flow-go/fvm/evm/types" + flowGo "github.com/onflow/flow-go/model/flow" + gethCommon "github.com/onflow/go-ethereum/common" + "github.com/onflow/go-ethereum/eth/tracers" +) + +type blockSnapshot struct { + *BlocksProvider + block models.Block +} + +var _ evmTypes.BlockSnapshot = (*blockSnapshot)(nil) + +func (bs *blockSnapshot) BlockContext() (evmTypes.BlockContext, error) { + blockContext, err := blocks.NewBlockContext( + bs.chainID, + bs.block.Height, + bs.block.Timestamp, + func(n uint64) gethCommon.Hash { + block, err := bs.blocks.GetByHeight(n) + if err != nil { + return gethCommon.Hash{} + } + blockHash, err := block.Hash() + if err != nil { + return gethCommon.Hash{} + } + + return blockHash + }, + bs.block.PrevRandao, + bs.tracer, + ) + if err != nil { + return evmTypes.BlockContext{}, err + } + + if bs.blockOverrides == nil { + return blockContext, nil + } + + if bs.blockOverrides.Number != nil { + blockContext.BlockNumber = bs.blockOverrides.Number.ToInt().Uint64() + } + + if bs.blockOverrides.Time != nil { + blockContext.BlockTimestamp = uint64(*bs.blockOverrides.Time) + } + + if bs.blockOverrides.Random != nil { + blockContext.Random = *bs.blockOverrides.Random + } + + if bs.blockOverrides.Coinbase != nil { + blockContext.GasFeeCollector = evmTypes.NewAddress(*bs.blockOverrides.Coinbase) + } + + return blockContext, nil +} + +type BlocksProvider struct { + blocks storage.BlockIndexer + chainID flowGo.ChainID + tracer *tracers.Tracer + blockOverrides *ethTypes.BlockOverrides +} + +var _ evmTypes.BlockSnapshotProvider = (*BlocksProvider)(nil) + +func NewBlocksProvider( + blocks storage.BlockIndexer, + chainID flowGo.ChainID, +) *BlocksProvider { + return &BlocksProvider{ + blocks: blocks, + chainID: chainID, + } +} + +func (bp *BlocksProvider) SetTracer(tracer *tracers.Tracer) { + bp.tracer = tracer +} + +func (bp *BlocksProvider) SetBlockOverrides(blockOverrides *ethTypes.BlockOverrides) { + bp.blockOverrides = blockOverrides +} + +func (bp *BlocksProvider) GetSnapshotAt(height uint64) ( + evmTypes.BlockSnapshot, + error, +) { + block, err := bp.blocks.GetByHeight(height) + if err != nil { + return nil, err + } + + return &blockSnapshot{ + BlocksProvider: bp, + block: *block, + }, nil +} diff --git a/services/requester/requester.go b/services/requester/requester.go index a2da7d38..ed2e2f9a 100644 --- a/services/requester/requester.go +++ b/services/requester/requester.go @@ -27,7 +27,6 @@ import ( "github.com/onflow/flow-evm-gateway/metrics" "github.com/onflow/flow-evm-gateway/models" errs "github.com/onflow/flow-evm-gateway/models/errors" - "github.com/onflow/flow-evm-gateway/services/replayer" "github.com/onflow/flow-evm-gateway/storage" "github.com/onflow/flow-evm-gateway/storage/pebble" @@ -66,6 +65,7 @@ type Requester interface { from common.Address, height uint64, stateOverrides *ethTypes.StateOverride, + blockOverrides *ethTypes.BlockOverrides, ) ([]byte, error) // EstimateGas executes the given signed transaction data on the state for the given EVM block height. @@ -96,7 +96,7 @@ var _ Requester = &EVM{} type EVM struct { registerStore *pebble.RegisterStorage - blocksProvider *replayer.BlocksProvider + blocksProvider *BlocksProvider client *CrossSporkClient config config.Config signer crypto.Signer @@ -114,7 +114,7 @@ type EVM struct { func NewEVM( registerStore *pebble.RegisterStorage, - blocksProvider *replayer.BlocksProvider, + blocksProvider *BlocksProvider, client *CrossSporkClient, config config.Config, signer crypto.Signer, @@ -294,7 +294,7 @@ func (e *EVM) GetBalance( address common.Address, height uint64, ) (*big.Int, error) { - view, err := e.getBlockView(height) + view, err := e.getBlockView(height, nil) if err != nil { return nil, err } @@ -306,7 +306,7 @@ func (e *EVM) GetNonce( address common.Address, height uint64, ) (uint64, error) { - view, err := e.getBlockView(height) + view, err := e.getBlockView(height, nil) if err != nil { return 0, err } @@ -319,7 +319,7 @@ func (e *EVM) GetStorageAt( hash common.Hash, height uint64, ) (common.Hash, error) { - view, err := e.getBlockView(height) + view, err := e.getBlockView(height, nil) if err != nil { return common.Hash{}, err } @@ -332,8 +332,9 @@ func (e *EVM) Call( from common.Address, height uint64, stateOverrides *ethTypes.StateOverride, + blockOverrides *ethTypes.BlockOverrides, ) ([]byte, error) { - result, err := e.dryRunTx(tx, from, height, stateOverrides) + result, err := e.dryRunTx(tx, from, height, stateOverrides, blockOverrides) if err != nil { return nil, err } @@ -371,7 +372,7 @@ func (e *EVM) EstimateGas( tx.Gas = passingGasLimit // We first execute the transaction at the highest allowable gas limit, // since if this fails we can return the error immediately. - result, err := e.dryRunTx(tx, from, height, stateOverrides) + result, err := e.dryRunTx(tx, from, height, stateOverrides, nil) if err != nil { return 0, err } @@ -396,7 +397,7 @@ func (e *EVM) EstimateGas( optimisticGasLimit := (result.GasConsumed + result.GasRefund + gethParams.CallStipend) * 64 / 63 if optimisticGasLimit < passingGasLimit { tx.Gas = optimisticGasLimit - result, err = e.dryRunTx(tx, from, height, stateOverrides) + result, err = e.dryRunTx(tx, from, height, stateOverrides, nil) if err != nil { // This should not happen under normal conditions since if we make it this far the // transaction had run without error at least once before. @@ -426,7 +427,7 @@ func (e *EVM) EstimateGas( mid = failingGasLimit * 2 } tx.Gas = mid - result, err = e.dryRunTx(tx, from, height, stateOverrides) + result, err = e.dryRunTx(tx, from, height, stateOverrides, nil) if err != nil { return 0, err } @@ -449,7 +450,7 @@ func (e *EVM) GetCode( address common.Address, height uint64, ) ([]byte, error) { - view, err := e.getBlockView(height) + view, err := e.getBlockView(height, nil) if err != nil { return nil, err } @@ -508,7 +509,14 @@ func (e *EVM) getSignerNetworkInfo(ctx context.Context) (uint32, uint64, error) ) } -func (e *EVM) getBlockView(height uint64) (*query.View, error) { +func (e *EVM) getBlockView( + height uint64, + blockOverrides *ethTypes.BlockOverrides, +) (*query.View, error) { + if blockOverrides != nil { + e.blocksProvider.SetBlockOverrides(blockOverrides) + } + viewProvider := query.NewViewProvider( e.config.FlowNetworkID, evm.StorageAccountAddress(e.config.FlowNetworkID), @@ -538,8 +546,9 @@ func (e *EVM) dryRunTx( from common.Address, height uint64, stateOverrides *ethTypes.StateOverride, + blockOverrides *ethTypes.BlockOverrides, ) (*evmTypes.Result, error) { - view, err := e.getBlockView(height) + view, err := e.getBlockView(height, blockOverrides) if err != nil { return nil, err } diff --git a/tests/e2e_web3js_test.go b/tests/e2e_web3js_test.go index 881c76ab..8ff1e93d 100644 --- a/tests/e2e_web3js_test.go +++ b/tests/e2e_web3js_test.go @@ -40,6 +40,10 @@ func TestWeb3_E2E(t *testing.T) { runWeb3Test(t, "debug_util_test") }) + t.Run("test eth_call overrides", func(t *testing.T) { + runWeb3Test(t, "eth_call_overrides_test") + }) + t.Run("test setup sanity check", func(t *testing.T) { runWeb3Test(t, "setup_test") }) diff --git a/tests/web3js/eth_call_overrides_test.js b/tests/web3js/eth_call_overrides_test.js new file mode 100644 index 00000000..76056881 --- /dev/null +++ b/tests/web3js/eth_call_overrides_test.js @@ -0,0 +1,98 @@ +const { assert } = require('chai') +const conf = require('./config') +const helpers = require('./helpers') +const web3 = conf.web3 + +let deployed = null +let contractAddress = null + +before(async () => { + deployed = await helpers.deployContract('storage') + contractAddress = deployed.receipt.contractAddress + + assert.equal(deployed.receipt.status, conf.successStatus) +}) + +it('should apply block overrides', async () => { + assert.equal(deployed.receipt.status, conf.successStatus) + + let receipt = await web3.eth.getTransactionReceipt(deployed.receipt.transactionHash) + assert.equal(receipt.contractAddress, contractAddress) + + let latestBlockNumber = await web3.eth.getBlockNumber() + + // Check the `block.number` value, without overrides + let blockNumberSelector = deployed.contract.methods.blockNumber().encodeABI() + let call = { + from: conf.eoa.address, + to: contractAddress, + gas: '0x75ab', + gasPrice: web3.utils.toHex(conf.minGasPrice), + value: '0x0', + data: blockNumberSelector, + } + + let response = await helpers.callRPCMethod( + 'eth_call', + [call, 'latest', null, null] + ) + assert.equal(response.status, 200) + assert.isDefined(response.body) + assert.equal(web3.utils.hexToNumber(response.body.result), latestBlockNumber) + + // Override the `block.number` value to `2`. + response = await helpers.callRPCMethod( + 'eth_call', + [call, 'latest', null, { number: '0x2' }] + ) + assert.equal(response.status, 200) + assert.isDefined(response.body) + assert.equal(web3.utils.hexToNumber(response.body.result), 2n) + + // Check the `block.timestamp` value, without overrides + let block = await web3.eth.getBlock(latestBlockNumber) + let blockTimeSelector = deployed.contract.methods.blockTime().encodeABI() + call.data = blockTimeSelector + + response = await helpers.callRPCMethod( + 'eth_call', + [call, 'latest', null, null] + ) + assert.equal(response.status, 200) + assert.isDefined(response.body) + assert.equal(web3.utils.hexToNumber(response.body.result), block.timestamp) + + // Override the `block.number` value to `0x674DB1E1`. + response = await helpers.callRPCMethod( + 'eth_call', + [call, 'latest', null, { time: '0x674DB1E1' }] + ) + assert.equal(response.status, 200) + assert.isDefined(response.body) + assert.equal(web3.utils.hexToNumber(response.body.result), 1733145057n) + + // Check the `block.prevrandao` value, without overrides + let randomSelector = deployed.contract.methods.random().encodeABI() + call.data = randomSelector + + response = await helpers.callRPCMethod( + 'eth_call', + [call, 'latest', null, null] + ) + assert.equal(response.status, 200) + assert.isDefined(response.body) + let currentPrevRandao = web3.utils.hexToNumber(response.body.result) + + // Override the `block.prevrandao` value to `0x7914bb5b13bac6f621bc37bbf6e406fbf4472aaaaf17ec2f309a92aca4e27fc0`. + response = await helpers.callRPCMethod( + 'eth_call', + [call, 'latest', null, { random: '0x7914bb5b13bac6f621bc37bbf6e406fbf4472aaaaf17ec2f309a92aca4e27fc0' }] + ) + assert.equal(response.status, 200) + assert.isDefined(response.body) + assert.equal( + web3.utils.hexToNumber(response.body.result), + 54766484701870566448245359081326515063439899359799766042879591799681605337024n + ) + assert.notEqual(web3.utils.hexToNumber(response.body.result), currentPrevRandao) +})