diff --git a/api/api.go b/api/api.go index b3b7db622..5dd85ee96 100644 --- a/api/api.go +++ b/api/api.go @@ -539,7 +539,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"). @@ -576,7 +576,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 6e22b0ea0..508767172 100644 --- a/api/debug.go +++ b/api/debug.go @@ -175,11 +175,18 @@ func (d *DebugAPI) TraceCall( return nil, err } - blocksProvider := replayer.NewBlocksProvider( + blocksProvider := requester.NewOverridableBlocksProvider( d.blocks, d.config.FlowNetworkID, - tracer, - ) + ).WithTracer(tracer) + if config.BlockOverrides != nil { + blocksProvider = blocksProvider.WithBlockOverrides(ð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 0499bad8b..3eea9bf83 100644 --- a/bootstrap/bootstrap.go +++ b/bootstrap/bootstrap.go @@ -193,12 +193,6 @@ func (b *Bootstrap) StartAPIServer(ctx context.Context) error { b.config, ) - blocksProvider := replayer.NewBlocksProvider( - b.storages.Blocks, - b.config.FlowNetworkID, - nil, - ) - accountKeys := make([]*requester.AccountKey, 0) if !b.config.IndexOnly { account, err := b.client.GetAccount(ctx, b.config.COAAddress) @@ -231,7 +225,6 @@ func (b *Bootstrap) StartAPIServer(ctx context.Context) error { evm, err := requester.NewEVM( b.storages.Registers, - blocksProvider, b.client, b.config, b.logger, diff --git a/services/replayer/blocks_provider.go b/services/replayer/blocks_provider.go index 9cb2dfbe1..ab1ed0b59 100644 --- a/services/replayer/blocks_provider.go +++ b/services/replayer/blocks_provider.go @@ -41,6 +41,13 @@ func (bs *blockSnapshot) BlockContext() (evmTypes.BlockContext, error) { ) } +// This BlocksProvider implementation is used in the EVM events ingestion pipeline. +// The ingestion module notifies the BlocksProvider of incoming EVM blocks, by +// calling the `OnBlockReceived` method. This method guarantees that blocks are +// processed sequentially, and keeps track of the latest block, which is used +// for generating the proper `BlockContext`. This is necessary for replaying +// EVM blocks/transactions locally, and verifying that there are no state +// mismatches. type BlocksProvider struct { blocks storage.BlockIndexer chainID flowGo.ChainID diff --git a/services/requester/overridable_blocks_provider.go b/services/requester/overridable_blocks_provider.go new file mode 100644 index 000000000..1c7582a76 --- /dev/null +++ b/services/requester/overridable_blocks_provider.go @@ -0,0 +1,124 @@ +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 { + *OverridableBlocksProvider + 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 +} + +// This OverridableBlocksProvider implementation is only used for the `eth_call` & +// `debug_traceCall` JSON-RPC endpoints. It accepts optional `Tracer` & +// `BlockOverrides` objects, which are used when constructing the +// `BlockContext` object. +type OverridableBlocksProvider struct { + blocks storage.BlockIndexer + chainID flowGo.ChainID + tracer *tracers.Tracer + blockOverrides *ethTypes.BlockOverrides +} + +var _ evmTypes.BlockSnapshotProvider = (*OverridableBlocksProvider)(nil) + +func NewOverridableBlocksProvider( + blocks storage.BlockIndexer, + chainID flowGo.ChainID, +) *OverridableBlocksProvider { + return &OverridableBlocksProvider{ + blocks: blocks, + chainID: chainID, + } +} + +func (bp *OverridableBlocksProvider) WithTracer(tracer *tracers.Tracer) *OverridableBlocksProvider { + return &OverridableBlocksProvider{ + blocks: bp.blocks, + chainID: bp.chainID, + tracer: tracer, + blockOverrides: bp.blockOverrides, + } +} + +func (bp *OverridableBlocksProvider) WithBlockOverrides( + blockOverrides *ethTypes.BlockOverrides, +) *OverridableBlocksProvider { + return &OverridableBlocksProvider{ + blocks: bp.blocks, + chainID: bp.chainID, + tracer: bp.tracer, + blockOverrides: blockOverrides, + } +} + +func (bp *OverridableBlocksProvider) GetSnapshotAt(height uint64) ( + evmTypes.BlockSnapshot, + error, +) { + block, err := bp.blocks.GetByHeight(height) + if err != nil { + return nil, err + } + + return &blockSnapshot{ + OverridableBlocksProvider: bp, + block: *block, + }, nil +} diff --git a/services/requester/requester.go b/services/requester/requester.go index 98a242983..9ec3abc43 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. @@ -95,15 +95,15 @@ type Requester interface { var _ Requester = &EVM{} type EVM struct { - registerStore *pebble.RegisterStorage - blocksProvider *replayer.BlocksProvider - client *CrossSporkClient - config config.Config - txPool *TxPool - logger zerolog.Logger - blocks storage.BlockIndexer - mux sync.Mutex - keystore *KeyStore + registerStore *pebble.RegisterStorage + client *CrossSporkClient + config config.Config + txPool *TxPool + logger zerolog.Logger + blocks storage.BlockIndexer + mux sync.Mutex + keystore *KeyStore + head *types.Header evmSigner types.Signer validationOptions *txpool.ValidationOptions @@ -112,7 +112,6 @@ type EVM struct { func NewEVM( registerStore *pebble.RegisterStorage, - blocksProvider *replayer.BlocksProvider, client *CrossSporkClient, config config.Config, logger zerolog.Logger, @@ -167,7 +166,6 @@ func NewEVM( evm := &EVM{ registerStore: registerStore, - blocksProvider: blocksProvider, client: client, config: config, logger: logger, @@ -250,7 +248,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 } @@ -262,7 +260,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 } @@ -275,7 +273,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 } @@ -288,8 +286,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 } @@ -327,7 +326,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 } @@ -352,7 +351,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. @@ -382,7 +381,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 } @@ -405,7 +404,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 } @@ -437,12 +436,21 @@ func (e *EVM) GetLatestEVMHeight(ctx context.Context) (uint64, error) { return height, nil } -func (e *EVM) getBlockView(height uint64) (*query.View, error) { +func (e *EVM) getBlockView( + height uint64, + blockOverrides *ethTypes.BlockOverrides, +) (*query.View, error) { + blocksProvider := NewOverridableBlocksProvider(e.blocks, e.config.FlowNetworkID) + + if blockOverrides != nil { + blocksProvider = blocksProvider.WithBlockOverrides(blockOverrides) + } + viewProvider := query.NewViewProvider( e.config.FlowNetworkID, evm.StorageAccountAddress(e.config.FlowNetworkID), e.registerStore, - e.blocksProvider, + blocksProvider, blockGasLimit, ) @@ -467,8 +475,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 } @@ -592,7 +601,7 @@ func (e *EVM) validateTransactionWithState( if err != nil { return err } - view, err := e.getBlockView(height) + view, err := e.getBlockView(height, nil) if err != nil { return err } diff --git a/tests/e2e_web3js_test.go b/tests/e2e_web3js_test.go index 96ba5d9e9..3ca77ed8f 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 contract call overrides", func(t *testing.T) { + runWeb3Test(t, "contract_call_overrides_test") + }) + t.Run("test setup sanity check", func(t *testing.T) { runWeb3Test(t, "setup_test") }) diff --git a/tests/web3js/contract_call_overrides_test.js b/tests/web3js/contract_call_overrides_test.js new file mode 100644 index 000000000..fde3837de --- /dev/null +++ b/tests/web3js/contract_call_overrides_test.js @@ -0,0 +1,191 @@ +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 on eth_call', 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.timestamp` 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`. + let random = '0x7914bb5b13bac6f621bc37bbf6e406fbf4472aaaaf17ec2f309a92aca4e27fc0' + response = await helpers.callRPCMethod( + 'eth_call', + [call, 'latest', null, { random: random }] + ) + assert.equal(response.status, 200) + assert.isDefined(response.body) + assert.equal(response.body.result, random) + assert.notEqual(web3.utils.hexToNumber(response.body.result), currentPrevRandao) +}) + +it('should apply block overrides on debug_traceCall', async () => { + assert.equal(deployed.receipt.status, conf.successStatus) + + let receipt = await web3.eth.getTransactionReceipt(deployed.receipt.transactionHash) + assert.equal(receipt.contractAddress, contractAddress) + + let callTracer = { + tracer: 'callTracer', + tracerConfig: { + withLog: false, + onlyTopCall: true + } + } + + 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( + 'debug_traceCall', + [call, 'latest', callTracer] + ) + assert.equal(response.status, 200) + assert.isDefined(response.body) + assert.equal(web3.utils.hexToNumber(response.body.result.output), latestBlockNumber) + + // Override the `block.number` value to `2`. + callTracer.blockOverrides = { number: '0x2' } + response = await helpers.callRPCMethod( + 'debug_traceCall', + [call, 'latest', callTracer] + ) + assert.equal(response.status, 200) + assert.isDefined(response.body) + assert.equal(web3.utils.hexToNumber(response.body.result.output), 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 + + callTracer.blockOverrides = {} + response = await helpers.callRPCMethod( + 'debug_traceCall', + [call, 'latest', callTracer] + ) + assert.equal(response.status, 200) + assert.isDefined(response.body) + assert.equal(web3.utils.hexToNumber(response.body.result.output), block.timestamp) + + // Override the `block.timestamp` value to `0x674DB1E1`. + callTracer.blockOverrides = { time: '0x674DB1E1' } + response = await helpers.callRPCMethod( + 'debug_traceCall', + [call, 'latest', callTracer] + ) + assert.equal(response.status, 200) + assert.isDefined(response.body) + assert.equal(web3.utils.hexToNumber(response.body.result.output), 1733145057n) + + // Check the `block.prevrandao` value, without overrides + let randomSelector = deployed.contract.methods.random().encodeABI() + call.data = randomSelector + + callTracer.blockOverrides = {} + response = await helpers.callRPCMethod( + 'debug_traceCall', + [call, 'latest', callTracer] + ) + assert.equal(response.status, 200) + assert.isDefined(response.body) + let currentPrevRandao = web3.utils.hexToNumber(response.body.result.output) + + // Override the `block.prevrandao` value to `0x7914bb5b13bac6f621bc37bbf6e406fbf4472aaaaf17ec2f309a92aca4e27fc0`. + let random = '0x7914bb5b13bac6f621bc37bbf6e406fbf4472aaaaf17ec2f309a92aca4e27fc0' + callTracer.blockOverrides = { random: random } + response = await helpers.callRPCMethod( + 'debug_traceCall', + [call, 'latest', callTracer] + ) + assert.equal(response.status, 200) + assert.isDefined(response.body) + assert.equal(response.body.result.output, random) + assert.notEqual(web3.utils.hexToNumber(response.body.result.output), currentPrevRandao) +})