Skip to content

Commit

Permalink
Merge pull request #6059 from m-Peter/evm-dry-run-compute-gas-refunds
Browse files Browse the repository at this point in the history
[EVM] Take into account gas refunds in `EVM.dryRun`
  • Loading branch information
sideninja authored Jun 12, 2024
2 parents da4f52c + 6a43039 commit 7a6d8a7
Show file tree
Hide file tree
Showing 3 changed files with 365 additions and 1 deletion.
13 changes: 12 additions & 1 deletion fvm/evm/emulator/emulator.go
Original file line number Diff line number Diff line change
Expand Up @@ -234,7 +234,17 @@ func (bl *BlockView) DryRunTransaction(
msg.SkipAccountChecks = true

// return without commiting the state
return proc.run(msg, tx.Hash(), 0, tx.Type())
txResult, err := proc.run(msg, tx.Hash(), 0, tx.Type())
if txResult.Successful() {
// Adding `gethParams.SstoreSentryGasEIP2200` is needed for this condition:
// https://github.com/onflow/go-ethereum/blob/master/core/vm/operations_acl.go#L29-L32
txResult.GasConsumed += gethParams.SstoreSentryGasEIP2200
// Take into account any gas refunds, which are calculated only after
// transaction execution.
txResult.GasConsumed += txResult.GasRefund
}

return txResult, err
}

func (bl *BlockView) newProcedure() (*procedure, error) {
Expand Down Expand Up @@ -522,6 +532,7 @@ func (proc *procedure) run(
// if prechecks are passed, the exec result won't be nil
if execResult != nil {
res.GasConsumed = execResult.UsedGas
res.GasRefund = proc.state.GetRefund()
res.Index = uint16(txIndex)
// we need to capture the returned value no matter the status
// if the tx is reverted the error message is returned as returned value
Expand Down
344 changes: 344 additions & 0 deletions fvm/evm/evm_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -12,6 +12,7 @@ import (

"github.com/onflow/cadence/encoding/ccf"
gethTypes "github.com/onflow/go-ethereum/core/types"
gethParams "github.com/onflow/go-ethereum/params"
"github.com/onflow/go-ethereum/rlp"
"github.com/stretchr/testify/assert"

Expand Down Expand Up @@ -1462,6 +1463,349 @@ func TestDryRun(t *testing.T) {
})
})

t.Run("test dry run store current value", 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(0))
tx := gethTypes.NewTransaction(
0,
testContract.DeployedAt.ToCommon(),
big.NewInt(0),
uint64(50_000),
big.NewInt(0),
data,
)
dryRunResult := dryRunTx(t, tx, ctx, vm, snapshot, testContract)

require.Equal(t, types.ErrCodeNoError, dryRunResult.ErrorCode)
require.Equal(t, types.StatusSuccessful, dryRunResult.Status)
require.Greater(t, dryRunResult.GasConsumed, uint64(0))

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(),
data,
big.NewInt(0),
dryRunResult.GasConsumed, // use the gas estimation from Evm.dryRun
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 that gas consumed from `EVM.dryRun` is bigger
// than the actual gas consumption of the equivalent
// `EVM.run`.
require.Equal(
t,
res.GasConsumed+gethParams.SstoreSentryGasEIP2200,
dryRunResult.GasConsumed,
)
})
})

