Skip to content
This repository has been archived by the owner on Apr 4, 2024. It is now read-only.

use stack of contexts to implement snapshot revert #399

Merged
merged 7 commits into from
Aug 10, 2021
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
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
1 change: 1 addition & 0 deletions CHANGELOG.md
Original file line number Diff line number Diff line change
Expand Up @@ -49,6 +49,7 @@ Ref: https://keepachangelog.com/en/1.0.0/
* (evm) [tharsis#342](https://github.com/tharsis/ethermint/issues/342) Don't clear balance when resetting the account.
* (evm) [tharsis#334](https://github.com/tharsis/ethermint/pull/334) Log index changed to the index in block rather than
tx.
* (evm) [tharsis#399](https://github.com/tharsis/ethermint/pull/399) Exception in sub-message call reverts the call if it's not propagated.

### API Breaking

Expand Down
44 changes: 44 additions & 0 deletions tests/solidity/suites/exception/contracts/TestRevert.sol
Original file line number Diff line number Diff line change
@@ -0,0 +1,44 @@
// SPDX-License-Identifier: MIT
pragma solidity >=0.8.0;

contract State {
uint256 a = 0;
function set(uint256 input) public {
a = input;
require(a < 10);
}
function force_set(uint256 input) public {
a = input;
}
function query() public view returns(uint256) {
return a;
}
}

contract TestRevert {
State state;
uint256 b = 0;
uint256 c = 0;
constructor() {
state = new State();
}
function try_set(uint256 input) public {
b = input;
try state.set(input) {
} catch (bytes memory) {
}
c = input;
}
function set(uint256 input) public {
state.force_set(input);
}
function query_a() public view returns(uint256) {
return state.query();
}
function query_b() public view returns(uint256) {
return b;
}
function query_c() public view returns(uint256) {
return c;
}
}
19 changes: 19 additions & 0 deletions tests/solidity/suites/exception/contracts/test/Migrations.sol
Original file line number Diff line number Diff line change
@@ -0,0 +1,19 @@
// SPDX-License-Identifier: MIT
pragma solidity >=0.8.0;

contract Migrations {
address public owner = msg.sender;
uint public last_completed_migration;

modifier restricted() {
require(
msg.sender == owner,
"This function is restricted to the contract's owner"
);
_;
}

function setCompleted(uint completed) public restricted {
last_completed_migration = completed;
}
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,5 @@
const Migrations = artifacts.require("Migrations");

module.exports = function (deployer) {
deployer.deploy(Migrations);
};
15 changes: 15 additions & 0 deletions tests/solidity/suites/exception/package.json
Original file line number Diff line number Diff line change
@@ -0,0 +1,15 @@
{
"name": "exception",
"version": "1.0.0",
"author": "huangyi <[email protected]>",
"license": "GPL-3.0-or-later",
"scripts": {
"test-ganache": "yarn truffle test",
"test-ethermint": "yarn truffle test --network ethermint"
},
"devDependencies": {
"truffle": "^5.1.42",
"truffle-assertions": "^0.9.2",
"web3": "^1.2.11"
}
}
Empty file.
35 changes: 35 additions & 0 deletions tests/solidity/suites/exception/test/revert.js
Original file line number Diff line number Diff line change
@@ -0,0 +1,35 @@
const TestRevert = artifacts.require("TestRevert")
const truffleAssert = require('truffle-assertions');

async function expectRevert(promise) {
try {
await promise;
} catch (error) {
if (error.message.indexOf('revert') === -1) {
expect('revert').to.equal(error.message, 'Wrong kind of exception received');
}
return;
}
expect.fail('Expected an exception but none was received');
}

contract('TestRevert', (accounts) => {
let revert

beforeEach(async () => {
revert = await TestRevert.new()
})
it('should revert', async () => {
await revert.try_set(10)
no = await revert.query_a()
assert.equal(no, '0', 'The modification on a should be reverted')
no = await revert.query_b()
assert.equal(no, '10', 'The modification on b should not be reverted')
no = await revert.query_c()
assert.equal(no, '10', 'The modification on c should not be reverted')

await revert.set(10)
no = await revert.query_a()
assert.equal(no, '10', 'The force set should not be reverted')
})
})
17 changes: 17 additions & 0 deletions tests/solidity/suites/exception/truffle-config.js
Original file line number Diff line number Diff line change
@@ -0,0 +1,17 @@
module.exports = {
networks: {
// Development network is just left as truffle's default settings
ethermint: {
host: "127.0.0.1", // Localhost (default: none)
port: 8545, // Standard Ethereum port (default: none)
network_id: "*", // Any network (default: none)
gas: 5000000, // Gas sent with each transaction
gasPrice: 1000000000, // 1 gwei (in wei)
},
},
compilers: {
solc: {
version: "0.8.6",
},
},
}
87 changes: 87 additions & 0 deletions x/evm/keeper/context_stack.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,87 @@
package keeper

import (
"fmt"

sdk "github.com/cosmos/cosmos-sdk/types"
)

// cachedContext is a pair of cache context and its corresponding commit method.
// They are obtained from the return value of `context.CacheContext()`.
type cachedContext struct {
fedekunze marked this conversation as resolved.
Show resolved Hide resolved
ctx sdk.Context
commit func()
}

// ContextStack manages the initial context and a stack of cached contexts,
// to support the `StateDB.Snapshot` and `StateDB.RevertToSnapshot` methods.
type ContextStack struct {
// Context of the initial state before transaction execution.
// It's the context used by `StateDB.CommitedState`.
initialCtx sdk.Context
cachedContexts []cachedContext
}

// CurrentContext returns the top context of cached stack,
// if the stack is empty, returns the initial context.
func (cs *ContextStack) CurrentContext() sdk.Context {
l := len(cs.cachedContexts)
if l == 0 {
return cs.initialCtx
}
return cs.cachedContexts[l-1].ctx
}

// Reset sets the initial context and clear the cache context stack.
func (cs *ContextStack) Reset(ctx sdk.Context) {
cs.initialCtx = ctx
if len(cs.cachedContexts) > 0 {
cs.cachedContexts = []cachedContext{}
}
}

// IsEmpty returns true if the cache context stack is empty.
func (cs *ContextStack) IsEmpty() bool {
return len(cs.cachedContexts) == 0
}

// Commit commits all the cached contexts from top to bottom in order and clears the stack by setting an empty slice of cache contexts.
func (cs *ContextStack) Commit() {
// commit in order from top to bottom
for i := len(cs.cachedContexts) - 1; i >= 0; i-- {
// keep all the cosmos events
cs.initialCtx.EventManager().EmitEvents(cs.cachedContexts[i].ctx.EventManager().Events())
if cs.cachedContexts[i].commit == nil {
panic(fmt.Sprintf("commit function at index %d should not be nil", i))
} else {
cs.cachedContexts[i].commit()
}
}
cs.cachedContexts = []cachedContext{}
}

// Snapshot pushes a new cached context to the stack,
// and returns the index of it.
func (cs *ContextStack) Snapshot() int {
i := len(cs.cachedContexts)
ctx, commit := cs.CurrentContext().CacheContext()
cs.cachedContexts = append(cs.cachedContexts, cachedContext{ctx: ctx, commit: commit})
return i
}

// RevertToSnapshot pops all the cached contexts after the target index (inclusive).
// the target should be snapshot index returned by `Snapshot`.
// This function panics if the index is out of bounds.
func (cs *ContextStack) RevertToSnapshot(target int) {
if target < 0 || target >= len(cs.cachedContexts) {
panic(fmt.Errorf("snapshot index %d out of bound [%d..%d)", target, 0, len(cs.cachedContexts)))
}
cs.cachedContexts = cs.cachedContexts[:target]
}

// RevertAll discards all the cache contexts.
func (cs *ContextStack) RevertAll() {
if len(cs.cachedContexts) > 0 {
cs.RevertToSnapshot(0)
}
}
9 changes: 5 additions & 4 deletions x/evm/keeper/grpc_query.go
Original file line number Diff line number Diff line change
Expand Up @@ -265,7 +265,7 @@ func (k Keeper) BlockBloom(c context.Context, req *types.QueryBlockBloomRequest)
bloom, found := k.GetBlockBloom(ctx, req.Height)
if !found {
// if the bloom is not found, query the transient store at the current height
k.ctx = ctx
k.WithContext(ctx)
bloomInt := k.GetBlockBloomTransient()

if bloomInt.Sign() == 0 {
Expand Down Expand Up @@ -381,6 +381,7 @@ func (k Keeper) EthCall(c context.Context, req *types.EthCallRequest) (*types.Ms
evm := k.NewEVM(msg, ethCfg, params, coinbase)
// pass true means execute in query mode, which don't do actual gas refund.
res, err := k.ApplyMessage(evm, msg, ethCfg, true)
k.ctxStack.RevertAll()
if err != nil {
return nil, status.Error(codes.Internal, err.Error())
}
Expand Down Expand Up @@ -443,15 +444,15 @@ func (k Keeper) EstimateGas(c context.Context, req *types.EthCallRequest) (*type
executable := func(gas uint64) (bool, *types.MsgEthereumTxResponse, error) {
args.Gas = (*hexutil.Uint64)(&gas)

// Execute the call in an isolated context
k.BeginCachedContext()
// Reset to the initial context
k.WithContext(ctx)

msg := args.ToMessage(req.GasCap)
evm := k.NewEVM(msg, ethCfg, params, coinbase)
// pass true means execute in query mode, which don't do actual gas refund.
rsp, err := k.ApplyMessage(evm, msg, ethCfg, true)

k.EndCachedContext()
k.ctxStack.RevertAll()

if err != nil {
if errors.Is(stacktrace.RootCause(err), core.ErrIntrinsicGas) {
Expand Down
Loading