From 80dd2d70eb928f7dda9e85aa6e69150e54366800 Mon Sep 17 00:00:00 2001 From: Unique-Divine Date: Fri, 25 Oct 2024 20:48:05 -0500 Subject: [PATCH] messy, working first version that allows for precompile reversion --- go.mod | 3 ++ go.sum | 8 ++-- x/evm/precompile/precompile.go | 9 +++-- x/evm/precompile/test/export.go | 62 +++++++++++++++++++++++++++++ x/evm/statedb/journal.go | 22 ++++++++--- x/evm/statedb/journal_test.go | 70 ++++++++++++++++++++++++++++++--- x/evm/statedb/statedb.go | 26 +++++++++--- 7 files changed, 175 insertions(+), 25 deletions(-) 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/precompile/precompile.go b/x/evm/precompile/precompile.go index 428af9775..ecf116f16 100644 --- a/x/evm/precompile/precompile.go +++ b/x/evm/precompile/precompile.go @@ -151,7 +151,7 @@ type OnRunStartResult struct { // SnapshotBeforeRun captures the state before precompile execution to enable // proper state reversal if the call fails or if [statedb.JournalChange] // is reverted in general. - SnapshotBeforeRun statedb.PrecompileSnapshotBeforeRun + SnapshotBeforeRun statedb.PrecompileCalled } // OnRunStart prepares the execution environment for a precompiled contract call. @@ -194,6 +194,7 @@ func OnRunStart( return } cacheCtx, snapshot := stateDB.CacheCtxForPrecompile(contract.Address()) + stateDB.SavePrecompileSnapshotToJournal(contract.Address(), snapshot) if err = stateDB.CommitCacheCtx(); err != nil { return res, fmt.Errorf("error committing dirty journal entries: %w", err) } @@ -221,10 +222,12 @@ func OnRunStart( // - Multiple precompiles are called within a single transaction func OnRunEnd( stateDB *statedb.StateDB, - snapshot statedb.PrecompileSnapshotBeforeRun, + snapshot statedb.PrecompileCalled, precompileAddr gethcommon.Address, ) error { - return stateDB.SavePrecompileSnapshotToJournal(precompileAddr, snapshot) + // TODO: UD-DEBUG: Not needed because it's been added to start. + // return stateDB.SavePrecompileSnapshotToJournal(precompileAddr, snapshot) + return nil } var precompileMethodIsTxMap map[PrecompileMethod]bool = map[PrecompileMethod]bool{ diff --git a/x/evm/precompile/test/export.go b/x/evm/precompile/test/export.go index 05405c730..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" @@ -315,3 +318,62 @@ func IncrementWasmCounterWithExecuteMulti( 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/statedb/journal.go b/x/evm/statedb/journal.go index 40a8cc3af..d5af3c479 100644 --- a/x/evm/statedb/journal.go +++ b/x/evm/statedb/journal.go @@ -341,13 +341,13 @@ func (ch accessListAddSlotChange) Dirtied() *common.Address { // ------------------------------------------------------ // PrecompileSnapshotBeforeRun -// PrecompileSnapshotBeforeRun: Precompiles can alter persistent storage of other +// 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 -// [PrecompileSnapshotBeforeRun] snapshots that sync and record the prior state +// [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. @@ -356,23 +356,33 @@ func (ch accessListAddSlotChange) Dirtied() *common.Address { // 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 PrecompileSnapshotBeforeRun struct { +type PrecompileCalled struct { MultiStore store.CacheMultiStore Events sdk.Events Precompile common.Address } -var _ JournalChange = PrecompileSnapshotBeforeRun{} +var _ JournalChange = PrecompileCalled{} -func (ch PrecompileSnapshotBeforeRun) Revert(s *StateDB) { +func (ch PrecompileCalled) Revert(s *StateDB) { + // TEMP: trying something + // If the wasm state is not in the cacheCtx, + // s.CommitCacheCtx() + + // Old Code s.cacheCtx = s.cacheCtx.WithMultiStore(ch.MultiStore) // Rewrite the `writeCacheCtxFn` using the same logic as sdk.Context.CacheCtx s.writeToCommitCtxFromCacheCtx = func() { s.ctx.EventManager().EmitEvents(ch.Events) + // TODO: UD-DEBUG: Overwriting events might fix an issue with + // appending too many + // s.ctx.WithEventManager( + // sdk.NewEventManager().EmitEvents(ch.Events), + // ) ch.MultiStore.Write() } } -func (ch PrecompileSnapshotBeforeRun) Dirtied() *common.Address { +func (ch PrecompileCalled) Dirtied() *common.Address { return &ch.Precompile } diff --git a/x/evm/statedb/journal_test.go b/x/evm/statedb/journal_test.go index 0297e8999..6be5571f9 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" @@ -147,10 +149,10 @@ func (s *Suite) TestComplexJournalChanges() { stateDB, ok = evmObj.StateDB.(*statedb.StateDB) s.Require().True(ok, "error retrieving StateDB from the EVM") - s.T().Log("Expect exactly 1 dirty journal entry for the precompile snapshot") - if stateDB.DirtiesCount() != 1 { + s.T().Log("Expect exactly 0 dirty journal entry for the precompile snapshot") + if stateDB.DirtiesCount() != 0 { debugDirtiesCountMismatch(stateDB, s.T()) - s.FailNow("expected 1 dirty journal changes") + s.FailNow("expected 0 dirty journal changes") } s.T().Log("Expect no change since the StateDB has not been committed") @@ -158,12 +160,68 @@ func (s *Suite) TestComplexJournalChanges() { &s.Suite, deps, wasmContract, 7, // 7 = 7 + 0 ) - s.T().Log("Expect change after the StateDB gets committed") - err = stateDB.Commit() - s.Require().NoError(err) + 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.GetContext() + test.AssertWasmCounterState( + &s.Suite, deps, wasmContract, 7, // state before precompile called + ) + }) + + s.Run("too many precompile calls in one tx will fail", func() { + // currently + // evmObj + }) } diff --git a/x/evm/statedb/statedb.go b/x/evm/statedb/statedb.go index 839b40815..81a4ce9e8 100644 --- a/x/evm/statedb/statedb.go +++ b/x/evm/statedb/statedb.go @@ -6,6 +6,7 @@ import ( "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" @@ -91,6 +92,14 @@ func (s *StateDB) GetContext() sdk.Context { return s.ctx } +// 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. func (s *StateDB) AddLog(log *gethcore.Log) { s.Journal.append(addLogChange{}) @@ -463,6 +472,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 @@ -515,6 +527,7 @@ func (s *StateDB) commitCtx(ctx sdk.Context) error { continue } if obj.IsPrecompile { + // TODO: UD-DEBUG: Assume clean to pretend for tests s.Journal.dirties[addr] = 0 continue } else if obj.Suicided { @@ -543,6 +556,7 @@ func (s *StateDB) commitCtx(ctx sdk.Context) error { obj.OriginStorage[key] = dirtyVal } } + // 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 @@ -551,29 +565,29 @@ func (s *StateDB) commitCtx(ctx sdk.Context) error { } func (s *StateDB) CacheCtxForPrecompile(precompileAddr common.Address) ( - sdk.Context, PrecompileSnapshotBeforeRun, + sdk.Context, PrecompileCalled, ) { if s.writeToCommitCtxFromCacheCtx == nil { s.cacheCtx, s.writeToCommitCtxFromCacheCtx = s.ctx.CacheContext() } - return s.cacheCtx, PrecompileSnapshotBeforeRun{ - MultiStore: s.cacheCtx.MultiStore().CacheMultiStore(), + return s.cacheCtx, PrecompileCalled{ + MultiStore: s.cacheCtx.MultiStore().(store.CacheMultiStore).Copy(), Events: s.cacheCtx.EventManager().Events(), Precompile: precompileAddr, } } // SavePrecompileSnapshotToJournal adds a snapshot of the commit multistore -// ([PrecompileSnapshotBeforeRun]) to the [StateDB] journal at the end of +// ([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 [PrecompileSnapshotBeforeRun] for more info. +// See [PrecompileCalled] for more info. func (s *StateDB) SavePrecompileSnapshotToJournal( precompileAddr common.Address, - snapshot PrecompileSnapshotBeforeRun, + snapshot PrecompileCalled, ) error { obj := s.getOrNewStateObject(precompileAddr) obj.db.Journal.append(snapshot)