t.Run("test dry run store new value", func(t *testing.T) {
RunWithNewEnvironment(t,
chain, func(
ctx fvm.Context,
vm fvm.VM,
snapshot snapshot.SnapshotTree,
testContract *TestContract,
testAccount *EOATestAccount,
) {
sc := systemcontracts.SystemContractsForChain(chain.ChainID())
code := []byte(fmt.Sprintf(
`
import EVM from %s
transaction(tx: [UInt8], coinbaseBytes: [UInt8; 20]){
prepare(account: &Account) {
let coinbase = EVM.EVMAddress(bytes: coinbaseBytes)
let res = EVM.run(tx: tx, coinbase: coinbase)
assert(res.status == EVM.Status.successful, message: "unexpected status")
assert(res.errorCode == 0, message: "unexpected error code")
}
}
`,
sc.EVMContract.Address.HexWithPrefix(),
))

num := int64(12)
innerTxBytes := testAccount.PrepareSignAndEncodeTx(t,
testContract.DeployedAt.ToCommon(),
testContract.MakeCallData(t, "store", big.NewInt(num)),
big.NewInt(0),
uint64(50_000),
big.NewInt(0),
)

innerTx := cadence.NewArray(
ConvertToCadence(innerTxBytes),
).WithType(stdlib.EVMTransactionBytesCadenceType)

coinbase := cadence.NewArray(
ConvertToCadence(testAccount.Address().Bytes()),
).WithType(stdlib.EVMAddressBytesCadenceType)

tx := fvm.Transaction(
flow.NewTransactionBody().
SetScript(code).
AddAuthorizer(sc.FlowServiceAccount.Address).
AddArgument(json.MustEncode(innerTx)).
AddArgument(json.MustEncode(coinbase)),
0)

_, output, err := vm.Run(
ctx,
tx,
snapshot,
)
require.NoError(t, err)
require.NoError(t, output.Err)

data := testContract.MakeCallData(t, "store", big.NewInt(100))
tx1 := gethTypes.NewTransaction(
0,
testContract.DeployedAt.ToCommon(),
big.NewInt(0),
uint64(50_000),
big.NewInt(0),
data,
)
dryRunResult := dryRunTx(t, tx1, ctx, vm, snapshot, testContract)

require.Equal(t, types.ErrCodeNoError, dryRunResult.ErrorCode)
require.Equal(t, types.StatusSuccessful, dryRunResult.Status)
require.Greater(t, dryRunResult.GasConsumed, uint64(0))

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,
))

// Decrease nonce because we are Cadence using scripts, and not
// transactions, which means that no state change is happening.
testAccount.SetNonce(testAccount.Nonce() - 1)
innerTxBytes = testAccount.PrepareSignAndEncodeTx(t,
testContract.DeployedAt.ToCommon(),
data,
big.NewInt(0),
dryRunResult.GasConsumed, // use the gas estimation from Evm.dryRun
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 that gas consumed from `EVM.dryRun` is bigger
// than the actual gas consumption of the equivalent
// `EVM.run`.
require.Equal(
t,
res.GasConsumed+gethParams.SstoreSentryGasEIP2200,
dryRunResult.GasConsumed,
)
})
})

t.Run("test dry run clear current value", func(t *testing.T) {
RunWithNewEnvironment(t,
chain, func(
ctx fvm.Context,
vm fvm.VM,
snapshot snapshot.SnapshotTree,
testContract *TestContract,
testAccount *EOATestAccount,
) {
sc := systemcontracts.SystemContractsForChain(chain.ChainID())
code := []byte(fmt.Sprintf(
`
import EVM from %s
transaction(tx: [UInt8], coinbaseBytes: [UInt8; 20]){
prepare(account: &Account) {
let coinbase = EVM.EVMAddress(bytes: coinbaseBytes)
let res = EVM.run(tx: tx, coinbase: coinbase)
assert(res.status == EVM.Status.successful, message: "unexpected status")
assert(res.errorCode == 0, message: "unexpected error code")
}
}
`,
sc.EVMContract.Address.HexWithPrefix(),
))

num := int64(100)
innerTxBytes := testAccount.PrepareSignAndEncodeTx(t,
testContract.DeployedAt.ToCommon(),
testContract.MakeCallData(t, "store", big.NewInt(num)),
big.NewInt(0),
uint64(50_000),
big.NewInt(0),
)

innerTx := cadence.NewArray(
ConvertToCadence(innerTxBytes),
).WithType(stdlib.EVMTransactionBytesCadenceType)

coinbase := cadence.NewArray(
ConvertToCadence(testAccount.Address().Bytes()),
).WithType(stdlib.EVMAddressBytesCadenceType)

tx := fvm.Transaction(
flow.NewTransactionBody().
SetScript(code).
AddAuthorizer(sc.FlowServiceAccount.Address).
AddArgument(json.MustEncode(innerTx)).
AddArgument(json.MustEncode(coinbase)),
0)

state, output, err := vm.Run(
ctx,
tx,
snapshot,
)
require.NoError(t, err)
require.NoError(t, output.Err)
snapshot = snapshot.Append(state)

data := testContract.MakeCallData(t, "store", big.NewInt(0))
tx1 := gethTypes.NewTransaction(
0,
testContract.DeployedAt.ToCommon(),
big.NewInt(0),
uint64(50_000),
big.NewInt(0),
data,
)
dryRunResult := dryRunTx(t, tx1, ctx, vm, snapshot, testContract)

require.Equal(t, types.ErrCodeNoError, dryRunResult.ErrorCode)
require.Equal(t, types.StatusSuccessful, dryRunResult.Status)
require.Greater(t, dryRunResult.GasConsumed, uint64(0))

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(),
data,
big.NewInt(0),
dryRunResult.GasConsumed, // use the gas estimation from Evm.dryRun
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 that gas consumed from `EVM.dryRun` is bigger
// than the actual gas consumption of the equivalent
// `EVM.run`.
require.Equal(
t,
res.GasConsumed+gethParams.SstoreSentryGasEIP2200+gethParams.SstoreClearsScheduleRefundEIP3529,
dryRunResult.GasConsumed,
)
})
})

// 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) {
Expand Down
Loading

0 comments on commit 7a6d8a7

Please sign in to comment.