Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

[EVM] Dry-run function #5749

Merged
merged 37 commits into from
Apr 29, 2024
Merged
Show file tree
Hide file tree
Changes from 36 commits
Commits
Show all changes
37 commits
Select commit Hold shift + click to select a range
0c6d690
estimate gas handler first draft
Apr 22, 2024
6a4261f
add method to the handler
Apr 22, 2024
f4339ce
change return type
Apr 22, 2024
0ba00cc
add simple estimate gas test
Apr 22, 2024
0702e41
add estimate gas to contract
Apr 22, 2024
220eba5
fix test
Apr 22, 2024
679cba0
change argument list
Apr 22, 2024
f940c1d
add direct call for estimation
Apr 22, 2024
a2720aa
change arguments
Apr 22, 2024
f884ed6
add estimate type
Apr 22, 2024
e666589
update test for estimate
Apr 22, 2024
f3ef516
wip
Apr 22, 2024
3a00218
change contract api
Apr 23, 2024
05ccbe4
change to dry run
Apr 24, 2024
88d6397
cadence change to dry run
Apr 24, 2024
db17a64
support dry run
Apr 24, 2024
b3588b0
update test for dry run
Apr 24, 2024
52a18b4
adding tests for dry-run
Apr 24, 2024
dfa5dfd
correctly handle optional arguments
Apr 24, 2024
cae19c4
add deploy dry-run
Apr 24, 2024
60913be
add contract test
Apr 24, 2024
1c8c5c3
add handler test
Apr 24, 2024
4ddf8a0
fix format
Apr 24, 2024
63dc22f
Merge branch 'master' into gregor/evm/gas-estimation
devbugging Apr 24, 2024
80710a1
fix comment
Apr 25, 2024
637f281
Add DryRunTransaction method to emulator
Apr 25, 2024
17759ad
Refactor ContractHandler dry run methods
Apr 25, 2024
5efe534
Refactor `dryRun` function in EVM contracts
Apr 25, 2024
318f479
Update handler test
Apr 25, 2024
f3fa2f6
update evm test with dry run changes
Apr 25, 2024
8221332
update test with dry run
Apr 25, 2024
3ac5f4d
fixed contract test with dry run
Apr 25, 2024
a781f9b
remove not needed code
Apr 26, 2024
6623f38
Merge branch 'master' into gregor/evm/gas-estimation
Apr 26, 2024
fcb9f7d
fix merge conflicts
Apr 26, 2024
9c02972
fix issues with merge
Apr 26, 2024
16e75d0
handle error
Apr 26, 2024
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
26 changes: 26 additions & 0 deletions fvm/evm/emulator/emulator.go
Original file line number Diff line number Diff line change
Expand Up @@ -205,6 +205,32 @@ 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
}

// 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 {
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 @@ -328,6 +328,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
12 changes: 12 additions & 0 deletions fvm/evm/stdlib/contract.cdc
Original file line number Diff line number Diff line change
Expand Up @@ -406,6 +406,18 @@ contract EVM {
return runResult
}

/// 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(tx: [UInt8], from: EVMAddress): Result {
return InternalEVM.dryRun(
tx: tx,
from: from.bytes,
) as! Result
}

/// Runs a batch of RLP-encoded EVM transactions, deducts the gas fees,
/// and deposits the gas fees into the provided coinbase address.
/// An invalid transaction is not executed and not included in the block.
Expand Down
Loading
Loading