From 0c6d690ca9a9f4901a7d0cd54d1dcc2ac5ee44bc Mon Sep 17 00:00:00 2001 From: Gregor G Date: Mon, 22 Apr 2024 13:25:34 +0200 Subject: [PATCH 01/35] estimate gas handler first draft --- fvm/evm/handler/handler.go | 45 ++++++++++++++++++++++++++++++++++++++ 1 file changed, 45 insertions(+) diff --git a/fvm/evm/handler/handler.go b/fvm/evm/handler/handler.go index 663c71c8c03..8b3ed73885a 100644 --- a/fvm/evm/handler/handler.go +++ b/fvm/evm/handler/handler.go @@ -219,6 +219,51 @@ func (h *ContractHandler) run( return res, nil } +func (h *ContractHandler) EstimateGas(rlpEncodedTx []byte) (*types.Result, error) { + // step 1 - transaction decoding + encodedLen := uint(len(rlpEncodedTx)) + err := h.backend.MeterComputation(environment.ComputationKindRLPDecoding, encodedLen) + if err != nil { + return nil, err + } + + tx := gethTypes.Transaction{} + err = tx.DecodeRLP( + rlp.NewStream( + bytes.NewReader(rlpEncodedTx), + uint64(encodedLen))) + if err != nil { + return nil, err + } + + ctx, err := h.getBlockContext() + if err != nil { + return nil, err + } + + blk, err := h.emulator.NewBlockView(ctx) + if err != nil { + return nil, err + } + + res, err := blk.RunTransaction(&tx) + if err != nil { + return nil, err + } + + // saftey check for result + if res == nil { + return nil, types.ErrUnexpectedEmptyResult + } + + // if is invalid tx skip the next steps (forming block, ...) + if res.Invalid() { + return res, nil + } + + return res, nil +} + func (h *ContractHandler) checkGasLimit(limit types.GasLimit) error { // check gas limit against what has been left on the transaction side if !h.backend.ComputationAvailable(environment.ComputationKindEVMGasUsage, uint(limit)) { From 6a4261f944efcd5336f21d1f6c7f6db25cd3822d Mon Sep 17 00:00:00 2001 From: Gregor G Date: Mon, 22 Apr 2024 13:27:17 +0200 Subject: [PATCH 02/35] add method to the handler --- fvm/evm/types/handler.go | 5 +++++ 1 file changed, 5 insertions(+) diff --git a/fvm/evm/types/handler.go b/fvm/evm/types/handler.go index 28e75daa7f0..f96d5d59428 100644 --- a/fvm/evm/types/handler.go +++ b/fvm/evm/types/handler.go @@ -45,6 +45,11 @@ type ContractHandler interface { // GenerateResourceUUID generates a new UUID for a resource GenerateResourceUUID() uint64 + + // EstimateGas takes an RLP encoded transaction and estimates how much gas it takes + // for the transaction to execute. + // Error is returned if transaction is not valid or unexpected error happens. + EstimateGas(tx []byte) (uint64, error) } // AddressAllocator allocates addresses, used by the handler From f4339ce8893cbdd67fdb31dc9ca78bfd4320142a Mon Sep 17 00:00:00 2001 From: Gregor G Date: Mon, 22 Apr 2024 13:28:35 +0200 Subject: [PATCH 03/35] change return type --- fvm/evm/handler/handler.go | 20 ++++++++++---------- 1 file changed, 10 insertions(+), 10 deletions(-) diff --git a/fvm/evm/handler/handler.go b/fvm/evm/handler/handler.go index 8b3ed73885a..c2a546f4fc4 100644 --- a/fvm/evm/handler/handler.go +++ b/fvm/evm/handler/handler.go @@ -219,12 +219,12 @@ func (h *ContractHandler) run( return res, nil } -func (h *ContractHandler) EstimateGas(rlpEncodedTx []byte) (*types.Result, error) { +func (h *ContractHandler) EstimateGas(rlpEncodedTx []byte) (uint64, error) { // step 1 - transaction decoding encodedLen := uint(len(rlpEncodedTx)) err := h.backend.MeterComputation(environment.ComputationKindRLPDecoding, encodedLen) if err != nil { - return nil, err + return 0, err } tx := gethTypes.Transaction{} @@ -233,35 +233,35 @@ func (h *ContractHandler) EstimateGas(rlpEncodedTx []byte) (*types.Result, error bytes.NewReader(rlpEncodedTx), uint64(encodedLen))) if err != nil { - return nil, err + return 0, err } ctx, err := h.getBlockContext() if err != nil { - return nil, err + return 0, err } blk, err := h.emulator.NewBlockView(ctx) if err != nil { - return nil, err + return 0, err } res, err := blk.RunTransaction(&tx) if err != nil { - return nil, err + return 0, err } // saftey check for result if res == nil { - return nil, types.ErrUnexpectedEmptyResult + return 0, types.ErrUnexpectedEmptyResult } - // if is invalid tx skip the next steps (forming block, ...) + // if invalid return the invalid error if res.Invalid() { - return res, nil + return 0, res.ValidationError } - return res, nil + return res.GasConsumed, nil } func (h *ContractHandler) checkGasLimit(limit types.GasLimit) error { From 0ba00cc7fb057a67823c8cbd6f6225f889a02d71 Mon Sep 17 00:00:00 2001 From: Gregor G Date: Mon, 22 Apr 2024 13:33:12 +0200 Subject: [PATCH 04/35] add simple estimate gas test --- fvm/evm/evm_test.go | 50 +++++++++++++++++++++++++++++++++++++++++++++ 1 file changed, 50 insertions(+) diff --git a/fvm/evm/evm_test.go b/fvm/evm/evm_test.go index ecbd3b88133..8d8306f05f5 100644 --- a/fvm/evm/evm_test.go +++ b/fvm/evm/evm_test.go @@ -702,6 +702,56 @@ func TestCadenceOwnedAccountFunctionalities(t *testing.T) { }) } +func TestGasEstimation(t *testing.T) { + t.Parallel() + + t.Run("test successful gas estimation", func(t *testing.T) { + chain := flow.Emulator.Chain() + sc := systemcontracts.SystemContractsForChain(chain.ChainID()) + RunWithNewEnvironment(t, + chain, func( + ctx fvm.Context, + vm fvm.VM, + snapshot snapshot.SnapshotTree, + testContract *TestContract, + testAccount *EOATestAccount, + ) { + code := []byte(fmt.Sprintf( + ` + import EVM from %s + + access(all) + fun main(tx: [UInt8]): Uint { + return EVM.estimateGas(tx: tx) + } + `, + sc.EVMContract.Address.HexWithPrefix(), + )) + innerTxBytes := testAccount.PrepareSignAndEncodeTx(t, + testContract.DeployedAt.ToCommon(), + testContract.MakeCallData(t, "store", big.NewInt(1337)), + big.NewInt(0), + uint64(10_000_000), + big.NewInt(20), // todo check price + ) + script := fvm.Script(code).WithArguments( + json.MustEncode( + cadence.NewArray( + ConvertToCadence(innerTxBytes), + ).WithType(stdlib.EVMTransactionBytesCadenceType), + ), + ) + _, output, err := vm.Run( + ctx, + script, + snapshot) + require.NoError(t, err) + require.NoError(t, output.Err) + fmt.Println("val", output.Value) + }) + }) +} + func TestCadenceArch(t *testing.T) { t.Parallel() From 0702e417d1ce17b6987b6bffaa3416f13aac68ab Mon Sep 17 00:00:00 2001 From: Gregor G Date: Mon, 22 Apr 2024 13:47:20 +0200 Subject: [PATCH 05/35] add estimate gas to contract --- fvm/evm/stdlib/contract.cdc | 5 ++++ fvm/evm/stdlib/contract.go | 56 +++++++++++++++++++++++++++++++++++++ 2 files changed, 61 insertions(+) diff --git a/fvm/evm/stdlib/contract.cdc b/fvm/evm/stdlib/contract.cdc index ff87ab299a3..94319b6c548 100644 --- a/fvm/evm/stdlib/contract.cdc +++ b/fvm/evm/stdlib/contract.cdc @@ -325,6 +325,11 @@ contract EVM { return runResult } + access(all) + fun estimateGas(tx [UInt8]): UInt { + return InternalEVM.estimateGas(tx) + } + access(all) fun encodeABI(_ values: [AnyStruct]): [UInt8] { return InternalEVM.encodeABI(values) diff --git a/fvm/evm/stdlib/contract.go b/fvm/evm/stdlib/contract.go index 8f680c322d4..aebf2b356cd 100644 --- a/fvm/evm/stdlib/contract.go +++ b/fvm/evm/stdlib/contract.go @@ -972,6 +972,55 @@ func newInternalEVMTypeRunFunction( ) } +// estimate gas + +const internalEVMTypeEstimateGasFunctionName = "estimateGas" + +var internalEVMTypeEstimateGasFunctionType = &sema.FunctionType{ + Parameters: []sema.Parameter{ + { + Label: "tx", + TypeAnnotation: sema.NewTypeAnnotation(evmTransactionBytesType), + }, + }, + ReturnTypeAnnotation: sema.NewTypeAnnotation(sema.UInt64Type), +} + +func newInternalEVMTypeEstimateGasFunction( + gauge common.MemoryGauge, + handler types.ContractHandler, +) *interpreter.HostFunctionValue { + return interpreter.NewHostFunctionValue( + gauge, + internalEVMTypeEstimateGasFunctionType, + func(invocation interpreter.Invocation) interpreter.Value { + inter := invocation.Interpreter + locationRange := invocation.LocationRange + + // Get transaction argument + + transactionValue, ok := invocation.Arguments[0].(*interpreter.ArrayValue) + if !ok { + panic(errors.NewUnreachableError()) + } + + transaction, err := interpreter.ByteArrayValueToByteSlice(inter, transactionValue, locationRange) + if err != nil { + panic(err) + } + + // Estiamte + + val, err := handler.EstimateGas(transaction) + if err != nil { + panic(err) // todo change + } + + return interpreter.NewUnmeteredUInt64Value(val) + }, + ) +} + func NewResultValue( handler types.ContractHandler, gauge common.MemoryGauge, @@ -1808,6 +1857,7 @@ func NewInternalEVMContractValue( internalEVMTypeCastToAttoFLOWFunctionName: newInternalEVMTypeCastToAttoFLOWFunction(gauge, handler), internalEVMTypeCastToFLOWFunctionName: newInternalEVMTypeCastToFLOWFunction(gauge, handler), internalEVMTypeGetLatestBlockFunctionName: newInternalEVMTypeGetLatestBlockFunction(gauge, handler), + internalEVMTypeEstimateGasFunctionName: newInternalEVMTypeEstimateGasFunction(gauge, handler), }, nil, nil, @@ -1830,6 +1880,12 @@ var InternalEVMContractType = func() *sema.CompositeType { internalEVMTypeRunFunctionType, "", ), + sema.NewUnmeteredPublicFunctionMember( + ty, + internalEVMTypeEstimateGasFunctionName, + internalEVMTypeEstimateGasFunctionType, + "", + ), sema.NewUnmeteredPublicFunctionMember( ty, internalEVMTypeCreateCadenceOwnedAccountFunctionName, From 220eba5bd06a2dc2b7ad052a1ee2989c14f8ace6 Mon Sep 17 00:00:00 2001 From: Gregor G Date: Mon, 22 Apr 2024 14:27:44 +0200 Subject: [PATCH 06/35] fix test --- fvm/evm/evm_test.go | 5 +++-- 1 file changed, 3 insertions(+), 2 deletions(-) diff --git a/fvm/evm/evm_test.go b/fvm/evm/evm_test.go index 8d8306f05f5..e9b99e5eb0f 100644 --- a/fvm/evm/evm_test.go +++ b/fvm/evm/evm_test.go @@ -721,18 +721,19 @@ func TestGasEstimation(t *testing.T) { import EVM from %s access(all) - fun main(tx: [UInt8]): Uint { + fun main(tx: [UInt8]): UInt64 { return EVM.estimateGas(tx: tx) } `, sc.EVMContract.Address.HexWithPrefix(), )) + innerTxBytes := testAccount.PrepareSignAndEncodeTx(t, testContract.DeployedAt.ToCommon(), testContract.MakeCallData(t, "store", big.NewInt(1337)), big.NewInt(0), uint64(10_000_000), - big.NewInt(20), // todo check price + big.NewInt(500000), // todo check price ) script := fvm.Script(code).WithArguments( json.MustEncode( From 679cba0d72f8c0fc112a7e0d618fdca0eb91b765 Mon Sep 17 00:00:00 2001 From: Gregor G Date: Mon, 22 Apr 2024 17:13:08 +0200 Subject: [PATCH 07/35] change argument list --- fvm/evm/stdlib/contract.cdc | 18 +++++++- fvm/evm/stdlib/contract.go | 85 ++++++++++++++++++++++++++++++++++--- 2 files changed, 94 insertions(+), 9 deletions(-) diff --git a/fvm/evm/stdlib/contract.cdc b/fvm/evm/stdlib/contract.cdc index 94319b6c548..fc860586c6d 100644 --- a/fvm/evm/stdlib/contract.cdc +++ b/fvm/evm/stdlib/contract.cdc @@ -326,8 +326,22 @@ contract EVM { } access(all) - fun estimateGas(tx [UInt8]): UInt { - return InternalEVM.estimateGas(tx) + fun estimateGas( + from: [UInt8; 20], + to: [UInt8; 20], + gasLimit: UInt64, + gasPrice: UInt64, + value: Balance, + data: [UInt8] + ): UInt64 { + return InternalEVM.estimateGas( + from: from, + to: to, + gasLimit: gasLimit, + gasPrice: gasPrice, + value: value.attoflow, + data: data + ) } access(all) diff --git a/fvm/evm/stdlib/contract.go b/fvm/evm/stdlib/contract.go index aebf2b356cd..10985da3e98 100644 --- a/fvm/evm/stdlib/contract.go +++ b/fvm/evm/stdlib/contract.go @@ -979,8 +979,28 @@ const internalEVMTypeEstimateGasFunctionName = "estimateGas" var internalEVMTypeEstimateGasFunctionType = &sema.FunctionType{ Parameters: []sema.Parameter{ { - Label: "tx", - TypeAnnotation: sema.NewTypeAnnotation(evmTransactionBytesType), + Label: "from", + TypeAnnotation: sema.NewTypeAnnotation(evmAddressBytesType), + }, + { + Label: "to", + TypeAnnotation: sema.NewTypeAnnotation(evmAddressBytesType), + }, + { + Label: "gasLimit", + TypeAnnotation: sema.NewTypeAnnotation(sema.UInt64Type), + }, + { + Label: "gasPrice", + TypeAnnotation: sema.NewTypeAnnotation(sema.UInt64Type), + }, + { + Label: "value", + TypeAnnotation: sema.NewTypeAnnotation(sema.UIntType), + }, + { + Label: "data", + TypeAnnotation: sema.NewTypeAnnotation(sema.ByteArrayType), }, }, ReturnTypeAnnotation: sema.NewTypeAnnotation(sema.UInt64Type), @@ -997,21 +1017,72 @@ func newInternalEVMTypeEstimateGasFunction( inter := invocation.Interpreter locationRange := invocation.LocationRange - // Get transaction argument + // Get from address - transactionValue, ok := invocation.Arguments[0].(*interpreter.ArrayValue) + fromAddressValue, ok := invocation.Arguments[0].(*interpreter.ArrayValue) if !ok { panic(errors.NewUnreachableError()) } - transaction, err := interpreter.ByteArrayValueToByteSlice(inter, transactionValue, locationRange) + fromAddress, err := AddressBytesArrayValueToEVMAddress(inter, locationRange, fromAddressValue) + if err != nil { + panic(err) + } + + // Get to address + + toAddressValue, ok := invocation.Arguments[1].(*interpreter.ArrayValue) + if !ok { + panic(errors.NewUnreachableError()) + } + + toAddress, err := AddressBytesArrayValueToEVMAddress(inter, locationRange, toAddressValue) + if err != nil { + panic(err) + } + + // Get gas + + gasLimitValue, ok := invocation.Arguments[2].(interpreter.UInt64Value) + if !ok { + panic(errors.NewUnreachableError()) + } + + gasLimit := types.GasLimit(gasLimitValue) + + // Get gas price + + gasPriceValue, ok := invocation.Arguments[3].(interpreter.UInt64Value) + if !ok { + panic(errors.NewUnreachableError()) + } + + gasPrice := uint64(gasPriceValue) + + // Get balance + + balanceValue, ok := invocation.Arguments[4].(interpreter.UIntValue) + if !ok { + panic(errors.NewUnreachableError()) + } + + balance := types.NewBalance(balanceValue.BigInt) + + // Get data + + dataValue, ok := invocation.Arguments[5].(*interpreter.ArrayValue) + if !ok { + panic(errors.NewUnreachableError()) + } + + data, err := interpreter.ByteArrayValueToByteSlice(inter, dataValue, locationRange) if err != nil { panic(err) } - // Estiamte + // call estimate - val, err := handler.EstimateGas(transaction) + val, err := handler.EstimateGas(fromAddress, toAddress, gasLimit, gasPrice, balance, data) if err != nil { panic(err) // todo change } From f940c1d6e4122b26a491b8ba9d1905a99dce0496 Mon Sep 17 00:00:00 2001 From: Gregor G Date: Mon, 22 Apr 2024 17:13:19 +0200 Subject: [PATCH 08/35] add direct call for estimation --- fvm/evm/emulator/emulator.go | 20 ++++++++++++++++++++ 1 file changed, 20 insertions(+) diff --git a/fvm/evm/emulator/emulator.go b/fvm/evm/emulator/emulator.go index 9e165c05c0b..2f6f5907d0b 100644 --- a/fvm/evm/emulator/emulator.go +++ b/fvm/evm/emulator/emulator.go @@ -117,6 +117,8 @@ func (bl *BlockView) DirectCall(call *types.DirectCall) (*types.Result, error) { return proc.mintTo(call, txHash) case types.WithdrawCallSubType: return proc.withdrawFrom(call, txHash) + case types.EstimateGasSubType: + return proc.estimateGas(call) case types.DeployCallSubType: if !call.EmptyToField() { return proc.deployAt(call.From, call.To, call.Data, call.GasLimit, call.Value, txHash) @@ -270,6 +272,24 @@ func (proc *procedure) withdrawFrom( return res, proc.commitAndFinalize() } +func (proc *procedure) estimateGas(call *types.DirectCall) (*types.Result, error) { + + // run the procedure, hash and index don't matter since it won't be emitted anywhere + res, err := proc.run( + call.Message(), + gethCommon.Hash{}, + 0, + types.EstimateGasSubType, + ) + if err != nil { + return nil, err + } + // we never commit the state, but still can reset for sake of safety + proc.state.Reset() + + return res, nil +} + // deployAt deploys a contract at the given target address // behaviour should be similar to what evm.create internal method does with // a few differences, don't need to check for previous forks given this From a2720aad83351c8816e349f828d4384d08d288ff Mon Sep 17 00:00:00 2001 From: Gregor G Date: Mon, 22 Apr 2024 17:13:28 +0200 Subject: [PATCH 09/35] change arguments --- fvm/evm/handler/handler.go | 37 ++++++++++++++++++++----------------- fvm/evm/types/handler.go | 13 ++++++++++--- 2 files changed, 30 insertions(+), 20 deletions(-) diff --git a/fvm/evm/handler/handler.go b/fvm/evm/handler/handler.go index c2a546f4fc4..0ddfddd78cc 100644 --- a/fvm/evm/handler/handler.go +++ b/fvm/evm/handler/handler.go @@ -219,22 +219,14 @@ func (h *ContractHandler) run( return res, nil } -func (h *ContractHandler) EstimateGas(rlpEncodedTx []byte) (uint64, error) { - // step 1 - transaction decoding - encodedLen := uint(len(rlpEncodedTx)) - err := h.backend.MeterComputation(environment.ComputationKindRLPDecoding, encodedLen) - if err != nil { - return 0, err - } - - tx := gethTypes.Transaction{} - err = tx.DecodeRLP( - rlp.NewStream( - bytes.NewReader(rlpEncodedTx), - uint64(encodedLen))) - if err != nil { - return 0, err - } +func (h *ContractHandler) EstimateGas( + from types.Address, + to types.Address, + gasLimit types.GasLimit, + gasPrice uint64, + value types.Balance, + data []byte, +) (uint64, error) { ctx, err := h.getBlockContext() if err != nil { @@ -246,7 +238,18 @@ func (h *ContractHandler) EstimateGas(rlpEncodedTx []byte) (uint64, error) { return 0, err } - res, err := blk.RunTransaction(&tx) + // todo create a factory + call := &types.DirectCall{ + Type: types.DirectCallTxType, + SubType: types.EstimateGasSubType, + From: from, + To: to, + Data: data, + Value: value, + GasLimit: uint64(gasLimit), // todo gas price + } + + res, err := blk.DirectCall(call) if err != nil { return 0, err } diff --git a/fvm/evm/types/handler.go b/fvm/evm/types/handler.go index f96d5d59428..cea6108eaea 100644 --- a/fvm/evm/types/handler.go +++ b/fvm/evm/types/handler.go @@ -46,10 +46,17 @@ type ContractHandler interface { // GenerateResourceUUID generates a new UUID for a resource GenerateResourceUUID() uint64 - // EstimateGas takes an RLP encoded transaction and estimates how much gas it takes - // for the transaction to execute. + // EstimateGas estimates gas requires for the transaction to succesfully execute. + // No changes are persisted to the state. // Error is returned if transaction is not valid or unexpected error happens. - EstimateGas(tx []byte) (uint64, error) + EstimateGas( + from Address, + to Address, + gasLimit GasLimit, + gasPrice uint64, + value Balance, + data []byte, + ) (uint64, error) } // AddressAllocator allocates addresses, used by the handler From f884ed6af53bd5625890da89339b8acdfc125ceb Mon Sep 17 00:00:00 2001 From: Gregor G Date: Mon, 22 Apr 2024 17:13:41 +0200 Subject: [PATCH 10/35] add estimate type --- fvm/evm/types/call.go | 1 + 1 file changed, 1 insertion(+) diff --git a/fvm/evm/types/call.go b/fvm/evm/types/call.go index c9a97bb2cf9..a27f870e9a7 100644 --- a/fvm/evm/types/call.go +++ b/fvm/evm/types/call.go @@ -21,6 +21,7 @@ const ( TransferCallSubType = byte(3) DeployCallSubType = byte(4) ContractCallSubType = byte(5) + EstimateGasSubType = byte(6) // Note that these gas values might need to change if we // change the transaction (e.g. add accesslist), From e6665890e57b680f7d254860040b92be19530d12 Mon Sep 17 00:00:00 2001 From: Gregor G Date: Mon, 22 Apr 2024 17:13:53 +0200 Subject: [PATCH 11/35] update test for estimate --- fvm/evm/evm_test.go | 25 +++++++++++++++---------- 1 file changed, 15 insertions(+), 10 deletions(-) diff --git a/fvm/evm/evm_test.go b/fvm/evm/evm_test.go index e9b99e5eb0f..839ad18637c 100644 --- a/fvm/evm/evm_test.go +++ b/fvm/evm/evm_test.go @@ -721,24 +721,29 @@ func TestGasEstimation(t *testing.T) { import EVM from %s access(all) - fun main(tx: [UInt8]): UInt64 { - return EVM.estimateGas(tx: tx) + fun main(contract: [UInt8; 20], data: [UInt8]): UInt64 { + return EVM.estimateGas( + from: [0, 1, 2, 3, 4, 5, 6, 7, 8, 9, 10, 11, 12, 13, 14, 15, 16, 17, 18, 19], // random address + to: contract, + gasLimit: 25216, + gasPrice: 100, + value: EVM.Balance(attoflow: 0), + data: data + ) } `, sc.EVMContract.Address.HexWithPrefix(), )) - innerTxBytes := testAccount.PrepareSignAndEncodeTx(t, - testContract.DeployedAt.ToCommon(), - testContract.MakeCallData(t, "store", big.NewInt(1337)), - big.NewInt(0), - uint64(10_000_000), - big.NewInt(500000), // todo check price - ) script := fvm.Script(code).WithArguments( json.MustEncode( cadence.NewArray( - ConvertToCadence(innerTxBytes), + ConvertToCadence(testContract.DeployedAt.ToCommon().Bytes()), + ).WithType(stdlib.EVMAddressBytesCadenceType), + ), + json.MustEncode( + cadence.NewArray( + ConvertToCadence(testContract.MakeCallData(t, "store", big.NewInt(1337))), ).WithType(stdlib.EVMTransactionBytesCadenceType), ), ) From f3ef5166417afdc94f3dd8791fd663921e4c2789 Mon Sep 17 00:00:00 2001 From: Gregor G Date: Mon, 22 Apr 2024 18:24:53 +0200 Subject: [PATCH 12/35] wip --- fvm/evm/stdlib/contract.cdc | 1 + 1 file changed, 1 insertion(+) diff --git a/fvm/evm/stdlib/contract.cdc b/fvm/evm/stdlib/contract.cdc index fc860586c6d..3c1205ce3d5 100644 --- a/fvm/evm/stdlib/contract.cdc +++ b/fvm/evm/stdlib/contract.cdc @@ -325,6 +325,7 @@ contract EVM { return runResult } + // todo add API desc comment access(all) fun estimateGas( from: [UInt8; 20], From 3a002182b6016a0625ec39dbb06191b379bbb44f Mon Sep 17 00:00:00 2001 From: Gregor G Date: Tue, 23 Apr 2024 19:40:20 +0200 Subject: [PATCH 13/35] change contract api --- fvm/evm/stdlib/contract.cdc | 42 +++++++++++++++++++------------------ 1 file changed, 22 insertions(+), 20 deletions(-) diff --git a/fvm/evm/stdlib/contract.cdc b/fvm/evm/stdlib/contract.cdc index 3c1205ce3d5..ef67a24b8a2 100644 --- a/fvm/evm/stdlib/contract.cdc +++ b/fvm/evm/stdlib/contract.cdc @@ -78,6 +78,28 @@ contract EVM { ) emit FLOWTokensDeposited(addressBytes: self.bytes, amount: amount) } + + /// Constructs and runs a transaction from the given arguments but + /// DOES NOT commit the state, this means the transaction will not + /// persist any changes. This is useful for estimating gas it takes + /// for transaction to execute, or for calling contract functions + /// to retrieve any values. Execution of this function does not + /// consume any gas and the gas price is set to 0. + access(all) + fun dryCall( + to: [UInt8; 20], + gasLimit: UInt64, + value: Balance, + data: [UInt8] + ): Result { + return InternalEVM.dryCall( + from: self.bytes, + to: to, + gasLimit: gasLimit, + value: value.attoflow, + data: data + ) + } } access(all) @@ -325,26 +347,6 @@ contract EVM { return runResult } - // todo add API desc comment - access(all) - fun estimateGas( - from: [UInt8; 20], - to: [UInt8; 20], - gasLimit: UInt64, - gasPrice: UInt64, - value: Balance, - data: [UInt8] - ): UInt64 { - return InternalEVM.estimateGas( - from: from, - to: to, - gasLimit: gasLimit, - gasPrice: gasPrice, - value: value.attoflow, - data: data - ) - } - access(all) fun encodeABI(_ values: [AnyStruct]): [UInt8] { return InternalEVM.encodeABI(values) From 05ccbe4fb1d525c78149e1a6806a7e56619bbeb2 Mon Sep 17 00:00:00 2001 From: Gregor G Date: Wed, 24 Apr 2024 16:35:27 +0200 Subject: [PATCH 14/35] change to dry run --- fvm/evm/types/call.go | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/fvm/evm/types/call.go b/fvm/evm/types/call.go index a27f870e9a7..51c0200c475 100644 --- a/fvm/evm/types/call.go +++ b/fvm/evm/types/call.go @@ -21,7 +21,7 @@ const ( TransferCallSubType = byte(3) DeployCallSubType = byte(4) ContractCallSubType = byte(5) - EstimateGasSubType = byte(6) + DryRunSubType = byte(6) // Note that these gas values might need to change if we // change the transaction (e.g. add accesslist), From 88d6397d140e66147ed9e0dc9208db1a992910fb Mon Sep 17 00:00:00 2001 From: Gregor G Date: Wed, 24 Apr 2024 16:35:40 +0200 Subject: [PATCH 15/35] cadence change to dry run --- fvm/evm/stdlib/contract.cdc | 44 ++++++++++++++++++------------------- 1 file changed, 22 insertions(+), 22 deletions(-) diff --git a/fvm/evm/stdlib/contract.cdc b/fvm/evm/stdlib/contract.cdc index ef67a24b8a2..c1be5e6d4b1 100644 --- a/fvm/evm/stdlib/contract.cdc +++ b/fvm/evm/stdlib/contract.cdc @@ -78,28 +78,6 @@ contract EVM { ) emit FLOWTokensDeposited(addressBytes: self.bytes, amount: amount) } - - /// Constructs and runs a transaction from the given arguments but - /// DOES NOT commit the state, this means the transaction will not - /// persist any changes. This is useful for estimating gas it takes - /// for transaction to execute, or for calling contract functions - /// to retrieve any values. Execution of this function does not - /// consume any gas and the gas price is set to 0. - access(all) - fun dryCall( - to: [UInt8; 20], - gasLimit: UInt64, - value: Balance, - data: [UInt8] - ): Result { - return InternalEVM.dryCall( - from: self.bytes, - to: to, - gasLimit: gasLimit, - value: value.attoflow, - data: data - ) - } } access(all) @@ -347,6 +325,28 @@ contract EVM { return runResult } + /// Constructs and runs a transaction from the given arguments but + /// DOES NOT commit the state, this means the transaction will not + /// persist any changes. This is useful for estimating gas it takes + /// for transaction to execute, or for calling contract functions + /// to retrieve any values. + access(all) + fun dryRun( + from: [UInt8; 20], + to: [UInt8; 20]?, + gasLimit: UInt64?, + value: Balance, + data: [UInt8] + ): Result { + return InternalEVM.dryRun( + from: from, + to: to, + gasLimit: gasLimit, + value: value.attoflow, + data: data + ) as! Result + } + access(all) fun encodeABI(_ values: [AnyStruct]): [UInt8] { return InternalEVM.encodeABI(values) From db17a648b68a46aee7e73c82884daa15e7ae6131 Mon Sep 17 00:00:00 2001 From: Gregor G Date: Wed, 24 Apr 2024 16:35:50 +0200 Subject: [PATCH 16/35] support dry run --- fvm/evm/handler/handler.go | 56 +++++++++++++++++++++++++------------- fvm/evm/types/handler.go | 11 ++++---- 2 files changed, 42 insertions(+), 25 deletions(-) diff --git a/fvm/evm/handler/handler.go b/fvm/evm/handler/handler.go index 0ddfddd78cc..4a924556341 100644 --- a/fvm/evm/handler/handler.go +++ b/fvm/evm/handler/handler.go @@ -2,6 +2,7 @@ package handler import ( "bytes" + "math" "math/big" "github.com/onflow/cadence/runtime/common" @@ -219,52 +220,69 @@ func (h *ContractHandler) run( return res, nil } -func (h *ContractHandler) EstimateGas( +func (h *ContractHandler) DryRun( from types.Address, - to types.Address, - gasLimit types.GasLimit, - gasPrice uint64, + to *types.Address, + gasLimit *types.GasLimit, value types.Balance, data []byte, -) (uint64, error) { +) *types.ResultSummary { + res, err := h.dryRun(from, to, gasLimit, value, data) + panicOnError(err) + return res.ResultSummary() +} + +func (h *ContractHandler) dryRun( + from types.Address, + to *types.Address, + gasLimit *types.GasLimit, + value types.Balance, + data []byte, +) (*types.Result, error) { ctx, err := h.getBlockContext() if err != nil { - return 0, err + return nil, err } blk, err := h.emulator.NewBlockView(ctx) if err != nil { - return 0, err + return nil, err } - // todo create a factory call := &types.DirectCall{ - Type: types.DirectCallTxType, - SubType: types.EstimateGasSubType, - From: from, - To: to, - Data: data, - Value: value, - GasLimit: uint64(gasLimit), // todo gas price + Type: types.DirectCallTxType, + SubType: types.DryRunSubType, + From: from, + Data: data, + Value: value, + } + + if to != nil { + call.To = *to + } + if gasLimit != nil { + call.GasLimit = uint64(*gasLimit) + } else { + call.GasLimit = math.MaxUint } res, err := blk.DirectCall(call) if err != nil { - return 0, err + return nil, err } // saftey check for result if res == nil { - return 0, types.ErrUnexpectedEmptyResult + return nil, types.ErrUnexpectedEmptyResult } // if invalid return the invalid error if res.Invalid() { - return 0, res.ValidationError + return nil, res.ValidationError } - return res.GasConsumed, nil + return res, nil } func (h *ContractHandler) checkGasLimit(limit types.GasLimit) error { diff --git a/fvm/evm/types/handler.go b/fvm/evm/types/handler.go index cea6108eaea..6b4913c279a 100644 --- a/fvm/evm/types/handler.go +++ b/fvm/evm/types/handler.go @@ -46,17 +46,16 @@ type ContractHandler interface { // GenerateResourceUUID generates a new UUID for a resource GenerateResourceUUID() uint64 - // EstimateGas estimates gas requires for the transaction to succesfully execute. + // DryRun estimates gas requires for the transaction to succesfully execute. // No changes are persisted to the state. // Error is returned if transaction is not valid or unexpected error happens. - EstimateGas( + DryRun( from Address, - to Address, - gasLimit GasLimit, - gasPrice uint64, + to *Address, + gasLimit *GasLimit, value Balance, data []byte, - ) (uint64, error) + ) *ResultSummary } // AddressAllocator allocates addresses, used by the handler From b3588b0c9027fb7acf9644c0912988430ab2319f Mon Sep 17 00:00:00 2001 From: Gregor G Date: Wed, 24 Apr 2024 16:36:04 +0200 Subject: [PATCH 17/35] update test for dry run --- fvm/evm/emulator/emulator.go | 8 ++-- fvm/evm/evm_test.go | 19 +++++---- fvm/evm/stdlib/contract.go | 81 +++++++++++++++++------------------- 3 files changed, 55 insertions(+), 53 deletions(-) diff --git a/fvm/evm/emulator/emulator.go b/fvm/evm/emulator/emulator.go index 2f6f5907d0b..3f8cb69fb11 100644 --- a/fvm/evm/emulator/emulator.go +++ b/fvm/evm/emulator/emulator.go @@ -117,8 +117,8 @@ func (bl *BlockView) DirectCall(call *types.DirectCall) (*types.Result, error) { return proc.mintTo(call, txHash) case types.WithdrawCallSubType: return proc.withdrawFrom(call, txHash) - case types.EstimateGasSubType: - return proc.estimateGas(call) + case types.DryRunSubType: + return proc.dryRun(call) case types.DeployCallSubType: if !call.EmptyToField() { return proc.deployAt(call.From, call.To, call.Data, call.GasLimit, call.Value, txHash) @@ -272,14 +272,14 @@ func (proc *procedure) withdrawFrom( return res, proc.commitAndFinalize() } -func (proc *procedure) estimateGas(call *types.DirectCall) (*types.Result, error) { +func (proc *procedure) dryRun(call *types.DirectCall) (*types.Result, error) { // run the procedure, hash and index don't matter since it won't be emitted anywhere res, err := proc.run( call.Message(), gethCommon.Hash{}, 0, - types.EstimateGasSubType, + types.DryRunSubType, ) if err != nil { return nil, err diff --git a/fvm/evm/evm_test.go b/fvm/evm/evm_test.go index 839ad18637c..6996674c576 100644 --- a/fvm/evm/evm_test.go +++ b/fvm/evm/evm_test.go @@ -702,10 +702,10 @@ func TestCadenceOwnedAccountFunctionalities(t *testing.T) { }) } -func TestGasEstimation(t *testing.T) { +func TestDryRun(t *testing.T) { t.Parallel() - t.Run("test successful gas estimation", func(t *testing.T) { + t.Run("test successful dry run storing a value", func(t *testing.T) { chain := flow.Emulator.Chain() sc := systemcontracts.SystemContractsForChain(chain.ChainID()) RunWithNewEnvironment(t, @@ -721,12 +721,11 @@ func TestGasEstimation(t *testing.T) { import EVM from %s access(all) - fun main(contract: [UInt8; 20], data: [UInt8]): UInt64 { - return EVM.estimateGas( + fun main(contract: [UInt8; 20], data: [UInt8]): EVM.Result { + return EVM.dryRun( from: [0, 1, 2, 3, 4, 5, 6, 7, 8, 9, 10, 11, 12, 13, 14, 15, 16, 17, 18, 19], // random address to: contract, - gasLimit: 25216, - gasPrice: 100, + gasLimit: 555, value: EVM.Balance(attoflow: 0), data: data ) @@ -753,7 +752,13 @@ func TestGasEstimation(t *testing.T) { snapshot) require.NoError(t, err) require.NoError(t, output.Err) - fmt.Println("val", output.Value) + + result, err := stdlib.ResultSummaryFromEVMResultValue(output.Value) + require.NoError(t, err) + fmt.Println(result, int(result.ErrorCode)) + require.Equal(t, types.ErrCodeNoError, result.ErrorCode) + require.Equal(t, types.StatusSuccessful, result.Status) + require.True(t, result.GasConsumed > 0) }) }) } diff --git a/fvm/evm/stdlib/contract.go b/fvm/evm/stdlib/contract.go index 10985da3e98..1f6e84cfaa2 100644 --- a/fvm/evm/stdlib/contract.go +++ b/fvm/evm/stdlib/contract.go @@ -50,6 +50,7 @@ var EVMTransactionBytesCadenceType = cadence.NewVariableSizedArrayType(cadence.T var evmTransactionBytesType = sema.NewVariableSizedType(nil, sema.UInt8Type) var evmAddressBytesType = sema.NewConstantSizedType(nil, sema.UInt8Type, types.AddressLength) +var evmOptionalAddressBytesType = sema.NewOptionalType(nil, evmAddressBytesType) var evmAddressBytesStaticType = interpreter.ConvertSemaArrayTypeToStaticArrayType(nil, evmAddressBytesType) var EVMAddressBytesCadenceType = cadence.NewConstantSizedArrayType(types.AddressLength, cadence.TheUInt8Type) @@ -974,9 +975,9 @@ func newInternalEVMTypeRunFunction( // estimate gas -const internalEVMTypeEstimateGasFunctionName = "estimateGas" +const internalEVMTypeDryRunFunctionName = "dryRun" -var internalEVMTypeEstimateGasFunctionType = &sema.FunctionType{ +var internalEVMTypeDryRunFunctionType = &sema.FunctionType{ Parameters: []sema.Parameter{ { Label: "from", @@ -984,15 +985,11 @@ var internalEVMTypeEstimateGasFunctionType = &sema.FunctionType{ }, { Label: "to", - TypeAnnotation: sema.NewTypeAnnotation(evmAddressBytesType), + TypeAnnotation: sema.NewTypeAnnotation(evmOptionalAddressBytesType), }, { Label: "gasLimit", - TypeAnnotation: sema.NewTypeAnnotation(sema.UInt64Type), - }, - { - Label: "gasPrice", - TypeAnnotation: sema.NewTypeAnnotation(sema.UInt64Type), + TypeAnnotation: sema.NewTypeAnnotation(sema.NewOptionalType(nil, sema.UInt64Type)), }, { Label: "value", @@ -1003,16 +1000,17 @@ var internalEVMTypeEstimateGasFunctionType = &sema.FunctionType{ TypeAnnotation: sema.NewTypeAnnotation(sema.ByteArrayType), }, }, - ReturnTypeAnnotation: sema.NewTypeAnnotation(sema.UInt64Type), + // Actually EVM.Result, but cannot refer to it here + ReturnTypeAnnotation: sema.NewTypeAnnotation(sema.AnyStructType), } -func newInternalEVMTypeEstimateGasFunction( +func newInternalEVMTypeDryRunFunction( gauge common.MemoryGauge, handler types.ContractHandler, ) *interpreter.HostFunctionValue { return interpreter.NewHostFunctionValue( gauge, - internalEVMTypeEstimateGasFunctionType, + internalEVMTypeDryRunFunctionType, func(invocation interpreter.Invocation) interpreter.Value { inter := invocation.Interpreter locationRange := invocation.LocationRange @@ -1030,38 +1028,41 @@ func newInternalEVMTypeEstimateGasFunction( } // Get to address - - toAddressValue, ok := invocation.Arguments[1].(*interpreter.ArrayValue) + optToAddressValue, ok := invocation.Arguments[1].(*interpreter.SomeValue) if !ok { panic(errors.NewUnreachableError()) } - toAddress, err := AddressBytesArrayValueToEVMAddress(inter, locationRange, toAddressValue) - if err != nil { - panic(err) - } - - // Get gas + var toAddress *types.Address + if val := optToAddressValue.InnerValue(inter, locationRange); val != nil { + toAddressValue, ok := val.(*interpreter.ArrayValue) + if !ok { + panic(errors.NewUnreachableError()) + } - gasLimitValue, ok := invocation.Arguments[2].(interpreter.UInt64Value) - if !ok { - panic(errors.NewUnreachableError()) + converted, err := AddressBytesArrayValueToEVMAddress(inter, locationRange, toAddressValue) + if err != nil { + panic(err) + } + toAddress = &converted } - gasLimit := types.GasLimit(gasLimitValue) - - // Get gas price - - gasPriceValue, ok := invocation.Arguments[3].(interpreter.UInt64Value) - if !ok { - panic(errors.NewUnreachableError()) + // Get gas + var gasLimit *types.GasLimit + x := invocation.Arguments[2].(type) + fmt.Println(x) + if _, notNil := invocation.Arguments[2].(interpreter.NilValue); notNil { + gasValue, ok := invocation.Arguments[2].(interpreter.UInt64Value) + if !ok { + panic(errors.NewUnreachableError()) + } + converted := types.GasLimit(gasValue) + gasLimit = &converted } - gasPrice := uint64(gasPriceValue) - // Get balance - balanceValue, ok := invocation.Arguments[4].(interpreter.UIntValue) + balanceValue, ok := invocation.Arguments[3].(interpreter.UIntValue) if !ok { panic(errors.NewUnreachableError()) } @@ -1070,7 +1071,7 @@ func newInternalEVMTypeEstimateGasFunction( // Get data - dataValue, ok := invocation.Arguments[5].(*interpreter.ArrayValue) + dataValue, ok := invocation.Arguments[4].(*interpreter.ArrayValue) if !ok { panic(errors.NewUnreachableError()) } @@ -1082,12 +1083,8 @@ func newInternalEVMTypeEstimateGasFunction( // call estimate - val, err := handler.EstimateGas(fromAddress, toAddress, gasLimit, gasPrice, balance, data) - if err != nil { - panic(err) // todo change - } - - return interpreter.NewUnmeteredUInt64Value(val) + res := handler.DryRun(fromAddress, toAddress, gasLimit, balance, data) + return NewResultValue(handler, gauge, inter, locationRange, res) }, ) } @@ -1928,7 +1925,7 @@ func NewInternalEVMContractValue( internalEVMTypeCastToAttoFLOWFunctionName: newInternalEVMTypeCastToAttoFLOWFunction(gauge, handler), internalEVMTypeCastToFLOWFunctionName: newInternalEVMTypeCastToFLOWFunction(gauge, handler), internalEVMTypeGetLatestBlockFunctionName: newInternalEVMTypeGetLatestBlockFunction(gauge, handler), - internalEVMTypeEstimateGasFunctionName: newInternalEVMTypeEstimateGasFunction(gauge, handler), + internalEVMTypeDryRunFunctionName: newInternalEVMTypeDryRunFunction(gauge, handler), }, nil, nil, @@ -1953,8 +1950,8 @@ var InternalEVMContractType = func() *sema.CompositeType { ), sema.NewUnmeteredPublicFunctionMember( ty, - internalEVMTypeEstimateGasFunctionName, - internalEVMTypeEstimateGasFunctionType, + internalEVMTypeDryRunFunctionName, + internalEVMTypeDryRunFunctionType, "", ), sema.NewUnmeteredPublicFunctionMember( From 52a18b42830e4004218b997b58becc8c42a55fc1 Mon Sep 17 00:00:00 2001 From: Gregor G Date: Wed, 24 Apr 2024 17:29:48 +0200 Subject: [PATCH 18/35] adding tests for dry-run --- fvm/evm/evm_test.go | 134 ++++++++++++++++++++++++++++++++++++++++++-- 1 file changed, 129 insertions(+), 5 deletions(-) diff --git a/fvm/evm/evm_test.go b/fvm/evm/evm_test.go index 6996674c576..f5fae1e202b 100644 --- a/fvm/evm/evm_test.go +++ b/fvm/evm/evm_test.go @@ -705,7 +705,83 @@ func TestCadenceOwnedAccountFunctionalities(t *testing.T) { func TestDryRun(t *testing.T) { t.Parallel() - t.Run("test successful dry run storing a value", func(t *testing.T) { + // this test checks that gas limit is correctly used and gas usage correctly reported + t.Run("test dry run storing a value with different gas limits", func(t *testing.T) { + chain := flow.Emulator.Chain() + sc := systemcontracts.SystemContractsForChain(chain.ChainID()) + RunWithNewEnvironment(t, + chain, func( + ctx fvm.Context, + vm fvm.VM, + snapshot snapshot.SnapshotTree, + testContract *TestContract, + testAccount *EOATestAccount, + ) { + runWithGasLimit := func(gasLimit string) *types.ResultSummary { + code := []byte(fmt.Sprintf( + ` + import EVM from %s + + access(all) + fun main(contract: [UInt8; 20], data: [UInt8]): EVM.Result { + return EVM.dryRun( + from: [0, 1, 2, 3, 4, 5, 6, 7, 8, 9, 10, 11, 12, 13, 14, 15, 16, 17, 18, 19], // random address + to: contract, + gasLimit: %s, + value: EVM.Balance(attoflow: 0), + data: data + ) + } + `, + sc.EVMContract.Address.HexWithPrefix(), + gasLimit, + )) + + script := fvm.Script(code).WithArguments( + json.MustEncode( + cadence.NewArray( + ConvertToCadence(testContract.DeployedAt.ToCommon().Bytes()), + ).WithType(stdlib.EVMAddressBytesCadenceType), + ), + json.MustEncode( + cadence.NewArray( + ConvertToCadence(testContract.MakeCallData(t, "store", big.NewInt(1337))), + ).WithType(stdlib.EVMTransactionBytesCadenceType), + ), + ) + _, output, err := vm.Run( + ctx, + script, + snapshot) + require.NoError(t, err) + require.NoError(t, output.Err) + + result, err := stdlib.ResultSummaryFromEVMResultValue(output.Value) + require.NoError(t, err) + return result + } + + result := runWithGasLimit("nil") + require.Equal(t, types.ErrCodeNoError, result.ErrorCode) + require.Equal(t, types.StatusSuccessful, result.Status) + require.Greater(t, result.GasConsumed, uint64(0)) + + result = runWithGasLimit("55555555") + require.Equal(t, types.ErrCodeNoError, result.ErrorCode) + require.Equal(t, types.StatusSuccessful, result.Status) + require.Greater(t, result.GasConsumed, uint64(0)) + + // gas limit too low + result = runWithGasLimit("21220") + require.Equal(t, types.ExecutionErrCodeOutOfGas, result.ErrorCode) + require.Equal(t, types.StatusFailed, result.Status) + require.Greater(t, result.GasConsumed, uint64(555)) + }) + }) + + // this test makes sure the dry-run that updates the value on the contract + // doesn't persist the change, and after when the value is read it isn't updated. + t.Run("test dry run for any side-effects", func(t *testing.T) { chain := flow.Emulator.Chain() sc := systemcontracts.SystemContractsForChain(chain.ChainID()) RunWithNewEnvironment(t, @@ -725,7 +801,7 @@ func TestDryRun(t *testing.T) { return EVM.dryRun( from: [0, 1, 2, 3, 4, 5, 6, 7, 8, 9, 10, 11, 12, 13, 14, 15, 16, 17, 18, 19], // random address to: contract, - gasLimit: 555, + gasLimit: nil, value: EVM.Balance(attoflow: 0), data: data ) @@ -734,6 +810,7 @@ func TestDryRun(t *testing.T) { sc.EVMContract.Address.HexWithPrefix(), )) + updatedValue := int64(1337) script := fvm.Script(code).WithArguments( json.MustEncode( cadence.NewArray( @@ -742,7 +819,7 @@ func TestDryRun(t *testing.T) { ), json.MustEncode( cadence.NewArray( - ConvertToCadence(testContract.MakeCallData(t, "store", big.NewInt(1337))), + ConvertToCadence(testContract.MakeCallData(t, "store", big.NewInt(updatedValue))), ).WithType(stdlib.EVMTransactionBytesCadenceType), ), ) @@ -755,10 +832,57 @@ func TestDryRun(t *testing.T) { result, err := stdlib.ResultSummaryFromEVMResultValue(output.Value) require.NoError(t, err) - fmt.Println(result, int(result.ErrorCode)) require.Equal(t, types.ErrCodeNoError, result.ErrorCode) require.Equal(t, types.StatusSuccessful, result.Status) - require.True(t, result.GasConsumed > 0) + require.Greater(t, result.GasConsumed, uint64(0)) + + // query the value make sure it's not updated + code = []byte(fmt.Sprintf( + ` + import EVM from %s + access(all) + fun main(tx: [UInt8], coinbaseBytes: [UInt8; 20]): EVM.Result { + let coinbase = EVM.EVMAddress(bytes: coinbaseBytes) + return EVM.run(tx: tx, coinbase: coinbase) + } + `, + sc.EVMContract.Address.HexWithPrefix(), + )) + + innerTxBytes := testAccount.PrepareSignAndEncodeTx(t, + testContract.DeployedAt.ToCommon(), + testContract.MakeCallData(t, "retrieve"), + big.NewInt(0), + uint64(100_000), + big.NewInt(0), + ) + + innerTx := cadence.NewArray( + ConvertToCadence(innerTxBytes), + ).WithType(stdlib.EVMTransactionBytesCadenceType) + + coinbase := cadence.NewArray( + ConvertToCadence(testAccount.Address().Bytes()), + ).WithType(stdlib.EVMAddressBytesCadenceType) + + script = fvm.Script(code).WithArguments( + json.MustEncode(innerTx), + json.MustEncode(coinbase), + ) + + _, output, err = vm.Run( + ctx, + script, + snapshot) + require.NoError(t, err) + require.NoError(t, output.Err) + + res, err := stdlib.ResultSummaryFromEVMResultValue(output.Value) + require.NoError(t, err) + require.Equal(t, types.StatusSuccessful, res.Status) + require.Equal(t, types.ErrCodeNoError, res.ErrorCode) + // make sure the value we used in the dry-run is not the same as the value stored in contract + require.NotEqual(t, updatedValue, new(big.Int).SetBytes(res.ReturnedValue).Int64()) }) }) } From dfa5dfd29fb2bfcbaa4938433c4b8be05735d886 Mon Sep 17 00:00:00 2001 From: Gregor G Date: Wed, 24 Apr 2024 17:30:02 +0200 Subject: [PATCH 19/35] correctly handle optional arguments --- fvm/evm/stdlib/contract.go | 19 ++++++------------- 1 file changed, 6 insertions(+), 13 deletions(-) diff --git a/fvm/evm/stdlib/contract.go b/fvm/evm/stdlib/contract.go index 1f6e84cfaa2..6eecda63189 100644 --- a/fvm/evm/stdlib/contract.go +++ b/fvm/evm/stdlib/contract.go @@ -1028,31 +1028,24 @@ func newInternalEVMTypeDryRunFunction( } // Get to address - optToAddressValue, ok := invocation.Arguments[1].(*interpreter.SomeValue) - if !ok { - panic(errors.NewUnreachableError()) - } - var toAddress *types.Address - if val := optToAddressValue.InnerValue(inter, locationRange); val != nil { - toAddressValue, ok := val.(*interpreter.ArrayValue) + if optToAddressValue, ok := invocation.Arguments[1].(*interpreter.SomeValue); ok { + toValue, ok := optToAddressValue.InnerValue(inter, locationRange).(*interpreter.ArrayValue) if !ok { panic(errors.NewUnreachableError()) } - converted, err := AddressBytesArrayValueToEVMAddress(inter, locationRange, toAddressValue) + converted, err := AddressBytesArrayValueToEVMAddress(inter, locationRange, toValue) if err != nil { panic(err) } toAddress = &converted } - // Get gas + // Get gas limit var gasLimit *types.GasLimit - x := invocation.Arguments[2].(type) - fmt.Println(x) - if _, notNil := invocation.Arguments[2].(interpreter.NilValue); notNil { - gasValue, ok := invocation.Arguments[2].(interpreter.UInt64Value) + if optGasValue, ok := invocation.Arguments[2].(*interpreter.SomeValue); ok { + gasValue, ok := optGasValue.InnerValue(inter, locationRange).(interpreter.UInt64Value) if !ok { panic(errors.NewUnreachableError()) } From cae19c4e9c203a64d89329f48ef3498b0b1269d6 Mon Sep 17 00:00:00 2001 From: Gregor G Date: Wed, 24 Apr 2024 17:50:58 +0200 Subject: [PATCH 20/35] add deploy dry-run --- fvm/evm/evm_test.go | 55 +++++++++++++++++++++++++++++++++++++++++++++ 1 file changed, 55 insertions(+) diff --git a/fvm/evm/evm_test.go b/fvm/evm/evm_test.go index f5fae1e202b..d3aa06e7702 100644 --- a/fvm/evm/evm_test.go +++ b/fvm/evm/evm_test.go @@ -885,6 +885,61 @@ func TestDryRun(t *testing.T) { require.NotEqual(t, updatedValue, new(big.Int).SetBytes(res.ReturnedValue).Int64()) }) }) + + t.Run("test dry run contract deployment", func(t *testing.T) { + chain := flow.Emulator.Chain() + sc := systemcontracts.SystemContractsForChain(chain.ChainID()) + RunWithNewEnvironment(t, + chain, func( + ctx fvm.Context, + vm fvm.VM, + snapshot snapshot.SnapshotTree, + testContract *TestContract, + testAccount *EOATestAccount, + ) { + code := []byte(fmt.Sprintf( + ` + import EVM from %s + + access(all) + fun main(data: [UInt8]): EVM.Result { + return EVM.dryRun( + from: [0, 1, 2, 3, 4, 5, 6, 7, 8, 9, 10, 11, 12, 13, 14, 15, 16, 17, 18, 19], // random address + to: nil, + gasLimit: nil, + value: EVM.Balance(attoflow: 0), + data: data + ) + } + `, + sc.EVMContract.Address.HexWithPrefix(), + )) + + script := fvm.Script(code).WithArguments( + json.MustEncode( + cadence.NewArray( + ConvertToCadence(testContract.ByteCode), + ).WithType(stdlib.EVMTransactionBytesCadenceType), + ), + ) + _, output, err := vm.Run( + ctx, + script, + snapshot) + require.NoError(t, err) + require.NoError(t, output.Err) + + result, err := stdlib.ResultSummaryFromEVMResultValue(output.Value) + require.NoError(t, err) + require.Equal(t, types.ErrCodeNoError, result.ErrorCode) + require.Equal(t, types.StatusSuccessful, result.Status) + require.Greater(t, result.GasConsumed, uint64(0)) + require.NotNil(t, result.ReturnedValue) + // todo add once https://github.com/onflow/flow-go/pull/5606 is merged + //require.NotNil(t, result.DeployedContractAddress) + //require.NotEmpty(t, result.DeployedContractAddress.String()) + }) + }) } func TestCadenceArch(t *testing.T) { From 60913be27e45f1794c87b1b94e9d26bf9d2f4efc Mon Sep 17 00:00:00 2001 From: Gregor G Date: Wed, 24 Apr 2024 18:18:25 +0200 Subject: [PATCH 21/35] add contract test --- fvm/evm/stdlib/contract_test.go | 117 ++++++++++++++++++++++++++++++++ 1 file changed, 117 insertions(+) diff --git a/fvm/evm/stdlib/contract_test.go b/fvm/evm/stdlib/contract_test.go index 32650c40506..a49ec682213 100644 --- a/fvm/evm/stdlib/contract_test.go +++ b/fvm/evm/stdlib/contract_test.go @@ -33,6 +33,7 @@ type testContractHandler struct { lastExecutedBlock func() *types.Block run func(tx []byte, coinbase types.Address) *types.ResultSummary generateResourceUUID func() uint64 + dryRun func(from types.Address, to *types.Address, gasLimit *types.GasLimit, value types.Balance, data []byte) *types.ResultSummary } var _ types.ContractHandler = &testContractHandler{} @@ -75,6 +76,19 @@ func (t *testContractHandler) Run(tx []byte, coinbase types.Address) *types.Resu return t.run(tx, coinbase) } +func (t *testContractHandler) DryRun( + from types.Address, + to *types.Address, + gasLimit *types.GasLimit, + value types.Balance, + data []byte, +) *types.ResultSummary { + if t.dryRun == nil { + panic("unexpected DryRun") + } + return t.dryRun(from, to, gasLimit, value, data) +} + func (t *testContractHandler) GenerateResourceUUID() uint64 { if t.generateResourceUUID == nil { panic("unexpected GenerateResourceUUID") @@ -2884,6 +2898,109 @@ func TestEVMRun(t *testing.T) { assert.True(t, runCalled) } +func TestEVMDryRun(t *testing.T) { + + t.Parallel() + + dryRunCalled := false + + contractsAddress := flow.BytesToAddress([]byte{0x1}) + handler := &testContractHandler{ + evmContractAddress: common.Address(contractsAddress), + dryRun: func(from types.Address, to *types.Address, gasLimit *types.GasLimit, value types.Balance, data []byte) *types.ResultSummary { + dryRunCalled = true + assert.Equal(t, types.Address{0, 1, 2, 3, 4, 5, 6, 7, 8, 9, 10, 11, 12, 13, 14, 15, 16, 17, 18, 19}, from) + assert.Nil(t, to) + require.NotNil(t, gasLimit) + assert.Equal(t, uint64(123), uint64(*gasLimit)) + assert.Equal(t, types.NewBalance(big.NewInt(1)), value) + assert.Equal(t, []byte{1, 3, 3, 7}, data) + + return &types.ResultSummary{ + Status: types.StatusSuccessful, + } + }, + } + + transactionEnvironment := newEVMTransactionEnvironment(handler, contractsAddress) + scriptEnvironment := newEVMScriptEnvironment(handler, contractsAddress) + + rt := runtime.NewInterpreterRuntime(runtime.Config{}) + + accountCodes := map[common.Location][]byte{} + var events []cadence.Event + + runtimeInterface := &TestRuntimeInterface{ + Storage: NewTestLedger(nil, nil), + OnGetSigningAccounts: func() ([]runtime.Address, error) { + return []runtime.Address{runtime.Address(contractsAddress)}, nil + }, + OnResolveLocation: LocationResolver, + OnUpdateAccountContractCode: func(location common.AddressLocation, code []byte) error { + accountCodes[location] = code + return nil + }, + OnGetAccountContractCode: func(location common.AddressLocation) (code []byte, err error) { + code = accountCodes[location] + return code, nil + }, + OnEmitEvent: func(event cadence.Event) error { + events = append(events, event) + return nil + }, + OnDecodeArgument: func(b []byte, t cadence.Type) (cadence.Value, error) { + return json.Decode(nil, b) + }, + } + + nextTransactionLocation := NewTransactionLocationGenerator() + nextScriptLocation := NewScriptLocationGenerator() + + // Deploy contracts + + deployContracts( + t, + rt, + contractsAddress, + runtimeInterface, + transactionEnvironment, + nextTransactionLocation, + ) + + // Run script + + script := []byte(` + import EVM from 0x1 + + access(all) + fun main(): EVM.Result { + return EVM.dryRun( + from: [0, 1, 2, 3, 4, 5, 6, 7, 8, 9, 10, 11, 12, 13, 14, 15, 16, 17, 18, 19], // random address + to: nil, + gasLimit: 123, + value: EVM.Balance(attoflow: 1), + data: [1, 3, 3, 7] + ) + } + `) + + val, err := rt.ExecuteScript( + runtime.Script{ + Source: script, + }, + runtime.Context{ + Interface: runtimeInterface, + Environment: scriptEnvironment, + Location: nextScriptLocation(), + }, + ) + require.NoError(t, err) + res, err := stdlib.ResultSummaryFromEVMResultValue(val) + require.NoError(t, err) + assert.Equal(t, types.StatusSuccessful, res.Status) + assert.True(t, dryRunCalled) +} + func TestEVMCreateCadenceOwnedAccount(t *testing.T) { t.Parallel() From 1c8c5c39e514a60c89aa54caae7a9e40f413df6d Mon Sep 17 00:00:00 2001 From: Gregor G Date: Wed, 24 Apr 2024 18:50:38 +0200 Subject: [PATCH 22/35] add handler test --- fvm/evm/handler/handler_test.go | 132 ++++++++++++++++++++++---------- 1 file changed, 90 insertions(+), 42 deletions(-) diff --git a/fvm/evm/handler/handler_test.go b/fvm/evm/handler/handler_test.go index ccbc1ae2d86..47e06573d8c 100644 --- a/fvm/evm/handler/handler_test.go +++ b/fvm/evm/handler/handler_test.go @@ -675,7 +675,7 @@ func TestHandler_COA(t *testing.T) { func TestHandler_TransactionRun(t *testing.T) { t.Parallel() - t.Run("test - transaction run (success)", func(t *testing.T) { + t.Run("test - transaction run (failed)", func(t *testing.T) { t.Parallel() testutils.RunWithTestBackend(t, func(backend *testutils.TestBackend) { @@ -686,6 +686,7 @@ func TestHandler_TransactionRun(t *testing.T) { aa := handler.NewAddressAllocator() result := &types.Result{ + VMError: gethVM.ErrOutOfGas, DeployedContractAddress: types.Address(testutils.RandomAddress(t)), ReturnedValue: testutils.RandomData(t), GasConsumed: testutils.RandomGas(1000), @@ -701,6 +702,7 @@ func TestHandler_TransactionRun(t *testing.T) { }, } handler := handler.NewContractHandler(flow.Testnet, rootAddr, flowTokenAddress, bs, aa, backend, em) + tx := eoa.PrepareSignAndEncodeTx( t, gethCommon.Address{}, @@ -711,16 +713,66 @@ func TestHandler_TransactionRun(t *testing.T) { ) rs := handler.Run(tx, types.NewAddress(gethCommon.Address{})) - require.Equal(t, types.StatusSuccessful, rs.Status) + require.Equal(t, types.StatusFailed, rs.Status) require.Equal(t, result.GasConsumed, rs.GasConsumed) - require.Equal(t, types.ErrCodeNoError, rs.ErrorCode) + require.Equal(t, types.ExecutionErrCodeOutOfGas, rs.ErrorCode) }) }) }) }) - t.Run("test - transaction run (failed)", func(t *testing.T) { + t.Run("test - transaction run (unhappy cases)", func(t *testing.T) { + t.Parallel() + + testutils.RunWithTestBackend(t, func(backend *testutils.TestBackend) { + testutils.RunWithTestFlowEVMRootAddress(t, backend, func(rootAddr flow.Address) { + testutils.RunWithEOATestAccount(t, backend, rootAddr, func(eoa *testutils.EOATestAccount) { + bs := handler.NewBlockStore(backend, rootAddr) + aa := handler.NewAddressAllocator() + evmErr := fmt.Errorf("%w: next nonce %v, tx nonce %v", gethCore.ErrNonceTooLow, 1, 0) + em := &testutils.TestEmulator{ + RunTransactionFunc: func(tx *gethTypes.Transaction) (*types.Result, error) { + return &types.Result{ValidationError: evmErr}, nil + }, + } + handler := handler.NewContractHandler(flow.Testnet, rootAddr, flowTokenAddress, bs, aa, backend, em) + + coinbase := types.NewAddress(gethCommon.Address{}) + + gasLimit := uint64(testutils.TestComputationLimit + 1) + tx := eoa.PrepareSignAndEncodeTx( + t, + gethCommon.Address{}, + nil, + nil, + gasLimit, + big.NewInt(1), + ) + + assertPanic(t, isNotFatal, func() { + rs := handler.Run([]byte(tx), coinbase) + require.Equal(t, types.StatusInvalid, rs.Status) + }) + + tx = eoa.PrepareSignAndEncodeTx( + t, + gethCommon.Address{}, + nil, + nil, + 100, + big.NewInt(1), + ) + + rs := handler.Run([]byte(tx), coinbase) + require.Equal(t, types.StatusInvalid, rs.Status) + require.Equal(t, types.ValidationErrCodeNonceTooLow, rs.ErrorCode) + }) + }) + }) + }) + + t.Run("test - transaction run (success)", func(t *testing.T) { t.Parallel() testutils.RunWithTestBackend(t, func(backend *testutils.TestBackend) { @@ -731,7 +783,6 @@ func TestHandler_TransactionRun(t *testing.T) { aa := handler.NewAddressAllocator() result := &types.Result{ - VMError: gethVM.ErrOutOfGas, DeployedContractAddress: types.Address(testutils.RandomAddress(t)), ReturnedValue: testutils.RandomData(t), GasConsumed: testutils.RandomGas(1000), @@ -747,7 +798,6 @@ func TestHandler_TransactionRun(t *testing.T) { }, } handler := handler.NewContractHandler(flow.Testnet, rootAddr, flowTokenAddress, bs, aa, backend, em) - tx := eoa.PrepareSignAndEncodeTx( t, gethCommon.Address{}, @@ -758,60 +808,58 @@ func TestHandler_TransactionRun(t *testing.T) { ) rs := handler.Run(tx, types.NewAddress(gethCommon.Address{})) - require.Equal(t, types.StatusFailed, rs.Status) + require.Equal(t, types.StatusSuccessful, rs.Status) require.Equal(t, result.GasConsumed, rs.GasConsumed) - require.Equal(t, types.ExecutionErrCodeOutOfGas, rs.ErrorCode) + require.Equal(t, types.ErrCodeNoError, rs.ErrorCode) }) }) }) }) - t.Run("test - transaction run (unhappy cases)", func(t *testing.T) { + t.Run("test dry run successful", func(t *testing.T) { t.Parallel() testutils.RunWithTestBackend(t, func(backend *testutils.TestBackend) { testutils.RunWithTestFlowEVMRootAddress(t, backend, func(rootAddr flow.Address) { testutils.RunWithEOATestAccount(t, backend, rootAddr, func(eoa *testutils.EOATestAccount) { + bs := handler.NewBlockStore(backend, rootAddr) aa := handler.NewAddressAllocator() - evmErr := fmt.Errorf("%w: next nonce %v, tx nonce %v", gethCore.ErrNonceTooLow, 1, 0) - em := &testutils.TestEmulator{ - RunTransactionFunc: func(tx *gethTypes.Transaction) (*types.Result, error) { - return &types.Result{ValidationError: evmErr}, nil - }, - } - handler := handler.NewContractHandler(flow.Testnet, rootAddr, flowTokenAddress, bs, aa, backend, em) - - coinbase := types.NewAddress(gethCommon.Address{}) - gasLimit := uint64(testutils.TestComputationLimit + 1) - tx := eoa.PrepareSignAndEncodeTx( - t, - gethCommon.Address{}, - nil, - nil, - gasLimit, - big.NewInt(1), - ) + gas := types.GasLimit(15) + value := types.NewBalance(big.NewInt(13)) + data := []byte{1} - assertPanic(t, isNotFatal, func() { - rs := handler.Run([]byte(tx), coinbase) - require.Equal(t, types.StatusInvalid, rs.Status) - }) + result := &types.Result{ + DeployedContractAddress: testutils.RandomAddress(t), + ReturnedValue: testutils.RandomData(t), + GasConsumed: testutils.RandomGas(1000), + Logs: []*gethTypes.Log{ + testutils.GetRandomLogFixture(t), + testutils.GetRandomLogFixture(t), + }, + } - tx = eoa.PrepareSignAndEncodeTx( - t, - gethCommon.Address{}, - nil, - nil, - 100, - big.NewInt(1), - ) + called := false + em := &testutils.TestEmulator{ + DirectCallFunc: func(call *types.DirectCall) (*types.Result, error) { + called = true + assert.Equal(t, uint64(gas), call.GasLimit) + assert.Equal(t, call.Value.Cmp(value), 0) + assert.Equal(t, data, call.Data) + assert.Equal(t, eoa.Address(), call.From) + assert.Equal(t, call.To, types.EmptyAddress) + return result, nil + }, + } - rs := handler.Run([]byte(tx), coinbase) - require.Equal(t, types.StatusInvalid, rs.Status) - require.Equal(t, types.ValidationErrCodeNonceTooLow, rs.ErrorCode) + handler := handler.NewContractHandler(flow.Testnet, rootAddr, flowTokenAddress, bs, aa, backend, em) + rs := handler.DryRun(eoa.Address(), nil, &gas, value, data) + require.Equal(t, types.StatusSuccessful, rs.Status) + require.Equal(t, result.GasConsumed, rs.GasConsumed) + require.Equal(t, types.ErrCodeNoError, rs.ErrorCode) + require.True(t, called) }) }) }) From 4ddf8a06f37c32fe812ec15e0721d466af8a4e45 Mon Sep 17 00:00:00 2001 From: Gregor G Date: Wed, 24 Apr 2024 18:53:40 +0200 Subject: [PATCH 23/35] fix format --- fvm/evm/evm_test.go | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/fvm/evm/evm_test.go b/fvm/evm/evm_test.go index d3aa06e7702..e8dafe75d34 100644 --- a/fvm/evm/evm_test.go +++ b/fvm/evm/evm_test.go @@ -732,7 +732,7 @@ func TestDryRun(t *testing.T) { data: data ) } - `, + `, sc.EVMContract.Address.HexWithPrefix(), gasLimit, )) From 80710a1568f5e1b582e602cc5a3b40fc79d7a5c9 Mon Sep 17 00:00:00 2001 From: Gregor G Date: Thu, 25 Apr 2024 11:58:31 +0200 Subject: [PATCH 24/35] fix comment --- fvm/evm/types/emulator.go | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/fvm/evm/types/emulator.go b/fvm/evm/types/emulator.go index b0118df2719..44389b64ebf 100644 --- a/fvm/evm/types/emulator.go +++ b/fvm/evm/types/emulator.go @@ -68,7 +68,7 @@ type ReadOnlyBlockView interface { // EVM validation errors and EVM execution errors are part of the returned result // and should be handled separately. type BlockView interface { - // executes a direct call + // DirectCall executes a direct call DirectCall(call *DirectCall) (*Result, error) // RunTransaction executes an evm transaction From 637f281ab8f67fde91c2e72ae295c32c1dc51711 Mon Sep 17 00:00:00 2001 From: Gregor G Date: Thu, 25 Apr 2024 19:59:35 +0200 Subject: [PATCH 25/35] Add DryRunTransaction method to emulator This commit includes the addition of a new method named DryRunTransaction in the emulator. This method performs a dry run of an unsigned transaction without persisting any state changes. The functionality also considers the 'from' address as the signer given that the transaction is not signed. --- fvm/evm/emulator/emulator.go | 26 ++++++++++++++++++++++++++ fvm/evm/types/emulator.go | 4 ++++ 2 files changed, 30 insertions(+) diff --git a/fvm/evm/emulator/emulator.go b/fvm/evm/emulator/emulator.go index 3f8cb69fb11..ebd3ae5a929 100644 --- a/fvm/evm/emulator/emulator.go +++ b/fvm/evm/emulator/emulator.go @@ -167,6 +167,32 @@ func (bl *BlockView) RunTransaction( return res, proc.commitAndFinalize() } +// DryRunTransaction run unsigned transaction without persisting the state +func (bl *BlockView) DryRunTransaction( + tx *gethTypes.Transaction, + from gethCommon.Address, +) (*types.Result, error) { + proc, err := bl.newProcedure() + if err != nil { + return nil, err + } + + // here we ignore the error because the only reason an error occurs + // is if the signature is invalid, which we don't care about since + // we use the from address as the signer + msg, _ := gethCore.TransactionToMessage( + tx, + GetSigner(bl.config), + proc.config.BlockContext.BaseFee) + + // use the from as the signer + proc.evm.TxContext.Origin = from + msg.From = from + + // return without commiting the state + return proc.run(msg, tx.Hash(), 0, tx.Type()) +} + func (bl *BlockView) newProcedure() (*procedure, error) { execState, err := state.NewStateDB(bl.ledger, bl.rootAddr) if err != nil { diff --git a/fvm/evm/types/emulator.go b/fvm/evm/types/emulator.go index 44389b64ebf..a0693aa6615 100644 --- a/fvm/evm/types/emulator.go +++ b/fvm/evm/types/emulator.go @@ -73,6 +73,10 @@ type BlockView interface { // RunTransaction executes an evm transaction RunTransaction(tx *gethTypes.Transaction) (*Result, error) + + // DryRunTransaction executes unsigned transaction but does not persist the state changes, + // since transaction is not signed, from address is used as the signer. + DryRunTransaction(tx *gethTypes.Transaction, from gethCommon.Address) (*Result, error) } // Emulator emulates an evm-compatible chain From 17759adcca97d9ddbe19c34a50b2dd0070037cef Mon Sep 17 00:00:00 2001 From: Gregor G Date: Thu, 25 Apr 2024 20:02:18 +0200 Subject: [PATCH 26/35] Refactor ContractHandler dry run methods The dry run methods in the ContractHandler have been significantly refactored to simplify their parameters and improve their clarity. Specifically, the transaction is now passed as an RLP-encoded byte array instead of separate parameters for each part of the transaction. This streamlines the function definition and the calls to it and relies more on standard transaction representation and less on custom data structures. --- fvm/evm/handler/handler.go | 43 +++++++++++++------------------------- fvm/evm/types/handler.go | 17 ++++++--------- 2 files changed, 20 insertions(+), 40 deletions(-) diff --git a/fvm/evm/handler/handler.go b/fvm/evm/handler/handler.go index 4a924556341..11aa66eed21 100644 --- a/fvm/evm/handler/handler.go +++ b/fvm/evm/handler/handler.go @@ -2,7 +2,6 @@ package handler import ( "bytes" - "math" "math/big" "github.com/onflow/cadence/runtime/common" @@ -221,57 +220,43 @@ func (h *ContractHandler) run( } func (h *ContractHandler) DryRun( + rlpEncodedTx []byte, from types.Address, - to *types.Address, - gasLimit *types.GasLimit, - value types.Balance, - data []byte, ) *types.ResultSummary { - res, err := h.dryRun(from, to, gasLimit, value, data) + res, err := h.dryRun(rlpEncodedTx, from) panicOnError(err) return res.ResultSummary() } func (h *ContractHandler) dryRun( + rlpEncodedTx []byte, from types.Address, - to *types.Address, - gasLimit *types.GasLimit, - value types.Balance, - data []byte, ) (*types.Result, error) { - - ctx, err := h.getBlockContext() + // step 1 - transaction decoding + encodedLen := uint(len(rlpEncodedTx)) + err := h.backend.MeterComputation(environment.ComputationKindRLPDecoding, encodedLen) if err != nil { return nil, err } - blk, err := h.emulator.NewBlockView(ctx) + tx := gethTypes.Transaction{} + err = tx.UnmarshalBinary(rlpEncodedTx) if err != nil { return nil, err } - call := &types.DirectCall{ - Type: types.DirectCallTxType, - SubType: types.DryRunSubType, - From: from, - Data: data, - Value: value, - } - - if to != nil { - call.To = *to - } - if gasLimit != nil { - call.GasLimit = uint64(*gasLimit) - } else { - call.GasLimit = math.MaxUint + ctx, err := h.getBlockContext() + if err != nil { + return nil, err } - res, err := blk.DirectCall(call) + blk, err := h.emulator.NewBlockView(ctx) if err != nil { return nil, err } + res, err := blk.DryRunTransaction(&tx, from.ToCommon()) + // saftey check for result if res == nil { return nil, types.ErrUnexpectedEmptyResult diff --git a/fvm/evm/types/handler.go b/fvm/evm/types/handler.go index 6b4913c279a..935b68b2098 100644 --- a/fvm/evm/types/handler.go +++ b/fvm/evm/types/handler.go @@ -37,6 +37,12 @@ type ContractHandler interface { // collects the gas fees, and transfers the gas fees to the given coinbase account. Run(tx []byte, coinbase Address) *ResultSummary + // DryRun simulates execution of the provided RLP-encoded and unsigned transaction. + // Because the transaction is unsigned the from address is required, since + // from address is normally derived from the transaction signature. + // The function should not have any persisted changes made to the state. + DryRun(tx []byte, from Address) *ResultSummary + // FlowTokenAddress returns the address where FLOW token is deployed FlowTokenAddress() common.Address @@ -45,17 +51,6 @@ type ContractHandler interface { // GenerateResourceUUID generates a new UUID for a resource GenerateResourceUUID() uint64 - - // DryRun estimates gas requires for the transaction to succesfully execute. - // No changes are persisted to the state. - // Error is returned if transaction is not valid or unexpected error happens. - DryRun( - from Address, - to *Address, - gasLimit *GasLimit, - value Balance, - data []byte, - ) *ResultSummary } // AddressAllocator allocates addresses, used by the handler From 5efe534ad6e576075370103d5dd9aa3ef255a580 Mon Sep 17 00:00:00 2001 From: Gregor G Date: Thu, 25 Apr 2024 20:03:25 +0200 Subject: [PATCH 27/35] Refactor `dryRun` function in EVM contracts The `dryRun` function in the EVM contracts has been simplified for easier usage. This new implementation takes a transaction and a from-address as arguments, instead of separate entities like `from`, `to`, `gasLimit`, `value`, and `data`. The changes provide a neater interface and potentially reduce errors stemming from incorrect or ambiguous inputs. --- fvm/evm/stdlib/contract.cdc | 24 ++++--------- fvm/evm/stdlib/contract.go | 69 ++++++------------------------------- 2 files changed, 18 insertions(+), 75 deletions(-) diff --git a/fvm/evm/stdlib/contract.cdc b/fvm/evm/stdlib/contract.cdc index 1b92385747c..e1fe0e58b53 100644 --- a/fvm/evm/stdlib/contract.cdc +++ b/fvm/evm/stdlib/contract.cdc @@ -393,25 +393,15 @@ contract EVM { return runResult } - /// Constructs and runs a transaction from the given arguments but - /// DOES NOT commit the state, this means the transaction will not - /// persist any changes. This is useful for estimating gas it takes - /// for transaction to execute, or for calling contract functions - /// to retrieve any values. + /// Simulates running unsigned RLP-encoded transaction using + /// the from address as the signer. + /// The transaction state changes are not persisted. + /// This is useful for gas estimation or calling view contract functions. access(all) - fun dryRun( - from: [UInt8; 20], - to: [UInt8; 20]?, - gasLimit: UInt64?, - value: Balance, - data: [UInt8] - ): Result { + fun dryRun(tx: [UInt8], from: EVMAddress): Result { return InternalEVM.dryRun( - from: from, - to: to, - gasLimit: gasLimit, - value: value.attoflow, - data: data + tx: tx, + from: from.bytes, ) as! Result } diff --git a/fvm/evm/stdlib/contract.go b/fvm/evm/stdlib/contract.go index edde6abb6d2..039e7a6ca43 100644 --- a/fvm/evm/stdlib/contract.go +++ b/fvm/evm/stdlib/contract.go @@ -991,24 +991,12 @@ const internalEVMTypeDryRunFunctionName = "dryRun" var internalEVMTypeDryRunFunctionType = &sema.FunctionType{ Parameters: []sema.Parameter{ { - Label: "from", - TypeAnnotation: sema.NewTypeAnnotation(evmAddressBytesType), - }, - { - Label: "to", - TypeAnnotation: sema.NewTypeAnnotation(evmOptionalAddressBytesType), - }, - { - Label: "gasLimit", - TypeAnnotation: sema.NewTypeAnnotation(sema.NewOptionalType(nil, sema.UInt64Type)), - }, - { - Label: "value", - TypeAnnotation: sema.NewTypeAnnotation(sema.UIntType), + Label: "tx", + TypeAnnotation: sema.NewTypeAnnotation(evmTransactionBytesType), }, { - Label: "data", - TypeAnnotation: sema.NewTypeAnnotation(sema.ByteArrayType), + Label: "from", + TypeAnnotation: sema.NewTypeAnnotation(evmAddressBytesType), }, }, // Actually EVM.Result, but cannot refer to it here @@ -1026,68 +1014,33 @@ func newInternalEVMTypeDryRunFunction( inter := invocation.Interpreter locationRange := invocation.LocationRange - // Get from address + // Get transaction argument - fromAddressValue, ok := invocation.Arguments[0].(*interpreter.ArrayValue) + transactionValue, ok := invocation.Arguments[0].(*interpreter.ArrayValue) if !ok { panic(errors.NewUnreachableError()) } - fromAddress, err := AddressBytesArrayValueToEVMAddress(inter, locationRange, fromAddressValue) + transaction, err := interpreter.ByteArrayValueToByteSlice(inter, transactionValue, locationRange) if err != nil { panic(err) } - // Get to address - var toAddress *types.Address - if optToAddressValue, ok := invocation.Arguments[1].(*interpreter.SomeValue); ok { - toValue, ok := optToAddressValue.InnerValue(inter, locationRange).(*interpreter.ArrayValue) - if !ok { - panic(errors.NewUnreachableError()) - } - - converted, err := AddressBytesArrayValueToEVMAddress(inter, locationRange, toValue) - if err != nil { - panic(err) - } - toAddress = &converted - } - - // Get gas limit - var gasLimit *types.GasLimit - if optGasValue, ok := invocation.Arguments[2].(*interpreter.SomeValue); ok { - gasValue, ok := optGasValue.InnerValue(inter, locationRange).(interpreter.UInt64Value) - if !ok { - panic(errors.NewUnreachableError()) - } - converted := types.GasLimit(gasValue) - gasLimit = &converted - } - - // Get balance - - balanceValue, ok := invocation.Arguments[3].(interpreter.UIntValue) - if !ok { - panic(errors.NewUnreachableError()) - } - - balance := types.NewBalance(balanceValue.BigInt) + // Get from argument - // Get data - - dataValue, ok := invocation.Arguments[4].(*interpreter.ArrayValue) + fromValue, ok := invocation.Arguments[1].(*interpreter.ArrayValue) if !ok { panic(errors.NewUnreachableError()) } - data, err := interpreter.ByteArrayValueToByteSlice(inter, dataValue, locationRange) + from, err := interpreter.ByteArrayValueToByteSlice(inter, fromValue, locationRange) if err != nil { panic(err) } // call estimate - res := handler.DryRun(fromAddress, toAddress, gasLimit, balance, data) + res := handler.DryRun(transaction, types.NewAddressFromBytes(from)) return NewResultValue(handler, gauge, inter, locationRange, res) }, ) From 318f479af7d8fa77b050a3b832a5e119c9610057 Mon Sep 17 00:00:00 2001 From: Gregor G Date: Thu, 25 Apr 2024 20:11:55 +0200 Subject: [PATCH 28/35] Update handler test --- fvm/evm/handler/handler_test.go | 37 ++++++++++++++++++++++++--------- fvm/evm/testutils/emulator.go | 22 ++++++++++++++------ 2 files changed, 43 insertions(+), 16 deletions(-) diff --git a/fvm/evm/handler/handler_test.go b/fvm/evm/handler/handler_test.go index 47e06573d8c..9eb63d7eece 100644 --- a/fvm/evm/handler/handler_test.go +++ b/fvm/evm/handler/handler_test.go @@ -827,9 +827,24 @@ func TestHandler_TransactionRun(t *testing.T) { bs := handler.NewBlockStore(backend, rootAddr) aa := handler.NewAddressAllocator() - gas := types.GasLimit(15) - value := types.NewBalance(big.NewInt(13)) - data := []byte{1} + nonce := uint64(1) + to := gethCommon.Address{1, 2} + amount := big.NewInt(13) + gasLimit := uint64(1337) + gasPrice := big.NewInt(2000) + data := []byte{1, 5} + from := types.Address{3, 4} + + tx := gethTypes.NewTransaction( + nonce, + to, + amount, + gasLimit, + gasPrice, + data, + ) + rlpTx, err := tx.MarshalBinary() + require.NoError(t, err) result := &types.Result{ DeployedContractAddress: testutils.RandomAddress(t), @@ -843,19 +858,21 @@ func TestHandler_TransactionRun(t *testing.T) { called := false em := &testutils.TestEmulator{ - DirectCallFunc: func(call *types.DirectCall) (*types.Result, error) { + DryRunTransactionFunc: func(tx *gethTypes.Transaction, address gethCommon.Address) (*types.Result, error) { + assert.Equal(t, nonce, tx.Nonce()) + assert.Equal(t, &to, tx.To()) + assert.Equal(t, gasLimit, tx.Gas()) + assert.Equal(t, gasPrice, tx.GasPrice()) + assert.Equal(t, data, tx.Data()) + assert.Equal(t, from.ToCommon(), address) called = true - assert.Equal(t, uint64(gas), call.GasLimit) - assert.Equal(t, call.Value.Cmp(value), 0) - assert.Equal(t, data, call.Data) - assert.Equal(t, eoa.Address(), call.From) - assert.Equal(t, call.To, types.EmptyAddress) return result, nil }, } handler := handler.NewContractHandler(flow.Testnet, rootAddr, flowTokenAddress, bs, aa, backend, em) - rs := handler.DryRun(eoa.Address(), nil, &gas, value, data) + + rs := handler.DryRun(rlpTx, from) require.Equal(t, types.StatusSuccessful, rs.Status) require.Equal(t, result.GasConsumed, rs.GasConsumed) require.Equal(t, types.ErrCodeNoError, rs.ErrorCode) diff --git a/fvm/evm/testutils/emulator.go b/fvm/evm/testutils/emulator.go index e02adee2232..d6101e53a6e 100644 --- a/fvm/evm/testutils/emulator.go +++ b/fvm/evm/testutils/emulator.go @@ -1,6 +1,7 @@ package testutils import ( + gethCommon "github.com/onflow/go-ethereum/common" "math/big" gethTypes "github.com/onflow/go-ethereum/core/types" @@ -9,12 +10,13 @@ import ( ) type TestEmulator struct { - BalanceOfFunc func(address types.Address) (*big.Int, error) - NonceOfFunc func(address types.Address) (uint64, error) - CodeOfFunc func(address types.Address) (types.Code, error) - CodeHashOfFunc func(address types.Address) ([]byte, error) - DirectCallFunc func(call *types.DirectCall) (*types.Result, error) - RunTransactionFunc func(tx *gethTypes.Transaction) (*types.Result, error) + BalanceOfFunc func(address types.Address) (*big.Int, error) + NonceOfFunc func(address types.Address) (uint64, error) + CodeOfFunc func(address types.Address) (types.Code, error) + CodeHashOfFunc func(address types.Address) ([]byte, error) + DirectCallFunc func(call *types.DirectCall) (*types.Result, error) + RunTransactionFunc func(tx *gethTypes.Transaction) (*types.Result, error) + DryRunTransactionFunc func(tx *gethTypes.Transaction, address gethCommon.Address) (*types.Result, error) } var _ types.Emulator = &TestEmulator{} @@ -76,3 +78,11 @@ func (em *TestEmulator) RunTransaction(tx *gethTypes.Transaction) (*types.Result } return em.RunTransactionFunc(tx) } + +// DryRunTransaction simulates transaction execution +func (em *TestEmulator) DryRunTransaction(tx *gethTypes.Transaction, address gethCommon.Address) (*types.Result, error) { + if em.DryRunTransactionFunc == nil { + panic("method not set") + } + return em.DryRunTransactionFunc(tx, address) +} From f3fa2f6f32d69f81ca1d1f09e024942f11ce567f Mon Sep 17 00:00:00 2001 From: Gregor G Date: Thu, 25 Apr 2024 20:32:01 +0200 Subject: [PATCH 29/35] update evm test with dry run changes --- fvm/evm/evm_test.go | 156 ++++++++++++++++++-------------------------- 1 file changed, 65 insertions(+), 91 deletions(-) diff --git a/fvm/evm/evm_test.go b/fvm/evm/evm_test.go index e8dafe75d34..8861cfb9c60 100644 --- a/fvm/evm/evm_test.go +++ b/fvm/evm/evm_test.go @@ -2,6 +2,8 @@ package evm_test import ( "fmt" + gethTypes "github.com/onflow/go-ethereum/core/types" + "math" "math/big" "testing" @@ -704,11 +706,62 @@ func TestCadenceOwnedAccountFunctionalities(t *testing.T) { func TestDryRun(t *testing.T) { t.Parallel() + chain := flow.Emulator.Chain() + sc := systemcontracts.SystemContractsForChain(chain.ChainID()) + + storeValueWithLimit := func( + gasLimit uint64, + value int64, + ctx fvm.Context, + vm fvm.VM, + snapshot snapshot.SnapshotTree, + testContract *TestContract, + ) *types.ResultSummary { + code := []byte(fmt.Sprintf(` + import EVM from %s + + access(all) + fun main(tx: [UInt8]): EVM.Result { + return EVM.dryRun( + tx: tx, + from: EVM.EVMAddress(bytes: [0, 1, 2, 3, 4, 5, 6, 7, 8, 9, 10, 11, 12, 13, 14, 15, 16, 17, 18, 19]) + ) + }`, + sc.EVMContract.Address.HexWithPrefix(), + )) + + tx := gethTypes.NewTransaction( + 0, + testContract.DeployedAt.ToCommon(), + big.NewInt(0), + gasLimit, + big.NewInt(0), + testContract.MakeCallData(t, "store", big.NewInt(value)), + ) + innerTxBytes, err := tx.MarshalBinary() + require.NoError(t, err) + + script := fvm.Script(code).WithArguments( + json.MustEncode( + cadence.NewArray( + ConvertToCadence(innerTxBytes), + ).WithType(stdlib.EVMTransactionBytesCadenceType), + ), + ) + _, output, err := vm.Run( + ctx, + script, + snapshot) + require.NoError(t, err) + require.NoError(t, output.Err) + + result, err := stdlib.ResultSummaryFromEVMResultValue(output.Value) + require.NoError(t, err) + return result + } // this test checks that gas limit is correctly used and gas usage correctly reported t.Run("test dry run storing a value with different gas limits", func(t *testing.T) { - chain := flow.Emulator.Chain() - sc := systemcontracts.SystemContractsForChain(chain.ChainID()) RunWithNewEnvironment(t, chain, func( ctx fvm.Context, @@ -717,65 +770,23 @@ func TestDryRun(t *testing.T) { testContract *TestContract, testAccount *EOATestAccount, ) { - runWithGasLimit := func(gasLimit string) *types.ResultSummary { - code := []byte(fmt.Sprintf( - ` - import EVM from %s - access(all) - fun main(contract: [UInt8; 20], data: [UInt8]): EVM.Result { - return EVM.dryRun( - from: [0, 1, 2, 3, 4, 5, 6, 7, 8, 9, 10, 11, 12, 13, 14, 15, 16, 17, 18, 19], // random address - to: contract, - gasLimit: %s, - value: EVM.Balance(attoflow: 0), - data: data - ) - } - `, - sc.EVMContract.Address.HexWithPrefix(), - gasLimit, - )) - - script := fvm.Script(code).WithArguments( - json.MustEncode( - cadence.NewArray( - ConvertToCadence(testContract.DeployedAt.ToCommon().Bytes()), - ).WithType(stdlib.EVMAddressBytesCadenceType), - ), - json.MustEncode( - cadence.NewArray( - ConvertToCadence(testContract.MakeCallData(t, "store", big.NewInt(1337))), - ).WithType(stdlib.EVMTransactionBytesCadenceType), - ), - ) - _, output, err := vm.Run( - ctx, - script, - snapshot) - require.NoError(t, err) - require.NoError(t, output.Err) - - result, err := stdlib.ResultSummaryFromEVMResultValue(output.Value) - require.NoError(t, err) - return result - } - - result := runWithGasLimit("nil") + result := storeValueWithLimit(math.MaxUint64-1, 1, ctx, vm, snapshot, testContract) require.Equal(t, types.ErrCodeNoError, result.ErrorCode) require.Equal(t, types.StatusSuccessful, result.Status) require.Greater(t, result.GasConsumed, uint64(0)) - result = runWithGasLimit("55555555") + result = storeValueWithLimit(uint64(5555555555), 1, ctx, vm, snapshot, testContract) require.Equal(t, types.ErrCodeNoError, result.ErrorCode) require.Equal(t, types.StatusSuccessful, result.Status) require.Greater(t, result.GasConsumed, uint64(0)) - // gas limit too low - result = runWithGasLimit("21220") + // gas limit too low, but still bigger than intrinsic gas value + limit := uint64(21216) + result = storeValueWithLimit(limit, 1, ctx, vm, snapshot, testContract) require.Equal(t, types.ExecutionErrCodeOutOfGas, result.ErrorCode) require.Equal(t, types.StatusFailed, result.Status) - require.Greater(t, result.GasConsumed, uint64(555)) + require.Equal(t, result.GasConsumed, limit) // burn it all!!! }) }) @@ -792,52 +803,15 @@ func TestDryRun(t *testing.T) { testContract *TestContract, testAccount *EOATestAccount, ) { - code := []byte(fmt.Sprintf( - ` - import EVM from %s - - access(all) - fun main(contract: [UInt8; 20], data: [UInt8]): EVM.Result { - return EVM.dryRun( - from: [0, 1, 2, 3, 4, 5, 6, 7, 8, 9, 10, 11, 12, 13, 14, 15, 16, 17, 18, 19], // random address - to: contract, - gasLimit: nil, - value: EVM.Balance(attoflow: 0), - data: data - ) - } - `, - sc.EVMContract.Address.HexWithPrefix(), - )) - updatedValue := int64(1337) - script := fvm.Script(code).WithArguments( - json.MustEncode( - cadence.NewArray( - ConvertToCadence(testContract.DeployedAt.ToCommon().Bytes()), - ).WithType(stdlib.EVMAddressBytesCadenceType), - ), - json.MustEncode( - cadence.NewArray( - ConvertToCadence(testContract.MakeCallData(t, "store", big.NewInt(updatedValue))), - ).WithType(stdlib.EVMTransactionBytesCadenceType), - ), - ) - _, output, err := vm.Run( - ctx, - script, - snapshot) - require.NoError(t, err) - require.NoError(t, output.Err) - result, err := stdlib.ResultSummaryFromEVMResultValue(output.Value) - require.NoError(t, err) + result := storeValueWithLimit(uint64(100000), updatedValue, ctx, vm, snapshot, testContract) require.Equal(t, types.ErrCodeNoError, result.ErrorCode) require.Equal(t, types.StatusSuccessful, result.Status) require.Greater(t, result.GasConsumed, uint64(0)) // query the value make sure it's not updated - code = []byte(fmt.Sprintf( + code := []byte(fmt.Sprintf( ` import EVM from %s access(all) @@ -865,12 +839,12 @@ func TestDryRun(t *testing.T) { ConvertToCadence(testAccount.Address().Bytes()), ).WithType(stdlib.EVMAddressBytesCadenceType) - script = fvm.Script(code).WithArguments( + script := fvm.Script(code).WithArguments( json.MustEncode(innerTx), json.MustEncode(coinbase), ) - _, output, err = vm.Run( + _, output, err := vm.Run( ctx, script, snapshot) From 82213320c60a092009c2027b9629eafca0158fd4 Mon Sep 17 00:00:00 2001 From: Gregor G Date: Thu, 25 Apr 2024 20:41:45 +0200 Subject: [PATCH 30/35] update test with dry run --- fvm/evm/evm_test.go | 120 +++++++++++++++++++------------------------- 1 file changed, 53 insertions(+), 67 deletions(-) diff --git a/fvm/evm/evm_test.go b/fvm/evm/evm_test.go index 8861cfb9c60..b429c3c326e 100644 --- a/fvm/evm/evm_test.go +++ b/fvm/evm/evm_test.go @@ -708,36 +708,28 @@ func TestDryRun(t *testing.T) { t.Parallel() chain := flow.Emulator.Chain() sc := systemcontracts.SystemContractsForChain(chain.ChainID()) + evmAddress := sc.EVMContract.Address.HexWithPrefix() - storeValueWithLimit := func( - gasLimit uint64, - value int64, + dryRunTx := func( + tx *gethTypes.Transaction, ctx fvm.Context, vm fvm.VM, snapshot snapshot.SnapshotTree, testContract *TestContract, ) *types.ResultSummary { code := []byte(fmt.Sprintf(` - import EVM from %s - - access(all) - fun main(tx: [UInt8]): EVM.Result { - return EVM.dryRun( - tx: tx, - from: EVM.EVMAddress(bytes: [0, 1, 2, 3, 4, 5, 6, 7, 8, 9, 10, 11, 12, 13, 14, 15, 16, 17, 18, 19]) - ) - }`, - sc.EVMContract.Address.HexWithPrefix(), + import EVM from %s + + access(all) + fun main(tx: [UInt8]): EVM.Result { + return EVM.dryRun( + tx: tx, + from: EVM.EVMAddress(bytes: [0, 1, 2, 3, 4, 5, 6, 7, 8, 9, 10, 11, 12, 13, 14, 15, 16, 17, 18, 19]) + ) + }`, + evmAddress, )) - tx := gethTypes.NewTransaction( - 0, - testContract.DeployedAt.ToCommon(), - big.NewInt(0), - gasLimit, - big.NewInt(0), - testContract.MakeCallData(t, "store", big.NewInt(value)), - ) innerTxBytes, err := tx.MarshalBinary() require.NoError(t, err) @@ -770,20 +762,34 @@ func TestDryRun(t *testing.T) { testContract *TestContract, testAccount *EOATestAccount, ) { + data := testContract.MakeCallData(t, "store", big.NewInt(1337)) - result := storeValueWithLimit(math.MaxUint64-1, 1, ctx, vm, snapshot, testContract) - require.Equal(t, types.ErrCodeNoError, result.ErrorCode) - require.Equal(t, types.StatusSuccessful, result.Status) - require.Greater(t, result.GasConsumed, uint64(0)) - - result = storeValueWithLimit(uint64(5555555555), 1, ctx, vm, snapshot, testContract) + limit := uint64(math.MaxUint64 - 1) + tx := gethTypes.NewTransaction( + 0, + testContract.DeployedAt.ToCommon(), + big.NewInt(0), + limit, + big.NewInt(0), + data, + ) + result := dryRunTx(tx, ctx, vm, snapshot, testContract) require.Equal(t, types.ErrCodeNoError, result.ErrorCode) require.Equal(t, types.StatusSuccessful, result.Status) require.Greater(t, result.GasConsumed, uint64(0)) + require.Less(t, result.GasConsumed, limit) // gas limit too low, but still bigger than intrinsic gas value - limit := uint64(21216) - result = storeValueWithLimit(limit, 1, ctx, vm, snapshot, testContract) + limit = uint64(21216) + tx = gethTypes.NewTransaction( + 0, + testContract.DeployedAt.ToCommon(), + big.NewInt(0), + limit, + big.NewInt(0), + data, + ) + result = dryRunTx(tx, ctx, vm, snapshot, testContract) require.Equal(t, types.ExecutionErrCodeOutOfGas, result.ErrorCode) require.Equal(t, types.StatusFailed, result.Status) require.Equal(t, result.GasConsumed, limit) // burn it all!!! @@ -793,8 +799,6 @@ func TestDryRun(t *testing.T) { // this test makes sure the dry-run that updates the value on the contract // doesn't persist the change, and after when the value is read it isn't updated. t.Run("test dry run for any side-effects", func(t *testing.T) { - chain := flow.Emulator.Chain() - sc := systemcontracts.SystemContractsForChain(chain.ChainID()) RunWithNewEnvironment(t, chain, func( ctx fvm.Context, @@ -804,8 +808,17 @@ func TestDryRun(t *testing.T) { testAccount *EOATestAccount, ) { updatedValue := int64(1337) + data := testContract.MakeCallData(t, "store", big.NewInt(updatedValue)) + tx := gethTypes.NewTransaction( + 0, + testContract.DeployedAt.ToCommon(), + big.NewInt(0), + uint64(1000000), + big.NewInt(0), + data, + ) - result := storeValueWithLimit(uint64(100000), updatedValue, ctx, vm, snapshot, testContract) + result := dryRunTx(tx, ctx, vm, snapshot, testContract) require.Equal(t, types.ErrCodeNoError, result.ErrorCode) require.Equal(t, types.StatusSuccessful, result.Status) require.Greater(t, result.GasConsumed, uint64(0)) @@ -820,7 +833,7 @@ func TestDryRun(t *testing.T) { return EVM.run(tx: tx, coinbase: coinbase) } `, - sc.EVMContract.Address.HexWithPrefix(), + evmAddress, )) innerTxBytes := testAccount.PrepareSignAndEncodeTx(t, @@ -861,8 +874,6 @@ func TestDryRun(t *testing.T) { }) t.Run("test dry run contract deployment", func(t *testing.T) { - chain := flow.Emulator.Chain() - sc := systemcontracts.SystemContractsForChain(chain.ChainID()) RunWithNewEnvironment(t, chain, func( ctx fvm.Context, @@ -871,40 +882,15 @@ func TestDryRun(t *testing.T) { testContract *TestContract, testAccount *EOATestAccount, ) { - code := []byte(fmt.Sprintf( - ` - import EVM from %s - - access(all) - fun main(data: [UInt8]): EVM.Result { - return EVM.dryRun( - from: [0, 1, 2, 3, 4, 5, 6, 7, 8, 9, 10, 11, 12, 13, 14, 15, 16, 17, 18, 19], // random address - to: nil, - gasLimit: nil, - value: EVM.Balance(attoflow: 0), - data: data - ) - } - `, - sc.EVMContract.Address.HexWithPrefix(), - )) - - script := fvm.Script(code).WithArguments( - json.MustEncode( - cadence.NewArray( - ConvertToCadence(testContract.ByteCode), - ).WithType(stdlib.EVMTransactionBytesCadenceType), - ), + tx := gethTypes.NewContractCreation( + 0, + big.NewInt(0), + uint64(1000000), + big.NewInt(0), + testContract.ByteCode, ) - _, output, err := vm.Run( - ctx, - script, - snapshot) - require.NoError(t, err) - require.NoError(t, output.Err) - result, err := stdlib.ResultSummaryFromEVMResultValue(output.Value) - require.NoError(t, err) + result := dryRunTx(tx, ctx, vm, snapshot, testContract) require.Equal(t, types.ErrCodeNoError, result.ErrorCode) require.Equal(t, types.StatusSuccessful, result.Status) require.Greater(t, result.GasConsumed, uint64(0)) From 3ac5f4d7b546a5eba3b02076737ddb826190b24e Mon Sep 17 00:00:00 2001 From: Gregor G Date: Thu, 25 Apr 2024 20:46:53 +0200 Subject: [PATCH 31/35] fixed contract test with dry run --- fvm/evm/stdlib/contract_test.go | 37 +++++++++++++-------------------- 1 file changed, 15 insertions(+), 22 deletions(-) diff --git a/fvm/evm/stdlib/contract_test.go b/fvm/evm/stdlib/contract_test.go index f4b12895feb..e57a645353e 100644 --- a/fvm/evm/stdlib/contract_test.go +++ b/fvm/evm/stdlib/contract_test.go @@ -33,7 +33,7 @@ type testContractHandler struct { lastExecutedBlock func() *types.Block run func(tx []byte, coinbase types.Address) *types.ResultSummary generateResourceUUID func() uint64 - dryRun func(from types.Address, to *types.Address, gasLimit *types.GasLimit, value types.Balance, data []byte) *types.ResultSummary + dryRun func(tx []byte, from types.Address) *types.ResultSummary } var _ types.ContractHandler = &testContractHandler{} @@ -76,17 +76,11 @@ func (t *testContractHandler) Run(tx []byte, coinbase types.Address) *types.Resu return t.run(tx, coinbase) } -func (t *testContractHandler) DryRun( - from types.Address, - to *types.Address, - gasLimit *types.GasLimit, - value types.Balance, - data []byte, -) *types.ResultSummary { +func (t *testContractHandler) DryRun(tx []byte, from types.Address) *types.ResultSummary { if t.dryRun == nil { panic("unexpected DryRun") } - return t.dryRun(from, to, gasLimit, value, data) + return t.dryRun(tx, from) } func (t *testContractHandler) GenerateResourceUUID() uint64 { @@ -2903,18 +2897,19 @@ func TestEVMDryRun(t *testing.T) { t.Parallel() dryRunCalled := false + evmTx := cadence.NewArray([]cadence.Value{ + cadence.UInt8(1), + cadence.UInt8(2), + cadence.UInt8(3), + }).WithType(stdlib.EVMTransactionBytesCadenceType) contractsAddress := flow.BytesToAddress([]byte{0x1}) handler := &testContractHandler{ evmContractAddress: common.Address(contractsAddress), - dryRun: func(from types.Address, to *types.Address, gasLimit *types.GasLimit, value types.Balance, data []byte) *types.ResultSummary { + dryRun: func(tx []byte, from types.Address) *types.ResultSummary { dryRunCalled = true assert.Equal(t, types.Address{0, 1, 2, 3, 4, 5, 6, 7, 8, 9, 10, 11, 12, 13, 14, 15, 16, 17, 18, 19}, from) - assert.Nil(t, to) - require.NotNil(t, gasLimit) - assert.Equal(t, uint64(123), uint64(*gasLimit)) - assert.Equal(t, types.NewBalance(big.NewInt(1)), value) - assert.Equal(t, []byte{1, 3, 3, 7}, data) + assert.Equal(t, tx, []byte{1, 2, 3}) return &types.ResultSummary{ Status: types.StatusSuccessful, @@ -2973,20 +2968,18 @@ func TestEVMDryRun(t *testing.T) { import EVM from 0x1 access(all) - fun main(): EVM.Result { + fun main(tx: [UInt8]): EVM.Result { return EVM.dryRun( - from: [0, 1, 2, 3, 4, 5, 6, 7, 8, 9, 10, 11, 12, 13, 14, 15, 16, 17, 18, 19], // random address - to: nil, - gasLimit: 123, - value: EVM.Balance(attoflow: 1), - data: [1, 3, 3, 7] + tx: tx, + from: EVM.EVMAddress(bytes: [0, 1, 2, 3, 4, 5, 6, 7, 8, 9, 10, 11, 12, 13, 14, 15, 16, 17, 18, 19]), // random address ) } `) val, err := rt.ExecuteScript( runtime.Script{ - Source: script, + Source: script, + Arguments: EncodeArgs([]cadence.Value{evmTx}), }, runtime.Context{ Interface: runtimeInterface, From a781f9bc31913c71af70edc0e655839ad364232c Mon Sep 17 00:00:00 2001 From: Gregor G Date: Fri, 26 Apr 2024 14:41:16 +0200 Subject: [PATCH 32/35] remove not needed code --- fvm/evm/emulator/emulator.go | 20 -------------------- fvm/evm/types/call.go | 1 - 2 files changed, 21 deletions(-) diff --git a/fvm/evm/emulator/emulator.go b/fvm/evm/emulator/emulator.go index ebd3ae5a929..2fc385da08a 100644 --- a/fvm/evm/emulator/emulator.go +++ b/fvm/evm/emulator/emulator.go @@ -117,8 +117,6 @@ func (bl *BlockView) DirectCall(call *types.DirectCall) (*types.Result, error) { return proc.mintTo(call, txHash) case types.WithdrawCallSubType: return proc.withdrawFrom(call, txHash) - case types.DryRunSubType: - return proc.dryRun(call) case types.DeployCallSubType: if !call.EmptyToField() { return proc.deployAt(call.From, call.To, call.Data, call.GasLimit, call.Value, txHash) @@ -298,24 +296,6 @@ func (proc *procedure) withdrawFrom( return res, proc.commitAndFinalize() } -func (proc *procedure) dryRun(call *types.DirectCall) (*types.Result, error) { - - // run the procedure, hash and index don't matter since it won't be emitted anywhere - res, err := proc.run( - call.Message(), - gethCommon.Hash{}, - 0, - types.DryRunSubType, - ) - if err != nil { - return nil, err - } - // we never commit the state, but still can reset for sake of safety - proc.state.Reset() - - return res, nil -} - // deployAt deploys a contract at the given target address // behaviour should be similar to what evm.create internal method does with // a few differences, don't need to check for previous forks given this diff --git a/fvm/evm/types/call.go b/fvm/evm/types/call.go index 51c0200c475..c9a97bb2cf9 100644 --- a/fvm/evm/types/call.go +++ b/fvm/evm/types/call.go @@ -21,7 +21,6 @@ const ( TransferCallSubType = byte(3) DeployCallSubType = byte(4) ContractCallSubType = byte(5) - DryRunSubType = byte(6) // Note that these gas values might need to change if we // change the transaction (e.g. add accesslist), From fcb9f7d864e50b19997a130388f70ca540ea28fc Mon Sep 17 00:00:00 2001 From: Gregor G Date: Fri, 26 Apr 2024 14:48:12 +0200 Subject: [PATCH 33/35] fix merge conflicts --- fvm/evm/evm_test.go | 1 - fvm/evm/handler/handler_test.go | 5 +++-- fvm/evm/stdlib/contract.go | 2 +- fvm/evm/testutils/emulator.go | 14 +++++++------- 4 files changed, 11 insertions(+), 11 deletions(-) diff --git a/fvm/evm/evm_test.go b/fvm/evm/evm_test.go index ab26a1f4b8a..9008641e1be 100644 --- a/fvm/evm/evm_test.go +++ b/fvm/evm/evm_test.go @@ -3,7 +3,6 @@ package evm_test import ( "encoding/hex" "fmt" - gethTypes "github.com/onflow/go-ethereum/core/types" "math" "math/big" "testing" diff --git a/fvm/evm/handler/handler_test.go b/fvm/evm/handler/handler_test.go index b0478ccacff..460ce5e2ef0 100644 --- a/fvm/evm/handler/handler_test.go +++ b/fvm/evm/handler/handler_test.go @@ -1008,8 +1008,9 @@ func TestHandler_TransactionRun(t *testing.T) { rlpTx, err := tx.MarshalBinary() require.NoError(t, err) + addr := testutils.RandomAddress(t) result := &types.Result{ - DeployedContractAddress: testutils.RandomAddress(t), + DeployedContractAddress: &addr, ReturnedValue: testutils.RandomData(t), GasConsumed: testutils.RandomGas(1000), Logs: []*gethTypes.Log{ @@ -1032,7 +1033,7 @@ func TestHandler_TransactionRun(t *testing.T) { }, } - handler := handler.NewContractHandler(flow.Testnet, rootAddr, flowTokenAddress, bs, aa, backend, em) + handler := handler.NewContractHandler(flow.Testnet, rootAddr, flowTokenAddress, randomBeaconAddress, bs, aa, backend, em) rs := handler.DryRun(rlpTx, from) require.Equal(t, types.StatusSuccessful, rs.Status) diff --git a/fvm/evm/stdlib/contract.go b/fvm/evm/stdlib/contract.go index 18065624cc1..3884f122d90 100644 --- a/fvm/evm/stdlib/contract.go +++ b/fvm/evm/stdlib/contract.go @@ -63,7 +63,7 @@ var ( evmTransactionBytesType = sema.NewVariableSizedType(nil, sema.UInt8Type) evmTransactionsBatchBytesType = sema.NewVariableSizedType(nil, evmTransactionBytesType) evmAddressBytesType = sema.NewConstantSizedType(nil, sema.UInt8Type, types.AddressLength) - evmOptionalAddressBytesType = sema.NewOptionalType(nil, evmAddressBytesType) + evmOptionalAddressBytesType = sema.NewOptionalType(nil, evmAddressBytesType) evmAddressBytesStaticType = interpreter.ConvertSemaArrayTypeToStaticArrayType(nil, evmAddressBytesType) EVMAddressBytesCadenceType = cadence.NewConstantSizedArrayType(types.AddressLength, cadence.TheUInt8Type) ) diff --git a/fvm/evm/testutils/emulator.go b/fvm/evm/testutils/emulator.go index 1b9a209afb3..4d37e94dfdf 100644 --- a/fvm/evm/testutils/emulator.go +++ b/fvm/evm/testutils/emulator.go @@ -10,13 +10,13 @@ import ( ) type TestEmulator struct { - BalanceOfFunc func(address types.Address) (*big.Int, error) - NonceOfFunc func(address types.Address) (uint64, error) - CodeOfFunc func(address types.Address) (types.Code, error) - CodeHashOfFunc func(address types.Address) ([]byte, error) - DirectCallFunc func(call *types.DirectCall) (*types.Result, error) - RunTransactionFunc func(tx *gethTypes.Transaction) (*types.Result, error) - DryRunTransactionFunc func(tx *gethTypes.Transaction, address gethCommon.Address) (*types.Result, error) + BalanceOfFunc func(address types.Address) (*big.Int, error) + NonceOfFunc func(address types.Address) (uint64, error) + CodeOfFunc func(address types.Address) (types.Code, error) + CodeHashOfFunc func(address types.Address) ([]byte, error) + DirectCallFunc func(call *types.DirectCall) (*types.Result, error) + RunTransactionFunc func(tx *gethTypes.Transaction) (*types.Result, error) + DryRunTransactionFunc func(tx *gethTypes.Transaction, address gethCommon.Address) (*types.Result, error) BatchRunTransactionFunc func(txs []*gethTypes.Transaction) ([]*types.Result, error) } From 9c02972cea9eeceebeb4a46de1d07dabff280b18 Mon Sep 17 00:00:00 2001 From: Gregor G Date: Fri, 26 Apr 2024 15:14:39 +0200 Subject: [PATCH 34/35] fix issues with merge --- fvm/evm/handler/handler.go | 3 +++ fvm/evm/stdlib/contract.go | 1 - fvm/evm/testutils/emulator.go | 3 ++- 3 files changed, 5 insertions(+), 2 deletions(-) diff --git a/fvm/evm/handler/handler.go b/fvm/evm/handler/handler.go index 1e07171f088..7fcdeaecfce 100644 --- a/fvm/evm/handler/handler.go +++ b/fvm/evm/handler/handler.go @@ -365,6 +365,9 @@ func (h *ContractHandler) dryRun( } res, err := blk.DryRunTransaction(&tx, from.ToCommon()) + if err != nil { + return nil, err + } // saftey check for result if res == nil { diff --git a/fvm/evm/stdlib/contract.go b/fvm/evm/stdlib/contract.go index 3884f122d90..b96fe09a3e3 100644 --- a/fvm/evm/stdlib/contract.go +++ b/fvm/evm/stdlib/contract.go @@ -63,7 +63,6 @@ var ( evmTransactionBytesType = sema.NewVariableSizedType(nil, sema.UInt8Type) evmTransactionsBatchBytesType = sema.NewVariableSizedType(nil, evmTransactionBytesType) evmAddressBytesType = sema.NewConstantSizedType(nil, sema.UInt8Type, types.AddressLength) - evmOptionalAddressBytesType = sema.NewOptionalType(nil, evmAddressBytesType) evmAddressBytesStaticType = interpreter.ConvertSemaArrayTypeToStaticArrayType(nil, evmAddressBytesType) EVMAddressBytesCadenceType = cadence.NewConstantSizedArrayType(types.AddressLength, cadence.TheUInt8Type) ) diff --git a/fvm/evm/testutils/emulator.go b/fvm/evm/testutils/emulator.go index 4d37e94dfdf..244e240225b 100644 --- a/fvm/evm/testutils/emulator.go +++ b/fvm/evm/testutils/emulator.go @@ -1,9 +1,10 @@ package testutils import ( - gethCommon "github.com/onflow/go-ethereum/common" "math/big" + gethCommon "github.com/onflow/go-ethereum/common" + gethTypes "github.com/onflow/go-ethereum/core/types" "github.com/onflow/flow-go/fvm/evm/types" From 16e75d00eda27fbc7d2cb17573cf477fee805caf Mon Sep 17 00:00:00 2001 From: Gregor G Date: Fri, 26 Apr 2024 18:38:06 +0200 Subject: [PATCH 35/35] handle error --- fvm/evm/emulator/emulator.go | 13 ++++++++----- 1 file changed, 8 insertions(+), 5 deletions(-) diff --git a/fvm/evm/emulator/emulator.go b/fvm/evm/emulator/emulator.go index 3ef81c88121..03beb25f1eb 100644 --- a/fvm/evm/emulator/emulator.go +++ b/fvm/evm/emulator/emulator.go @@ -1,6 +1,7 @@ package emulator import ( + "errors" "math/big" "github.com/onflow/atree" @@ -215,13 +216,15 @@ func (bl *BlockView) DryRunTransaction( return nil, err } - // here we ignore the error because the only reason an error occurs - // is if the signature is invalid, which we don't care about since - // we use the from address as the signer - msg, _ := gethCore.TransactionToMessage( + msg, err := gethCore.TransactionToMessage( tx, GetSigner(bl.config), - proc.config.BlockContext.BaseFee) + proc.config.BlockContext.BaseFee, + ) + // we can ignore invalid signature errors since we don't expect signed transctions + if !errors.Is(err, gethTypes.ErrInvalidSig) { + return nil, err + } // use the from as the signer proc.evm.TxContext.Origin = from