evm: use clean context for GetCommittedState (#383)

* keep the original context for GetCommittedState api

* fix method mutation

* keep estimateGas consistant

* added test after the original context is recovered

* add integration test for the gas consumption of sstore

* test the committed case

* move methods to keeper module

Co-authored-by: Federico Kunze Küllmer <31522760+fedekunze@users.noreply.github.com>
This commit is contained in:
yihuang 2021-08-04 15:49:02 +08:00 committed by GitHub
parent f7bcc8d12e
commit 9bf3659718
No known key found for this signature in database
GPG Key ID: 4AEE18F83AFDEB23
7 changed files with 112 additions and 15 deletions

View File

@ -0,0 +1,29 @@
// SPDX-License-Identifier: GPL-3.0
pragma solidity ^0.5.11;
/**
* @title Storage
* @dev Store & retrieve value in a variable
*/
contract Storage {
uint256 number;
/**
* @dev Store value in variable
* @param num value to store
*/
function store(uint256 num) public {
number = num + 1;
number = num;
}
/**
* @dev Return value
* @return value of 'number'
*/
function retrieve() public view returns (uint256){
return number;
}
}

View File

@ -0,0 +1,22 @@
const Storage = artifacts.require("Storage")
contract('Storage', (accounts) => {
let storage
beforeEach(async () => {
storage = await Storage.new()
})
it('estimated gas should match', async () => {
// set new value
let gasUsage = await storage.store.estimateGas(10);
expect(gasUsage.toString()).to.equal('43754');
await storage.store(10);
// set existing value
gasUsage = await storage.store.estimateGas(10);
expect(gasUsage.toString()).to.equal('28754');
})
})

View File

@ -444,15 +444,14 @@ func (k Keeper) EstimateGas(c context.Context, req *types.EthCallRequest) (*type
args.Gas = (*hexutil.Uint64)(&gas) args.Gas = (*hexutil.Uint64)(&gas)
// Execute the call in an isolated context // Execute the call in an isolated context
sandboxCtx, _ := ctx.CacheContext() k.BeginCachedContext()
k.WithContext(sandboxCtx)
msg := args.ToMessage(req.GasCap) msg := args.ToMessage(req.GasCap)
evm := k.NewEVM(msg, ethCfg, params, coinbase) evm := k.NewEVM(msg, ethCfg, params, coinbase)
// pass true means execute in query mode, which don't do actual gas refund. // pass true means execute in query mode, which don't do actual gas refund.
rsp, err := k.ApplyMessage(evm, msg, ethCfg, true) rsp, err := k.ApplyMessage(evm, msg, ethCfg, true)
k.WithContext(ctx) k.EndCachedContext()
if err != nil { if err != nil {
if errors.Is(stacktrace.RootCause(err), core.ErrIntrinsicGas) { if errors.Is(stacktrace.RootCause(err), core.ErrIntrinsicGas) {

View File

@ -38,6 +38,8 @@ type Keeper struct {
stakingKeeper types.StakingKeeper stakingKeeper types.StakingKeeper
ctx sdk.Context ctx sdk.Context
// set in `BeginCachedContext`, used by `GetCommittedState` api.
committedCtx sdk.Context
// chain ID number obtained from the context's chain id // chain ID number obtained from the context's chain id
eip155ChainID *big.Int eip155ChainID *big.Int
@ -75,6 +77,11 @@ func NewKeeper(
} }
} }
// CommittedCtx returns the committed context
func (k Keeper) CommittedCtx() sdk.Context {
return k.committedCtx
}
// Logger returns a module-specific logger. // Logger returns a module-specific logger.
func (k Keeper) Logger(ctx sdk.Context) log.Logger { func (k Keeper) Logger(ctx sdk.Context) log.Logger {
return ctx.Logger().With("module", types.ModuleName) return ctx.Logger().With("module", types.ModuleName)
@ -83,6 +90,7 @@ func (k Keeper) Logger(ctx sdk.Context) log.Logger {
// WithContext sets an updated SDK context to the keeper // WithContext sets an updated SDK context to the keeper
func (k *Keeper) WithContext(ctx sdk.Context) { func (k *Keeper) WithContext(ctx sdk.Context) {
k.ctx = ctx k.ctx = ctx
k.committedCtx = ctx
} }
// WithChainID sets the chain id to the local variable in the keeper // WithChainID sets the chain id to the local variable in the keeper
@ -341,3 +349,15 @@ func (k Keeper) ResetAccount(addr common.Address) {
k.DeleteCode(addr) k.DeleteCode(addr)
k.DeleteAccountStorage(addr) k.DeleteAccountStorage(addr)
} }
// BeginCachedContext create the cached context
func (k *Keeper) BeginCachedContext() (commit func()) {
k.committedCtx = k.ctx
k.ctx, commit = k.ctx.CacheContext()
return
}
// EndCachedContext recover the committed context
func (k *Keeper) EndCachedContext() {
k.ctx = k.committedCtx
}

View File

@ -137,11 +137,8 @@ func (k *Keeper) ApplyTransaction(tx *ethtypes.Transaction) (*types.MsgEthereumT
return nil, stacktrace.Propagate(err, "failed to return ethereum transaction as core message") return nil, stacktrace.Propagate(err, "failed to return ethereum transaction as core message")
} }
// create an ethereum StateTransition instance and run TransitionDb // we use a cached context to avoid modifying to state in case EVM msg is reverted
// we use a ctx context to avoid modifying to state in case EVM msg is reverted commit := k.BeginCachedContext()
originalCtx := k.ctx
cacheCtx, commit := k.ctx.CacheContext()
k.ctx = cacheCtx
// get the coinbase address from the block proposer // get the coinbase address from the block proposer
coinbase, err := k.GetCoinbaseAddress() coinbase, err := k.GetCoinbaseAddress()
@ -149,6 +146,7 @@ func (k *Keeper) ApplyTransaction(tx *ethtypes.Transaction) (*types.MsgEthereumT
return nil, stacktrace.Propagate(err, "failed to obtain coinbase address") return nil, stacktrace.Propagate(err, "failed to obtain coinbase address")
} }
// create an ethereum EVM instance and run the message
evm := k.NewEVM(msg, ethCfg, params, coinbase) evm := k.NewEVM(msg, ethCfg, params, coinbase)
k.SetTxHashTransient(tx.Hash()) k.SetTxHashTransient(tx.Hash())
@ -163,11 +161,12 @@ func (k *Keeper) ApplyTransaction(tx *ethtypes.Transaction) (*types.MsgEthereumT
res.Hash = txHash.Hex() res.Hash = txHash.Hex()
logs := k.GetTxLogs(txHash) logs := k.GetTxLogs(txHash)
// Commit and switch to original context // Commit and switch to committed context
if !res.Failed() { if !res.Failed() {
commit() commit()
} }
k.ctx = originalCtx
k.EndCachedContext()
// Logs needs to be ignored when tx is reverted // Logs needs to be ignored when tx is reverted
// Set the log and bloom filter only when the tx is NOT REVERTED // Set the log and bloom filter only when the tx is NOT REVERTED

View File

@ -352,10 +352,8 @@ func (k *Keeper) GetRefund() uint64 {
// State // State
// ---------------------------------------------------------------------------- // ----------------------------------------------------------------------------
// GetCommittedState returns the value set in store for the given key hash. If the key is not registered func doGetState(ctx sdk.Context, storeKey sdk.StoreKey, addr common.Address, hash common.Hash) common.Hash {
// this function returns the empty hash. store := prefix.NewStore(ctx.KVStore(storeKey), types.AddressStoragePrefix(addr))
func (k *Keeper) GetCommittedState(addr common.Address, hash common.Hash) common.Hash {
store := prefix.NewStore(k.ctx.KVStore(k.storeKey), types.AddressStoragePrefix(addr))
key := types.KeyAddressStorage(addr, hash) key := types.KeyAddressStorage(addr, hash)
value := store.Get(key.Bytes()) value := store.Get(key.Bytes())
@ -366,10 +364,16 @@ func (k *Keeper) GetCommittedState(addr common.Address, hash common.Hash) common
return common.BytesToHash(value) return common.BytesToHash(value)
} }
// GetCommittedState returns the value set in store for the given key hash. If the key is not registered
// this function returns the empty hash.
func (k *Keeper) GetCommittedState(addr common.Address, hash common.Hash) common.Hash {
return doGetState(k.committedCtx, k.storeKey, addr, hash)
}
// GetState returns the committed state for the given key hash, as all changes are committed directly // GetState returns the committed state for the given key hash, as all changes are committed directly
// to the KVStore. // to the KVStore.
func (k *Keeper) GetState(addr common.Address, hash common.Hash) common.Hash { func (k *Keeper) GetState(addr common.Address, hash common.Hash) common.Hash {
return k.GetCommittedState(addr, hash) return doGetState(k.ctx, k.storeKey, addr, hash)
} }
// SetState sets the given hashes (key, value) to the KVStore. If the value hash is empty, this // SetState sets the given hashes (key, value) to the KVStore. If the value hash is empty, this

View File

@ -383,6 +383,30 @@ func (suite *KeeperTestSuite) TestState() {
} }
} }
func (suite *KeeperTestSuite) TestCommittedState() {
suite.SetupTest()
var key = common.BytesToHash([]byte("key"))
var value1 = common.BytesToHash([]byte("value1"))
var value2 = common.BytesToHash([]byte("value2"))
suite.app.EvmKeeper.SetState(suite.address, key, value1)
commit := suite.app.EvmKeeper.BeginCachedContext()
suite.app.EvmKeeper.SetState(suite.address, key, value2)
tmp := suite.app.EvmKeeper.GetState(suite.address, key)
suite.Require().Equal(value2, tmp)
tmp = suite.app.EvmKeeper.GetCommittedState(suite.address, key)
suite.Require().Equal(value1, tmp)
commit()
suite.app.EvmKeeper.EndCachedContext()
tmp = suite.app.EvmKeeper.GetCommittedState(suite.address, key)
suite.Require().Equal(value2, tmp)
}
func (suite *KeeperTestSuite) TestSuicide() { func (suite *KeeperTestSuite) TestSuicide() {
testCases := []struct { testCases := []struct {
name string name string