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