Skip to content

Commit e6fe094

Browse files
authored
feat(x/vm): apply stack-based StateDB snapshot mechanism for precompile call (#244)
* feat(x/vm): add cacheStack for CacheMultiStore snapshot * fix(x/vm): StateDB.snapshotter * feat(x/vm): add keys field to x/vm keeper and use keys for snapshot * deps: remove cosmossdk.io/store fork * refactor(x/vm): refactor snapshot multi store and add comments * test(x/vm): add benchmark test for snapshotmulti.Store * test(precompiles): add edge case test for precompile * refactor(x/vm): modify snapshot stores dep(evmd): go mod tidy refactor: change package name of stack based store refactor(x/vm/store): modify type casting refactor(x/vm): rename store packages * fix(x/vm): order store keys for snapshot store * chore(x/vm/store): modify comments * chore(x/vm/store): add comments * test(x/vm/store): add unit tests * chore(x/vm): fix lint * chore: fix lint * chore: fix lint * chore(x/vm/store): remove unnecessary function call * test: modify TestCMS * fix(tests): precompile test case that intermittently fails * chore(x/vm/store): rename variable cms into snapshotStore in test code * chore: resolve merge conflict * chore(x/vm) fix lint * chore: fix typo * chore: modify comment * chore(x/vm/store): add comments * test(x/vm/store): add test case for overwrite to same key
1 parent 029ed3b commit e6fe094

File tree

23 files changed

+889
-44
lines changed

23 files changed

+889
-44
lines changed

contracts/solidity/precompiles/testutil/contracts/StakingReverter.sol

Lines changed: 14 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -31,6 +31,20 @@ contract StakingReverter {
3131
}
3232
}
3333

