diff --git a/app/keepers.go b/app/keepers.go index 6695a6194..be75b2357 100644 --- a/app/keepers.go +++ b/app/keepers.go @@ -140,6 +140,7 @@ type AppKeepers struct { } type privateKeepers struct { + bankBaseKeeper bankkeeper.BaseKeeper capabilityKeeper *capabilitykeeper.Keeper slashingKeeper slashingkeeper.Keeper crisisKeeper crisiskeeper.Keeper @@ -262,13 +263,26 @@ func (app *NibiruApp) InitKeepers( sdk.GetConfig().GetBech32AccountAddrPrefix(), govModuleAddr, ) - app.BankKeeper = bankkeeper.NewBaseKeeper( + + app.bankBaseKeeper = bankkeeper.NewBaseKeeper( appCodec, keys[banktypes.StoreKey], app.AccountKeeper, BlockedAddresses(), govModuleAddr, ) + nibiruBankKeeper := evmkeeper.NibiruBankKeeper{ + BaseKeeper: bankkeeper.NewBaseKeeper( + appCodec, + keys[banktypes.StoreKey], + app.AccountKeeper, + BlockedAddresses(), + govModuleAddr, + ), + StateDB: nil, + } + app.BankKeeper = nibiruBankKeeper + app.StakingKeeper = stakingkeeper.NewKeeper( appCodec, keys[stakingtypes.StoreKey], @@ -370,7 +384,7 @@ func (app *NibiruApp) InitKeepers( tkeys[evm.TransientKey], authtypes.NewModuleAddress(govtypes.ModuleName), app.AccountKeeper, - app.BankKeeper, + &nibiruBankKeeper, app.StakingKeeper, cast.ToString(appOpts.Get("evm.tracer")), ) @@ -605,7 +619,7 @@ func (app *NibiruApp) initAppModules( ), auth.NewAppModule(appCodec, app.AccountKeeper, authsims.RandomGenesisAccounts, app.GetSubspace(authtypes.ModuleName)), vesting.NewAppModule(app.AccountKeeper, app.BankKeeper), - bank.NewAppModule(appCodec, app.BankKeeper, app.AccountKeeper, app.GetSubspace(banktypes.ModuleName)), + bank.NewAppModule(appCodec, app.bankBaseKeeper, app.AccountKeeper, app.GetSubspace(banktypes.ModuleName)), capability.NewAppModule(appCodec, *app.capabilityKeeper, false), feegrantmodule.NewAppModule(appCodec, app.AccountKeeper, app.BankKeeper, app.FeeGrantKeeper, app.interfaceRegistry), gov.NewAppModule(appCodec, &app.GovKeeper, app.AccountKeeper, app.BankKeeper, app.GetSubspace(govtypes.ModuleName)), diff --git a/x/evm/deps.go b/x/evm/deps.go index 04327db06..2325def18 100644 --- a/x/evm/deps.go +++ b/x/evm/deps.go @@ -4,7 +4,6 @@ package evm import ( sdk "github.com/cosmos/cosmos-sdk/types" authtypes "github.com/cosmos/cosmos-sdk/x/auth/types" - bank "github.com/cosmos/cosmos-sdk/x/bank/types" stakingtypes "github.com/cosmos/cosmos-sdk/x/staking/types" ) @@ -32,18 +31,6 @@ type AccountKeeper interface { SetModuleAccount(ctx sdk.Context, macc authtypes.ModuleAccountI) } -// BankKeeper defines the expected interface needed to retrieve account balances. -type BankKeeper interface { - authtypes.BankKeeper - GetBalance(ctx sdk.Context, addr sdk.AccAddress, denom string) sdk.Coin - SendCoinsFromModuleToAccount(ctx sdk.Context, senderModule string, recipientAddr sdk.AccAddress, amt sdk.Coins) error - MintCoins(ctx sdk.Context, moduleName string, amt sdk.Coins) error - BurnCoins(ctx sdk.Context, moduleName string, amt sdk.Coins) error - - GetDenomMetaData(ctx sdk.Context, denom string) (metadata bank.Metadata, isFound bool) - SetDenomMetaData(ctx sdk.Context, denomMetaData bank.Metadata) -} - // StakingKeeper returns the historical headers kept in store. type StakingKeeper interface { GetHistoricalInfo(ctx sdk.Context, height int64) (stakingtypes.HistoricalInfo, bool) diff --git a/x/evm/evmtest/test_deps.go b/x/evm/evmtest/test_deps.go index 1810b1c8f..44fb34c31 100644 --- a/x/evm/evmtest/test_deps.go +++ b/x/evm/evmtest/test_deps.go @@ -46,7 +46,8 @@ func NewTestDeps() TestDeps { } func (deps TestDeps) StateDB() *statedb.StateDB { - return statedb.New(deps.Ctx, &deps.App.EvmKeeper, + return deps.EvmKeeper.NewStateDB( + deps.Ctx, statedb.NewEmptyTxConfig( gethcommon.BytesToHash(deps.Ctx.HeaderHash().Bytes()), ), diff --git a/x/evm/keeper/bank_extension.go b/x/evm/keeper/bank_extension.go new file mode 100644 index 000000000..a2bcd6d27 --- /dev/null +++ b/x/evm/keeper/bank_extension.go @@ -0,0 +1,163 @@ +package keeper + +import ( + sdk "github.com/cosmos/cosmos-sdk/types" + auth "github.com/cosmos/cosmos-sdk/x/auth/types" + bankkeeper "github.com/cosmos/cosmos-sdk/x/bank/keeper" + + "github.com/NibiruChain/nibiru/v2/eth" + "github.com/NibiruChain/nibiru/v2/x/evm" + "github.com/NibiruChain/nibiru/v2/x/evm/statedb" +) + +var ( + _ bankkeeper.Keeper = &NibiruBankKeeper{} + _ bankkeeper.SendKeeper = &NibiruBankKeeper{} +) + +type NibiruBankKeeper struct { + bankkeeper.BaseKeeper + StateDB *statedb.StateDB + balanceChangesForStateDB uint64 +} + +func (evmKeeper *Keeper) NewStateDB( + ctx sdk.Context, txConfig statedb.TxConfig, +) *statedb.StateDB { + stateDB := statedb.New(ctx, evmKeeper, txConfig) + bk := evmKeeper.bankKeeper + bk.StateDB = stateDB + bk.balanceChangesForStateDB = 0 + return stateDB +} + +// BalanceChangesForStateDB returns the count of [statedb.JournalChange] entries +// that were added to the current [statedb.StateDB] +func (bk *NibiruBankKeeper) BalanceChangesForStateDB() uint64 { return bk.balanceChangesForStateDB } + +func (bk NibiruBankKeeper) MintCoins( + ctx sdk.Context, + moduleName string, + coins sdk.Coins, +) error { + // Use the embedded function from [bankkeeper.Keeper] + if err := bk.BaseKeeper.MintCoins(ctx, moduleName, coins); err != nil { + return err + } + if findEtherBalanceChangeFromCoins(coins) { + moduleBech32Addr := auth.NewModuleAddress(evm.ModuleName) + bk.SyncStateDBWithAccount(ctx, moduleBech32Addr) + } + return nil +} + +func (bk NibiruBankKeeper) BurnCoins( + ctx sdk.Context, + moduleName string, + coins sdk.Coins, +) error { + // Use the embedded function from [bankkeeper.Keeper] + if err := bk.BaseKeeper.BurnCoins(ctx, moduleName, coins); err != nil { + return err + } + if findEtherBalanceChangeFromCoins(coins) { + moduleBech32Addr := auth.NewModuleAddress(evm.ModuleName) + bk.SyncStateDBWithAccount(ctx, moduleBech32Addr) + } + return nil +} + +func (bk NibiruBankKeeper) SendCoins( + ctx sdk.Context, + fromAddr sdk.AccAddress, + toAddr sdk.AccAddress, + coins sdk.Coins, +) error { + // Use the embedded function from [bankkeeper.Keeper] + if err := bk.BaseKeeper.SendCoins(ctx, fromAddr, toAddr, coins); err != nil { + return err + } + if findEtherBalanceChangeFromCoins(coins) { + bk.SyncStateDBWithAccount(ctx, fromAddr) + bk.SyncStateDBWithAccount(ctx, toAddr) + } + return nil +} + +func (bk *NibiruBankKeeper) SyncStateDBWithAccount( + ctx sdk.Context, acc sdk.AccAddress, +) { + // If there's no StateDB set, it means we're not in an EthereumTx. + if bk.StateDB == nil { + return + } + balanceWei := evm.NativeToWei( + bk.GetBalance(ctx, acc, evm.EVMBankDenom).Amount.BigInt(), + ) + bk.StateDB.SetBalanceWei(eth.NibiruAddrToEthAddr(acc), balanceWei) + bk.balanceChangesForStateDB += 1 +} + +func findEtherBalanceChangeFromCoins(coins sdk.Coins) (found bool) { + for _, c := range coins { + if c.Denom == evm.EVMBankDenom { + return true + } + } + return false +} + +func (bk NibiruBankKeeper) SendCoinsFromAccountToModule( + ctx sdk.Context, + senderAddr sdk.AccAddress, + recipientModule string, + coins sdk.Coins, +) error { + // Use the embedded function from [bankkeeper.Keeper] + if err := bk.BaseKeeper.SendCoinsFromAccountToModule(ctx, senderAddr, recipientModule, coins); err != nil { + return err + } + if findEtherBalanceChangeFromCoins(coins) { + bk.SyncStateDBWithAccount(ctx, senderAddr) + moduleBech32Addr := auth.NewModuleAddress(recipientModule) + bk.SyncStateDBWithAccount(ctx, moduleBech32Addr) + } + return nil +} + +func (bk NibiruBankKeeper) SendCoinsFromModuleToAccount( + ctx sdk.Context, + senderModule string, + recipientAddr sdk.AccAddress, + coins sdk.Coins, +) error { + // Use the embedded function from [bankkeeper.Keeper] + if err := bk.BaseKeeper.SendCoinsFromModuleToAccount(ctx, senderModule, recipientAddr, coins); err != nil { + return err + } + if findEtherBalanceChangeFromCoins(coins) { + moduleBech32Addr := auth.NewModuleAddress(senderModule) + bk.SyncStateDBWithAccount(ctx, moduleBech32Addr) + bk.SyncStateDBWithAccount(ctx, recipientAddr) + } + return nil +} + +func (bk NibiruBankKeeper) SendCoinsFromModuleToModule( + ctx sdk.Context, + senderModule string, + recipientModule string, + coins sdk.Coins, +) error { + // Use the embedded function from [bankkeeper.Keeper] + if err := bk.BaseKeeper.SendCoinsFromModuleToModule(ctx, senderModule, recipientModule, coins); err != nil { + return err + } + if findEtherBalanceChangeFromCoins(coins) { + senderBech32Addr := auth.NewModuleAddress(senderModule) + recipientBech32Addr := auth.NewModuleAddress(recipientModule) + bk.SyncStateDBWithAccount(ctx, senderBech32Addr) + bk.SyncStateDBWithAccount(ctx, recipientBech32Addr) + } + return nil +} diff --git a/x/evm/keeper/keeper.go b/x/evm/keeper/keeper.go index 49ea0c9bf..c6b0720d8 100644 --- a/x/evm/keeper/keeper.go +++ b/x/evm/keeper/keeper.go @@ -40,7 +40,7 @@ type Keeper struct { // this should be the x/gov module account. authority sdk.AccAddress - bankKeeper evm.BankKeeper + bankKeeper *NibiruBankKeeper accountKeeper evm.AccountKeeper stakingKeeper evm.StakingKeeper @@ -63,13 +63,14 @@ func NewKeeper( storeKey, transientKey storetypes.StoreKey, authority sdk.AccAddress, accKeeper evm.AccountKeeper, - bankKeeper evm.BankKeeper, + bankKeeper *NibiruBankKeeper, stakingKeeper evm.StakingKeeper, tracer string, ) Keeper { if err := sdk.VerifyAddressFormat(authority); err != nil { panic(err) } + return Keeper{ cdc: cdc, storeKey: storeKey, diff --git a/x/evm/keeper/msg_server.go b/x/evm/keeper/msg_server.go index 89a249dd3..be8abb236 100644 --- a/x/evm/keeper/msg_server.go +++ b/x/evm/keeper/msg_server.go @@ -252,7 +252,7 @@ func (k *Keeper) ApplyEvmMsg(ctx sdk.Context, vmErr error // vm errors do not effect consensus and are therefore not assigned to err ) - stateDB := statedb.New(ctx, k, txConfig) + stateDB := k.NewStateDB(ctx, txConfig) evmObj = k.NewEVM(ctx, msg, evmConfig, tracer, stateDB) leftoverGas := msg.Gas() diff --git a/x/evm/keeper/statedb.go b/x/evm/keeper/statedb.go index 6eb46f990..575962d02 100644 --- a/x/evm/keeper/statedb.go +++ b/x/evm/keeper/statedb.go @@ -65,31 +65,34 @@ func (k *Keeper) ForEachStorage( } } -// SetAccBalance update account's balance, compare with current balance first, then decide to mint or burn. +// SetAccBalance update account's balance, compare with current balance first, +// then decide to mint or burn. +// Implements the `statedb.Keeper` interface. +// Only called by `StateDB.Commit()`. func (k *Keeper) SetAccBalance( ctx sdk.Context, addr gethcommon.Address, amountEvmDenom *big.Int, ) error { nativeAddr := sdk.AccAddress(addr.Bytes()) - balance := k.bankKeeper.GetBalance(ctx, nativeAddr, evm.EVMBankDenom).Amount.BigInt() + balance := k.bankKeeper.BaseKeeper.GetBalance(ctx, nativeAddr, evm.EVMBankDenom).Amount.BigInt() delta := new(big.Int).Sub(amountEvmDenom, balance) switch delta.Sign() { case 1: // mint coins := sdk.NewCoins(sdk.NewCoin(evm.EVMBankDenom, sdkmath.NewIntFromBigInt(delta))) - if err := k.bankKeeper.MintCoins(ctx, evm.ModuleName, coins); err != nil { + if err := k.bankKeeper.BaseKeeper.MintCoins(ctx, evm.ModuleName, coins); err != nil { return err } - if err := k.bankKeeper.SendCoinsFromModuleToAccount(ctx, evm.ModuleName, nativeAddr, coins); err != nil { + if err := k.bankKeeper.BaseKeeper.SendCoinsFromModuleToAccount(ctx, evm.ModuleName, nativeAddr, coins); err != nil { return err } case -1: // burn coins := sdk.NewCoins(sdk.NewCoin(evm.EVMBankDenom, sdkmath.NewIntFromBigInt(new(big.Int).Neg(delta)))) - if err := k.bankKeeper.SendCoinsFromAccountToModule(ctx, nativeAddr, evm.ModuleName, coins); err != nil { + if err := k.bankKeeper.BaseKeeper.SendCoinsFromAccountToModule(ctx, nativeAddr, evm.ModuleName, coins); err != nil { return err } - if err := k.bankKeeper.BurnCoins(ctx, evm.ModuleName, coins); err != nil { + if err := k.bankKeeper.BaseKeeper.BurnCoins(ctx, evm.ModuleName, coins); err != nil { return err } default: diff --git a/x/evm/precompile/funtoken.go b/x/evm/precompile/funtoken.go index b95fd0084..505c106b4 100644 --- a/x/evm/precompile/funtoken.go +++ b/x/evm/precompile/funtoken.go @@ -72,7 +72,7 @@ func (p precompileFunToken) Run( if err != nil { return nil, err } - return bz, OnRunEnd(start.StateDB, start.SnapshotBeforeRun, p.Address()) + return bz, err } func PrecompileFunToken(keepers keepers.PublicKeepers) vm.PrecompiledContract { diff --git a/x/evm/precompile/precompile.go b/x/evm/precompile/precompile.go index ecf116f16..40d0c74b4 100644 --- a/x/evm/precompile/precompile.go +++ b/x/evm/precompile/precompile.go @@ -148,10 +148,7 @@ type OnRunStartResult struct { StateDB *statedb.StateDB - // SnapshotBeforeRun captures the state before precompile execution to enable - // proper state reversal if the call fails or if [statedb.JournalChange] - // is reverted in general. - SnapshotBeforeRun statedb.PrecompileCalled + PrecompileJournalEntry statedb.PrecompileCalled } // OnRunStart prepares the execution environment for a precompiled contract call. @@ -193,43 +190,26 @@ func OnRunStart( err = fmt.Errorf("failed to load the sdk.Context from the EVM StateDB") return } - cacheCtx, snapshot := stateDB.CacheCtxForPrecompile(contract.Address()) - stateDB.SavePrecompileSnapshotToJournal(contract.Address(), snapshot) + + // journalEntry captures the state before precompile execution to enable + // proper state reversal if the call fails or if [statedb.JournalChange] + // is reverted in general. + cacheCtx, journalEntry := stateDB.CacheCtxForPrecompile(contract.Address()) + if err = stateDB.SavePrecompileCalledJournalChange(contract.Address(), journalEntry); err != nil { + return res, err + } if err = stateDB.CommitCacheCtx(); err != nil { return res, fmt.Errorf("error committing dirty journal entries: %w", err) } return OnRunStartResult{ - Args: args, - Ctx: cacheCtx, - Method: method, - StateDB: stateDB, - SnapshotBeforeRun: snapshot, + Args: args, + Ctx: cacheCtx, + Method: method, + StateDB: stateDB, }, nil } -// OnRunEnd finalizes a precompile execution by saving its state snapshot to the -// journal. This ensures that any state changes can be properly reverted if needed. -// -// Args: -// - stateDB: The EVM state database -// - snapshot: The state snapshot taken before the precompile executed -// - precompileAddr: The address of the precompiled contract -// -// The snapshot is critical for maintaining state consistency when: -// - The operation gets reverted ([statedb.JournalChange] Revert). -// - The precompile modifies state in other modules (e.g., bank, wasm) -// - Multiple precompiles are called within a single transaction -func OnRunEnd( - stateDB *statedb.StateDB, - snapshot statedb.PrecompileCalled, - precompileAddr gethcommon.Address, -) error { - // TODO: UD-DEBUG: Not needed because it's been added to start. - // return stateDB.SavePrecompileSnapshotToJournal(precompileAddr, snapshot) - return nil -} - var precompileMethodIsTxMap map[PrecompileMethod]bool = map[PrecompileMethod]bool{ WasmMethod_execute: true, WasmMethod_instantiate: true, diff --git a/x/evm/precompile/wasm.go b/x/evm/precompile/wasm.go index 49fe40f87..a7b21684c 100644 --- a/x/evm/precompile/wasm.go +++ b/x/evm/precompile/wasm.go @@ -62,7 +62,7 @@ func (p precompileWasm) Run( if err != nil { return nil, err } - return bz, OnRunEnd(start.StateDB, start.SnapshotBeforeRun, p.Address()) + return bz, err } type precompileWasm struct { diff --git a/x/evm/statedb/access_list.go b/x/evm/statedb/access_list.go index 4513a9164..f62b45171 100644 --- a/x/evm/statedb/access_list.go +++ b/x/evm/statedb/access_list.go @@ -1,3 +1,5 @@ +package statedb + // Copyright 2020 The go-ethereum Authors // This file is part of the go-ethereum library. // @@ -14,8 +16,6 @@ // You should have received a copy of the GNU Lesser General Public License // along with the go-ethereum library. If not, see . -package statedb - import ( "github.com/ethereum/go-ethereum/common" ) diff --git a/x/evm/statedb/config.go b/x/evm/statedb/config.go index 887f591c5..417e480ac 100644 --- a/x/evm/statedb/config.go +++ b/x/evm/statedb/config.go @@ -1,6 +1,7 @@ -// Copyright (c) 2023-2024 Nibi, Inc. package statedb +// Copyright (c) 2023-2024 Nibi, Inc. + import ( "math/big" diff --git a/x/evm/statedb/interfaces.go b/x/evm/statedb/interfaces.go index a9a0f37b4..c242771ca 100644 --- a/x/evm/statedb/interfaces.go +++ b/x/evm/statedb/interfaces.go @@ -1,22 +1,12 @@ -// Copyright (c) 2023-2024 Nibi, Inc. package statedb +// Copyright (c) 2023-2024 Nibi, Inc. + import ( sdk "github.com/cosmos/cosmos-sdk/types" "github.com/ethereum/go-ethereum/common" - "github.com/ethereum/go-ethereum/core/vm" ) -// ExtStateDB defines an extension to the interface provided by the go-ethereum -// codebase to support additional state transition functionalities. In particular -// it supports appending a new entry to the state journal through -// AppendJournalEntry so that the state can be reverted after running -// stateful precompiled contracts. -type ExtStateDB interface { - vm.StateDB - AppendJournalEntry(JournalChange) -} - // Keeper provide underlying storage of StateDB type Keeper interface { // GetAccount: Ethereum account getter for a [statedb.Account]. diff --git a/x/evm/statedb/journal.go b/x/evm/statedb/journal.go index d5af3c479..e684fd574 100644 --- a/x/evm/statedb/journal.go +++ b/x/evm/statedb/journal.go @@ -1,3 +1,5 @@ +package statedb + // Copyright 2016 The go-ethereum Authors // This file is part of the go-ethereum library. // @@ -14,8 +16,6 @@ // You should have received a copy of the GNU Lesser General Public License // along with the go-ethereum library. If not, see . -package statedb - import ( "bytes" "math/big" @@ -359,7 +359,6 @@ func (ch accessListAddSlotChange) Dirtied() *common.Address { type PrecompileCalled struct { MultiStore store.CacheMultiStore Events sdk.Events - Precompile common.Address } var _ JournalChange = PrecompileCalled{} @@ -373,16 +372,14 @@ func (ch PrecompileCalled) Revert(s *StateDB) { s.cacheCtx = s.cacheCtx.WithMultiStore(ch.MultiStore) // Rewrite the `writeCacheCtxFn` using the same logic as sdk.Context.CacheCtx s.writeToCommitCtxFromCacheCtx = func() { - s.ctx.EventManager().EmitEvents(ch.Events) + s.evmTxCtx.EventManager().EmitEvents(ch.Events) // TODO: UD-DEBUG: Overwriting events might fix an issue with // appending too many - // s.ctx.WithEventManager( - // sdk.NewEventManager().EmitEvents(ch.Events), - // ) + // Check correctness of the emitted events ch.MultiStore.Write() } } func (ch PrecompileCalled) Dirtied() *common.Address { - return &ch.Precompile + return nil } diff --git a/x/evm/statedb/journal_test.go b/x/evm/statedb/journal_test.go index 6be5571f9..046fc514c 100644 --- a/x/evm/statedb/journal_test.go +++ b/x/evm/statedb/journal_test.go @@ -74,7 +74,8 @@ func (s *Suite) TestComplexJournalChanges() { s.FailNow("expected 4 dirty journal changes") } - err = stateDB.Commit() // Dirties should be gone + s.T().Log("StateDB.Commit, then Dirties should be gone") + err = stateDB.Commit() s.NoError(err) if stateDB.DirtiesCount() != 0 { debugDirtiesCountMismatch(stateDB, s.T()) @@ -212,7 +213,7 @@ snapshots and see the prior states.`)) ) err = stateDB.Commit() - deps.Ctx = stateDB.GetContext() + deps.Ctx = stateDB.GetEvmTxContext() test.AssertWasmCounterState( &s.Suite, deps, wasmContract, 7, // state before precompile called ) @@ -221,7 +222,6 @@ snapshots and see the prior states.`)) s.Run("too many precompile calls in one tx will fail", func() { // currently // evmObj - }) } diff --git a/x/evm/statedb/state_object.go b/x/evm/statedb/state_object.go index 3e546362b..28ba2d85a 100644 --- a/x/evm/statedb/state_object.go +++ b/x/evm/statedb/state_object.go @@ -1,6 +1,7 @@ -// Copyright (c) 2023-2024 Nibi, Inc. package statedb +// Copyright (c) 2023-2024 Nibi, Inc. + import ( "bytes" "math/big" @@ -121,9 +122,8 @@ type stateObject struct { address common.Address // flags - DirtyCode bool - Suicided bool - IsPrecompile bool + DirtyCode bool + Suicided bool } // newObject creates a state object. @@ -199,7 +199,7 @@ func (s *stateObject) Code() []byte { if bytes.Equal(s.CodeHash(), emptyCodeHash) { return nil } - code := s.db.keeper.GetCode(s.db.ctx, common.BytesToHash(s.CodeHash())) + code := s.db.keeper.GetCode(s.db.evmTxCtx, common.BytesToHash(s.CodeHash())) s.code = code return code } @@ -261,7 +261,7 @@ func (s *stateObject) GetCommittedState(key common.Hash) common.Hash { return value } // If no live objects are available, load it from keeper - value := s.db.keeper.GetState(s.db.ctx, s.Address(), key) + value := s.db.keeper.GetState(s.db.evmTxCtx, s.Address(), key) s.OriginStorage[key] = value return value } diff --git a/x/evm/statedb/statedb.go b/x/evm/statedb/statedb.go index 81a4ce9e8..957da7888 100644 --- a/x/evm/statedb/statedb.go +++ b/x/evm/statedb/statedb.go @@ -1,6 +1,22 @@ -// Copyright (c) 2023-2024 Nibi, Inc. +// The "evm/statedb" package implements a go-ethereum [vm.StateDB] with state +// management and journal changes specific to the Nibiru EVM. +// +// This package plays a critical role in managing the state of accounts, +// contracts, and storage while handling atomicity, caching, and state +// modifications. It ensures that state transitions made during the +// execution of smart contracts are either committed or reverted based +// on transaction outcomes. +// +// StateDB structs used to store anything within the state tree, including +// accounts, contracts, and contract storage. +// Note that Nibiru's state tree is an IAVL tree, which differs from the Merkle +// Patricia Trie structure seen on Ethereum mainnet. +// +// StateDBs also take care of caching and handling nested states. package statedb +// Copyright (c) 2023-2024 Nibi, Inc. + import ( "fmt" "math/big" @@ -14,14 +30,6 @@ import ( "github.com/ethereum/go-ethereum/crypto" ) -// revision is the identifier of a version of state. -// it consists of an auto-increment id and a journal index. -// it's safer to use than using journal index alone. -type revision struct { - id int - journalIndex int -} - var _ vm.StateDB = &StateDB{} // StateDB structs within the ethereum protocol are used to store anything @@ -31,8 +39,9 @@ var _ vm.StateDB = &StateDB{} // * Accounts type StateDB struct { keeper Keeper - // ctx is the persistent context used for official `StateDB.Commit` calls. - ctx sdk.Context + + // evmTxCtx is the persistent context used for official `StateDB.Commit` calls. + evmTxCtx sdk.Context // Journal of state modifications. This is the backbone of // Snapshot and RevertToSnapshot. @@ -51,13 +60,13 @@ type StateDB struct { cacheCtx sdk.Context // writeToCommitCtxFromCacheCtx is the "write" function received from - // `s.ctx.CacheContext()`. It saves mutations on s.cacheCtx to the StateDB's - // commit context (s.ctx). This synchronizes the multistore and event manager + // `s.evmTxCtx.CacheContext()`. It saves mutations on s.cacheCtx to the StateDB's + // commit context (s.evmTxCtx). This synchronizes the multistore and event manager // of the two contexts. writeToCommitCtxFromCacheCtx func() // The number of precompiled contract calls within the current transaction - precompileSnapshotsCount uint8 + multistoreCacheCount uint8 // The refund counter, also used by state transitioning. refund uint64 @@ -73,7 +82,7 @@ type StateDB struct { func New(ctx sdk.Context, keeper Keeper, txConfig TxConfig) *StateDB { return &StateDB{ keeper: keeper, - ctx: ctx, + evmTxCtx: ctx, stateObjects: make(map[common.Address]*stateObject), Journal: newJournal(), accessList: newAccessList(), @@ -82,14 +91,22 @@ func New(ctx sdk.Context, keeper Keeper, txConfig TxConfig) *StateDB { } } +// revision is the identifier of a version of state. +// it consists of an auto-increment id and a journal index. +// it's safer to use than using journal index alone. +type revision struct { + id int + journalIndex int +} + // Keeper returns the underlying `Keeper` func (s *StateDB) Keeper() Keeper { return s.keeper } -// GetContext returns the transaction Context. -func (s *StateDB) GetContext() sdk.Context { - return s.ctx +// GetEvmTxContext returns the EVM transaction context. +func (s *StateDB) GetEvmTxContext() sdk.Context { + return s.evmTxCtx } // GetCacheContext: Getter for testing purposes. @@ -237,17 +254,8 @@ func (s *StateDB) getStateObject(addr common.Address) *stateObject { return obj } - if s.keeper.IsPrecompile(addr) { - obj := newObject(s, addr, Account{ - Nonce: 0, - }) - obj.IsPrecompile = true - s.setStateObject(obj) - return obj - } - // If no live objects are available, load it from keeper - account := s.keeper.GetAccount(s.ctx, addr) + account := s.keeper.GetAccount(s.evmTxCtx, addr) if account == nil { return nil } @@ -308,7 +316,7 @@ func (s *StateDB) ForEachStorage(addr common.Address, cb func(key, value common. if so == nil { return nil } - s.keeper.ForEachStorage(s.ctx, addr, func(key, value common.Hash) bool { + s.keeper.ForEachStorage(s.evmTxCtx, addr, func(key, value common.Hash) bool { if value, dirty := so.DirtyStorage[key]; dirty { return cb(key, value) } @@ -497,19 +505,22 @@ func errorf(format string, args ...any) error { // Commit writes the dirty journal state changes to the EVM Keeper. The // StateDB object cannot be reused after [Commit] has completed. A new // object needs to be created from the EVM. +// +// cacheCtxSyncNeeded: If one of the [Nibiru-Specific Precompiled Contracts] was +// called, a [JournalChange] of type [PrecompileSnapshotBeforeRun] gets added and +// we branch off a cache of the commit context (s.evmTxCtx). +// +// [Nibiru-Specific Precompiled Contracts]: https://nibiru.fi/docs/evm/precompiles/nibiru.html func (s *StateDB) Commit() error { if s.writeToCommitCtxFromCacheCtx != nil { - // cacheCtxSyncNeeded: If a precompile was called, a [JournalChange] - // of type [PrecompileSnapshotBeforeRun] gets added and we branch off a - // cache of the commit context (s.ctx). s.writeToCommitCtxFromCacheCtx() } - return s.commitCtx(s.GetContext()) + return s.commitCtx(s.GetEvmTxContext()) } // CommitCacheCtx is identical to [StateDB.Commit], except it: // (1) uses the cacheCtx of the [StateDB] and -// (2) does not save mutations of the cacheCtx to the commit context (s.ctx). +// (2) does not save mutations of the cacheCtx to the commit context (s.evmTxCtx). // The reason for (2) is that the overall EVM transaction (block, not internal) // is only finalized when [Commit] is called, not when [CommitCacheCtx] is // called. @@ -524,13 +535,10 @@ func (s *StateDB) commitCtx(ctx sdk.Context) error { for _, addr := range s.Journal.sortedDirties() { obj := s.getStateObject(addr) if obj == nil { - continue - } - if obj.IsPrecompile { - // TODO: UD-DEBUG: Assume clean to pretend for tests s.Journal.dirties[addr] = 0 continue - } else if obj.Suicided { + } + if obj.Suicided { // Invariant: After [StateDB.Suicide] for some address, the // corresponding account's state object is only available until the // state is committed. @@ -568,16 +576,15 @@ func (s *StateDB) CacheCtxForPrecompile(precompileAddr common.Address) ( sdk.Context, PrecompileCalled, ) { if s.writeToCommitCtxFromCacheCtx == nil { - s.cacheCtx, s.writeToCommitCtxFromCacheCtx = s.ctx.CacheContext() + s.cacheCtx, s.writeToCommitCtxFromCacheCtx = s.evmTxCtx.CacheContext() } return s.cacheCtx, PrecompileCalled{ MultiStore: s.cacheCtx.MultiStore().(store.CacheMultiStore).Copy(), Events: s.cacheCtx.EventManager().Events(), - Precompile: precompileAddr, } } -// SavePrecompileSnapshotToJournal adds a snapshot of the commit multistore +// SavePrecompileCalledJournalChange adds a snapshot of the commit multistore // ([PrecompileCalled]) to the [StateDB] journal at the end of // successful invocation of a precompiled contract. This is necessary to revert // intermediate states where an EVM contract augments the multistore with a @@ -585,20 +592,22 @@ func (s *StateDB) CacheCtxForPrecompile(precompileAddr common.Address) ( // modules. // // See [PrecompileCalled] for more info. -func (s *StateDB) SavePrecompileSnapshotToJournal( +func (s *StateDB) SavePrecompileCalledJournalChange( precompileAddr common.Address, - snapshot PrecompileCalled, + journalChange PrecompileCalled, ) error { - obj := s.getOrNewStateObject(precompileAddr) - obj.db.Journal.append(snapshot) - s.precompileSnapshotsCount++ - if s.precompileSnapshotsCount > maxPrecompileCalls { - return fmt.Errorf("exceeded maximum allowed number of precompiled contract calls in one transaction (%d)", maxPrecompileCalls) + s.Journal.append(journalChange) + s.multistoreCacheCount++ + if s.multistoreCacheCount > maxMultistoreCacheCount { + return fmt.Errorf( + "exceeded maximum number Nibiru-specific precompiled contract calls in one transaction (%d). Called address %s", + maxMultistoreCacheCount, precompileAddr.Hex(), + ) } return nil } -const maxPrecompileCalls uint8 = 10 +const maxMultistoreCacheCount uint8 = 10 // StateObjects: Returns a copy of the [StateDB.stateObjects] map. func (s *StateDB) StateObjects() map[common.Address]*stateObject {