Skip to content

Commit

Permalink
Merge pull request #5749 from onflow/gregor/evm/gas-estimation
Browse files Browse the repository at this point in the history
[EVM] Dry-run function
  • Loading branch information
devbugging authored Apr 29, 2024
2 parents ed9f53f + 16e75d0 commit c20f91f
Show file tree
Hide file tree
Showing 10 changed files with 560 additions and 0 deletions.
29 changes: 29 additions & 0 deletions fvm/evm/emulator/emulator.go
Original file line number Diff line number Diff line change
@@ -1,6 +1,7 @@
package emulator

import (
"errors"
"math/big"

"github.com/onflow/atree"
Expand Down Expand Up @@ -205,6 +206,34 @@ func (bl *BlockView) BatchRunTransactions(txs []*gethTypes.Transaction) ([]*type
return batchResults, nil
}

// 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
}

msg, err := gethCore.TransactionToMessage(
tx,
GetSigner(bl.config),
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
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 {
Expand Down
199 changes: 199 additions & 0 deletions fvm/evm/evm_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -3,6 +3,7 @@ package evm_test
import (
"encoding/hex"
"fmt"
"math"
"math/big"
"testing"

Expand Down Expand Up @@ -1262,6 +1263,204 @@ func TestCadenceOwnedAccountFunctionalities(t *testing.T) {
})
}

func TestDryRun(t *testing.T) {
t.Parallel()
chain := flow.Emulator.Chain()
sc := systemcontracts.SystemContractsForChain(chain.ChainID())
evmAddress := sc.EVMContract.Address.HexWithPrefix()

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])
)
}`,
evmAddress,
))

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) {
RunWithNewEnvironment(t,
chain, func(
ctx fvm.Context,
vm fvm.VM,
snapshot snapshot.SnapshotTree,
testContract *TestContract,
testAccount *EOATestAccount,
) {
data := testContract.MakeCallData(t, "store", big.NewInt(1337))

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)
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!!!
})
})

// 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) {
RunWithNewEnvironment(t,
chain, func(
ctx fvm.Context,
vm fvm.VM,
snapshot snapshot.SnapshotTree,
testContract *TestContract,
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 := 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))

// 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)
}
`,
evmAddress,
))

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())
})
})

t.Run("test dry run contract deployment", func(t *testing.T) {
RunWithNewEnvironment(t,
chain, func(
ctx fvm.Context,
vm fvm.VM,
snapshot snapshot.SnapshotTree,
testContract *TestContract,
testAccount *EOATestAccount,
) {
tx := gethTypes.NewContractCreation(
0,
big.NewInt(0),
uint64(1000000),
big.NewInt(0),
testContract.ByteCode,
)

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.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) {
t.Parallel()

Expand Down
54 changes: 54 additions & 0 deletions fvm/evm/handler/handler.go
Original file line number Diff line number Diff line change
Expand Up @@ -322,6 +322,60 @@ func (h *ContractHandler) run(
return res, nil
}

func (h *ContractHandler) DryRun(
rlpEncodedTx []byte,
from types.Address,
) *types.ResultSummary {
res, err := h.dryRun(rlpEncodedTx, from)
panicOnError(err)
return res.ResultSummary()
}

func (h *ContractHandler) dryRun(
rlpEncodedTx []byte,
from types.Address,
) (*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.UnmarshalBinary(rlpEncodedTx)
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.DryRunTransaction(&tx, from.ToCommon())
if err != nil {
return nil, err
}

// saftey check for result
if res == nil {
return nil, types.ErrUnexpectedEmptyResult
}

// if invalid return the invalid error
if res.Invalid() {
return nil, res.ValidationError
}

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)) {
Expand Down
66 changes: 66 additions & 0 deletions fvm/evm/handler/handler_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -978,6 +978,72 @@ func TestHandler_TransactionRun(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()

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)

addr := testutils.RandomAddress(t)
result := &types.Result{
DeployedContractAddress: &addr,
ReturnedValue: testutils.RandomData(t),
GasConsumed: testutils.RandomGas(1000),
Logs: []*gethTypes.Log{
testutils.GetRandomLogFixture(t),
testutils.GetRandomLogFixture(t),
},
}

called := false
em := &testutils.TestEmulator{
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
return result, nil
},
}

handler := handler.NewContractHandler(flow.Testnet, rootAddr, flowTokenAddress, randomBeaconAddress, bs, aa, backend, em)

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)
require.True(t, called)
})
})
})
})
}

// returns true if error passes the checks
Expand Down
Loading

0 comments on commit c20f91f

Please sign in to comment.