34+
function callPrecompileBeforeAndAfterRevert(uint numTimes, string calldata validatorAddress) external {
35+
STAKING_CONTRACT.delegate(address(this), validatorAddress, 10);
36+
37+
for (uint i = 0; i < numTimes; i++) {
38+
try
39+
StakingReverter(address(this)).performDelegation(
40+
validatorAddress
41+
)
42+
{} catch {}
43+
}
44+
45+
STAKING_CONTRACT.delegate(address(this), validatorAddress, 10);
46+
}
47+
3448
function performDelegation(string calldata validatorAddress) external {
3549
STAKING_CONTRACT.delegate(address(this), validatorAddress, 10);
3650
revert();

evmd/app.go

Lines changed: 2 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -30,6 +30,7 @@ import (
3030
feemarketkeeper "github.com/cosmos/evm/x/feemarket/keeper"
3131
feemarkettypes "github.com/cosmos/evm/x/feemarket/types"
3232
ibccallbackskeeper "github.com/cosmos/evm/x/ibc/callbacks/keeper"
33+
3334
// NOTE: override ICS20 keeper to support IBC transfers of ERC20 tokens
3435
"github.com/cosmos/evm/x/ibc/transfer"
3536
transferkeeper "github.com/cosmos/evm/x/ibc/transfer/keeper"
@@ -481,7 +482,7 @@ func NewExampleApp(
481482
// NOTE: it's required to set up the EVM keeper before the ERC-20 keeper, because it is used in its instantiation.
482483
app.EVMKeeper = evmkeeper.NewKeeper(
483484
// TODO: check why this is not adjusted to use the runtime module methods like SDK native keepers
484-
appCodec, keys[evmtypes.StoreKey], tkeys[evmtypes.TransientKey],
485+
appCodec, keys[evmtypes.StoreKey], tkeys[evmtypes.TransientKey], keys,
485486
authtypes.NewModuleAddress(govtypes.ModuleName),
486487
app.AccountKeeper,
487488
app.PreciseBankKeeper,

evmd/go.mod

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -13,6 +13,7 @@ require (
1313
cosmossdk.io/tools/confix v0.1.2
1414
cosmossdk.io/x/evidence v0.2.0
1515
cosmossdk.io/x/feegrant v0.2.0
16+
cosmossdk.io/x/tx v0.14.0
1617
cosmossdk.io/x/upgrade v0.2.0
1718
github.com/cometbft/cometbft v0.38.17
1819
github.com/cosmos/cosmos-db v1.1.3
@@ -43,7 +44,6 @@ require (
4344
cosmossdk.io/collections v1.2.1 // indirect
4445
cosmossdk.io/depinject v1.2.1 // indirect
4546
cosmossdk.io/schema v1.1.0 // indirect
46-
cosmossdk.io/x/tx v0.14.0 // indirect
4747
filippo.io/edwards25519 v1.1.0 // indirect
4848
github.com/99designs/go-keychain v0.0.0-20191008050251-8e49817e8af4 // indirect
4949
github.com/99designs/keyring v1.2.2 // indirect

precompiles/testutil/contracts/StakingReverter.json

Lines changed: 20 additions & 2 deletions
Large diffs are not rendered by default.

precompiles/testutil/contracts/StakingReverter.sol

Lines changed: 18 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -31,6 +31,24 @@ contract StakingReverter {
3131
}
3232
}
3333

34+
/// @dev callPrecompileBeforeAndAfterRevert tests whether precompile calls that occur
35+
/// before and after an intentionally ignored revert correctly modify the state.
36+
/// This method assumes that the StakingReverter.sol contract holds a native balance.
37+
/// Therefore, in order to call this method, the contract must be funded with a balance in advance.
38+
function callPrecompileBeforeAndAfterRevert(uint numTimes, string calldata validatorAddress) external {
39+
STAKING_CONTRACT.delegate(address(this), validatorAddress, 10);
40+
41+
for (uint i = 0; i < numTimes; i++) {
42+
try
43+
StakingReverter(address(this)).performDelegation(
44+
validatorAddress
45+
)
46+
{} catch {}
47+
}
48+
49+
STAKING_CONTRACT.delegate(address(this), validatorAddress, 10);
50+
}
51+
3452
function performDelegation(string calldata validatorAddress) external {
3553
STAKING_CONTRACT.delegate(address(this), validatorAddress, 10);
3654
revert();

tests/integration/precompiles/distribution/test_distribution.go

Lines changed: 8 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -497,13 +497,18 @@ func (s *PrecompileTestSuite) TestCMS() {
497497
if tc.expPass {
498498
s.Require().NoError(err, "expected no error when running the precompile")
499499
s.Require().NotNil(resp.Ret, "expected returned bytes not to be nil")
500-
testutil.ValidateWrites(s.T(), cms, 2)
500+
// NOTES: After stack-based snapshot mechanism is added for precompile call,
501+
// CacheMultiStore.Write() is always called once when tx succeeds.
502+
// It is because CacheMultiStore() is not called when creating snapshot for MultiStore,
503+
// Count of Write() is not accumulated.
504+
testutil.ValidateWrites(s.T(), cms, 1)
501505
} else {
502506
s.Require().Error(err, "expected error to be returned when running the precompile")
503507
s.Require().Nil(resp.Ret, "expected returned bytes to be nil")
504508
s.Require().ErrorContains(err, tc.errContains)
505-
// Writes once because of gas usage
506-
testutil.ValidateWrites(s.T(), cms, 1)
509+
// NOTES: After stack-based snapshot mechanism is added for precompile call,
510+
// CacheMultiStore.Write() is not called when tx fails.
511+
testutil.ValidateWrites(s.T(), cms, 0)
507512
}
508513
})
509514
}

tests/integration/precompiles/distribution/test_integration.go

Lines changed: 3 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -2274,7 +2274,7 @@ func TestPrecompileIntegrationTestSuite(t *testing.T, create network.CreateEvmAp
22742274

22752275
// set gas such that the internal keeper function called by the precompile fails out mid-execution
22762276
txArgs.GasLimit = 80_000
2277-
_, _, err = s.factory.CallContractAndCheckLogs(
2277+
_, txRes, err := s.factory.CallContractAndCheckLogs(
22782278
s.keyring.GetPrivKey(0),
22792279
txArgs,
22802280
callArgs,
@@ -2286,7 +2286,8 @@ func TestPrecompileIntegrationTestSuite(t *testing.T, create network.CreateEvmAp
22862286
balRes, err := s.grpcHandler.GetBalanceFromBank(s.keyring.GetAccAddr(0), s.bondDenom)
22872287
Expect(err).To(BeNil())
22882288
finalBalance := balRes.Balance
2289-
expectedGasCost := math.NewInt(79_416_000_000_000)
2289+
2290+
expectedGasCost := math.NewIntFromUint64(txRes.GasUsed).Mul(math.NewIntFromBigInt(txArgs.GasPrice))
22902291
Expect(finalBalance.Amount.Equal(initialBalance.Amount.Sub(expectedGasCost))).To(BeTrue(), "expected final balance must be initial balance minus any gas spent")
22912292

22922293
res, err = s.grpcHandler.GetDelegationTotalRewards(s.keyring.GetAccAddr(0).String())

tests/integration/precompiles/staking/test_integration.go

Lines changed: 51 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1834,6 +1834,57 @@ func TestPrecompileIntegrationTestSuite(t *testing.T, create network.CreateEvmAp
18341834
Expect(err).NotTo(BeNil())
18351835
Expect(err.Error()).To(ContainSubstring("not found"), "expected NO delegation created")
18361836
})
1837+
1838+
It("should delegate before and after intentionaly ignored delegation revert - successful tx", func() {
1839+
delegationAmount := math.NewInt(10)
1840+
expectedDelegationAmount := delegationAmount.Add(delegationAmount)
1841+
1842+
callArgs := testutiltypes.CallArgs{
1843+
ContractABI: stakingReverterContract.ABI,
1844+
MethodName: "callPrecompileBeforeAndAfterRevert",
1845+
Args: []interface{}{
1846+
big.NewInt(5), s.network.GetValidators()[0].OperatorAddress,
1847+
},
1848+
}
1849+
1850+
delegateCheck := passCheck.WithExpEvents(staking.EventTypeDelegate, staking.EventTypeDelegate)
1851+
1852+
// The transaction should succeed with delegations occurring both before and after the intended revert.
1853+
// The revert itself is not propagated because it occurs within the scope of a try-catch statement,
1854+
// but is not caught by the catch block.
1855+
res, _, err := s.factory.CallContractAndCheckLogs(
1856+
s.keyring.GetPrivKey(0),
1857+
evmtypes.EvmTxArgs{
1858+
To: &stkReverterAddr,
1859+
GasPrice: gasPrice.BigInt(),
1860+
},
1861+
callArgs,
1862+
delegateCheck,
1863+
)
1864+
Expect(err).To(BeNil(), "error while calling the smart contract: %v", err)
1865+
Expect(s.network.NextBlock()).To(BeNil())
1866+
1867+
fees := gasPrice.MulRaw(res.GasUsed)
1868+
1869+
// delegation should have been created
1870+
qRes, err := s.grpcHandler.GetDelegation(sdk.AccAddress(stkReverterAddr.Bytes()).String(), s.network.GetValidators()[0].OperatorAddress)
1871+
Expect(err).To(BeNil())
1872+
Expect(qRes.DelegationResponse.Delegation.GetDelegatorAddr()).To(Equal(sdk.AccAddress(stkReverterAddr.Bytes()).String()), "expected delegator address is equal to contract address")
1873+
Expect(qRes.DelegationResponse.Delegation.GetShares().BigInt()).To(Equal(expectedDelegationAmount.BigInt()), "expected different delegation shares")
1874+
1875+
// contract balance should be deducted by delegation amount
1876+
balRes, err := s.grpcHandler.GetBalanceFromBank(stkReverterAddr.Bytes(), s.bondDenom)
1877+
Expect(err).To(BeNil())
1878+
contractFinalBalance := balRes.Balance
1879+
Expect(contractFinalBalance.Amount).To(Equal(contractInitialBalance.Amount.Sub(expectedDelegationAmount)))
1880+
1881+
// fees deducted on tx sender.
1882+
// delegation amount is deducted on contract balance that is previously funded.
1883+
balRes, err = s.grpcHandler.GetBalanceFromBank(s.keyring.GetAccAddr(0), s.bondDenom)
1884+
Expect(err).To(BeNil())
1885+
txSenderFinalBal := balRes.Balance
1886+
Expect(txSenderFinalBal.Amount).To(Equal(txSenderInitialBal.Amount.Sub(fees)), "expected tx sender balance to be deducted by fees")
1887+
})
18371888
})
18381889

18391890
Context("Table-driven tests for Delegate method", func() {

tests/integration/precompiles/staking/test_staking.go

Lines changed: 8 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -777,7 +777,11 @@ func (s *PrecompileTestSuite) TestCMS() {
777777
if tc.expPass {
778778
s.Require().NoError(err, "expected no error when running the precompile")
779779
s.Require().NotNil(resp.Ret, "expected returned bytes not to be nil")
780-
testutil.ValidateWrites(s.T(), cms, 2)
780+
// NOTES: After stack-based snapshot mechanism is added for precompile call,
781+
// CacheMultiStore.Write() is always called once when tx succeeds.
782+
// It is because CacheMultiStore() is not called when creating snapshot for MultiStore,
783+
// Count of Write() is not accumulated.
784+
testutil.ValidateWrites(s.T(), cms, 1)
781785
} else {
782786
if tc.expKeeperPass {
783787
s.Require().Contains(resp.VmError, tc.errContains,
@@ -786,8 +790,9 @@ func (s *PrecompileTestSuite) TestCMS() {
786790
consumed := ctx.GasMeter().GasConsumed()
787791
// LessThanOrEqual because the gas is consumed before the error is returned
788792
s.Require().LessOrEqual(tc.gas, consumed, "expected gas consumed to be equal to gas limit")
789-
// Writes once because of gas usage
790-
testutil.ValidateWrites(s.T(), cms, 1)
793+
// NOTES: After stack-based snapshot mechanism is added for precompile call,
794+
// CacheMultiStore.Write() is not called when tx fails.
795+
testutil.ValidateWrites(s.T(), cms, 0)
791796
} else {
792797
s.Require().Error(err, "expected error to be returned when running the precompile")
793798
s.Require().Nil(resp, "expected returned response to be nil")

x/vm/keeper/keeper.go

Lines changed: 10 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -40,6 +40,9 @@ type Keeper struct {
4040
// key to access the transient store, which is reset on every block during Commit
4141
transientKey storetypes.StoreKey
4242

43+
// KVStore Keys for modules wired to app
44+
storeKeys map[string]*storetypes.KVStoreKey
45+
4346
// the address capable of executing a MsgUpdateParams message. Typically, this should be the x/gov module account.
4447
authority sdk.AccAddress
4548

@@ -73,6 +76,7 @@ type Keeper struct {
7376
func NewKeeper(
7477
cdc codec.BinaryCodec,
7578
storeKey, transientKey storetypes.StoreKey,
79+
keys map[string]*storetypes.KVStoreKey,
7680
authority sdk.AccAddress,
7781
ak types.AccountKeeper,
7882
bankKeeper types.BankKeeper,
@@ -106,6 +110,7 @@ func NewKeeper(
106110
transientKey: transientKey,
107111
tracer: tracer,
108112
erc20Keeper: erc20Keeper,
113+
storeKeys: keys,
109114
}
110115
}
111116

@@ -351,3 +356,8 @@ func (k Keeper) AddTransientGasUsed(ctx sdk.Context, gasUsed uint64) (uint64, er
351356
k.SetTransientGasUsed(ctx, result)
352357
return result, nil
353358
}
359+
360+
// KVStoreKeys returns KVStore keys injected to keeper
361+
func (k Keeper) KVStoreKeys() map[string]*storetypes.KVStoreKey {
362+
return k.storeKeys
363+
}

0 commit comments

Comments
 (0)