* adr 001: state * additional notes * more updates * context * state transition
12 KiB
ADR 001: State
Changelog
- 2021-05-15: first draft
Status
DRAFT, Not Implemented
Abstract
The current ADR proposes a state machine breaking change to the EVM module state operations
(Keeper
, StateDB
and StateTransition
) with the goal of reducing code maintainance, increase
performance, and document all the transaction and state cycles and flows.
Context
This ADR addresses the issues of 3 different components of the EVM state: the StateDB
interface,
the live stateObject
accounts, and the StateTransition
functionality. These issues are outlined
below in the section for each corresponding component:
StateDB
In order to execute state transitions, the EVM receives a reference to a database interface to
perform CRUD operations on accounts, balances, code and state storage, among other state queries.
This database interface is defined by go-ethereum's vm.StateDB
, which is currently implemented
using the CommitStateDB
concrete type.
The CommitStateDB
performs state updates by having a direct access to the sdk.Context
, the evm's
sdk.StoreKey
and external Keepers
for account and balances. Currently, the context field needs
to be set on every block or state transition using WithContext(ctx)
in order to pass the updated
block and transaction data to the CommitStateDB
.
However, traditionally in Cosmos SDK-based chains, the Keeper
type has been the de-facto abstraction
that manages access the key-value store (KVStore
) owned by the module through the store key.
Keepers
usually hold a reference to external module Keepers
to perform functionality outside of
the scope of their module.
In the existing architecture of the EVM module, both CommitStateDB
and Keeper
have access to
state.
State Objects
The CommitStateDB
also holds references of stateObjects
, defined as "live ethereum consensus
accounts (i.e any balance, account nonces or storage) which will get modified while processing a
state transition".
Upon a state transition, these objects will be modified and marked as 'dirty' (a.k.a stateless
update) on the CommitStateDB
. Then, at every EndBlock
, the state of these modified objects will
be 'finalized' and commited to the store, resetting all the dirty list of objects.
The core issue arises when a chain that uses the EVM module can have also have their account and
balances updated through operations from other modules. This means that an EVM state object can be
modified through an EVM transaction (evm.MsgEthereumTx
) and other transactions like bank.MsgSend
or ibctransfer.MsgTransfer
. This can lead to unexpected behaviors like state overwrites, due to
the current behaviour that caches the dirty state on the EVM instead of commiting any changes
directly.
State Transition
A general EVM state transition is performed by calling the ethereum vm.EVM
Create
or Call
functions, depending on wheather the transaction creates a contract or performs a transfer or call to a given contract.
In the case of the x/evm
module, it currently uses a modified version of Geth's TransitionDB
, that wraps these two vm.EVM
methods. The reason for using this modified function, is due to several reasons:
- The use of
sdk.Msg
(MsgEthereumTx
) instead of the ethereumcore.Message
type for thevm.EVM
functions, preventing the direct use of thecore.ApplyMessage
. - The use of custom gas accounting through the transaction
GasMeter
available on thesdk.Context
to consume the same amount of gas as on Ethereum. - Simulate logic via ABCI
CheckTx
, that prevents the state from being finalized.
Decision
StateDB
The CommitStateDB
type will be removed in favor turning the module's Keeper
into a StateDB
concrete implementation.
// Keeper now fully implements the StateDB interface
var _ vm.StateDB = (*Keeper)(nil)
// Keeper defines the EVM module state keeper for CRUD operations.
// It also implements the go-ethereum vm.StateDB interface. Instead of using
// a trie and database for querying and persistence, the Keeper uses KVStores
// and external Keepers to facilitate state transitions for accounts and balance
// accounting.
type Keeper struct {
// store key and encoding codec
// external module keepers (account, bank, etc) and params subspace
// cache fields and sdk.Context (reset every block)
// other CommitStateDB fields (journal, accessList, etc)
}
This means that a Keeper
pointer will now directly be passed to the vm.EVM
for accessing the state and performing state transitions.
The ABCI BeginBlock
and EndBlock
are have now been refactored to only (1) reset cached fields, and (2) keep track of internal mappings (hashes, height, etc).
func (k *Keeper) BeginBlock(ctx sdk.Context, req abci.RequestBeginBlock) {
// ...
// reset cache values and context
k.ResetCacheFields(ctx)
}
func (k Keeper) EndBlock(ctx sdk.Context, req abci.RequestEndBlock) []abci.ValidatorUpdate {
// NOTE: UpdateAccounts, Commit and Reset execution steps have been removed in favor of directly
// updating the state.
// set the block bloom filter bytes to store
bloom := ethtypes.BytesToBloom(k.Bloom.Bytes())
k.SetBlockBloom(ctx, req.Height, bloom)
return []abci.ValidatorUpdate{}
}
State Objects
The stateObject
type will be completely removed in favor of updating the store directly through
the use of the auth AccountKeeper
and the bank Keeper
. For the storage State
and Code
, the
evm module Keeper
will store these values directly on the KVStore using the EVM module store key
and corresponding prefix keys.
For accounts marked as 'suicided', a new relationship will be added to the Keeper
to map Address (bytes) -> suicided (bool)
.
// HasSuicided implements the vm.StoreDB interface
func (k Keeper) HasSuicided(address common.Address) bool {
store := prefix.NewStore(k.ctx.KVStore(csdb.storeKey), KeyPrefixSuicide)
key := types.KeySuicide(address.Bytes())
return store.Has(key)
}
// Suicide implements the vm.StoreDB interface
func (k Keeper) Suicide(address common.Address) bool {
store := prefix.NewStore(k.ctx.KVStore(csdb.storeKey), KeyPrefixSuicide)
key := types.KeySuicide(address.Bytes())
store.Set(key, []byte{0x1})
return true
}
State Transition
The state transition logic will be refactored to use the ApplyMessage
function from the core/
package of go-ethereum as the backbone. This method calls creates a go-ethereum StateTransition
instance and, as it name implies, applies a Ethereum message to execute it and update the state.
This ApplyMessage
call will be wrapped in the Keeper
's TransitionDb
function, which will
generate the required arguments for this call (EVM, chain config, and gas pool), thus performing the
same gas accounting as before.
This will lead to the switching from the existing Ethermint's evm StateTransition
type to the
go-ethereum vm.ApplyMessage
type, thus reducing code necessary perform a state transition.
func (k *Keeper) TransitionDb(ctx sdk.Context, msg core.Message) (*types.ExecutionResult, error) {
defer telemetry.ModuleMeasureSince(types.ModuleName, time.Now(), types.MetricKeyTransitionDB)
initialGasMeter := ctx.GasMeter()
// NOTE: Since CRUD operations on the SDK store consume gasm we need to set up an infinite gas meter so that we only consume
// the gas used by the Ethereum message execution.
// Not setting the infinite gas meter here would mean that we are incurring in additional gas costs
k.ctx = ctx.WithGasMeter(sdk.NewInfiniteGasMeter())
params := k.GetParams(ctx)
cfg, found := k.GetChainConfig(ctx)
if !found {
// error
}
evm := k.NewEVM(msg, cfg.EthereumConfig(chainID))
gasPool := &core.GasPool(ctx.BlockGasMeter().Limit()) // available gas left in the block for the tx execution
// create an ethereum StateTransition instance and run TransitionDb
result, err := core.ApplyMessage(evm, msg, gasPool)
// return precheck errors (nonce, signature, balance and gas)
// NOTE: these should be checked previously on the AnteHandler
if err != nil {
// log error
return err
}
// The gas used on the state transition will
// be returned in the execution result so we need to deduct it from the transaction (?) GasMeter // TODO: double-check
initialGasMeter.ConsumeGas(resp.UsedGas, "evm state transition")
// set the gas meter to current_gas = initial_gas - used_gas
k.ctx = k.ctx.WithGasMeter(initialGasMeter)
// return the VM Execution error (see go-ethereum/core/vm/errors.go)
if result.Err != nil {
// log error
return result.Err
}
// return logs
executionRes := &ExecutionResult{
Response: &MsgEthereumTxResponse{
Ret: result.ret,
},
GasInfo: GasInfo{
GasConsumed: result.UsedGas,
GasLimit: gasPool,
}
return executionRes, nil
}
The EVM is created then as follows:
func (k *Keeper) NewEVM(msg core.Message, config *params.ChainConfig) *vm.EVM {
blockCtx := vm.BlockContext{
CanTransfer: core.CanTransfer,
Transfer: core.Transfer,
GetHash: k.GetHashFn(),
Coinbase: common.Address{}, // there's no beneficiary since we're not mining
BlockNumber: big.NewInt(k.ctx.BlockHeight()),
Time: big.NewInt(k.ctx.BlockHeader().Time.Unix()),
Difficulty: big.NewInt(0), // unused. Only required in PoW context
GasLimit: gasLimit,
}
txCtx := core.NewEVMTxContext(msg)
vmConfig := k.VMConfig(st.Debug)
return vm.NewEVM(blockCtx, txCtx, k, config, vmConfig)
}
func (k Keeper) VMConfig(debug bool) vm.Config{
params := k.GetParams(ctx)
eips := make([]int, len(params.ExtraEIPs))
for i, eip := range params.ExtraEIPs {
eips[i] = int(eip)
}
return vm.Config{
ExtraEips: eips,
Tracer: vm.NewJSONLogger(&vm.LogConfig{Debug: debug}, os.Stderr),
Debug: debug,
}
}
Consequences
Backwards Compatibility
The proposed ADR is a breaking state machine change and will not have any backwards compatibility since no chain that uses this code is in a production ready-state (at the moment of writing).
Positive
- Improve maintenance by simplifying the state transition logic
- Defines a single option for accessing the store through the
Keeper
, thus removing theCommitStateDB
type. - State operations and tests are now all located in the
evm/keeper/
package - Removes the concept of
stateObject
by commiting to the store directly - Delete operations on
EndBlock
for updating and commiting dirty state objects. - Split the state transition functionality (
NewEVM
fromTransitionDb
) allows to further modularize certain components that can be beneficial for customization (eg: using other EVMs other than Geth's)
Negative
- Increases the dependency of external packages (eg:
go-ethereum
) - Some state changes will have to be kept in store (eg: suicide state)
Neutral
- Some of the fields from the
CommitStateDB
will have to be added to theKeeper