From f3cbcaec58f23c54f6b75204b0c4009856b47250 Mon Sep 17 00:00:00 2001 From: Unique Divine <51418232+Unique-Divine@users.noreply.github.com> Date: Tue, 29 Oct 2024 16:19:03 -0500 Subject: [PATCH] fix(evm): StateDB multistore cache for precompile reversion and a safer Nibiru bank keeper that respects the EVM (#2094) * statedb: add cacheing for multistore before precompile runs * messy, working first version that allows for precompile reversion * wip!: Save checkpoint. 1. Created NibiruBankKeeper with safety around NIBI transfers inside of EthereumTx. 2. The "PrecompileCalled" JournalChange now has a propery implementation and a strong test case to show that reverting the precompile calls works as intended. 3. Remove unneeded functions created for testing with low-level struct fields. * chore: changelog * finalize bank keeper changes * revert to previous commit 7f904a07ac4d5555d8c088411024fc50ff65d085 * fix strange ignored file issue * remove new bank keeper * chore: comments from self-review --- CHANGELOG.md | 9 ++ app/keepers.go | 7 +- go.mod | 3 + go.sum | 8 +- x/evm/deps.go | 13 -- x/evm/evmtest/test_deps.go | 3 +- x/evm/evmtest/tx.go | 203 ++++++++++++++++++-------------- x/evm/keeper/erc20.go | 6 +- x/evm/keeper/keeper.go | 6 +- x/evm/keeper/msg_server.go | 2 +- x/evm/keeper/precompiles.go | 4 + x/evm/keeper/statedb.go | 12 +- x/evm/precompile/funtoken.go | 3 +- x/evm/precompile/precompile.go | 15 ++- x/evm/precompile/test/export.go | 65 +++++++++- x/evm/precompile/wasm.go | 4 +- x/evm/precompile/wasm_test.go | 4 +- x/evm/statedb/access_list.go | 4 +- x/evm/statedb/config.go | 3 +- x/evm/statedb/debug.go | 39 ++++++ x/evm/statedb/interfaces.go | 16 +-- x/evm/statedb/journal.go | 68 +++++++---- x/evm/statedb/journal_test.go | 130 +++++++++++++++----- x/evm/statedb/state_object.go | 7 +- x/evm/statedb/statedb.go | 152 ++++++++++++++++++++---- 25 files changed, 563 insertions(+), 223 deletions(-) create mode 100644 x/evm/statedb/debug.go diff --git a/CHANGELOG.md b/CHANGELOG.md index 41cf28c19..c99138718 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -69,6 +69,15 @@ consistent setup and dynamic gas calculations, addressing the following tickets. - [#2088](https://github.com/NibiruChain/nibiru/pull/2088) - refactor(evm): remove outdated comment and improper error message text - [#2089](https://github.com/NibiruChain/nibiru/pull/2089) - better handling of gas consumption within erc20 contract execution - [#2091](https://github.com/NibiruChain/nibiru/pull/2091) - feat(evm): add fun token creation fee validation +- [#2094](https://github.com/NibiruChain/nibiru/pull/2094) - fix(evm): Following +from the changs in #2086, this pull request implements a new `JournalChange` +struct that saves a deep copy of the state multi store before each +state-modifying, Nibiru-specific precompiled contract is called (`OnRunStart`). +Additionally, we commit the `StateDB` there as well. This guarantees that the +non-EVM and EVM state will be in sync even if there are complex, multi-step +Ethereum transactions, such as in the case of an EthereumTx that influences the +`StateDB`, then calls a precompile that also changes non-EVM state, and then EVM +reverts inside of a try-catch. - [#2098](https://github.com/NibiruChain/nibiru/pull/2098) - test(evm): statedb tests for race conditions within funtoken precompile #### Nibiru EVM | Before Audit 1 - 2024-10-18 diff --git a/app/keepers.go b/app/keepers.go index 6695a6194..4c5721e83 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,15 @@ func (app *NibiruApp) InitKeepers( sdk.GetConfig().GetBech32AccountAddrPrefix(), govModuleAddr, ) - app.BankKeeper = bankkeeper.NewBaseKeeper( + + app.bankBaseKeeper = bankkeeper.NewBaseKeeper( appCodec, keys[banktypes.StoreKey], app.AccountKeeper, BlockedAddresses(), govModuleAddr, ) + app.BankKeeper = app.bankBaseKeeper app.StakingKeeper = stakingkeeper.NewKeeper( appCodec, keys[stakingtypes.StoreKey], @@ -605,7 +608,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/go.mod b/go.mod index 158dc9b80..bc14078f3 100644 --- a/go.mod +++ b/go.mod @@ -244,6 +244,9 @@ require ( replace ( cosmossdk.io/api => cosmossdk.io/api v0.3.1 + github.com/CosmWasm/wasmd => github.com/NibiruChain/wasmd v0.44.0-nibiru + github.com/cosmos/cosmos-sdk => github.com/NibiruChain/cosmos-sdk v0.47.11-nibiru + github.com/cosmos/iavl => github.com/cosmos/iavl v0.20.0 github.com/ethereum/go-ethereum => github.com/NibiruChain/go-ethereum v1.10.27-nibiru diff --git a/go.sum b/go.sum index 2c789f74f..213ee9c84 100644 --- a/go.sum +++ b/go.sum @@ -221,8 +221,6 @@ github.com/BurntSushi/toml v0.3.1/go.mod h1:xHWCNGjB5oqiDr8zfno3MHue2Ht5sIBksp03 github.com/BurntSushi/toml v1.1.0/go.mod h1:CxXYINrC8qIiEnFrOxCa7Jy5BFHlXnUU2pbicEuybxQ= github.com/ChainSafe/go-schnorrkel v1.0.0 h1:3aDA67lAykLaG1y3AOjs88dMxC88PgUuHRrLeDnvGIM= github.com/ChainSafe/go-schnorrkel v1.0.0/go.mod h1:dpzHYVxLZcp8pjlV+O+UR8K0Hp/z7vcchBSbMBEhCw4= -github.com/CosmWasm/wasmd v0.44.0 h1:2sbcoCAvfjCs1O0SWt53xULKjkV06dbSFthEViIC6Zg= -github.com/CosmWasm/wasmd v0.44.0/go.mod h1:tDyYN050qUcdd7LOxGeo2e185sEShyO3nJGl2Cf59+k= github.com/CosmWasm/wasmvm v1.5.5 h1:XlZI3xO5iUhiBqMiyzsrWEfUtk5gcBMNYIdHnsTB+NI= github.com/CosmWasm/wasmvm v1.5.5/go.mod h1:Q0bSEtlktzh7W2hhEaifrFp1Erx11ckQZmjq8FLCyys= github.com/DATA-DOG/go-sqlmock v1.3.3/go.mod h1:f/Ixk793poVmq4qj/V1dPUg2JEAKC73Q5eFN3EC/SaM= @@ -237,8 +235,12 @@ github.com/Microsoft/go-winio v0.6.1 h1:9/kr64B9VUZrLm5YYwbGtUJnMgqWVOdUAXu6Migc github.com/Microsoft/go-winio v0.6.1/go.mod h1:LRdKpFKfdobln8UmuiYcKPot9D2v6svN5+sAH+4kjUM= github.com/NibiruChain/collections v0.5.0 h1:33pXpVTe1PK/tfdZlAJF1JF7AdzGNARG+iL9G/z3X7k= github.com/NibiruChain/collections v0.5.0/go.mod h1:43L6yjuF0BMre/mw4gqn/kUOZz1c2Y3huZ/RQfBFrOQ= +github.com/NibiruChain/cosmos-sdk v0.47.11-nibiru h1:PgFpxDe+7+OzWHs4zXlml5j2i9sGq2Zpd3ndYQG29/0= +github.com/NibiruChain/cosmos-sdk v0.47.11-nibiru/go.mod h1:ADjORYzUQqQv/FxDi0H0K5gW/rAk1CiDR3ZKsExfJV0= github.com/NibiruChain/go-ethereum v1.10.27-nibiru h1:o6lRFt57izoYwzN5cG8tnnBtJcaO3X7MjjN7PGGNCFg= github.com/NibiruChain/go-ethereum v1.10.27-nibiru/go.mod h1:kvvL3nDceUcB+1qGUBAsVf5dW23RBR77fqxgx2PGNrQ= +github.com/NibiruChain/wasmd v0.44.0-nibiru h1:b+stNdbMFsl0+o4KedXyF83qRnEpB/jCiTGZZgv2h2U= +github.com/NibiruChain/wasmd v0.44.0-nibiru/go.mod h1:inrbdsixQ0Kdu4mFUg1u7fn3XPOEkzqieGv0H/gR0ck= github.com/Nvveen/Gotty v0.0.0-20120604004816-cd527374f1e5 h1:TngWCqHvy9oXAN6lEVMRuU21PR1EtLVZJmdB18Gu3Rw= github.com/Nvveen/Gotty v0.0.0-20120604004816-cd527374f1e5/go.mod h1:lmUJ/7eu/Q8D7ML55dXQrVaamCz2vxCfdQBasLZfHKk= github.com/OneOfOne/xxhash v1.2.2 h1:KMrpdQIwFcEqXDklaen+P1axHaj9BSKzvpUUfnHldSE= @@ -426,8 +428,6 @@ github.com/cosmos/cosmos-db v1.0.2 h1:hwMjozuY1OlJs/uh6vddqnk9j7VamLv+0DBlbEXbAK github.com/cosmos/cosmos-db v1.0.2/go.mod h1:Z8IXcFJ9PqKK6BIsVOB3QXtkKoqUOp1vRvPT39kOXEA= github.com/cosmos/cosmos-proto v1.0.0-beta.5 h1:eNcayDLpip+zVLRLYafhzLvQlSmyab+RC5W7ZfmxJLA= github.com/cosmos/cosmos-proto v1.0.0-beta.5/go.mod h1:hQGLpiIUloJBMdQMMWb/4wRApmI9hjHH05nefC0Ojec= -github.com/cosmos/cosmos-sdk v0.47.11 h1:0Qx7eORw0RJqPv+mvDuU8NQ1LV3nJJKJnPoYblWHolc= -github.com/cosmos/cosmos-sdk v0.47.11/go.mod h1:ADjORYzUQqQv/FxDi0H0K5gW/rAk1CiDR3ZKsExfJV0= github.com/cosmos/go-bip39 v0.0.0-20180819234021-555e2067c45d/go.mod h1:tSxLoYXyBmiFeKpvmq4dzayMdCjCnu8uqmCysIGBT2Y= github.com/cosmos/go-bip39 v1.0.0 h1:pcomnQdrdH22njcAatO0yWojsUnCO3y2tNoV1cb6hHY= github.com/cosmos/go-bip39 v1.0.0/go.mod h1:RNJv0H/pOIVgxw6KS7QeX2a0Uo0aKUlfhZ4xuwvCdJw= 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/evmtest/tx.go b/x/evm/evmtest/tx.go index dde679851..6b876f16d 100644 --- a/x/evm/evmtest/tx.go +++ b/x/evm/evmtest/tx.go @@ -26,95 +26,6 @@ import ( "github.com/NibiruChain/nibiru/v2/x/evm/embeds" ) -type GethTxType = uint8 - -func TxTemplateAccessListTx() *gethcore.AccessListTx { - return &gethcore.AccessListTx{ - GasPrice: big.NewInt(1), - Gas: gethparams.TxGas, - To: &gethcommon.Address{}, - Value: big.NewInt(0), - Data: []byte{}, - } -} - -func TxTemplateLegacyTx() *gethcore.LegacyTx { - return &gethcore.LegacyTx{ - GasPrice: big.NewInt(1), - Gas: gethparams.TxGas, - To: &gethcommon.Address{}, - Value: big.NewInt(0), - Data: []byte{}, - } -} - -func TxTemplateDynamicFeeTx() *gethcore.DynamicFeeTx { - return &gethcore.DynamicFeeTx{ - GasFeeCap: big.NewInt(10), - GasTipCap: big.NewInt(2), - Gas: gethparams.TxGas, - To: &gethcommon.Address{}, - Value: big.NewInt(0), - Data: []byte{}, - } -} - -func NewEthTxMsgFromTxData( - deps *TestDeps, - txType GethTxType, - innerTxData []byte, - nonce uint64, - to *gethcommon.Address, - value *big.Int, - gas uint64, - accessList gethcore.AccessList, -) (*evm.MsgEthereumTx, error) { - if innerTxData == nil { - innerTxData = []byte{} - } - - var ethCoreTx *gethcore.Transaction - switch txType { - case gethcore.LegacyTxType: - innerTx := TxTemplateLegacyTx() - innerTx.Nonce = nonce - innerTx.Data = innerTxData - innerTx.To = to - innerTx.Value = value - innerTx.Gas = gas - ethCoreTx = gethcore.NewTx(innerTx) - case gethcore.AccessListTxType: - innerTx := TxTemplateAccessListTx() - innerTx.Nonce = nonce - innerTx.Data = innerTxData - innerTx.AccessList = accessList - innerTx.To = to - innerTx.Value = value - innerTx.Gas = gas - ethCoreTx = gethcore.NewTx(innerTx) - case gethcore.DynamicFeeTxType: - innerTx := TxTemplateDynamicFeeTx() - innerTx.Nonce = nonce - innerTx.Data = innerTxData - innerTx.To = to - innerTx.Value = value - innerTx.Gas = gas - innerTx.AccessList = accessList - ethCoreTx = gethcore.NewTx(innerTx) - default: - return nil, fmt.Errorf( - "received unknown tx type (%v) in NewEthTxMsgFromTxData", txType) - } - - ethTxMsg := new(evm.MsgEthereumTx) - if err := ethTxMsg.FromEthereumTx(ethCoreTx); err != nil { - return ethTxMsg, err - } - - ethTxMsg.From = deps.Sender.EthAddr.Hex() - return ethTxMsg, ethTxMsg.Sign(deps.GethSigner(), deps.Sender.KeyringSigner) -} - // ExecuteNibiTransfer executes nibi transfer func ExecuteNibiTransfer(deps *TestDeps, t *testing.T) *evm.MsgEthereumTx { nonce := deps.StateDB().GetNonce(deps.Sender.EthAddr) @@ -326,6 +237,10 @@ func TransferWei( return err } +// -------------------------------------------------- +// Templates +// -------------------------------------------------- + // ValidLegacyTx: Useful initial condition for tests // Exported only for use in tests. func ValidLegacyTx() *evm.LegacyTx { @@ -342,3 +257,113 @@ func ValidLegacyTx() *evm.LegacyTx { S: []byte{}, } } + +// GethTxType represents different Ethereum transaction types as defined in +// go-ethereum, such as Legacy, AccessList, and DynamicFee transactions. +type GethTxType = uint8 + +func TxTemplateAccessListTx() *gethcore.AccessListTx { + return &gethcore.AccessListTx{ + GasPrice: big.NewInt(1), + Gas: gethparams.TxGas, + To: &gethcommon.Address{}, + Value: big.NewInt(0), + Data: []byte{}, + } +} + +func TxTemplateLegacyTx() *gethcore.LegacyTx { + return &gethcore.LegacyTx{ + GasPrice: big.NewInt(1), + Gas: gethparams.TxGas, + To: &gethcommon.Address{}, + Value: big.NewInt(0), + Data: []byte{}, + } +} + +func TxTemplateDynamicFeeTx() *gethcore.DynamicFeeTx { + return &gethcore.DynamicFeeTx{ + GasFeeCap: big.NewInt(10), + GasTipCap: big.NewInt(2), + Gas: gethparams.TxGas, + To: &gethcommon.Address{}, + Value: big.NewInt(0), + Data: []byte{}, + } +} + +// NewEthTxMsgFromTxData creates an Ethereum transaction message based on +// the specified txType (Legacy, AccessList, or DynamicFee). This function +// populates transaction fields like nonce, recipient, value, and gas, with +// an optional access list for AccessList and DynamicFee types. The transaction +// is signed using the provided dependencies. +// +// Parameters: +// - deps: Required dependencies including the sender address and signer. +// - txType: Transaction type (Legacy, AccessList, or DynamicFee). +// - innerTxData: Byte slice of transaction data (input). +// - nonce: Transaction nonce. +// - to: Recipient address. +// - value: ETH value (in wei) to transfer. +// - gas: Gas limit for the transaction. +// - accessList: Access list for AccessList and DynamicFee types. +// +// Returns: +// - *evm.MsgEthereumTx: Ethereum transaction message ready for submission. +// - error: Any error encountered during creation or signing. +func NewEthTxMsgFromTxData( + deps *TestDeps, + txType GethTxType, + innerTxData []byte, + nonce uint64, + to *gethcommon.Address, + value *big.Int, + gas uint64, + accessList gethcore.AccessList, +) (*evm.MsgEthereumTx, error) { + if innerTxData == nil { + innerTxData = []byte{} + } + + var ethCoreTx *gethcore.Transaction + switch txType { + case gethcore.LegacyTxType: + innerTx := TxTemplateLegacyTx() + innerTx.Nonce = nonce + innerTx.Data = innerTxData + innerTx.To = to + innerTx.Value = value + innerTx.Gas = gas + ethCoreTx = gethcore.NewTx(innerTx) + case gethcore.AccessListTxType: + innerTx := TxTemplateAccessListTx() + innerTx.Nonce = nonce + innerTx.Data = innerTxData + innerTx.AccessList = accessList + innerTx.To = to + innerTx.Value = value + innerTx.Gas = gas + ethCoreTx = gethcore.NewTx(innerTx) + case gethcore.DynamicFeeTxType: + innerTx := TxTemplateDynamicFeeTx() + innerTx.Nonce = nonce + innerTx.Data = innerTxData + innerTx.To = to + innerTx.Value = value + innerTx.Gas = gas + innerTx.AccessList = accessList + ethCoreTx = gethcore.NewTx(innerTx) + default: + return nil, fmt.Errorf( + "received unknown tx type (%v) in NewEthTxMsgFromTxData", txType) + } + + ethTxMsg := new(evm.MsgEthereumTx) + if err := ethTxMsg.FromEthereumTx(ethCoreTx); err != nil { + return ethTxMsg, err + } + + ethTxMsg.From = deps.Sender.EthAddr.Hex() + return ethTxMsg, ethTxMsg.Sign(deps.GethSigner(), deps.Sender.KeyringSigner) +} diff --git a/x/evm/keeper/erc20.go b/x/evm/keeper/erc20.go index 10404bea4..328452ecf 100644 --- a/x/evm/keeper/erc20.go +++ b/x/evm/keeper/erc20.go @@ -218,11 +218,8 @@ func (k Keeper) CallContractWithInput( // sent by a user txConfig := k.TxConfig(ctx, gethcommon.BigToHash(big.NewInt(0))) - // Using tmp context to not modify the state in case of evm revert - tmpCtx, commitCtx := ctx.CacheContext() - evmResp, evmObj, err = k.ApplyEvmMsg( - tmpCtx, evmMsg, evm.NewNoOpTracer(), commit, evmCfg, txConfig, + ctx, evmMsg, evm.NewNoOpTracer(), commit, evmCfg, txConfig, ) if err != nil { // We don't know the actual gas used, so consuming the gas limit @@ -245,7 +242,6 @@ func (k Keeper) CallContractWithInput( } else { // Success, committing the state to ctx if commit { - commitCtx() totalGasUsed, err := k.AddToBlockGasUsed(ctx, evmResp.GasUsed) if err != nil { k.ResetGasMeterAndConsumeGas(ctx, ctx.GasMeter().Limit()) diff --git a/x/evm/keeper/keeper.go b/x/evm/keeper/keeper.go index 49ea0c9bf..dd31229fd 100644 --- a/x/evm/keeper/keeper.go +++ b/x/evm/keeper/keeper.go @@ -15,6 +15,7 @@ import ( "github.com/cosmos/cosmos-sdk/codec" storetypes "github.com/cosmos/cosmos-sdk/store/types" sdk "github.com/cosmos/cosmos-sdk/types" + bankkeeper "github.com/cosmos/cosmos-sdk/x/bank/keeper" gethcommon "github.com/ethereum/go-ethereum/common" "github.com/NibiruChain/nibiru/v2/app/appconst" @@ -40,7 +41,7 @@ type Keeper struct { // this should be the x/gov module account. authority sdk.AccAddress - bankKeeper evm.BankKeeper + bankKeeper bankkeeper.Keeper accountKeeper evm.AccountKeeper stakingKeeper evm.StakingKeeper @@ -63,13 +64,14 @@ func NewKeeper( storeKey, transientKey storetypes.StoreKey, authority sdk.AccAddress, accKeeper evm.AccountKeeper, - bankKeeper evm.BankKeeper, + bankKeeper bankkeeper.Keeper, 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/precompiles.go b/x/evm/keeper/precompiles.go index 965866660..3b1a0e480 100644 --- a/x/evm/keeper/precompiles.go +++ b/x/evm/keeper/precompiles.go @@ -21,3 +21,7 @@ func (k *Keeper) AddPrecompiles( } } } + +func (k *Keeper) IsPrecompile(addr gethcommon.Address) bool { + return k.precompiles.Has(addr) +} diff --git a/x/evm/keeper/statedb.go b/x/evm/keeper/statedb.go index 6eb46f990..7aff8c02d 100644 --- a/x/evm/keeper/statedb.go +++ b/x/evm/keeper/statedb.go @@ -17,6 +17,13 @@ import ( var _ statedb.Keeper = &Keeper{} +func (k *Keeper) NewStateDB( + ctx sdk.Context, + txConfig statedb.TxConfig, +) *statedb.StateDB { + return statedb.New(ctx, k, txConfig) +} + // ---------------------------------------------------------------------------- // StateDB Keeper implementation // ---------------------------------------------------------------------------- @@ -65,7 +72,10 @@ 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 { diff --git a/x/evm/precompile/funtoken.go b/x/evm/precompile/funtoken.go index a937d111f..69e756ed3 100644 --- a/x/evm/precompile/funtoken.go +++ b/x/evm/precompile/funtoken.go @@ -72,8 +72,7 @@ func (p precompileFunToken) Run( if err != nil { return nil, err } - // Dirty journal entries in `StateDB` must be committed - return bz, start.StateDB.Commit() + 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 a6bbfefc4..40d0c74b4 100644 --- a/x/evm/precompile/precompile.go +++ b/x/evm/precompile/precompile.go @@ -147,6 +147,8 @@ type OnRunStartResult struct { Method *gethabi.Method StateDB *statedb.StateDB + + PrecompileJournalEntry statedb.PrecompileCalled } // OnRunStart prepares the execution environment for a precompiled contract call. @@ -188,14 +190,21 @@ func OnRunStart( err = fmt.Errorf("failed to load the sdk.Context from the EVM StateDB") return } - ctx := stateDB.GetContext() - if err = stateDB.Commit(); err != nil { + + // 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: ctx, + Ctx: cacheCtx, Method: method, StateDB: stateDB, }, nil diff --git a/x/evm/precompile/test/export.go b/x/evm/precompile/test/export.go index 966dd3359..28670e3e0 100644 --- a/x/evm/precompile/test/export.go +++ b/x/evm/precompile/test/export.go @@ -2,11 +2,14 @@ package test import ( "encoding/json" + "math/big" "os" "os/exec" "path" "strings" + serverconfig "github.com/NibiruChain/nibiru/v2/app/server/config" + wasmkeeper "github.com/CosmWasm/wasmd/x/wasm/keeper" wasm "github.com/CosmWasm/wasmd/x/wasm/types" "github.com/ethereum/go-ethereum/core/vm" @@ -267,6 +270,7 @@ func IncrementWasmCounterWithExecuteMulti( deps *evmtest.TestDeps, wasmContract sdk.AccAddress, times uint, + finalizeTx bool, ) (evmObj *vm.EVM) { msgArgsBz := []byte(` { @@ -308,9 +312,68 @@ func IncrementWasmCounterWithExecuteMulti( s.Require().NoError(err) ethTxResp, evmObj, err := deps.EvmKeeper.CallContractWithInput( - deps.Ctx, deps.Sender.EthAddr, &precompile.PrecompileAddr_Wasm, true, input, + deps.Ctx, deps.Sender.EthAddr, &precompile.PrecompileAddr_Wasm, finalizeTx, input, ) s.Require().NoError(err) s.Require().NotEmpty(ethTxResp.Ret) return evmObj } + +func IncrementWasmCounterWithExecuteMultiViaVMCall( + s *suite.Suite, + deps *evmtest.TestDeps, + wasmContract sdk.AccAddress, + times uint, + finalizeTx bool, + evmObj *vm.EVM, +) error { + msgArgsBz := []byte(` + { + "increment": {} + } + `) + + // Parse funds argument. + var funds []precompile.WasmBankCoin // blank funds + fundsJson, err := json.Marshal(funds) + s.NoErrorf(err, "fundsJson: %s", fundsJson) + err = json.Unmarshal(fundsJson, &funds) + s.Require().NoError(err, "fundsJson %s, funds %s", fundsJson, funds) + + // The "times" arg determines the number of messages in the executeMsgs slice + executeMsgs := []struct { + ContractAddr string `json:"contractAddr"` + MsgArgs []byte `json:"msgArgs"` + Funds []precompile.WasmBankCoin `json:"funds"` + }{ + {wasmContract.String(), msgArgsBz, funds}, + } + if times == 0 { + executeMsgs = executeMsgs[:0] // force empty + } else { + for i := uint(1); i < times; i++ { + executeMsgs = append(executeMsgs, executeMsgs[0]) + } + } + s.Require().Len(executeMsgs, int(times)) // sanity check assertion + + callArgs := []any{ + executeMsgs, + } + input, err := embeds.SmartContract_Wasm.ABI.Pack( + string(precompile.WasmMethod_executeMulti), + callArgs..., + ) + s.Require().NoError(err) + + contract := precompile.PrecompileAddr_Wasm + leftoverGas := serverconfig.DefaultEthCallGasLimit + _, _, err = evmObj.Call( + vm.AccountRef(deps.Sender.EthAddr), + contract, + input, + leftoverGas, + big.NewInt(0), + ) + return err +} diff --git a/x/evm/precompile/wasm.go b/x/evm/precompile/wasm.go index 10817c673..a7b21684c 100644 --- a/x/evm/precompile/wasm.go +++ b/x/evm/precompile/wasm.go @@ -62,9 +62,7 @@ func (p precompileWasm) Run( if err != nil { return nil, err } - - // Dirty journal entries in `StateDB` must be committed - return bz, start.StateDB.Commit() + return bz, err } type precompileWasm struct { diff --git a/x/evm/precompile/wasm_test.go b/x/evm/precompile/wasm_test.go index d796f8b89..dbe35b839 100644 --- a/x/evm/precompile/wasm_test.go +++ b/x/evm/precompile/wasm_test.go @@ -100,13 +100,13 @@ func (s *WasmSuite) TestExecuteMultiHappy() { test.AssertWasmCounterState(&s.Suite, deps, wasmContract, 0) // count += 2 test.IncrementWasmCounterWithExecuteMulti( - &s.Suite, &deps, wasmContract, 2) + &s.Suite, &deps, wasmContract, 2, true) // count = 2 test.AssertWasmCounterState(&s.Suite, deps, wasmContract, 2) s.assertWasmCounterStateRaw(deps, wasmContract, 2) // count += 67 test.IncrementWasmCounterWithExecuteMulti( - &s.Suite, &deps, wasmContract, 67) + &s.Suite, &deps, wasmContract, 67, true) // count = 69 test.AssertWasmCounterState(&s.Suite, deps, wasmContract, 69) s.assertWasmCounterStateRaw(deps, wasmContract, 69) 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/debug.go b/x/evm/statedb/debug.go new file mode 100644 index 000000000..c2b5fb968 --- /dev/null +++ b/x/evm/statedb/debug.go @@ -0,0 +1,39 @@ +package statedb + +// Copyright (c) 2023-2024 Nibi, Inc. + +import ( + "github.com/ethereum/go-ethereum/common" +) + +// DebugDirtiesCount is a test helper to inspect how many entries in the journal +// are still dirty (uncommitted). After calling [StateDB.Commit], this function +// should return zero. +func (s *StateDB) DebugDirtiesCount() int { + dirtiesCount := 0 + for _, dirtyCount := range s.Journal.dirties { + dirtiesCount += dirtyCount + } + return dirtiesCount +} + +// DebugDirties is a test helper that returns the journal's dirty account changes map. +func (s *StateDB) DebugDirties() map[common.Address]int { + return s.Journal.dirties +} + +// DebugEntries is a test helper that returns the sequence of [JournalChange] +// objects added during execution. +func (s *StateDB) DebugEntries() []JournalChange { + return s.Journal.entries +} + +// DebugStateObjects is a test helper that returns returns a copy of the +// [StateDB.stateObjects] map. +func (s *StateDB) DebugStateObjects() map[common.Address]*stateObject { + copyOfMap := make(map[common.Address]*stateObject) + for key, val := range s.stateObjects { + copyOfMap[key] = val + } + return copyOfMap +} diff --git a/x/evm/statedb/interfaces.go b/x/evm/statedb/interfaces.go index a4c1c3b59..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]. @@ -38,4 +28,6 @@ type Keeper interface { // DeleteAccount handles contract's suicide call, clearing the balance, // contract bytecode, contract state, and its native account. DeleteAccount(ctx sdk.Context, addr common.Address) error + + IsPrecompile(addr common.Address) bool } diff --git a/x/evm/statedb/journal.go b/x/evm/statedb/journal.go index ac041b617..acbba5eab 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,13 +16,13 @@ // 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" "sort" + store "github.com/cosmos/cosmos-sdk/store/types" + sdk "github.com/cosmos/cosmos-sdk/types" "github.com/ethereum/go-ethereum/common" ) @@ -91,25 +93,6 @@ func (j *journal) Length() int { return len(j.entries) } -// DirtiesCount is a test helper to inspect how many entries in the journal are -// still dirty (uncommitted). After calling [StateDB.Commit], this function should -// return zero. -func (s *StateDB) DirtiesCount() int { - dirtiesCount := 0 - for _, dirtyCount := range s.Journal.dirties { - dirtiesCount += dirtyCount - } - return dirtiesCount -} - -func (s *StateDB) Dirties() map[common.Address]int { - return s.Journal.dirties -} - -func (s *StateDB) Entries() []JournalChange { - return s.Journal.entries -} - // ------------------------------------------------------ // createObjectChange @@ -335,3 +318,46 @@ func (ch accessListAddSlotChange) Revert(s *StateDB) { func (ch accessListAddSlotChange) Dirtied() *common.Address { return nil } + +// ------------------------------------------------------ +// PrecompileSnapshotBeforeRun + +// PrecompileCalled: Precompiles can alter persistent storage of other +// modules. These changes to persistent storage are not reverted by a `Revert` of +// [JournalChange] by default, as it generally manages only changes to accounts +// and Bank balances for ether (NIBI). +// +// As a workaround to make state changes from precompiles reversible, we store +// [PrecompileCalled] snapshots that sync and record the prior state +// of the other modules, allowing precompile calls to truly be reverted. +// +// As a simple example, suppose that a transaction calls a precompile. +// 1. If the precompile changes the state in the Bank Module or Wasm module +// 2. The call gets reverted (`revert()` in Solidity), which shoud restore the +// state to a in-memory snapshot recorded on the StateDB journal. +// 3. This could cause a problem where changes to the rest of the blockchain state +// are still in effect following the reversion in the EVM state DB. +type PrecompileCalled struct { + MultiStore store.CacheMultiStore + Events sdk.Events +} + +var _ JournalChange = PrecompileCalled{} + +// Revert rolls back the [StateDB] cache context to the state it was in prior to +// the precompile call. Modifications to this cache context are pushed to the +// commit context (s.evmTxCtx) when [StateDB.Commit] is executed. +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.evmTxCtx.EventManager().EmitEvents(ch.Events) + // TODO: Check correctness of the emitted events + // https://github.com/NibiruChain/nibiru/issues/2096 + ch.MultiStore.Write() + } +} + +func (ch PrecompileCalled) Dirtied() *common.Address { + return nil +} diff --git a/x/evm/statedb/journal_test.go b/x/evm/statedb/journal_test.go index 5863face5..6390b640f 100644 --- a/x/evm/statedb/journal_test.go +++ b/x/evm/statedb/journal_test.go @@ -6,10 +6,12 @@ import ( "strings" "testing" + "github.com/MakeNowJust/heredoc/v2" sdk "github.com/cosmos/cosmos-sdk/types" "github.com/ethereum/go-ethereum/core/vm" serverconfig "github.com/NibiruChain/nibiru/v2/app/server/config" + "github.com/NibiruChain/nibiru/v2/x/common" "github.com/NibiruChain/nibiru/v2/x/common/testutil/testapp" "github.com/NibiruChain/nibiru/v2/x/evm" "github.com/NibiruChain/nibiru/v2/x/evm/embeds" @@ -18,7 +20,7 @@ import ( "github.com/NibiruChain/nibiru/v2/x/evm/statedb" ) -func (s *Suite) TestPrecompileSnapshots() { +func (s *Suite) TestComplexJournalChanges() { deps := evmtest.NewTestDeps() bankDenom := evm.EVMBankDenom s.Require().NoError(testapp.FundAccount( @@ -32,25 +34,11 @@ func (s *Suite) TestPrecompileSnapshots() { wasmContract := test.SetupWasmContracts(&deps, &s.Suite)[1] fmt.Printf("wasmContract: %s\n", wasmContract) - assertionsBeforeRun := func(deps *evmtest.TestDeps) { - test.AssertWasmCounterState( - &s.Suite, *deps, wasmContract, 0, - ) - } - run := func(deps *evmtest.TestDeps) *vm.EVM { - return test.IncrementWasmCounterWithExecuteMulti( - &s.Suite, deps, wasmContract, 7, - ) - } - assertionsAfterRun := func(deps *evmtest.TestDeps) { - test.AssertWasmCounterState( - &s.Suite, *deps, wasmContract, 7, - ) - } s.T().Log("Assert before transition") - - assertionsBeforeRun(&deps) + test.AssertWasmCounterState( + &s.Suite, deps, wasmContract, 0, + ) deployArgs := []any{"name", "SYMBOL", uint8(18)} deployResp, err := evmtest.DeployContract( @@ -71,7 +59,7 @@ func (s *Suite) TestPrecompileSnapshots() { s.Run("Populate dirty journal entries. Remove with Commit", func() { stateDB := evmObj.StateDB.(*statedb.StateDB) - s.Equal(0, stateDB.DirtiesCount()) + s.Equal(0, stateDB.DebugDirtiesCount()) randomAcc := evmtest.NewEthPrivAcc().EthAddr balDelta := evm.NativeToWei(big.NewInt(4)) @@ -81,14 +69,15 @@ func (s *Suite) TestPrecompileSnapshots() { stateDB.AddBalance(randomAcc, balDelta) // 1 dirties from [balanceChange] stateDB.SubBalance(randomAcc, balDelta) - if stateDB.DirtiesCount() != 4 { + if stateDB.DebugDirtiesCount() != 4 { debugDirtiesCountMismatch(stateDB, s.T()) 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 { + if stateDB.DebugDirtiesCount() != 0 { debugDirtiesCountMismatch(stateDB, s.T()) s.FailNow("expected 0 dirty journal changes") } @@ -110,7 +99,7 @@ func (s *Suite) TestPrecompileSnapshots() { ) s.Require().NoError(err) stateDB := evmObj.StateDB.(*statedb.StateDB) - if stateDB.DirtiesCount() != 2 { + if stateDB.DebugDirtiesCount() != 2 { debugDirtiesCountMismatch(stateDB, s.T()) s.FailNow("expected 2 dirty journal changes") } @@ -136,22 +125,105 @@ func (s *Suite) TestPrecompileSnapshots() { s.Require().ErrorContains(err, vm.ErrExecutionReverted.Error()) }) - s.Run("Precompile calls also start and end clean (no dirty changes)", func() { - evmObj = run(&deps) - assertionsAfterRun(&deps) + s.Run("Precompile calls populate snapshots", func() { + s.T().Log("commitEvmTx=true, expect 0 dirty journal entries") + commitEvmTx := true + evmObj = test.IncrementWasmCounterWithExecuteMulti( + &s.Suite, &deps, wasmContract, 7, commitEvmTx, + ) + // assertions after run + test.AssertWasmCounterState( + &s.Suite, deps, wasmContract, 7, + ) stateDB, ok := evmObj.StateDB.(*statedb.StateDB) s.Require().True(ok, "error retrieving StateDB from the EVM") - if stateDB.DirtiesCount() != 0 { + if stateDB.DebugDirtiesCount() != 0 { + debugDirtiesCountMismatch(stateDB, s.T()) + s.FailNow("expected 0 dirty journal changes") + } + + s.T().Log("commitEvmTx=false, expect dirty journal entries") + commitEvmTx = false + evmObj = test.IncrementWasmCounterWithExecuteMulti( + &s.Suite, &deps, wasmContract, 5, commitEvmTx, + ) + stateDB, ok = evmObj.StateDB.(*statedb.StateDB) + s.Require().True(ok, "error retrieving StateDB from the EVM") + + s.T().Log("Expect exactly 0 dirty journal entry for the precompile snapshot") + if stateDB.DebugDirtiesCount() != 0 { debugDirtiesCountMismatch(stateDB, s.T()) s.FailNow("expected 0 dirty journal changes") } + + s.T().Log("Expect no change since the StateDB has not been committed") + test.AssertWasmCounterState( + &s.Suite, deps, wasmContract, 7, // 7 = 7 + 0 + ) + + s.T().Log("Expect change to persist on the StateDB cacheCtx") + cacheCtx := stateDB.GetCacheContext() + s.NotNil(cacheCtx) + deps.Ctx = *cacheCtx + test.AssertWasmCounterState( + &s.Suite, deps, wasmContract, 12, // 12 = 7 + 5 + ) + // NOTE: that the [StateDB.Commit] fn has not been called yet. We're still + // mid-transaction. + + s.T().Log("EVM revert operation should bring about the old state") + err = test.IncrementWasmCounterWithExecuteMultiViaVMCall( + &s.Suite, &deps, wasmContract, 50, commitEvmTx, evmObj, + ) + stateDBPtr := evmObj.StateDB.(*statedb.StateDB) + s.Require().Equal(stateDB, stateDBPtr) + s.Require().NoError(err) + s.T().Log(heredoc.Doc(`At this point, 2 precompile calls have succeeded. +One that increments the counter to 7 + 5, and another for +50. +The StateDB has not been committed. We expect to be able to revert to both +snapshots and see the prior states.`)) + cacheCtx = stateDB.GetCacheContext() + deps.Ctx = *cacheCtx + test.AssertWasmCounterState( + &s.Suite, deps, wasmContract, 7+5+50, + ) + + errFn := common.TryCatch(func() { + // There were only two EVM calls. + // Thus, there are only 2 snapshots: 0 and 1. + // We should not be able to revert to a third one. + stateDB.RevertToSnapshot(2) + }) + s.Require().ErrorContains(errFn(), "revision id 2 cannot be reverted") + + stateDB.RevertToSnapshot(1) + cacheCtx = stateDB.GetCacheContext() + s.NotNil(cacheCtx) + deps.Ctx = *cacheCtx + test.AssertWasmCounterState( + &s.Suite, deps, wasmContract, 7+5, + ) + + stateDB.RevertToSnapshot(0) + cacheCtx = stateDB.GetCacheContext() + s.NotNil(cacheCtx) + deps.Ctx = *cacheCtx + test.AssertWasmCounterState( + &s.Suite, deps, wasmContract, 7, // state before precompile called + ) + + err = stateDB.Commit() + deps.Ctx = stateDB.GetEvmTxContext() + test.AssertWasmCounterState( + &s.Suite, deps, wasmContract, 7, // state before precompile called + ) }) } func debugDirtiesCountMismatch(db *statedb.StateDB, t *testing.T) string { lines := []string{} - dirties := db.Dirties() - stateObjects := db.StateObjects() + dirties := db.DebugDirties() + stateObjects := db.DebugStateObjects() for addr, dirtyCountForAddr := range dirties { lines = append(lines, fmt.Sprintf("Dirty addr: %s, dirtyCountForAddr=%d", addr, dirtyCountForAddr)) diff --git a/x/evm/statedb/state_object.go b/x/evm/statedb/state_object.go index e371beae0..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" @@ -198,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 } @@ -260,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 223e92edb..4c79e61af 100644 --- a/x/evm/statedb/statedb.go +++ b/x/evm/statedb/statedb.go @@ -1,11 +1,28 @@ -// 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" "sort" + store "github.com/cosmos/cosmos-sdk/store/types" sdk "github.com/cosmos/cosmos-sdk/types" "github.com/ethereum/go-ethereum/common" gethcore "github.com/ethereum/go-ethereum/core/types" @@ -13,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 @@ -30,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. @@ -43,6 +53,21 @@ type StateDB struct { txConfig TxConfig + // cacheCtx: An sdk.Context produced from the [StateDB.ctx] with the + // multi-store cached and a new event manager. The cached context + // (`cacheCtx`) is written to the persistent context (`ctx`) when + // `writeCacheCtx` is called. + cacheCtx sdk.Context + + // writeToCommitCtxFromCacheCtx is the "write" function received from + // `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 + multistoreCacheCount uint8 + // The refund counter, also used by state transitioning. refund uint64 @@ -57,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(), @@ -66,14 +91,30 @@ 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. +func (s *StateDB) GetCacheContext() *sdk.Context { + if s.writeToCommitCtxFromCacheCtx == nil { + return nil + } + return &s.cacheCtx } // AddLog adds a log, called by evm. @@ -212,8 +253,9 @@ func (s *StateDB) getStateObject(addr common.Address) *stateObject { if obj := s.stateObjects[addr]; obj != nil { 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 } @@ -274,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) } @@ -438,6 +480,9 @@ func (s *StateDB) Snapshot() int { // RevertToSnapshot reverts all state changes made since the given revision. func (s *StateDB) RevertToSnapshot(revid int) { + fmt.Printf("len(s.validRevisions): %d\n", len(s.validRevisions)) + fmt.Printf("s.validRevisions: %v\n", s.validRevisions) + // Find the snapshot in the stack of valid snapshots. idx := sort.Search(len(s.validRevisions), func(i int) bool { return s.validRevisions[i].id >= revid @@ -460,11 +505,37 @@ 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 { - ctx := s.GetContext() + if s.writeToCommitCtxFromCacheCtx != nil { + s.writeToCommitCtxFromCacheCtx() + } + 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.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. +func (s *StateDB) CommitCacheCtx() error { + return s.commitCtx(s.cacheCtx) +} + +// commitCtx writes the dirty journal state changes to the EVM Keeper. The +// StateDB object cannot be reused after [commitCtx] has completed. A new +// object needs to be created from the EVM. +func (s *StateDB) commitCtx(ctx sdk.Context) error { for _, addr := range s.Journal.sortedDirties() { obj := s.getStateObject(addr) if obj == nil { + s.Journal.dirties[addr] = 0 continue } if obj.Suicided { @@ -493,18 +564,47 @@ func (s *StateDB) Commit() error { obj.OriginStorage[key] = dirtyVal } } - // Clear the dirty counts because all state changes have been - // committed. + // TODO: UD-DEBUG: Assume clean to pretend for tests + // Reset the dirty count to 0 because all state changes for this dirtied + // address in the journal have been committed. s.Journal.dirties[addr] = 0 } return nil } -// StateObjects: Returns a copy of the [StateDB.stateObjects] map. -func (s *StateDB) StateObjects() map[common.Address]*stateObject { - copyOfMap := make(map[common.Address]*stateObject) - for key, val := range s.stateObjects { - copyOfMap[key] = val +func (s *StateDB) CacheCtxForPrecompile(precompileAddr common.Address) ( + sdk.Context, PrecompileCalled, +) { + if s.writeToCommitCtxFromCacheCtx == nil { + s.cacheCtx, s.writeToCommitCtxFromCacheCtx = s.evmTxCtx.CacheContext() + } + return s.cacheCtx, PrecompileCalled{ + MultiStore: s.cacheCtx.MultiStore().(store.CacheMultiStore).Copy(), + Events: s.cacheCtx.EventManager().Events(), + } +} + +// 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 +// precompile and an inconsistency occurs between the EVM module and other +// modules. +// +// See [PrecompileCalled] for more info. +func (s *StateDB) SavePrecompileCalledJournalChange( + precompileAddr common.Address, + journalChange PrecompileCalled, +) error { + 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 copyOfMap + return nil } + +const maxMultistoreCacheCount uint8 = 10