diff --git a/Makefile b/Makefile index 82e9c89a0a..e2d34c31f4 100644 --- a/Makefile +++ b/Makefile @@ -409,7 +409,7 @@ test-sim-fullappsimulation: $(call run-sim-test,"TestFullAppSimulation",TestFullAppSimulation,100,200,30m) test-sim-import-export: - $(call run-sim-test,"test-import-export",TestAppImportExport,100,200,30m) + $(call run-sim-test,"test-import-export",TestAppImportExport,50,100,30m) test-sim-after-import: $(call run-sim-test,"test-sim-after-import",TestAppSimulationAfterImport,100,200,30m) diff --git a/app/app.go b/app/app.go index 90bbe8576b..c79552bc74 100644 --- a/app/app.go +++ b/app/app.go @@ -481,6 +481,8 @@ func New( app.SlashingKeeper, app.AuthorityKeeper, app.LightclientKeeper, + app.BankKeeper, + app.AccountKeeper, authtypes.NewModuleAddress(govtypes.ModuleName).String(), ) diff --git a/app/modules.go b/app/modules.go index 6f164a9409..0feeb7cb9a 100644 --- a/app/modules.go +++ b/app/modules.go @@ -178,5 +178,8 @@ func simulationModules( evm.NewAppModule(app.EvmKeeper, app.AccountKeeper, app.GetSubspace(evmtypes.ModuleName)), authzmodule.NewAppModule(appCodec, app.AuthzKeeper, app.AccountKeeper, app.BankKeeper, app.interfaceRegistry), groupmodule.NewAppModule(appCodec, app.GroupKeeper, app.AccountKeeper, app.BankKeeper, app.interfaceRegistry), + crosschainmodule.NewAppModule(appCodec, app.CrosschainKeeper), + observermodule.NewAppModule(appCodec, *app.ObserverKeeper), + fungiblemodule.NewAppModule(appCodec, app.FungibleKeeper), } } diff --git a/changelog.md b/changelog.md index 45eb187472..791af078fe 100644 --- a/changelog.md +++ b/changelog.md @@ -10,6 +10,7 @@ * [3205](https://github.com/zeta-chain/node/issues/3205) - move Bitcoin revert address test to advanced group to avoid upgrade test failure * [3254](https://github.com/zeta-chain/node/pull/3254) - rename v2 E2E tests as evm tests and rename old evm tests as legacy +* [3095](https://github.com/zeta-chain/node/pull/3095) - initialize simulation tests for custom zetachain modules ## Refactor diff --git a/codecov.yml b/codecov.yml index da90e44bd9..092dd4baa4 100644 --- a/codecov.yml +++ b/codecov.yml @@ -52,7 +52,7 @@ ignore: - "x/**/events.go" - "x/**/migrator.go" - "x/**/module_simulation.go" - - "x/**/simulation/**/*" + - "x/**/simulation/*.go" - "**/*.proto" - "**/*.md" - "**/*.yml" diff --git a/contrib/docker-scripts/start.sh b/contrib/docker-scripts/start.sh index 6d79effa57..bb151a49fe 100644 --- a/contrib/docker-scripts/start.sh +++ b/contrib/docker-scripts/start.sh @@ -332,4 +332,4 @@ else logt "Start Network" start_network -fi +fi \ No newline at end of file diff --git a/server/start.go b/server/start.go index 14c64482f8..9e2711d265 100644 --- a/server/start.go +++ b/server/start.go @@ -172,7 +172,7 @@ which accepts a path for the resulting pprof file. cmd.Flags(). String(server.FlagMinGasPrices, "", "Minimum gas prices to accept for transactions; Any fee in a tx must meet this minimum (e.g. 0.01photon;0.0001stake)") - //nolint:lll + //nolint:lll cmd.Flags(). IntSlice(server.FlagUnsafeSkipUpgrades, []int{}, "Skip a set of upgrade heights to continue the old binary") cmd.Flags(). @@ -189,7 +189,7 @@ which accepts a path for the resulting pprof file. cmd.Flags(). Uint64(server.FlagPruningInterval, 0, "Height interval at which pruned heights are removed from disk (ignored if pruning is not 'custom')") - //nolint:lll + //nolint:lll cmd.Flags().Uint(server.FlagInvCheckPeriod, 0, "Assert registered invariants every N blocks") cmd.Flags(). Uint64(server.FlagMinRetainBlocks, 0, "Minimum block height offset during ABCI commit to prune Tendermint blocks") @@ -217,11 +217,11 @@ which accepts a path for the resulting pprof file. cmd.Flags(). Uint64(srvflags.JSONRPCGasCap, config.DefaultGasCap, "Sets a cap on gas that can be used in eth_call/estimateGas unit is aphoton (0=infinite)") - //nolint:lll + //nolint:lll cmd.Flags(). Float64(srvflags.JSONRPCTxFeeCap, config.DefaultTxFeeCap, "Sets a cap on transaction fee that can be sent via the RPC APIs (1 = default 1 photon)") - //nolint:lll + //nolint:lll cmd.Flags(). Int32(srvflags.JSONRPCFilterCap, config.DefaultFilterCap, "Sets the global cap for total number of filters that can be created") cmd.Flags(). @@ -233,7 +233,7 @@ which accepts a path for the resulting pprof file. cmd.Flags(). Bool(srvflags.JSONRPCAllowUnprotectedTxs, config.DefaultAllowUnprotectedTxs, "Allow for unprotected (non EIP155 signed) transactions to be submitted via the node's RPC when the global parameter is disabled") - //nolint:lll + //nolint:lll cmd.Flags(). Int32(srvflags.JSONRPCLogsCap, config.DefaultLogsCap, "Sets the max number of results can be returned from single `eth_getLogs` query") cmd.Flags(). @@ -241,18 +241,18 @@ which accepts a path for the resulting pprof file. cmd.Flags(). Int(srvflags.JSONRPCMaxOpenConnections, config.DefaultMaxOpenConnections, "Sets the maximum number of simultaneous connections for the server listener") - //nolint:lll + //nolint:lll cmd.Flags().Bool(srvflags.JSONRPCEnableIndexer, false, "Enable the custom tx indexer for json-rpc") cmd.Flags().Bool(srvflags.JSONRPCEnableMetrics, false, "Define if EVM rpc metrics server should be enabled") cmd.Flags(). String(srvflags.EVMTracer, config.DefaultEVMTracer, "the EVM tracer type to collect execution traces from the EVM transaction execution (json|struct|access_list|markdown)") - //nolint:lll + //nolint:lll cmd.Flags(). Uint64(srvflags.EVMMaxTxGasWanted, config.DefaultMaxTxGasWanted, "the gas wanted for each eth tx returned in ante handler in check tx mode") - //nolint:lll + //nolint:lll cmd.Flags().String(srvflags.TLSCertPath, "", "the cert.pem file path for the server TLS configuration") cmd.Flags().String(srvflags.TLSKeyPath, "", "the key.pem file path for the server TLS configuration") diff --git a/simulation/simulation_test.go b/simulation/simulation_test.go index 3f39c77fa1..818250cbaf 100644 --- a/simulation/simulation_test.go +++ b/simulation/simulation_test.go @@ -10,29 +10,30 @@ import ( abci "github.com/cometbft/cometbft/abci/types" tmproto "github.com/cometbft/cometbft/proto/tendermint/types" + "github.com/cosmos/cosmos-sdk/baseapp" + "github.com/cosmos/cosmos-sdk/client/flags" + "github.com/cosmos/cosmos-sdk/server" "github.com/cosmos/cosmos-sdk/store" storetypes "github.com/cosmos/cosmos-sdk/store/types" + cosmossimutils "github.com/cosmos/cosmos-sdk/testutil/sims" sdk "github.com/cosmos/cosmos-sdk/types" - authtypes "github.com/cosmos/cosmos-sdk/x/auth/types" + cosmossim "github.com/cosmos/cosmos-sdk/types/simulation" banktypes "github.com/cosmos/cosmos-sdk/x/bank/types" distrtypes "github.com/cosmos/cosmos-sdk/x/distribution/types" evidencetypes "github.com/cosmos/cosmos-sdk/x/evidence/types" govtypes "github.com/cosmos/cosmos-sdk/x/gov/types" paramtypes "github.com/cosmos/cosmos-sdk/x/params/types" + "github.com/cosmos/cosmos-sdk/x/simulation" + cosmossimcli "github.com/cosmos/cosmos-sdk/x/simulation/client/cli" slashingtypes "github.com/cosmos/cosmos-sdk/x/slashing/types" stakingtypes "github.com/cosmos/cosmos-sdk/x/staking/types" "github.com/stretchr/testify/require" evmtypes "github.com/zeta-chain/ethermint/x/evm/types" "github.com/zeta-chain/node/app" zetasimulation "github.com/zeta-chain/node/simulation" - - "github.com/cosmos/cosmos-sdk/baseapp" - "github.com/cosmos/cosmos-sdk/client/flags" - "github.com/cosmos/cosmos-sdk/server" - cosmossimutils "github.com/cosmos/cosmos-sdk/testutil/sims" - cosmossim "github.com/cosmos/cosmos-sdk/types/simulation" - "github.com/cosmos/cosmos-sdk/x/simulation" - cosmossimcli "github.com/cosmos/cosmos-sdk/x/simulation/client/cli" + crosschaintypes "github.com/zeta-chain/node/x/crosschain/types" + fungibletypes "github.com/zeta-chain/node/x/fungible/types" + observertypes "github.com/zeta-chain/node/x/observer/types" ) // AppChainID hardcoded chainID for simulation @@ -41,10 +42,12 @@ func init() { zetasimulation.GetSimulatorFlags() } +// StoreKeysPrefixes defines a struct used in comparing two keys for two different stores +// SkipPrefixes is used to skip certain prefixes when comparing the stores type StoreKeysPrefixes struct { - A storetypes.StoreKey - B storetypes.StoreKey - Prefixes [][]byte + A storetypes.StoreKey + B storetypes.StoreKey + SkipPrefixes [][]byte } const ( @@ -133,9 +136,11 @@ func TestAppStateDeterminism(t *testing.T) { os.Stdout, simApp.BaseApp, zetasimulation.AppStateFn( + t, simApp.AppCodec(), simApp.SimulationManager(), simApp.BasicManager().DefaultGenesis(simApp.AppCodec()), + nil, ), cosmossim.RandomAccounts, cosmossimutils.SimulationOperations(simApp, simApp.AppCodec(), config), @@ -222,9 +227,11 @@ func TestFullAppSimulation(t *testing.T) { os.Stdout, simApp.BaseApp, zetasimulation.AppStateFn( + t, simApp.AppCodec(), simApp.SimulationManager(), simApp.BasicManager().DefaultGenesis(simApp.AppCodec()), + nil, ), cosmossim.RandomAccounts, cosmossimutils.SimulationOperations(simApp, simApp.AppCodec(), config), @@ -292,9 +299,11 @@ func TestAppImportExport(t *testing.T) { os.Stdout, simApp.BaseApp, zetasimulation.AppStateFn( + t, simApp.AppCodec(), simApp.SimulationManager(), simApp.BasicManager().DefaultGenesis(simApp.AppCodec()), + nil, ), cosmossim.RandomAccounts, cosmossimutils.SimulationOperations(simApp, simApp.AppCodec(), config), @@ -314,7 +323,6 @@ func TestAppImportExport(t *testing.T) { exported, err := simApp.ExportAppStateAndValidators(false, []string{}, []string{}) require.NoError(t, err) - t.Log("importing genesis") newDB, newDir, _, _, err := cosmossimutils.SetupSimulation( config, SimDBBackend+"_new", @@ -360,20 +368,29 @@ func TestAppImportExport(t *testing.T) { ctxSimApp := simApp.NewContext(true, tmproto.Header{ Height: simApp.LastBlockHeight(), ChainID: SimAppChainID, - }).WithChainID(SimAppChainID) + }) + ctxNewSimApp := newSimApp.NewContext(true, tmproto.Header{ Height: simApp.LastBlockHeight(), ChainID: SimAppChainID, - }).WithChainID(SimAppChainID) + }) + t.Log("initializing genesis for the new app using exported genesis state") // Use genesis state from the first app to initialize the second app newSimApp.ModuleManager().InitGenesis(ctxNewSimApp, newSimApp.AppCodec(), genesisState) newSimApp.StoreConsensusParams(ctxNewSimApp, exported.ConsensusParams) t.Log("comparing stores") + // The ordering of the keys is not important, we compare the same prefix for both simulations storeKeysPrefixes := []StoreKeysPrefixes{ - {simApp.GetKey(authtypes.StoreKey), newSimApp.GetKey(authtypes.StoreKey), [][]byte{}}, + // Interaction with EVM module, + // such as deploying contracts or interacting with them such as setting gas price, + // causes the state for the auth module to change on export.The order of keys within the store is modified. + // We will need to explore this further to find a definitive answer + // TODO:https://github.com/zeta-chain/node/issues/3263 + + // {simApp.GetKey(authtypes.StoreKey), newSimApp.GetKey(authtypes.StoreKey), [][]byte{}}, { simApp.GetKey(stakingtypes.StoreKey), newSimApp.GetKey(stakingtypes.StoreKey), [][]byte{ @@ -388,13 +405,23 @@ func TestAppImportExport(t *testing.T) { {simApp.GetKey(govtypes.StoreKey), newSimApp.GetKey(govtypes.StoreKey), [][]byte{}}, {simApp.GetKey(evidencetypes.StoreKey), newSimApp.GetKey(evidencetypes.StoreKey), [][]byte{}}, {simApp.GetKey(evmtypes.StoreKey), newSimApp.GetKey(evmtypes.StoreKey), [][]byte{}}, + {simApp.GetKey(crosschaintypes.StoreKey), newSimApp.GetKey(crosschaintypes.StoreKey), [][]byte{ + // We update the timestamp for cctx when importing the genesis state which results in a different value + crosschaintypes.KeyPrefix(crosschaintypes.CCTXKey), + }}, + + {simApp.GetKey(observertypes.StoreKey), newSimApp.GetKey(observertypes.StoreKey), [][]byte{ + // The order of ballots when importing is not preserved which causes the value to be different. + observertypes.KeyPrefix(observertypes.BallotListKey), + }}, + {simApp.GetKey(fungibletypes.StoreKey), newSimApp.GetKey(fungibletypes.StoreKey), [][]byte{}}, } for _, skp := range storeKeysPrefixes { storeA := ctxSimApp.KVStore(skp.A) storeB := ctxNewSimApp.KVStore(skp.B) - failedKVAs, failedKVBs := sdk.DiffKVStores(storeA, storeB, skp.Prefixes) + failedKVAs, failedKVBs := sdk.DiffKVStores(storeA, storeB, skp.SkipPrefixes) require.Equal(t, len(failedKVAs), len(failedKVBs), "unequal sets of key-values to compare") t.Logf("compared %d different key/value pairs between %s and %s\n", len(failedKVAs), skp.A, skp.B) @@ -459,9 +486,11 @@ func TestAppSimulationAfterImport(t *testing.T) { os.Stdout, simApp.BaseApp, zetasimulation.AppStateFn( + t, simApp.AppCodec(), simApp.SimulationManager(), simApp.BasicManager().DefaultGenesis(simApp.AppCodec()), + nil, ), cosmossim.RandomAccounts, cosmossimutils.SimulationOperations(simApp, simApp.AppCodec(), config), @@ -487,8 +516,6 @@ func TestAppSimulationAfterImport(t *testing.T) { exported, err := simApp.ExportAppStateAndValidators(true, []string{}, []string{}) require.NoError(t, err) - t.Log("importing genesis") - newDB, newDir, _, _, err := cosmossimutils.SetupSimulation( config, SimDBBackend+"_new", @@ -517,6 +544,7 @@ func TestAppSimulationAfterImport(t *testing.T) { ) require.NoError(t, err) + t.Log("Importing genesis into the new app") newSimApp.InitChain(abci.RequestInitChain{ ChainId: SimAppChainID, AppStateBytes: exported.AppState, @@ -527,9 +555,11 @@ func TestAppSimulationAfterImport(t *testing.T) { os.Stdout, newSimApp.BaseApp, zetasimulation.AppStateFn( - simApp.AppCodec(), - simApp.SimulationManager(), - simApp.BasicManager().DefaultGenesis(simApp.AppCodec()), + t, + nil, + nil, + nil, + exported.AppState, ), cosmossim.RandomAccounts, cosmossimutils.SimulationOperations(newSimApp, newSimApp.AppCodec(), config), diff --git a/simulation/state.go b/simulation/state.go index 540cd0aca7..9bf2c24949 100644 --- a/simulation/state.go +++ b/simulation/state.go @@ -6,6 +6,7 @@ import ( "io" "math/rand" "os" + "testing" "time" "cosmossdk.io/math" @@ -19,24 +20,228 @@ import ( authtypes "github.com/cosmos/cosmos-sdk/x/auth/types" banktypes "github.com/cosmos/cosmos-sdk/x/bank/types" stakingtypes "github.com/cosmos/cosmos-sdk/x/staking/types" + "github.com/stretchr/testify/require" evmtypes "github.com/zeta-chain/ethermint/x/evm/types" zetaapp "github.com/zeta-chain/node/app" + "github.com/zeta-chain/node/testutil/sample" + authoritytypes "github.com/zeta-chain/node/x/authority/types" + fungibletypes "github.com/zeta-chain/node/x/fungible/types" + observertypes "github.com/zeta-chain/node/x/observer/types" ) -// Simulation parameter constants +// simulation parameter constants const ( StakePerAccount = "stake_per_account" InitiallyBondedValidators = "initially_bonded_validators" ) +func updateBankState( + t *testing.T, + rawState map[string]json.RawMessage, + cdc codec.Codec, + notBondedCoins sdk.Coin, +) *banktypes.GenesisState { + bankStateBz, ok := rawState[banktypes.ModuleName] + require.True(t, ok, "bank genesis state is missing") + + bankState := new(banktypes.GenesisState) + err := cdc.UnmarshalJSON(bankStateBz, bankState) + require.NoError(t, err) + + stakingAddr := authtypes.NewModuleAddress(stakingtypes.NotBondedPoolName).String() + var found bool + for _, balance := range bankState.Balances { + if balance.Address == stakingAddr { + found = true + break + } + } + if !found { + bankState.Balances = append(bankState.Balances, banktypes.Balance{ + Address: stakingAddr, + Coins: sdk.NewCoins(notBondedCoins), + }) + } + + return bankState +} + +func updateEVMState( + t *testing.T, + rawState map[string]json.RawMessage, + cdc codec.Codec, + bondDenom string, +) *evmtypes.GenesisState { + evmStateBz, ok := rawState[evmtypes.ModuleName] + require.True(t, ok, "evm genesis state is missing") + + evmState := new(evmtypes.GenesisState) + cdc.MustUnmarshalJSON(evmStateBz, evmState) + + // replace the EvmDenom with BondDenom + evmState.Params.EvmDenom = bondDenom + + return evmState +} + +func updateStakingState( + t *testing.T, + rawState map[string]json.RawMessage, + cdc codec.Codec, +) (*stakingtypes.GenesisState, sdk.Coin) { + stakingStateBz, ok := rawState[stakingtypes.ModuleName] + require.True(t, ok, "staking genesis state is missing") + + stakingState := new(stakingtypes.GenesisState) + err := cdc.UnmarshalJSON(stakingStateBz, stakingState) + if err != nil { + panic(err) + } + + // compute not bonded balance + notBondedTokens := math.ZeroInt() + for _, val := range stakingState.Validators { + if val.Status != stakingtypes.Unbonded { + continue + } + notBondedTokens = notBondedTokens.Add(val.GetTokens()) + } + notBondedCoins := sdk.NewCoin(stakingState.Params.BondDenom, notBondedTokens) + + return stakingState, notBondedCoins +} + +func updateObserverState( + t *testing.T, + rawState map[string]json.RawMessage, + cdc codec.Codec, + r *rand.Rand, + validators stakingtypes.Validators, +) *observertypes.GenesisState { + observerStateBz, ok := rawState[observertypes.ModuleName] + require.True(t, ok, "observer genesis state is missing") + + observerState := new(observertypes.GenesisState) + cdc.MustUnmarshalJSON(observerStateBz, observerState) + + observers := make([]string, 0) + for _, validator := range validators { + accAddress, err := observertypes.GetAccAddressFromOperatorAddress(validator.OperatorAddress) + if err != nil { + continue + } + observers = append(observers, accAddress.String()) + } + + r.Shuffle(len(observers), func(i, j int) { + observers[i], observers[j] = observers[j], observers[i] + }) + + numObservers := r.Intn(11) + 5 + if numObservers > len(observers) { + numObservers = len(observers) + } + observers = observers[:numObservers] + + observerState.Observers.ObserverList = observers + observerState.CrosschainFlags.IsInboundEnabled = true + observerState.CrosschainFlags.IsOutboundEnabled = true + + tss := sample.TSSFromRand(t, r) + tss.OperatorAddressList = observers + observerState.Tss = &tss + + return observerState +} + +func updateAuthorityState( + t *testing.T, + rawState map[string]json.RawMessage, + cdc codec.Codec, + r *rand.Rand, + accs []simtypes.Account, +) *authoritytypes.GenesisState { + authorityStateBz, ok := rawState[authoritytypes.ModuleName] + require.True(t, ok, "authority genesis state is missing") + + authorityState := new(authoritytypes.GenesisState) + cdc.MustUnmarshalJSON(authorityStateBz, authorityState) + + randomAccount := accs[r.Intn(len(accs))] + policies := authoritytypes.Policies{ + Items: []*authoritytypes.Policy{ + { + Address: randomAccount.Address.String(), + PolicyType: authoritytypes.PolicyType_groupEmergency, + }, + { + Address: randomAccount.Address.String(), + PolicyType: authoritytypes.PolicyType_groupAdmin, + }, + { + Address: randomAccount.Address.String(), + PolicyType: authoritytypes.PolicyType_groupOperational, + }, + }, + } + authorityState.Policies = policies + + return authorityState +} + +func updateFungibleState( + t *testing.T, + rawState map[string]json.RawMessage, + cdc codec.Codec, + r *rand.Rand, +) *fungibletypes.GenesisState { + fungibleStateBz, ok := rawState[fungibletypes.ModuleName] + require.True(t, ok, "fungible genesis state is missing") + + fungibleState := new(fungibletypes.GenesisState) + cdc.MustUnmarshalJSON(fungibleStateBz, fungibleState) + fungibleState.SystemContract = &fungibletypes.SystemContract{ + SystemContract: sample.EthAddressFromRand(r).String(), + ConnectorZevm: sample.EthAddressFromRand(r).String(), + Gateway: sample.EthAddressFromRand(r).String(), + } + + return fungibleState +} + +func updateRawState( + t *testing.T, + rawState map[string]json.RawMessage, + cdc codec.Codec, + r *rand.Rand, + accs []simtypes.Account, +) { + stakingState, notBondedCoins := updateStakingState(t, rawState, cdc) + bankState := updateBankState(t, rawState, cdc, notBondedCoins) + evmState := updateEVMState(t, rawState, cdc, stakingState.Params.BondDenom) + observerState := updateObserverState(t, rawState, cdc, r, stakingState.Validators) + authorityState := updateAuthorityState(t, rawState, cdc, r, accs) + fungibleState := updateFungibleState(t, rawState, cdc, r) + + rawState[stakingtypes.ModuleName] = cdc.MustMarshalJSON(stakingState) + rawState[banktypes.ModuleName] = cdc.MustMarshalJSON(bankState) + rawState[evmtypes.ModuleName] = cdc.MustMarshalJSON(evmState) + rawState[observertypes.ModuleName] = cdc.MustMarshalJSON(observerState) + rawState[authoritytypes.ModuleName] = cdc.MustMarshalJSON(authorityState) + rawState[fungibletypes.ModuleName] = cdc.MustMarshalJSON(fungibleState) +} + // AppStateFn returns the initial application state using a genesis or the simulation parameters. // It panics if the user provides files for both of them. // If a file is not given for the genesis or the sim params, it creates a randomized one. +// All modifications to the genesis state should be done in this function. func AppStateFn( + t *testing.T, cdc codec.Codec, simManager *module.SimulationManager, genesisState map[string]json.RawMessage, + exportedState json.RawMessage, ) simtypes.AppStateFn { return func(r *rand.Rand, accs []simtypes.Account, config simtypes.Config, ) (appState json.RawMessage, simAccs []simtypes.Account, chainID string, genesisTimestamp time.Time) { @@ -47,135 +252,35 @@ func AppStateFn( } chainID = config.ChainID - switch { - case config.ParamsFile != "" && config.GenesisFile != "": - panic("cannot provide both a genesis file and a params file") - - case config.GenesisFile != "": - // override the default chain-id from simapp to set it later to the config - genesisDoc, accounts, err := AppStateFromGenesisFileFn(r, cdc, config.GenesisFile) - if err != nil { - panic(err) - } - - if FlagGenesisTimeValue == 0 { - // use genesis timestamp if no custom timestamp is provided (i.e no random timestamp) - genesisTimestamp = genesisDoc.GenesisTime - } - - appState = genesisDoc.AppState - chainID = genesisDoc.ChainID - simAccs = accounts - - case config.ParamsFile != "": - appParams := make(simtypes.AppParams) - bz, err := os.ReadFile(config.ParamsFile) - if err != nil { - panic(err) - } - - err = json.Unmarshal(bz, &appParams) - if err != nil { - panic(err) - } - appState, simAccs = AppStateRandomizedFn( - simManager, - r, - cdc, - accs, - genesisTimestamp, - appParams, - genesisState, - ) - - default: - appParams := make(simtypes.AppParams) - appState, simAccs = AppStateRandomizedFn( - simManager, - r, - cdc, - accs, - genesisTimestamp, - appParams, - genesisState, - ) - } - rawState := make(map[string]json.RawMessage) - err := json.Unmarshal(appState, &rawState) - if err != nil { - panic(err) + // if exported state is provided then use it + if exportedState != nil { + return exportedState, accs, chainID, genesisTimestamp } - stakingStateBz, ok := rawState[stakingtypes.ModuleName] - if !ok { - panic("staking genesis state is missing") - } + appParams := make(simtypes.AppParams) + appState, simAccs = AppStateRandomizedFn( + simManager, + r, + cdc, + accs, + genesisTimestamp, + appParams, + genesisState, + ) - stakingState := new(stakingtypes.GenesisState) - err = cdc.UnmarshalJSON(stakingStateBz, stakingState) - if err != nil { - panic(err) - } - - // compute not bonded balance - notBondedTokens := math.ZeroInt() - for _, val := range stakingState.Validators { - if val.Status != stakingtypes.Unbonded { - continue - } - notBondedTokens = notBondedTokens.Add(val.GetTokens()) - } - notBondedCoins := sdk.NewCoin(stakingState.Params.BondDenom, notBondedTokens) - - // edit bank state to make it have the not bonded pool tokens - bankStateBz, ok := rawState[banktypes.ModuleName] - if !ok { - panic("bank genesis state is missing") - } - bankState := new(banktypes.GenesisState) - err = cdc.UnmarshalJSON(bankStateBz, bankState) + rawState := make(map[string]json.RawMessage) + err := json.Unmarshal(appState, &rawState) if err != nil { panic(err) } - stakingAddr := authtypes.NewModuleAddress(stakingtypes.NotBondedPoolName).String() - var found bool - for _, balance := range bankState.Balances { - if balance.Address == stakingAddr { - found = true - break - } - } - if !found { - bankState.Balances = append(bankState.Balances, banktypes.Balance{ - Address: stakingAddr, - Coins: sdk.NewCoins(notBondedCoins), - }) - } - - // Set the bond denom in the EVM genesis state - evmStateBz, ok := rawState[evmtypes.ModuleName] - if !ok { - panic("evm genesis state is missing") - } - - evmState := new(evmtypes.GenesisState) - cdc.MustUnmarshalJSON(evmStateBz, evmState) - - // we should replace the EvmDenom with BondDenom - evmState.Params.EvmDenom = stakingState.Params.BondDenom - - // change appState back - rawState[evmtypes.ModuleName] = cdc.MustMarshalJSON(evmState) - rawState[stakingtypes.ModuleName] = cdc.MustMarshalJSON(stakingState) - rawState[banktypes.ModuleName] = cdc.MustMarshalJSON(bankState) + updateRawState(t, rawState, cdc, r, simAccs) // replace appstate appState, err = json.Marshal(rawState) - if err != nil { - panic(err) - } + require.NoError(t, err) + return appState, simAccs, chainID, genesisTimestamp } } @@ -208,7 +313,7 @@ func AppStateRandomizedFn( numInitiallyBonded = numAccs } - // Set the default power reduction to be one less than the initial stake so that all randomised validators are part of the validator set + // set the default power reduction to be one less than the initial stake so that all randomised validators are part of the validator set sdk.DefaultPowerReduction = initialStake.Sub(sdk.OneInt()) fmt.Printf( diff --git a/testutil/keeper/crosschain.go b/testutil/keeper/crosschain.go index 8418bbdaaf..d0f7d0220c 100644 --- a/testutil/keeper/crosschain.go +++ b/testutil/keeper/crosschain.go @@ -129,6 +129,8 @@ func CrosschainKeeperWithMocks( sdkKeepers.SlashingKeeper, authorityKeeperTmp, lightclientKeeperTmp, + sdkKeepers.BankKeeper, + sdkKeepers.AuthKeeper, authtypes.NewModuleAddress(govtypes.ModuleName).String(), ) diff --git a/testutil/keeper/emissions.go b/testutil/keeper/emissions.go index b2ce7a004c..bfbe2813d9 100644 --- a/testutil/keeper/emissions.go +++ b/testutil/keeper/emissions.go @@ -54,6 +54,8 @@ func EmissionKeeperWithMockOptions( sdkKeepers.StakingKeeper, sdkKeepers.SlashingKeeper, authorityKeeper, + sdkKeepers.BankKeeper, + sdkKeepers.AuthKeeper, initLightclientKeeper(cdc, stateStore, authorityKeeper), ) diff --git a/testutil/keeper/fungible.go b/testutil/keeper/fungible.go index aa0e975c97..5b51d08934 100644 --- a/testutil/keeper/fungible.go +++ b/testutil/keeper/fungible.go @@ -116,6 +116,8 @@ func FungibleKeeperWithMocks( sdkKeepers.SlashingKeeper, authorityKeeperTmp, lightclientKeeperTmp, + sdkKeepers.BankKeeper, + sdkKeepers.AuthKeeper, authtypes.NewModuleAddress(govtypes.ModuleName).String(), ) diff --git a/testutil/keeper/ibccrosschain.go b/testutil/keeper/ibccrosschain.go index 95cf7559e7..5bd222141d 100644 --- a/testutil/keeper/ibccrosschain.go +++ b/testutil/keeper/ibccrosschain.go @@ -77,6 +77,8 @@ func IBCCrosschainKeeperWithMocks( sdkKeepers.StakingKeeper, sdkKeepers.SlashingKeeper, authorityKeeper, + sdkKeepers.BankKeeper, + sdkKeepers.AuthKeeper, lightclientKeeper, ) fungibleKeeper := initFungibleKeeper( diff --git a/testutil/keeper/keeper.go b/testutil/keeper/keeper.go index 5819c6b74b..1342cdfac8 100644 --- a/testutil/keeper/keeper.go +++ b/testutil/keeper/keeper.go @@ -525,6 +525,8 @@ func NewSDKKeepersWithKeys( slashingKeeper, authorityKeeper, lightclientKeeper, + bankKeeper, + authKeeper, authtypes.NewModuleAddress(govtypes.ModuleName).String(), ) emissionsKeeper := emissionskeeper.NewKeeper( diff --git a/testutil/keeper/mocks/crosschain/account.go b/testutil/keeper/mocks/crosschain/account.go index fbd7c0377b..3ff30725e6 100644 --- a/testutil/keeper/mocks/crosschain/account.go +++ b/testutil/keeper/mocks/crosschain/account.go @@ -14,6 +14,26 @@ type CrosschainAccountKeeper struct { mock.Mock } +// GetAccount provides a mock function with given fields: ctx, addr +func (_m *CrosschainAccountKeeper) GetAccount(ctx types.Context, addr types.AccAddress) authtypes.AccountI { + ret := _m.Called(ctx, addr) + + if len(ret) == 0 { + panic("no return value specified for GetAccount") + } + + var r0 authtypes.AccountI + if rf, ok := ret.Get(0).(func(types.Context, types.AccAddress) authtypes.AccountI); ok { + r0 = rf(ctx, addr) + } else { + if ret.Get(0) != nil { + r0 = ret.Get(0).(authtypes.AccountI) + } + } + + return r0 +} + // GetModuleAccount provides a mock function with given fields: ctx, name func (_m *CrosschainAccountKeeper) GetModuleAccount(ctx types.Context, name string) authtypes.ModuleAccountI { ret := _m.Called(ctx, name) diff --git a/testutil/keeper/mocks/crosschain/bank.go b/testutil/keeper/mocks/crosschain/bank.go index 90f4e17e29..7e6a3ae058 100644 --- a/testutil/keeper/mocks/crosschain/bank.go +++ b/testutil/keeper/mocks/crosschain/bank.go @@ -49,6 +49,26 @@ func (_m *CrosschainBankKeeper) MintCoins(ctx types.Context, moduleName string, return r0 } +// SpendableCoins provides a mock function with given fields: ctx, addr +func (_m *CrosschainBankKeeper) SpendableCoins(ctx types.Context, addr types.AccAddress) types.Coins { + ret := _m.Called(ctx, addr) + + if len(ret) == 0 { + panic("no return value specified for SpendableCoins") + } + + var r0 types.Coins + if rf, ok := ret.Get(0).(func(types.Context, types.AccAddress) types.Coins); ok { + r0 = rf(ctx, addr) + } else { + if ret.Get(0) != nil { + r0 = ret.Get(0).(types.Coins) + } + } + + return r0 +} + // NewCrosschainBankKeeper creates a new instance of CrosschainBankKeeper. It also registers a testing interface on the mock and a cleanup function to assert the mocks expectations. // The first argument is typically a *testing.T value. func NewCrosschainBankKeeper(t interface { diff --git a/testutil/keeper/mocks/fungible/authority.go b/testutil/keeper/mocks/fungible/authority.go index 214834ac86..ccf891e5f7 100644 --- a/testutil/keeper/mocks/fungible/authority.go +++ b/testutil/keeper/mocks/fungible/authority.go @@ -3,8 +3,10 @@ package mocks import ( - mock "github.com/stretchr/testify/mock" chains "github.com/zeta-chain/node/pkg/chains" + authoritytypes "github.com/zeta-chain/node/x/authority/types" + + mock "github.com/stretchr/testify/mock" types "github.com/cosmos/cosmos-sdk/types" ) @@ -52,6 +54,34 @@ func (_m *FungibleAuthorityKeeper) GetAdditionalChainList(ctx types.Context) []c return r0 } +// GetPolicies provides a mock function with given fields: ctx +func (_m *FungibleAuthorityKeeper) GetPolicies(ctx types.Context) (authoritytypes.Policies, bool) { + ret := _m.Called(ctx) + + if len(ret) == 0 { + panic("no return value specified for GetPolicies") + } + + var r0 authoritytypes.Policies + var r1 bool + if rf, ok := ret.Get(0).(func(types.Context) (authoritytypes.Policies, bool)); ok { + return rf(ctx) + } + if rf, ok := ret.Get(0).(func(types.Context) authoritytypes.Policies); ok { + r0 = rf(ctx) + } else { + r0 = ret.Get(0).(authoritytypes.Policies) + } + + if rf, ok := ret.Get(1).(func(types.Context) bool); ok { + r1 = rf(ctx) + } else { + r1 = ret.Get(1).(bool) + } + + return r0, r1 +} + // NewFungibleAuthorityKeeper creates a new instance of FungibleAuthorityKeeper. It also registers a testing interface on the mock and a cleanup function to assert the mocks expectations. // The first argument is typically a *testing.T value. func NewFungibleAuthorityKeeper(t interface { diff --git a/testutil/keeper/mocks/fungible/bank.go b/testutil/keeper/mocks/fungible/bank.go index 1c46b35688..26c07bb9ad 100644 --- a/testutil/keeper/mocks/fungible/bank.go +++ b/testutil/keeper/mocks/fungible/bank.go @@ -67,6 +67,26 @@ func (_m *FungibleBankKeeper) SendCoinsFromModuleToAccount(ctx types.Context, se return r0 } +// SpendableCoins provides a mock function with given fields: ctx, addr +func (_m *FungibleBankKeeper) SpendableCoins(ctx types.Context, addr types.AccAddress) types.Coins { + ret := _m.Called(ctx, addr) + + if len(ret) == 0 { + panic("no return value specified for SpendableCoins") + } + + var r0 types.Coins + if rf, ok := ret.Get(0).(func(types.Context, types.AccAddress) types.Coins); ok { + r0 = rf(ctx, addr) + } else { + if ret.Get(0) != nil { + r0 = ret.Get(0).(types.Coins) + } + } + + return r0 +} + // NewFungibleBankKeeper creates a new instance of FungibleBankKeeper. It also registers a testing interface on the mock and a cleanup function to assert the mocks expectations. // The first argument is typically a *testing.T value. func NewFungibleBankKeeper(t interface { diff --git a/testutil/keeper/mocks/observer/authority.go b/testutil/keeper/mocks/observer/authority.go index 24a105fb4d..0bfc08f9fe 100644 --- a/testutil/keeper/mocks/observer/authority.go +++ b/testutil/keeper/mocks/observer/authority.go @@ -54,6 +54,34 @@ func (_m *ObserverAuthorityKeeper) GetAdditionalChainList(ctx types.Context) []c return r0 } +// GetPolicies provides a mock function with given fields: ctx +func (_m *ObserverAuthorityKeeper) GetPolicies(ctx types.Context) (authoritytypes.Policies, bool) { + ret := _m.Called(ctx) + + if len(ret) == 0 { + panic("no return value specified for GetPolicies") + } + + var r0 authoritytypes.Policies + var r1 bool + if rf, ok := ret.Get(0).(func(types.Context) (authoritytypes.Policies, bool)); ok { + return rf(ctx) + } + if rf, ok := ret.Get(0).(func(types.Context) authoritytypes.Policies); ok { + r0 = rf(ctx) + } else { + r0 = ret.Get(0).(authoritytypes.Policies) + } + + if rf, ok := ret.Get(1).(func(types.Context) bool); ok { + r1 = rf(ctx) + } else { + r1 = ret.Get(1).(bool) + } + + return r0, r1 +} + // SetPolicies provides a mock function with given fields: ctx, policies func (_m *ObserverAuthorityKeeper) SetPolicies(ctx types.Context, policies authoritytypes.Policies) { _m.Called(ctx, policies) diff --git a/testutil/keeper/observer.go b/testutil/keeper/observer.go index d84f722b0c..39b6796c7f 100644 --- a/testutil/keeper/observer.go +++ b/testutil/keeper/observer.go @@ -10,7 +10,9 @@ import ( "github.com/cosmos/cosmos-sdk/store/rootmulti" storetypes "github.com/cosmos/cosmos-sdk/store/types" sdk "github.com/cosmos/cosmos-sdk/types" + authkeeper "github.com/cosmos/cosmos-sdk/x/auth/keeper" authtypes "github.com/cosmos/cosmos-sdk/x/auth/types" + bankkeeper "github.com/cosmos/cosmos-sdk/x/bank/keeper" govtypes "github.com/cosmos/cosmos-sdk/x/gov/types" slashingkeeper "github.com/cosmos/cosmos-sdk/x/slashing/keeper" stakingkeeper "github.com/cosmos/cosmos-sdk/x/staking/keeper" @@ -47,6 +49,8 @@ func initObserverKeeper( stakingKeeper stakingkeeper.Keeper, slashingKeeper slashingkeeper.Keeper, authorityKeeper types.AuthorityKeeper, + bankKeeper bankkeeper.Keeper, + authKeeper authkeeper.AccountKeeper, lightclientKeeper types.LightclientKeeper, ) *keeper.Keeper { storeKey := sdk.NewKVStoreKey(types.StoreKey) @@ -62,6 +66,8 @@ func initObserverKeeper( slashingKeeper, authorityKeeper, lightclientKeeper, + bankKeeper, + authKeeper, authtypes.NewModuleAddress(govtypes.ModuleName).String(), ) } @@ -103,6 +109,8 @@ func ObserverKeeperWithMocks( var slashingKeeper types.SlashingKeeper = sdkKeepers.SlashingKeeper var authorityKeeper types.AuthorityKeeper = authorityKeeperTmp var lightclientKeeper types.LightclientKeeper = lightclientKeeperTmp + var bankKeeper types.BankKeeper = sdkKeepers.BankKeeper + var authKeeper types.AccountKeeper = sdkKeepers.AuthKeeper if mockOptions.UseStakingMock { stakingKeeper = observermocks.NewObserverStakingKeeper(t) } @@ -124,6 +132,8 @@ func ObserverKeeperWithMocks( slashingKeeper, authorityKeeper, lightclientKeeper, + bankKeeper, + authKeeper, authtypes.NewModuleAddress(govtypes.ModuleName).String(), ) diff --git a/testutil/sample/crosschain.go b/testutil/sample/crosschain.go index e58c457073..82e683604a 100644 --- a/testutil/sample/crosschain.go +++ b/testutil/sample/crosschain.go @@ -11,6 +11,7 @@ import ( "cosmossdk.io/math" sdk "github.com/cosmos/cosmos-sdk/types" + ethcommon "github.com/ethereum/go-ethereum/common" ethtypes "github.com/ethereum/go-ethereum/core/types" "github.com/ethereum/go-ethereum/crypto" "github.com/stretchr/testify/require" @@ -290,6 +291,7 @@ func ZetaAccounting(t *testing.T, index string) types.ZetaAccounting { } } +// InboundVote creates a sample inbound vote message func InboundVote(coinType coin.CoinType, from, to int64) types.MsgVoteInbound { return types.MsgVoteInbound{ Creator: Bech32AccAddress().String(), @@ -311,6 +313,29 @@ func InboundVote(coinType coin.CoinType, from, to int64) types.MsgVoteInbound { } } +// InboundVoteFromRand creates a simulated inbound vote message. This function uses the provided source of randomness to generate the vote +func InboundVoteFromRand(coinType coin.CoinType, from, to int64, r *rand.Rand) types.MsgVoteInbound { + EthAddress() + return types.MsgVoteInbound{ + Creator: "", + Sender: EthAddressFromRand(r).String(), + SenderChainId: from, + Receiver: EthAddressFromRand(r).String(), + ReceiverChain: to, + Amount: math.NewUint(r.Uint64()), + Message: base64.StdEncoding.EncodeToString(RandomBytes(r)), + InboundBlockHeight: r.Uint64(), + CallOptions: &types.CallOptions{ + GasLimit: 1000000000, + }, + InboundHash: ethcommon.BytesToHash(RandomBytes(r)).String(), + CoinType: coinType, + TxOrigin: EthAddressFromRand(r).String(), + Asset: StringRandom(r, 32), + EventIndex: r.Uint64(), + } +} + func ZRC20Withdrawal(to []byte, value *big.Int) *zrc20.ZRC20Withdrawal { return &zrc20.ZRC20Withdrawal{ From: EthAddress(), diff --git a/testutil/sample/crypto.go b/testutil/sample/crypto.go index 7cc565936a..9e643fa123 100644 --- a/testutil/sample/crypto.go +++ b/testutil/sample/crypto.go @@ -60,6 +60,10 @@ func EthAddress() ethcommon.Address { return ethcommon.BytesToAddress(sdk.AccAddress(ed25519.GenPrivKey().PubKey().Address()).Bytes()) } +func EthAddressFromRand(r *rand.Rand) ethcommon.Address { + return ethcommon.BytesToAddress(sdk.AccAddress(PubKey(r).Address()).Bytes()) +} + // BtcAddressP2WPKH returns a sample btc P2WPKH address func BtcAddressP2WPKH(t *testing.T, net *chaincfg.Params) string { privateKey, err := btcec.NewPrivateKey() diff --git a/testutil/sample/observer.go b/testutil/sample/observer.go index 8d0877ce9b..e9e8eb7cd7 100644 --- a/testutil/sample/observer.go +++ b/testutil/sample/observer.go @@ -2,12 +2,14 @@ package sample import ( "fmt" + "math/rand" "testing" "github.com/cosmos/cosmos-sdk/crypto/keys/ed25519" "github.com/cosmos/cosmos-sdk/testutil/testdata" sdk "github.com/cosmos/cosmos-sdk/types" "github.com/ethereum/go-ethereum/crypto" + "github.com/stretchr/testify/require" "github.com/zeta-chain/node/pkg/chains" "github.com/zeta-chain/node/pkg/cosmos" @@ -119,6 +121,25 @@ func ChainParamsList() (cpl types.ChainParamsList) { return } +// TSSFromRand returns a random TSS,it uses the randomness provided as a parameter +func TSSFromRand(t *testing.T, r *rand.Rand) types.TSS { + pubKey := PubKey(r) + spk, err := cosmos.Bech32ifyPubKey(cosmos.Bech32PubKeyTypeAccPub, pubKey) + require.NoError(t, err) + pk, err := zetacrypto.NewPubKey(spk) + require.NoError(t, err) + pubkeyString := pk.String() + return types.TSS{ + TssPubkey: pubkeyString, + TssParticipantList: []string{}, + OperatorAddressList: []string{}, + FinalizedZetaHeight: r.Int63(), + KeyGenZetaHeight: r.Int63(), + } +} + +// TODO: rename to TSS +// https://github.com/zeta-chain/node/issues/3098 func Tss() types.TSS { _, pubKey, _ := testdata.KeyTestPubAddr() spk, err := cosmos.Bech32ifyPubKey(cosmos.Bech32PubKeyTypeAccPub, pubKey) diff --git a/testutil/sample/sample.go b/testutil/sample/sample.go index 365d1c7e85..3f0390ddad 100644 --- a/testutil/sample/sample.go +++ b/testutil/sample/sample.go @@ -61,6 +61,12 @@ func Bytes() []byte { return []byte("sample") } +func RandomBytes(r *rand.Rand) []byte { + b := make([]byte, 10) + _, _ = r.Read(b) + return b +} + // String returns a sample string func String() string { return "sample" diff --git a/x/crosschain/keeper/cctx_orchestrator_validate_inbound.go b/x/crosschain/keeper/cctx_orchestrator_validate_inbound.go index d4764d9637..3ae74fe45c 100644 --- a/x/crosschain/keeper/cctx_orchestrator_validate_inbound.go +++ b/x/crosschain/keeper/cctx_orchestrator_validate_inbound.go @@ -20,7 +20,6 @@ func (k Keeper) ValidateInbound( if !tssFound { return nil, types.ErrCannotFindTSSKeys } - err := k.CheckIfTSSMigrationTransfer(ctx, msg) if err != nil { return nil, err diff --git a/x/crosschain/keeper/msg_server_vote_inbound_tx.go b/x/crosschain/keeper/msg_server_vote_inbound_tx.go index a34e75c026..263b7b23bc 100644 --- a/x/crosschain/keeper/msg_server_vote_inbound_tx.go +++ b/x/crosschain/keeper/msg_server_vote_inbound_tx.go @@ -93,6 +93,7 @@ func (k msgServer) VoteInbound( } } commit() + // If the ballot is not finalized return nil here to add vote to commit state if !finalized { return &types.MsgVoteInboundResponse{}, nil @@ -102,9 +103,9 @@ func (k msgServer) VoteInbound( if err != nil { return nil, sdkerrors.Wrap(err, voteInboundID) } - // Save the inbound CCTX to the store. This is called irrespective of the status of the CCTX or the outcome of the process function. k.SaveObservedInboundInformation(ctx, cctx, msg.EventIndex) + return &types.MsgVoteInboundResponse{}, nil } diff --git a/x/crosschain/keeper/msg_server_vote_inbound_tx_test.go b/x/crosschain/keeper/msg_server_vote_inbound_tx_test.go index 27f5a8ad98..7fc5c9be99 100644 --- a/x/crosschain/keeper/msg_server_vote_inbound_tx_test.go +++ b/x/crosschain/keeper/msg_server_vote_inbound_tx_test.go @@ -51,19 +51,20 @@ func TestKeeper_VoteInbound(t *testing.T) { k, ctx, sdkk, zk := keepertest.CrosschainKeeper(t) msgServer := keeper.NewMsgServerImpl(*k) validatorList := setObservers(t, k, ctx, zk) + to, from := int64(1337), int64(101) supportedChains := zk.ObserverKeeper.GetSupportedChains(ctx) for _, chain := range supportedChains { - if chains.IsEVMChain(chain.ChainId, []chains.Chain{}) { + if chains.IsEthereumChain(chain.ChainId, []chains.Chain{}) { from = chain.ChainId } if chains.IsZetaChain(chain.ChainId, []chains.Chain{}) { to = chain.ChainId } } - zk.ObserverKeeper.SetTSS(ctx, sample.Tss()) msg := sample.InboundVote(0, from, to) + zk.ObserverKeeper.SetTSS(ctx, sample.Tss()) err := sdkk.EvmKeeper.SetAccount(ctx, ethcommon.HexToAddress(msg.Receiver), statedb.Account{ Nonce: 0, @@ -210,7 +211,7 @@ func TestKeeper_VoteInbound(t *testing.T) { to, from := int64(1337), int64(101) supportedChains := zk.ObserverKeeper.GetSupportedChains(ctx) for _, chain := range supportedChains { - if chains.IsEVMChain(chain.ChainId, []chains.Chain{}) { + if chains.IsEthereumChain(chain.ChainId, []chains.Chain{}) { from = chain.ChainId } if chains.IsZetaChain(chain.ChainId, []chains.Chain{}) { diff --git a/x/crosschain/module_simulation.go b/x/crosschain/module_simulation.go index 38ff067fbf..1399b27cdb 100644 --- a/x/crosschain/module_simulation.go +++ b/x/crosschain/module_simulation.go @@ -5,17 +5,14 @@ import ( "github.com/cosmos/cosmos-sdk/types/module" simtypes "github.com/cosmos/cosmos-sdk/types/simulation" + "github.com/zeta-chain/node/x/crosschain/simulation" "github.com/zeta-chain/node/x/crosschain/types" ) -// GenerateGenesisState creates a randomized GenState of the module +// GenerateGenesisState creates a GenState of the module used to initialize the simulation runs func (AppModule) GenerateGenesisState(simState *module.SimulationState) { - accs := make([]string, len(simState.Accounts)) - for i, acc := range simState.Accounts { - accs[i] = acc.Address.String() - } - crosschainGenesis := types.GenesisState{} - simState.GenState[types.ModuleName] = simState.Cdc.MustMarshalJSON(&crosschainGenesis) + crosschainGenesis := types.DefaultGenesis() + simState.GenState[types.ModuleName] = simState.Cdc.MustMarshalJSON(crosschainGenesis) } // ProposalContents doesn't return any content functions for governance proposals @@ -28,11 +25,13 @@ func (AppModule) ProposalMsgs(_ module.SimulationState) []simtypes.WeightedPropo } // RegisterStoreDecoder registers a decoder -func (am AppModule) RegisterStoreDecoder(_ sdk.StoreDecoderRegistry) {} +func (am AppModule) RegisterStoreDecoder(sdr sdk.StoreDecoderRegistry) { + sdr[types.StoreKey] = simulation.NewDecodeStore(am.cdc) +} // WeightedOperations returns the all the gov module operations with their respective weights. -func (am AppModule) WeightedOperations(_ module.SimulationState) []simtypes.WeightedOperation { - operations := make([]simtypes.WeightedOperation, 0) - - return operations +func (am AppModule) WeightedOperations(simState module.SimulationState) []simtypes.WeightedOperation { + return simulation.WeightedOperations( + simState.AppParams, simState.Cdc, am.keeper, + ) } diff --git a/x/crosschain/simulation/decoders.go b/x/crosschain/simulation/decoders.go new file mode 100644 index 0000000000..dd2c6c4e07 --- /dev/null +++ b/x/crosschain/simulation/decoders.go @@ -0,0 +1,62 @@ +package simulation + +import ( + "bytes" + "fmt" + + "github.com/cosmos/cosmos-sdk/codec" + "github.com/cosmos/cosmos-sdk/types/kv" + + "github.com/zeta-chain/node/x/crosschain/types" +) + +// NewDecodeStore returns a decoder function closure that unmarshals the KVPair's +// Value to the corresponding crosschain types. +func NewDecodeStore(cdc codec.Codec) func(kvA, kvB kv.Pair) string { + return func(kvA, kvB kv.Pair) string { + switch { + case bytes.Equal(kvA.Key, types.KeyPrefix(types.CCTXKey)): + var cctxA, cctxB types.CrossChainTx + cdc.MustUnmarshal(kvA.Value, &cctxA) + cdc.MustUnmarshal(kvB.Value, &cctxB) + return fmt.Sprintf("%v\n%v", cctxA, cctxB) + case bytes.Equal(kvA.Key, types.KeyPrefix(types.LastBlockHeightKey)): + var lastBlockHeightA, lastBlockHeightB types.LastBlockHeight + cdc.MustUnmarshal(kvA.Value, &lastBlockHeightA) + cdc.MustUnmarshal(kvB.Value, &lastBlockHeightB) + return fmt.Sprintf("%v\n%v", lastBlockHeightA, lastBlockHeightB) + case bytes.Equal(kvA.Key, types.KeyPrefix(types.FinalizedInboundsKey)): + var finalizedInboundsA, finalizedInboundsB []byte + finalizedInboundsA = kvA.Value + finalizedInboundsB = kvB.Value + return fmt.Sprintf("%v\n%v", finalizedInboundsA, finalizedInboundsB) + case bytes.Equal(kvA.Key, types.KeyPrefix(types.GasPriceKey)): + var gasPriceA, gasPriceB types.GasPrice + cdc.MustUnmarshal(kvA.Value, &gasPriceA) + cdc.MustUnmarshal(kvB.Value, &gasPriceB) + return fmt.Sprintf("%v\n%v", gasPriceA, gasPriceB) + case bytes.Equal(kvA.Key, types.KeyPrefix(types.OutboundTrackerKeyPrefix)): + var outboundTrackerA, outboundTrackerB types.OutboundTracker + cdc.MustUnmarshal(kvA.Value, &outboundTrackerA) + cdc.MustUnmarshal(kvB.Value, &outboundTrackerB) + return fmt.Sprintf("%v\n%v", outboundTrackerA, outboundTrackerB) + case bytes.Equal(kvA.Key, types.KeyPrefix(types.InboundTrackerKeyPrefix)): + var inboundTrackerA, inboundTrackerB types.InboundTracker + cdc.MustUnmarshal(kvA.Value, &inboundTrackerA) + cdc.MustUnmarshal(kvB.Value, &inboundTrackerB) + return fmt.Sprintf("%v\n%v", inboundTrackerA, inboundTrackerB) + case bytes.Equal(kvA.Key, types.KeyPrefix(types.ZetaAccountingKey)): + var zetaAccountingA, zetaAccountingB types.ZetaAccounting + cdc.MustUnmarshal(kvA.Value, &zetaAccountingA) + cdc.MustUnmarshal(kvB.Value, &zetaAccountingB) + return fmt.Sprintf("%v\n%v", zetaAccountingA, zetaAccountingB) + case bytes.Equal(kvA.Key, types.KeyPrefix(types.RateLimiterFlagsKey)): + var rateLimiterFlagsA, rateLimiterFlagsB types.RateLimiterFlags + cdc.MustUnmarshal(kvA.Value, &rateLimiterFlagsA) + cdc.MustUnmarshal(kvB.Value, &rateLimiterFlagsB) + return fmt.Sprintf("%v\n%v", rateLimiterFlagsA, rateLimiterFlagsB) + default: + panic(fmt.Sprintf("invalid crosschain key prefix %X", kvA.Key[:1])) + } + } +} diff --git a/x/crosschain/simulation/decoders_test.go b/x/crosschain/simulation/decoders_test.go new file mode 100644 index 0000000000..9765aca43d --- /dev/null +++ b/x/crosschain/simulation/decoders_test.go @@ -0,0 +1,60 @@ +package simulation_test + +import ( + "fmt" + "testing" + + "github.com/cosmos/cosmos-sdk/types/kv" + "github.com/stretchr/testify/require" + keepertest "github.com/zeta-chain/node/testutil/keeper" + "github.com/zeta-chain/node/testutil/sample" + "github.com/zeta-chain/node/x/crosschain/simulation" + "github.com/zeta-chain/node/x/crosschain/types" +) + +func TestDecodeStore(t *testing.T) { + k, _, _, _ := keepertest.CrosschainKeeper(t) + cdc := k.GetCodec() + dec := simulation.NewDecodeStore(cdc) + cctx := sample.CrossChainTx(t, "sample") + lastBlockHeight := sample.LastBlockHeight(t, "sample") + gasPrice := sample.GasPrice(t, "sample") + outboundTracker := sample.OutboundTracker(t, "sample") + inboundTracker := sample.InboundTracker(t, "sample") + zetaAccounting := sample.ZetaAccounting(t, "sample") + rateLimiterFlags := sample.RateLimiterFlags() + + kvPairs := kv.Pairs{ + Pairs: []kv.Pair{ + {Key: types.KeyPrefix(types.CCTXKey), Value: cdc.MustMarshal(cctx)}, + {Key: types.KeyPrefix(types.LastBlockHeightKey), Value: cdc.MustMarshal(lastBlockHeight)}, + {Key: types.KeyPrefix(types.GasPriceKey), Value: cdc.MustMarshal(gasPrice)}, + {Key: types.KeyPrefix(types.OutboundTrackerKeyPrefix), Value: cdc.MustMarshal(&outboundTracker)}, + {Key: types.KeyPrefix(types.InboundTrackerKeyPrefix), Value: cdc.MustMarshal(&inboundTracker)}, + {Key: types.KeyPrefix(types.ZetaAccountingKey), Value: cdc.MustMarshal(&zetaAccounting)}, + {Key: types.KeyPrefix(types.RateLimiterFlagsKey), Value: cdc.MustMarshal(&rateLimiterFlags)}, + {Key: types.KeyPrefix(types.FinalizedInboundsKey), Value: []byte{1}}, + }, + } + + tests := []struct { + name string + expectedLog string + }{ + {"CrossChainTx", fmt.Sprintf("%v\n%v", *cctx, *cctx)}, + {"LastBlockHeight", fmt.Sprintf("%v\n%v", *lastBlockHeight, *lastBlockHeight)}, + {"GasPrice", fmt.Sprintf("%v\n%v", *gasPrice, *gasPrice)}, + {"OutboundTracker", fmt.Sprintf("%v\n%v", outboundTracker, outboundTracker)}, + {"InboundTracker", fmt.Sprintf("%v\n%v", inboundTracker, inboundTracker)}, + {"ZetaAccounting", fmt.Sprintf("%v\n%v", zetaAccounting, zetaAccounting)}, + {"RateLimiterFlags", fmt.Sprintf("%v\n%v", rateLimiterFlags, rateLimiterFlags)}, + {"FinalizedInbounds", fmt.Sprintf("%v\n%v", []byte{1}, []byte{1})}, + } + + for i, tt := range tests { + i, tt := i, tt + t.Run(tt.name, func(t *testing.T) { + require.Equal(t, tt.expectedLog, dec(kvPairs.Pairs[i], kvPairs.Pairs[i])) + }) + } +} diff --git a/x/crosschain/simulation/operations.go b/x/crosschain/simulation/operations.go new file mode 100644 index 0000000000..96b04970b9 --- /dev/null +++ b/x/crosschain/simulation/operations.go @@ -0,0 +1,492 @@ +package simulation + +import ( + "fmt" + "math" + "math/rand" + + "github.com/cosmos/cosmos-sdk/baseapp" + "github.com/cosmos/cosmos-sdk/codec" + simtestutil "github.com/cosmos/cosmos-sdk/testutil/sims" + sdk "github.com/cosmos/cosmos-sdk/types" + moduletestutil "github.com/cosmos/cosmos-sdk/types/module/testutil" + simtypes "github.com/cosmos/cosmos-sdk/types/simulation" + "github.com/cosmos/cosmos-sdk/x/simulation" + + "github.com/zeta-chain/node/pkg/authz" + "github.com/zeta-chain/node/pkg/chains" + "github.com/zeta-chain/node/testutil/sample" + "github.com/zeta-chain/node/x/crosschain/keeper" + "github.com/zeta-chain/node/x/crosschain/types" + observerTypes "github.com/zeta-chain/node/x/observer/types" +) + +// Simulation operation weights constants +// Operation weights are used by the `SimulateFromSeed` +// function to pick a random operation based on the weights.The functions with higher weights are more likely to be picked. + +// Therefore, this decides the percentage of a certain operation that is part of a block. + +// Based on the weights assigned in the cosmos sdk modules, +// 100 seems to the max weight used,and we should use relative weights +// to signify the number of each operation in a block. + +// TODO Add more details to comment based on what the number represents in terms of percentage of operations in a block +// https://github.com/zeta-chain/node/issues/3100 +const ( + DefaultWeightMsgAddOutboundTracker = 50 + DefaultWeightAddInboundTracker = 50 + DefaultWeightRemoveOutboundTracker = 5 + DefaultWeightVoteGasPrice = 100 + DefaultWeightVoteOutbound = 50 + DefaultWeightVoteInbound = 100 + DefaultWeightWhitelistERC20 = 1 + DefaultWeightMigrateTssFunds = 1 + DefaultWeightUpdateTssAddress = 1 + DefaultWeightAbortStuckCCTX = 10 + DefaultWeightUpdateRateLimiterFlags = 1 + + OpWeightMsgAddOutboundTracker = "op_weight_msg_add_outbound_tracker" // #nosec G101 not a hardcoded credential + OpWeightAddInboundTracker = "op_weight_msg_add_inbound_tracker" // #nosec G101 not a hardcoded credential + OpWeightRemoveOutboundTracker = "op_weight_msg_remove_outbound_tracker" // #nosec G101 not a hardcoded credential + OpWeightVoteGasPrice = "op_weight_msg_vote_gas_price" // #nosec G101 not a hardcoded credential + OpWeightVoteOutbound = "op_weight_msg_vote_outbound" // #nosec G101 not a hardcoded credential + OpWeightVoteInbound = "op_weight_msg_vote_inbound" // #nosec G101 not a hardcoded credential + OpWeightWhitelistERC20 = "op_weight_msg_whitelist_erc20" // #nosec G101 not a hardcoded credential + OpWeightMigrateTssFunds = "op_weight_msg_migrate_tss_funds" // #nosec G101 not a hardcoded credential + OpWeightUpdateTssAddress = "op_weight_msg_update_tss_address" // #nosec G101 not a hardcoded credential + OpWeightAbortStuckCCTX = "op_weight_msg_abort_stuck_cctx" // #nosec G101 not a hardcoded credential + OpWeightUpdateRateLimiterFlags = "op_weight_msg_update_rate_limiter_flags" // #nosec G101 not a hardcoded credential + +) + +func WeightedOperations( + appParams simtypes.AppParams, cdc codec.JSONCodec, k keeper.Keeper) simulation.WeightedOperations { + var ( + weightMsgAddOutboundTracker int + weightAddInboundTracker int + weightRemoveOutboundTracker int + weightVoteGasPrice int + weightVoteOutbound int + weightVoteInbound int + weightWhitelistERC20 int + weightMigrateTssFunds int + weightUpdateTssAddress int + weightAbortStuckCCTX int + weightUpdateRateLimiterFlags int + ) + + appParams.GetOrGenerate(cdc, OpWeightMsgAddOutboundTracker, &weightMsgAddOutboundTracker, nil, + func(_ *rand.Rand) { + weightMsgAddOutboundTracker = DefaultWeightMsgAddOutboundTracker + }, + ) + + appParams.GetOrGenerate(cdc, OpWeightAddInboundTracker, &weightAddInboundTracker, nil, + func(_ *rand.Rand) { + weightAddInboundTracker = DefaultWeightAddInboundTracker + }, + ) + + appParams.GetOrGenerate(cdc, OpWeightRemoveOutboundTracker, &weightRemoveOutboundTracker, nil, + func(_ *rand.Rand) { + weightRemoveOutboundTracker = DefaultWeightRemoveOutboundTracker + }, + ) + + appParams.GetOrGenerate(cdc, OpWeightVoteGasPrice, &weightVoteGasPrice, nil, + func(_ *rand.Rand) { + weightVoteGasPrice = DefaultWeightVoteGasPrice + }, + ) + + appParams.GetOrGenerate(cdc, OpWeightVoteOutbound, &weightVoteOutbound, nil, + func(_ *rand.Rand) { + weightVoteOutbound = DefaultWeightVoteOutbound + }, + ) + + appParams.GetOrGenerate(cdc, OpWeightVoteInbound, &weightVoteInbound, nil, + func(_ *rand.Rand) { + weightVoteInbound = DefaultWeightVoteInbound + }, + ) + + appParams.GetOrGenerate(cdc, OpWeightWhitelistERC20, &weightWhitelistERC20, nil, + func(_ *rand.Rand) { + weightWhitelistERC20 = DefaultWeightWhitelistERC20 + }, + ) + + appParams.GetOrGenerate(cdc, OpWeightMigrateTssFunds, &weightMigrateTssFunds, nil, + func(_ *rand.Rand) { + weightMigrateTssFunds = DefaultWeightMigrateTssFunds + }, + ) + + appParams.GetOrGenerate(cdc, OpWeightUpdateTssAddress, &weightUpdateTssAddress, nil, + func(_ *rand.Rand) { + weightUpdateTssAddress = DefaultWeightUpdateTssAddress + }, + ) + + appParams.GetOrGenerate(cdc, OpWeightAbortStuckCCTX, &weightAbortStuckCCTX, nil, + func(_ *rand.Rand) { + weightAbortStuckCCTX = DefaultWeightAbortStuckCCTX + }, + ) + + appParams.GetOrGenerate(cdc, OpWeightUpdateRateLimiterFlags, &weightUpdateRateLimiterFlags, nil, + func(_ *rand.Rand) { + weightUpdateRateLimiterFlags = DefaultWeightUpdateRateLimiterFlags + }, + ) + + return simulation.WeightedOperations{ + simulation.NewWeightedOperation( + weightVoteGasPrice, + SimulateMsgVoteGasPrice(k), + ), + simulation.NewWeightedOperation( + weightVoteInbound, + SimulateVoteInbound(k), + ), + } +} + +// operationSimulateVoteInbound generates a MsgVoteInbound with a random vote and delivers it. +func operationSimulateVoteInbound( + k keeper.Keeper, + msg types.MsgVoteInbound, + simAccount simtypes.Account, +) simtypes.Operation { + return func(r *rand.Rand, app *baseapp.BaseApp, ctx sdk.Context, _ []simtypes.Account, _ string, + ) (OperationMsg simtypes.OperationMsg, futureOps []simtypes.FutureOperation, err error) { + // Fetch the account from the auth keeper which can then be used to fetch spendable coins + authAccount := k.GetAuthKeeper().GetAccount(ctx, simAccount.Address) + spendable := k.GetBankKeeper().SpendableCoins(ctx, authAccount.GetAddress()) + + // Generate a transaction with a random fee and deliver it + txCtx := simulation.OperationInput{ + R: r, + App: app, + TxGen: moduletestutil.MakeTestEncodingConfig().TxConfig, + Cdc: nil, + Msg: &msg, + MsgType: msg.Type(), + Context: ctx, + SimAccount: simAccount, + AccountKeeper: k.GetAuthKeeper(), + Bankkeeper: k.GetBankKeeper(), + ModuleName: types.ModuleName, + CoinsSpentInMsg: spendable, + } + + // Generate and deliver the transaction using the function defined by us instead of using the default function provided by the cosmos-sdk + // The main difference between the two functions is that the one defined by us does not error out if the vote fails. + // We need this behaviour as the votes are assigned to future operations, i.e., they are scheduled to be executed in a future block. We do not know at the time of scheduling if the vote will be successful or not. + // There might be multiple reasons for a vote to fail , like the observer not being present in the observer set, the observer not being an observer, etc. + return GenAndDeliverTxWithRandFees(txCtx) + } +} + +func SimulateVoteInbound(k keeper.Keeper) simtypes.Operation { + // The states are: + // column 1: All observers vote + // column 2: 90% vote + // column 3: 75% vote + // column 4: 40% vote + // column 5: 15% vote + // column 6: noone votes + // All columns sum to 100 for simplicity, but this is arbitrary and can be changed + numVotesTransitionMatrix, _ := simulation.CreateTransitionMatrix([][]int{ + {20, 10, 0, 0, 0, 0}, + {55, 50, 20, 10, 0, 0}, + {25, 25, 30, 25, 30, 15}, + {0, 15, 30, 25, 30, 30}, + {0, 0, 20, 30, 30, 30}, + {0, 0, 0, 10, 10, 25}, + }) + + statePercentageArray := []float64{1, .9, .75, .4, .15, 0} + curNumVotesState := 1 + + return func( + r *rand.Rand, + app *baseapp.BaseApp, + ctx sdk.Context, + accs []simtypes.Account, + chainID string, + ) (simtypes.OperationMsg, []simtypes.FutureOperation, error) { + // TODO : randomize these values + // Right now we use a constant value for cctx creation , this is the same as the one used in unit tests for the successful condition. + // TestKeeper_VoteInbound/successfully vote on evm deposit + // But this can improved by adding more randomization + + to, from := int64(1337), int64(101) + supportedChains := k.GetObserverKeeper().GetSupportedChains(ctx) + for _, chain := range supportedChains { + if chains.IsEthereumChain(chain.ChainId, []chains.Chain{}) { + from = chain.ChainId + } + if chains.IsZetaChain(chain.ChainId, []chains.Chain{}) { + to = chain.ChainId + } + } + + msg := sample.InboundVoteFromRand(0, from, to, r) + + // Pick a random observer to create the ballot + // If this returns an error, it is likely that the entire observer set has been removed + simAccount, firstVoter, err := GetRandomAccountAndObserver(r, ctx, k, accs) + if err != nil { + return simtypes.OperationMsg{}, nil, nil + } + + txGen := moduletestutil.MakeTestEncodingConfig().TxConfig + account := k.GetAuthKeeper().GetAccount(ctx, simAccount.Address) + firstMsg := msg + firstMsg.Creator = firstVoter + + err = firstMsg.ValidateBasic() + if err != nil { + return simtypes.NoOpMsg(types.ModuleName, msg.Type(), "unable to validate first inbound vote"), nil, err + } + + tx, err := simtestutil.GenSignedMockTx( + r, + txGen, + []sdk.Msg{&firstMsg}, + sdk.Coins{sdk.NewInt64Coin(sdk.DefaultBondDenom, 0)}, + simtestutil.DefaultGenTxGas, + chainID, + []uint64{account.GetAccountNumber()}, + []uint64{account.GetSequence()}, + simAccount.PrivKey, + ) + if err != nil { + return simtypes.NoOpMsg(types.ModuleName, msg.Type(), "unable to generate mock tx"), nil, err + } + + // We can return error here as we can guarantee that the first vote will be successful. + // Since we query the observer set before adding votes + _, _, err = app.SimDeliver(txGen.TxEncoder(), tx) + if err != nil { + return simtypes.NoOpMsg(types.ModuleName, msg.Type(), "unable to deliver tx"), nil, err + } + + opMsg := simtypes.NewOperationMsg(&msg, true, "", nil) + + // Add subsequent votes + observerSet, found := k.GetObserverKeeper().GetObserverSet(ctx) + if !found { + return simtypes.NoOpMsg(types.ModuleName, authz.InboundVoter.String(), "observer set not found"), nil, nil + } + + // 1) Schedule operations for votes + // 1.1) first pick a number of people to vote. + curNumVotesState = numVotesTransitionMatrix.NextState(r, curNumVotesState) + numVotes := int(math.Ceil(float64(len(observerSet.ObserverList)) * statePercentageArray[curNumVotesState])) + + // 1.2) select who votes + whoVotes := r.Perm(len(observerSet.ObserverList)) + whoVotes = whoVotes[:numVotes] + + var fops []simtypes.FutureOperation + + for _, observerIdx := range whoVotes { + observerAddress := observerSet.ObserverList[observerIdx] + // firstVoter has already voted. + if observerAddress == firstVoter { + continue + } + observerAccount, err := GetObserverAccount(observerAddress, accs) + if err != nil { + continue + } + // 1.3) schedule the vote + votingMsg := msg + votingMsg.Creator = observerAddress + + e := votingMsg.ValidateBasic() + if e != nil { + return simtypes.NoOpMsg(types.ModuleName, msg.Type(), "unable to validate voting msg"), nil, e + } + + fops = append(fops, simtypes.FutureOperation{ + // Submit all subsequent votes in the next block. + // We can consider adding a random block height between 1 and ballot maturity blocks in the future. + BlockHeight: int(ctx.BlockHeight() + 1), + Op: operationSimulateVoteInbound(k, votingMsg, observerAccount), + }) + } + return opMsg, fops, nil + } +} + +// SimulateMsgVoteGasPrice generates a MsgVoteGasPrice and delivers it +func SimulateMsgVoteGasPrice(k keeper.Keeper) simtypes.Operation { + return func(r *rand.Rand, app *baseapp.BaseApp, ctx sdk.Context, accounts []simtypes.Account, _ string, + ) (OperationMsg simtypes.OperationMsg, futureOps []simtypes.FutureOperation, err error) { + // Get a random account and observer + // If this returns an error, it is likely that the entire observer set has been removed + simAccount, randomObserver, err := GetRandomAccountAndObserver(r, ctx, k, accounts) + if err != nil { + return simtypes.OperationMsg{}, nil, nil + } + authAccount := k.GetAuthKeeper().GetAccount(ctx, simAccount.Address) + spendable := k.GetBankKeeper().SpendableCoins(ctx, authAccount.GetAddress()) + + supportedChains := k.GetObserverKeeper().GetSupportedChains(ctx) + if len(supportedChains) == 0 { + return simtypes.NoOpMsg( + types.ModuleName, + authz.GasPriceVoter.String(), + "no supported chains found", + ), nil, nil + } + randomChainID := GetRandomChainID(r, supportedChains) + + // Vote for random gas price. Gas prices do not use a ballot system, so we can vote directly without having to schedule future operations. + // The random nature of the price might create weird gas prices for the chain, but it is fine for now. We can remove the randomness if needed + msg := types.MsgVoteGasPrice{ + Creator: randomObserver, + ChainId: randomChainID, + Price: r.Uint64(), + PriorityFee: r.Uint64(), + BlockNumber: r.Uint64(), + Supply: fmt.Sprintf("%d", r.Int63()), + } + + // System contracts are deployed on the first block, so we cannot vote on gas prices before that + if ctx.BlockHeight() <= 1 { + return simtypes.NewOperationMsg(&msg, true, "block height less than 1", nil), nil, nil + } + + err = msg.ValidateBasic() + if err != nil { + return simtypes.NoOpMsg(types.ModuleName, msg.Type(), "unable to validate vote gas price msg"), nil, err + } + + txCtx := simulation.OperationInput{ + R: r, + App: app, + TxGen: moduletestutil.MakeTestEncodingConfig().TxConfig, + Cdc: nil, + Msg: &msg, + MsgType: msg.Type(), + Context: ctx, + SimAccount: simAccount, + AccountKeeper: k.GetAuthKeeper(), + Bankkeeper: k.GetBankKeeper(), + ModuleName: types.ModuleName, + CoinsSpentInMsg: spendable, + } + + return simulation.GenAndDeliverTxWithRandFees(txCtx) + } +} + +func GetRandomObserver(r *rand.Rand, observerList []string) string { + idx := r.Intn(len(observerList)) + return observerList[idx] +} + +func GetRandomChainID(r *rand.Rand, chains []chains.Chain) int64 { + idx := r.Intn(len(chains)) + return chains[idx].ChainId +} + +// GetRandomAccountAndObserver returns a random account and the associated observer address +func GetRandomAccountAndObserver( + r *rand.Rand, + ctx sdk.Context, + k keeper.Keeper, + accounts []simtypes.Account, +) (simtypes.Account, string, error) { + observers, found := k.GetObserverKeeper().GetObserverSet(ctx) + if !found { + return simtypes.Account{}, "", fmt.Errorf("observer set not found") + } + + if len(observers.ObserverList) == 0 { + return simtypes.Account{}, "", fmt.Errorf("no observers present in observer set found") + } + + randomObserver := GetRandomObserver(r, observers.ObserverList) + simAccount, err := GetObserverAccount(randomObserver, accounts) + if err != nil { + return simtypes.Account{}, "", err + } + return simAccount, randomObserver, nil +} + +// GetObserverAccount returns the account associated with the observer address from the list of accounts provided +// GetObserverAccount can fail if all the observers are removed from the observer set ,this can happen +//if the other modules create transactions which affect the validator +//and triggers any of the staking hooks defined in the observer modules + +func GetObserverAccount(observerAddress string, accounts []simtypes.Account) (simtypes.Account, error) { + operatorAddress, err := observerTypes.GetOperatorAddressFromAccAddress(observerAddress) + if err != nil { + return simtypes.Account{}, fmt.Errorf("validator not found for observer ") + } + + simAccount, found := simtypes.FindAccount(accounts, operatorAddress) + if !found { + return simtypes.Account{}, fmt.Errorf("operator account not found") + } + return simAccount, nil +} + +// GenAndDeliverTxWithRandFees generates a transaction with a random fee and delivers it. +func GenAndDeliverTxWithRandFees( + txCtx simulation.OperationInput, +) (simtypes.OperationMsg, []simtypes.FutureOperation, error) { + account := txCtx.AccountKeeper.GetAccount(txCtx.Context, txCtx.SimAccount.Address) + spendable := txCtx.Bankkeeper.SpendableCoins(txCtx.Context, account.GetAddress()) + + var fees sdk.Coins + var err error + + coins, hasNeg := spendable.SafeSub(txCtx.CoinsSpentInMsg...) + if hasNeg { + return simtypes.NoOpMsg(txCtx.ModuleName, txCtx.MsgType, "message doesn't leave room for fees"), nil, err + } + + fees, err = simtypes.RandomFees(txCtx.R, txCtx.Context, coins) + if err != nil { + return simtypes.NoOpMsg(txCtx.ModuleName, txCtx.MsgType, "unable to generate fees"), nil, err + } + return GenAndDeliverTx(txCtx, fees) +} + +// GenAndDeliverTx generates a transactions and delivers it with the provided fees. +// This function does not return an error if the transaction fails to deliver. +func GenAndDeliverTx( + txCtx simulation.OperationInput, + fees sdk.Coins, +) (simtypes.OperationMsg, []simtypes.FutureOperation, error) { + account := txCtx.AccountKeeper.GetAccount(txCtx.Context, txCtx.SimAccount.Address) + tx, err := simtestutil.GenSignedMockTx( + txCtx.R, + txCtx.TxGen, + []sdk.Msg{txCtx.Msg}, + fees, + simtestutil.DefaultGenTxGas, + txCtx.Context.ChainID(), + []uint64{account.GetAccountNumber()}, + []uint64{account.GetSequence()}, + txCtx.SimAccount.PrivKey, + ) + if err != nil { + return simtypes.NoOpMsg(txCtx.ModuleName, txCtx.MsgType, "unable to generate mock tx"), nil, err + } + + _, _, err = txCtx.App.SimDeliver(txCtx.TxGen.TxEncoder(), tx) + if err != nil { + return simtypes.NoOpMsg(txCtx.ModuleName, txCtx.MsgType, "unable to deliver tx"), nil, nil + } + + return simtypes.NewOperationMsg(txCtx.Msg, true, "", txCtx.Cdc), nil, nil +} diff --git a/x/crosschain/simulation/simap.go b/x/crosschain/simulation/simap.go deleted file mode 100644 index 92c437c0d1..0000000000 --- a/x/crosschain/simulation/simap.go +++ /dev/null @@ -1,15 +0,0 @@ -package simulation - -import ( - sdk "github.com/cosmos/cosmos-sdk/types" - simtypes "github.com/cosmos/cosmos-sdk/types/simulation" -) - -// FindAccount find a specific address from an account list -func FindAccount(accs []simtypes.Account, address string) (simtypes.Account, bool) { - creator, err := sdk.AccAddressFromBech32(address) - if err != nil { - panic(err) - } - return simtypes.FindAccount(accs, creator) -} diff --git a/x/crosschain/types/expected_keepers.go b/x/crosschain/types/expected_keepers.go index 0f496c13d3..2fee9cb31a 100644 --- a/x/crosschain/types/expected_keepers.go +++ b/x/crosschain/types/expected_keepers.go @@ -24,12 +24,14 @@ type StakingKeeper interface { // AccountKeeper defines the expected account keeper (noalias) type AccountKeeper interface { GetModuleAccount(ctx sdk.Context, name string) types.ModuleAccountI + GetAccount(ctx sdk.Context, addr sdk.AccAddress) types.AccountI } // BankKeeper defines the expected interface needed to retrieve account balances. type BankKeeper interface { BurnCoins(ctx sdk.Context, name string, amt sdk.Coins) error MintCoins(ctx sdk.Context, moduleName string, amt sdk.Coins) error + SpendableCoins(ctx sdk.Context, addr sdk.AccAddress) sdk.Coins } type ObserverKeeper interface { diff --git a/x/fungible/keeper/evm.go b/x/fungible/keeper/evm.go index 95b1b05dae..6abd4444d9 100644 --- a/x/fungible/keeper/evm.go +++ b/x/fungible/keeper/evm.go @@ -322,6 +322,7 @@ func (k Keeper) DepositZRC20AndCallContract(ctx sdk.Context, if !found { return nil, cosmoserrors.Wrapf(types.ErrContractNotFound, "GetSystemContract address not found") } + systemAddress := common.HexToAddress(system.SystemContract) sysConABI, err := systemcontract.SystemContractMetaData.GetAbi() diff --git a/x/fungible/keeper/keeper.go b/x/fungible/keeper/keeper.go index ce1e9d5fdc..6e4af7f194 100644 --- a/x/fungible/keeper/keeper.go +++ b/x/fungible/keeper/keeper.go @@ -13,7 +13,7 @@ import ( type ( Keeper struct { - cdc codec.BinaryCodec + cdc codec.Codec storeKey storetypes.StoreKey memKey storetypes.StoreKey authKeeper types.AccountKeeper @@ -25,7 +25,7 @@ type ( ) func NewKeeper( - cdc codec.BinaryCodec, + cdc codec.Codec, storeKey, memKey storetypes.StoreKey, authKeeper types.AccountKeeper, @@ -54,6 +54,10 @@ func (k Keeper) GetAuthKeeper() types.AccountKeeper { return k.authKeeper } +func (k Keeper) GetCodec() codec.Codec { + return k.cdc +} + func (k Keeper) GetEVMKeeper() types.EVMKeeper { return k.evmKeeper } diff --git a/x/fungible/keeper/msg_server_update_gateway_contract_test.go b/x/fungible/keeper/msg_server_update_gateway_contract_test.go index b1ab5a7fbf..ba838b3674 100644 --- a/x/fungible/keeper/msg_server_update_gateway_contract_test.go +++ b/x/fungible/keeper/msg_server_update_gateway_contract_test.go @@ -1,10 +1,11 @@ package keeper_test import ( + "testing" + "github.com/ethereum/go-ethereum/common" "github.com/zeta-chain/node/pkg/chains" "github.com/zeta-chain/protocol-contracts/v2/pkg/zrc20.sol" - "testing" "github.com/stretchr/testify/require" keepertest "github.com/zeta-chain/node/testutil/keeper" diff --git a/x/fungible/module.go b/x/fungible/module.go index fcec27c9d4..8e87fdbd43 100644 --- a/x/fungible/module.go +++ b/x/fungible/module.go @@ -31,10 +31,10 @@ var ( // AppModuleBasic implements the AppModuleBasic interface for the fungible module. type AppModuleBasic struct { - cdc codec.BinaryCodec + cdc codec.Codec } -func NewAppModuleBasic(cdc codec.BinaryCodec) AppModuleBasic { +func NewAppModuleBasic(cdc codec.Codec) AppModuleBasic { return AppModuleBasic{cdc: cdc} } diff --git a/x/fungible/module_simulation.go b/x/fungible/module_simulation.go index e502102502..120f9158f4 100644 --- a/x/fungible/module_simulation.go +++ b/x/fungible/module_simulation.go @@ -5,17 +5,14 @@ import ( "github.com/cosmos/cosmos-sdk/types/module" simtypes "github.com/cosmos/cosmos-sdk/types/simulation" + "github.com/zeta-chain/node/x/fungible/simulation" "github.com/zeta-chain/node/x/fungible/types" ) -// GenerateGenesisState creates a randomized GenState of the module +// GenerateGenesisState creates a GenState of the module used to initialize the simulation runs func (AppModule) GenerateGenesisState(simState *module.SimulationState) { - accs := make([]string, len(simState.Accounts)) - for i, acc := range simState.Accounts { - accs[i] = acc.Address.String() - } - fungibleGenesis := types.GenesisState{} - simState.GenState[types.ModuleName] = simState.Cdc.MustMarshalJSON(&fungibleGenesis) + fungibleGenesis := types.DefaultGenesis() + simState.GenState[types.ModuleName] = simState.Cdc.MustMarshalJSON(fungibleGenesis) } // ProposalContents doesn't return any content functions for governance proposals @@ -28,11 +25,13 @@ func (AppModule) ProposalMsgs(_ module.SimulationState) []simtypes.WeightedPropo } // RegisterStoreDecoder registers a decoder -func (am AppModule) RegisterStoreDecoder(_ sdk.StoreDecoderRegistry) {} +func (am AppModule) RegisterStoreDecoder(sdr sdk.StoreDecoderRegistry) { + sdr[types.StoreKey] = simulation.NewDecodeStore(am.cdc) +} // WeightedOperations returns the all the gov module operations with their respective weights. -func (am AppModule) WeightedOperations(_ module.SimulationState) []simtypes.WeightedOperation { - operations := make([]simtypes.WeightedOperation, 0) - - return operations +func (am AppModule) WeightedOperations(simState module.SimulationState) []simtypes.WeightedOperation { + return simulation.WeightedOperations( + simState.AppParams, simState.Cdc, am.keeper, + ) } diff --git a/x/fungible/simulation/decoders.go b/x/fungible/simulation/decoders.go new file mode 100644 index 0000000000..04be66f781 --- /dev/null +++ b/x/fungible/simulation/decoders.go @@ -0,0 +1,35 @@ +package simulation + +import ( + "bytes" + "fmt" + + "github.com/cosmos/cosmos-sdk/codec" + "github.com/cosmos/cosmos-sdk/types/kv" + + "github.com/zeta-chain/node/x/fungible/types" +) + +// NewDecodeStore returns a decoder function closure that unmarshals the KVPair's +// Value to the corresponding fungible types. +func NewDecodeStore(cdc codec.Codec) func(kvA, kvB kv.Pair) string { + return func(kvA, kvB kv.Pair) string { + if !bytes.Equal(kvA.Key[:1], kvB.Key[:1]) { + return fmt.Sprintf("key prefixes do not match. A: %X, B: %X", kvA.Key[:1], kvB.Key[:1]) + } + switch { + case bytes.Equal(kvA.Key, types.KeyPrefix(types.SystemContractKey)): + var systemContractA, systemContractB types.SystemContract + cdc.MustUnmarshal(kvA.Value, &systemContractA) + cdc.MustUnmarshal(kvB.Value, &systemContractB) + return fmt.Sprintf("%v\n%v", systemContractA, systemContractB) + case bytes.Equal(kvA.Key, types.KeyPrefix(types.ForeignCoinsKeyPrefix)): + var foreignCoinsA, foreignCoinsB types.ForeignCoins + cdc.MustUnmarshal(kvA.Value, &foreignCoinsA) + cdc.MustUnmarshal(kvB.Value, &foreignCoinsB) + return fmt.Sprintf("%v\n%v", foreignCoinsA, foreignCoinsB) + default: + return fmt.Sprintf("invalid fungible key prefix %X", kvA.Key[:1]) + } + } +} diff --git a/x/fungible/simulation/decoders_test.go b/x/fungible/simulation/decoders_test.go new file mode 100644 index 0000000000..c2e4cdfd44 --- /dev/null +++ b/x/fungible/simulation/decoders_test.go @@ -0,0 +1,43 @@ +package simulation_test + +import ( + "fmt" + "testing" + + "github.com/cosmos/cosmos-sdk/types/kv" + "github.com/stretchr/testify/require" + keepertest "github.com/zeta-chain/node/testutil/keeper" + "github.com/zeta-chain/node/testutil/sample" + "github.com/zeta-chain/node/x/fungible/simulation" + "github.com/zeta-chain/node/x/fungible/types" +) + +func TestDecodeStore(t *testing.T) { + k, _, _, _ := keepertest.FungibleKeeper(t) + cdc := k.GetCodec() + dec := simulation.NewDecodeStore(cdc) + systemContract := sample.SystemContract() + foreignCoins := sample.ForeignCoins(t, sample.EthAddress().String()) + + kvPairs := kv.Pairs{ + Pairs: []kv.Pair{ + {Key: []byte(types.SystemContractKey), Value: cdc.MustMarshal(systemContract)}, + {Key: []byte(types.ForeignCoinsKeyPrefix), Value: cdc.MustMarshal(&foreignCoins)}, + }, + } + + tests := []struct { + name string + expectedLog string + }{ + {"SystemContract", fmt.Sprintf("%v\n%v", *systemContract, *systemContract)}, + {"ForeignCoins", fmt.Sprintf("%v\n%v", foreignCoins, foreignCoins)}, + } + + for i, tt := range tests { + i, tt := i, tt + t.Run(tt.name, func(t *testing.T) { + require.Equal(t, tt.expectedLog, dec(kvPairs.Pairs[i], kvPairs.Pairs[i])) + }) + } +} diff --git a/x/fungible/simulation/operations.go b/x/fungible/simulation/operations.go new file mode 100644 index 0000000000..2a14713979 --- /dev/null +++ b/x/fungible/simulation/operations.go @@ -0,0 +1,116 @@ +package simulation + +import ( + "math/rand" + + "github.com/cosmos/cosmos-sdk/baseapp" + "github.com/cosmos/cosmos-sdk/codec" + sdk "github.com/cosmos/cosmos-sdk/types" + moduletestutil "github.com/cosmos/cosmos-sdk/types/module/testutil" + simtypes "github.com/cosmos/cosmos-sdk/types/simulation" + "github.com/cosmos/cosmos-sdk/x/simulation" + + "github.com/zeta-chain/node/x/fungible/keeper" + "github.com/zeta-chain/node/x/fungible/types" + observerTypes "github.com/zeta-chain/node/x/observer/types" +) + +// Simulation operation weights constants +// Operation weights are used by the `SimulateFromSeed` +// function to pick a random operation based on the weights.The functions with higher weights are more likely to be picked. + +// Therefore, this decides the percentage of a certain operation that is part of a block. + +// Based on the weights assigned in the cosmos sdk modules, +// 100 seems to the max weight used,and we should use relative weights +// to signify the number of each operation in a block. + +// TODO Add more details to comment based on what the number represents in terms of percentage of operations in a block +// https://github.com/zeta-chain/node/issues/3100 +const ( + // #nosec G101 not a hardcoded credential + OpWeightMsgDeploySystemContracts = "op_weight_msg_deploy_system_contracts" + DefaultWeightMsgDeploySystemContracts = 5 +) + +// DeployedSystemContracts Use a flag to ensure that the system contracts are deployed only once +// https://github.com/zeta-chain/node/issues/3102 +func WeightedOperations( + appParams simtypes.AppParams, cdc codec.JSONCodec, k keeper.Keeper) simulation.WeightedOperations { + var weightMsgDeploySystemContracts int + + appParams.GetOrGenerate(cdc, OpWeightMsgDeploySystemContracts, &weightMsgDeploySystemContracts, nil, + func(_ *rand.Rand) { + weightMsgDeploySystemContracts = DefaultWeightMsgDeploySystemContracts + }) + + return simulation.WeightedOperations{ + simulation.NewWeightedOperation( + weightMsgDeploySystemContracts, + SimulateMsgDeploySystemContracts(k), + ), + } +} + +// SimulateMsgDeploySystemContracts deploy system contracts.It is run only once in first block. +func SimulateMsgDeploySystemContracts(k keeper.Keeper) simtypes.Operation { + return func(r *rand.Rand, app *baseapp.BaseApp, ctx sdk.Context, accounts []simtypes.Account, _ string, + ) (OperationMsg simtypes.OperationMsg, futureOps []simtypes.FutureOperation, err error) { + policies, found := k.GetAuthorityKeeper().GetPolicies(ctx) + if !found { + return simtypes.NoOpMsg( + types.ModuleName, + types.TypeMsgDeploySystemContracts, + "policies object not found", + ), nil, nil + } + if len(policies.Items) == 0 { + return simtypes.NoOpMsg( + types.ModuleName, + types.TypeMsgDeploySystemContracts, + "no policies found", + ), nil, nil + } + admin := policies.Items[0].Address + + address, err := observerTypes.GetOperatorAddressFromAccAddress(admin) + if err != nil { + return simtypes.NoOpMsg( + types.ModuleName, + types.TypeMsgDeploySystemContracts, + "unable to get operator address", + ), nil, err + } + simAccount, found := simtypes.FindAccount(accounts, address) + if !found { + return simtypes.NoOpMsg( + types.ModuleName, + types.TypeMsgDeploySystemContracts, + "sim account for admin address not found", + ), nil, nil + } + + msg := types.MsgDeploySystemContracts{Creator: admin} + + err = msg.ValidateBasic() + if err != nil { + return simtypes.NoOpMsg(types.ModuleName, msg.Type(), "failed to validate basic msg"), nil, err + } + + txCtx := simulation.OperationInput{ + R: r, + App: app, + TxGen: moduletestutil.MakeTestEncodingConfig().TxConfig, + Cdc: nil, + Msg: &msg, + MsgType: msg.Type(), + Context: ctx, + SimAccount: simAccount, + AccountKeeper: k.GetAuthKeeper(), + Bankkeeper: k.GetBankKeeper(), + ModuleName: types.ModuleName, + } + + return simulation.GenAndDeliverTxWithRandFees(txCtx) + } +} diff --git a/x/fungible/simulation/simap.go b/x/fungible/simulation/simap.go deleted file mode 100644 index 92c437c0d1..0000000000 --- a/x/fungible/simulation/simap.go +++ /dev/null @@ -1,15 +0,0 @@ -package simulation - -import ( - sdk "github.com/cosmos/cosmos-sdk/types" - simtypes "github.com/cosmos/cosmos-sdk/types/simulation" -) - -// FindAccount find a specific address from an account list -func FindAccount(accs []simtypes.Account, address string) (simtypes.Account, bool) { - creator, err := sdk.AccAddressFromBech32(address) - if err != nil { - panic(err) - } - return simtypes.FindAccount(accs, creator) -} diff --git a/x/fungible/types/expected_keepers.go b/x/fungible/types/expected_keepers.go index 442f625c10..a0ee828878 100644 --- a/x/fungible/types/expected_keepers.go +++ b/x/fungible/types/expected_keepers.go @@ -13,6 +13,7 @@ import ( evmtypes "github.com/zeta-chain/ethermint/x/evm/types" "github.com/zeta-chain/node/pkg/chains" + authoritytypes "github.com/zeta-chain/node/x/authority/types" ) // AccountKeeper defines the expected account keeper used for simulations (noalias) @@ -33,6 +34,7 @@ type BankKeeper interface { amt sdk.Coins, ) error MintCoins(ctx sdk.Context, moduleName string, amt sdk.Coins) error + SpendableCoins(ctx sdk.Context, addr sdk.AccAddress) sdk.Coins GetSupply(ctx sdk.Context, denom string) sdk.Coin } @@ -62,4 +64,5 @@ type EVMKeeper interface { type AuthorityKeeper interface { CheckAuthorization(ctx sdk.Context, msg sdk.Msg) error GetAdditionalChainList(ctx sdk.Context) (list []chains.Chain) + GetPolicies(ctx sdk.Context) (val authoritytypes.Policies, found bool) } diff --git a/x/observer/keeper/keeper.go b/x/observer/keeper/keeper.go index ad7fa984c9..b9fdc0f97d 100644 --- a/x/observer/keeper/keeper.go +++ b/x/observer/keeper/keeper.go @@ -13,25 +13,29 @@ import ( type ( Keeper struct { - cdc codec.BinaryCodec + cdc codec.Codec storeKey storetypes.StoreKey memKey storetypes.StoreKey stakingKeeper types.StakingKeeper slashingKeeper types.SlashingKeeper authorityKeeper types.AuthorityKeeper lightclientKeeper types.LightclientKeeper + bankKeeper types.BankKeeper + authKeeper types.AccountKeeper authority string } ) func NewKeeper( - cdc codec.BinaryCodec, + cdc codec.Codec, storeKey, memKey storetypes.StoreKey, stakingKeeper types.StakingKeeper, slashinKeeper types.SlashingKeeper, authorityKeeper types.AuthorityKeeper, lightclientKeeper types.LightclientKeeper, + bankKeeper types.BankKeeper, + authKeeper types.AccountKeeper, authority string, ) *Keeper { if _, err := sdk.AccAddressFromBech32(authority); err != nil { @@ -46,6 +50,8 @@ func NewKeeper( slashingKeeper: slashinKeeper, authorityKeeper: authorityKeeper, lightclientKeeper: lightclientKeeper, + bankKeeper: bankKeeper, + authKeeper: authKeeper, authority: authority, } } @@ -62,6 +68,14 @@ func (k Keeper) GetAuthorityKeeper() types.AuthorityKeeper { return k.authorityKeeper } +func (k Keeper) GetBankKeeper() types.BankKeeper { + return k.bankKeeper +} + +func (k Keeper) GetAuthKeeper() types.AccountKeeper { + return k.authKeeper +} + func (k Keeper) GetLightclientKeeper() types.LightclientKeeper { return k.lightclientKeeper } @@ -74,7 +88,7 @@ func (k Keeper) StoreKey() storetypes.StoreKey { return k.storeKey } -func (k Keeper) Codec() codec.BinaryCodec { +func (k Keeper) Codec() codec.Codec { return k.cdc } diff --git a/x/observer/keeper/msg_server_disable_cctx_flags.go b/x/observer/keeper/msg_server_disable_cctx_flags.go index bd5a0a9f17..70a77849cb 100644 --- a/x/observer/keeper/msg_server_disable_cctx_flags.go +++ b/x/observer/keeper/msg_server_disable_cctx_flags.go @@ -37,7 +37,6 @@ func (k msgServer) DisableCCTX( if msg.DisableOutbound { flags.IsOutboundEnabled = false } - k.SetCrosschainFlags(ctx, flags) err = ctx.EventManager().EmitTypedEvents(&types.EventCCTXDisabled{ diff --git a/x/observer/module.go b/x/observer/module.go index 41724bc8b1..1136be6ecd 100644 --- a/x/observer/module.go +++ b/x/observer/module.go @@ -31,10 +31,10 @@ var ( // AppModuleBasic implements the AppModuleBasic interface for the observer module. type AppModuleBasic struct { - cdc codec.BinaryCodec + cdc codec.Codec } -func NewAppModuleBasic(cdc codec.BinaryCodec) AppModuleBasic { +func NewAppModuleBasic(cdc codec.Codec) AppModuleBasic { return AppModuleBasic{cdc: cdc} } diff --git a/x/observer/module_simulation.go b/x/observer/module_simulation.go index 555532c090..373682019c 100644 --- a/x/observer/module_simulation.go +++ b/x/observer/module_simulation.go @@ -1,29 +1,18 @@ package observer import ( - "math/rand" - sdk "github.com/cosmos/cosmos-sdk/types" "github.com/cosmos/cosmos-sdk/types/module" simtypes "github.com/cosmos/cosmos-sdk/types/simulation" + "github.com/zeta-chain/node/x/observer/simulation" "github.com/zeta-chain/node/x/observer/types" ) -const ( - // #nosec G101 not a hardcoded credential - opWeightMsgUpdateClientParams = "op_weight_msg_update_client_params" - defaultWeightMsgUpdateClientParams int = 100 -) - -// GenerateGenesisState creates a randomized GenState of the module +// GenerateGenesisState creates a GenState of the module used to initialize the simulation runs func (AppModule) GenerateGenesisState(simState *module.SimulationState) { - accs := make([]string, len(simState.Accounts)) - for i, acc := range simState.Accounts { - accs[i] = acc.Address.String() - } - observerGenesis := types.GenesisState{} - simState.GenState[types.ModuleName] = simState.Cdc.MustMarshalJSON(&observerGenesis) + observerGenesis := types.DefaultGenesis() + simState.GenState[types.ModuleName] = simState.Cdc.MustMarshalJSON(observerGenesis) } // ProposalContents doesn't return any content functions for governance proposals @@ -36,18 +25,13 @@ func (AppModule) ProposalMsgs(_ module.SimulationState) []simtypes.WeightedPropo } // RegisterStoreDecoder registers a decoder -func (am AppModule) RegisterStoreDecoder(_ sdk.StoreDecoderRegistry) {} +func (am AppModule) RegisterStoreDecoder(sdr sdk.StoreDecoderRegistry) { + sdr[types.StoreKey] = simulation.NewDecodeStore(am.cdc) +} // WeightedOperations returns the all the gov module operations with their respective weights. func (am AppModule) WeightedOperations(simState module.SimulationState) []simtypes.WeightedOperation { - operations := make([]simtypes.WeightedOperation, 0) - - var weightMsgUpdateClientParams int - simState.AppParams.GetOrGenerate(simState.Cdc, opWeightMsgUpdateClientParams, &weightMsgUpdateClientParams, nil, - func(_ *rand.Rand) { - weightMsgUpdateClientParams = defaultWeightMsgUpdateClientParams - }, + return simulation.WeightedOperations( + simState.AppParams, simState.Cdc, am.keeper, ) - - return operations } diff --git a/x/observer/simulation/decoders.go b/x/observer/simulation/decoders.go new file mode 100644 index 0000000000..fd8eae7535 --- /dev/null +++ b/x/observer/simulation/decoders.go @@ -0,0 +1,97 @@ +package simulation + +import ( + "bytes" + "fmt" + + "github.com/cosmos/cosmos-sdk/codec" + "github.com/cosmos/cosmos-sdk/types/kv" + + "github.com/zeta-chain/node/x/observer/types" +) + +// NewDecodeStore returns a decoder function closure that unmarshals the KVPair's +// Value to the corresponding observer types. +func NewDecodeStore(cdc codec.Codec) func(kvA, kvB kv.Pair) string { + return func(kvA, kvB kv.Pair) string { + switch { + case bytes.Equal(kvA.Key, types.KeyPrefix(types.CrosschainFlagsKey)): + var crosschainFlagsA, crosschainFlagsB types.CrosschainFlags + cdc.MustUnmarshal(kvA.Value, &crosschainFlagsA) + cdc.MustUnmarshal(kvB.Value, &crosschainFlagsB) + return fmt.Sprintf("%v\n%v", crosschainFlagsA, crosschainFlagsB) + case bytes.Equal(kvA.Key, types.KeyPrefix(types.LastBlockObserverCountKey)): + var lastBlockObserverCountA, lastBlockObserverCountB types.LastObserverCount + cdc.MustUnmarshal(kvA.Value, &lastBlockObserverCountA) + cdc.MustUnmarshal(kvB.Value, &lastBlockObserverCountB) + return fmt.Sprintf("%v\n%v", lastBlockObserverCountA, lastBlockObserverCountB) + case bytes.Equal(kvA.Key, types.KeyPrefix(types.NodeAccountKey)): + var nodeAccountA, nodeAccountB types.NodeAccount + cdc.MustUnmarshal(kvA.Value, &nodeAccountA) + cdc.MustUnmarshal(kvB.Value, &nodeAccountB) + return fmt.Sprintf("%v\n%v", nodeAccountA, nodeAccountB) + case bytes.Equal(kvA.Key, types.KeyPrefix(types.KeygenKey)): + var keygenA, keygenB types.Keygen + cdc.MustUnmarshal(kvA.Value, &keygenA) + cdc.MustUnmarshal(kvB.Value, &keygenB) + return fmt.Sprintf("%v\n%v", keygenA, keygenB) + case bytes.Equal(kvA.Key, types.KeyPrefix(types.BallotListKey)): + var ballotListA, ballotListB types.BallotListForHeight + cdc.MustUnmarshal(kvA.Value, &ballotListA) + cdc.MustUnmarshal(kvB.Value, &ballotListB) + return fmt.Sprintf("%v\n%v", ballotListA, ballotListB) + case bytes.Equal(kvA.Key, types.KeyPrefix(types.VoterKey)): + var voterA, voterB types.Ballot + cdc.MustUnmarshal(kvA.Value, &voterA) + cdc.MustUnmarshal(kvB.Value, &voterB) + return fmt.Sprintf("%v\n%v", voterA, voterB) + case bytes.Equal(kvA.Key, types.KeyPrefix(types.TSSKey)): + var tssA, tssB types.TSS + cdc.MustUnmarshal(kvA.Value, &tssA) + cdc.MustUnmarshal(kvB.Value, &tssB) + return fmt.Sprintf("%v\n%v", tssA, tssB) + case bytes.Equal(kvA.Key, types.KeyPrefix(types.ObserverSetKey)): + var observerSetA, observerSetB types.ObserverSet + cdc.MustUnmarshal(kvA.Value, &observerSetA) + cdc.MustUnmarshal(kvB.Value, &observerSetB) + return fmt.Sprintf("%v\n%v", observerSetA, observerSetB) + case bytes.Equal(kvA.Key, types.KeyPrefix(types.AllChainParamsKey)): + var allChainParamsA, allChainParamsB types.ChainParamsList + cdc.MustUnmarshal(kvA.Value, &allChainParamsA) + cdc.MustUnmarshal(kvB.Value, &allChainParamsB) + return fmt.Sprintf("%v\n%v", allChainParamsA, allChainParamsB) + case bytes.Equal(kvA.Key, types.KeyPrefix(types.TSSHistoryKey)): + var tssHistoryA, tssHistoryB types.TSS + cdc.MustUnmarshal(kvA.Value, &tssHistoryA) + cdc.MustUnmarshal(kvB.Value, &tssHistoryB) + return fmt.Sprintf("%v\n%v", tssHistoryA, tssHistoryB) + case bytes.Equal(kvA.Key, types.KeyPrefix(types.TssFundMigratorKey)): + var tssFundMigratorA, tssFundMigratorB types.TssFundMigratorInfo + cdc.MustUnmarshal(kvA.Value, &tssFundMigratorA) + cdc.MustUnmarshal(kvB.Value, &tssFundMigratorB) + return fmt.Sprintf("%v\n%v", tssFundMigratorA, tssFundMigratorB) + case bytes.Equal(kvA.Key, types.KeyPrefix(types.PendingNoncesKeyPrefix)): + var pendingNoncesA, pendingNoncesB types.PendingNonces + cdc.MustUnmarshal(kvA.Value, &pendingNoncesA) + cdc.MustUnmarshal(kvB.Value, &pendingNoncesB) + return fmt.Sprintf("%v\n%v", pendingNoncesA, pendingNoncesB) + case bytes.Equal(kvA.Key, types.KeyPrefix(types.ChainNoncesKey)): + var chainNoncesA, chainNoncesB types.ChainNonces + cdc.MustUnmarshal(kvA.Value, &chainNoncesA) + cdc.MustUnmarshal(kvB.Value, &chainNoncesB) + return fmt.Sprintf("%v\n%v", chainNoncesA, chainNoncesB) + case bytes.Equal(kvA.Key, types.KeyPrefix(types.NonceToCctxKeyPrefix)): + var nonceToCctxA, nonceToCctxB types.NonceToCctx + cdc.MustUnmarshal(kvA.Value, &nonceToCctxA) + cdc.MustUnmarshal(kvB.Value, &nonceToCctxB) + return fmt.Sprintf("%v\n%v", nonceToCctxA, nonceToCctxB) + case bytes.Equal(kvA.Key, types.KeyPrefix(types.ParamsKey)): + var paramsA, paramsB types.Params + cdc.MustUnmarshal(kvA.Value, ¶msA) + cdc.MustUnmarshal(kvB.Value, ¶msB) + return fmt.Sprintf("%v\n%v", paramsA, paramsB) + default: + panic(fmt.Sprintf("invalid observer key prefix %X", kvA.Key)) + } + } +} diff --git a/x/observer/simulation/decoders_test.go b/x/observer/simulation/decoders_test.go new file mode 100644 index 0000000000..616994a664 --- /dev/null +++ b/x/observer/simulation/decoders_test.go @@ -0,0 +1,88 @@ +package simulation_test + +import ( + "fmt" + "testing" + + "github.com/cosmos/cosmos-sdk/types/kv" + "github.com/stretchr/testify/require" + "github.com/zeta-chain/node/pkg/chains" + keepertest "github.com/zeta-chain/node/testutil/keeper" + "github.com/zeta-chain/node/testutil/sample" + "github.com/zeta-chain/node/x/observer/simulation" + "github.com/zeta-chain/node/x/observer/types" +) + +func TestNewDecodeStore(t *testing.T) { + k, _, _, _ := keepertest.ObserverKeeper(t) + cdc := k.Codec() + dec := simulation.NewDecodeStore(cdc) + crosschainFlags := sample.CrosschainFlags() + lastBlockObserverCount := sample.LastObserverCount(10) + nodeAccount := sample.NodeAccount() + keygen := sample.Keygen(t) + + ballotList := types.BallotListForHeight{ + Height: 10, + BallotsIndexList: []string{sample.ZetaIndex(t)}, + } + + ballot := sample.Ballot(t, "sample") + tss := sample.Tss() + observerSet := sample.ObserverSet(10) + chainParamsList := sample.ChainParamsList() + //tssHistory := sample.TssList(10) + tssFundMigrator := sample.TssFundsMigrator(chains.Ethereum.ChainId) + pendingNonce := sample.PendingNoncesList(t, "index", 10)[0] + chainNonces := sample.ChainNonces(chains.Ethereum.ChainId) + nonceToCctx := sample.NonceToCCTX(t, "index") + params := types.Params{BallotMaturityBlocks: 100} + + kvPairs := kv.Pairs{ + Pairs: []kv.Pair{ + {Key: types.KeyPrefix(types.CrosschainFlagsKey), Value: cdc.MustMarshal(crosschainFlags)}, + {Key: types.KeyPrefix(types.LastBlockObserverCountKey), Value: cdc.MustMarshal(lastBlockObserverCount)}, + {Key: types.KeyPrefix(types.NodeAccountKey), Value: cdc.MustMarshal(nodeAccount)}, + {Key: types.KeyPrefix(types.KeygenKey), Value: cdc.MustMarshal(keygen)}, + {Key: types.KeyPrefix(types.BallotListKey), Value: cdc.MustMarshal(&ballotList)}, + {Key: types.KeyPrefix(types.VoterKey), Value: cdc.MustMarshal(ballot)}, + {Key: types.KeyPrefix(types.TSSKey), Value: cdc.MustMarshal(&tss)}, + {Key: types.KeyPrefix(types.TSSHistoryKey), Value: cdc.MustMarshal(&tss)}, + {Key: types.KeyPrefix(types.ObserverSetKey), Value: cdc.MustMarshal(&observerSet)}, + {Key: types.KeyPrefix(types.AllChainParamsKey), Value: cdc.MustMarshal(&chainParamsList)}, + {Key: types.KeyPrefix(types.TssFundMigratorKey), Value: cdc.MustMarshal(&tssFundMigrator)}, + {Key: types.KeyPrefix(types.PendingNoncesKeyPrefix), Value: cdc.MustMarshal(&pendingNonce)}, + {Key: types.KeyPrefix(types.ChainNoncesKey), Value: cdc.MustMarshal(&chainNonces)}, + {Key: types.KeyPrefix(types.NonceToCctxKeyPrefix), Value: cdc.MustMarshal(&nonceToCctx)}, + {Key: types.KeyPrefix(types.ParamsKey), Value: cdc.MustMarshal(¶ms)}, + }, + } + + tests := []struct { + name string + expectedLog string + }{ + {"CrosschainFlags", fmt.Sprintf("%v\n%v", *crosschainFlags, *crosschainFlags)}, + {"LastBlockObserverCount", fmt.Sprintf("%v\n%v", *lastBlockObserverCount, *lastBlockObserverCount)}, + {"NodeAccount", fmt.Sprintf("%v\n%v", *nodeAccount, *nodeAccount)}, + {"Keygen", fmt.Sprintf("%v\n%v", *keygen, *keygen)}, + {"BallotList", fmt.Sprintf("%v\n%v", ballotList, ballotList)}, + {"Ballot", fmt.Sprintf("%v\n%v", *ballot, *ballot)}, + {"TSS", fmt.Sprintf("%v\n%v", tss, tss)}, + {"TSSHistory", fmt.Sprintf("%v\n%v", tss, tss)}, + {"ObserverSet", fmt.Sprintf("%v\n%v", observerSet, observerSet)}, + {"ChainParamsList", fmt.Sprintf("%v\n%v", chainParamsList, chainParamsList)}, + {"TssFundMigrator", fmt.Sprintf("%v\n%v", tssFundMigrator, tssFundMigrator)}, + {"PendingNonces", fmt.Sprintf("%v\n%v", pendingNonce, pendingNonce)}, + {"ChainNonces", fmt.Sprintf("%v\n%v", chainNonces, chainNonces)}, + {"NonceToCctx", fmt.Sprintf("%v\n%v", nonceToCctx, nonceToCctx)}, + {"Params", fmt.Sprintf("%v\n%v", params, params)}, + } + + for i, tt := range tests { + i, tt := i, tt + t.Run(tt.name, func(t *testing.T) { + require.Equal(t, tt.expectedLog, dec(kvPairs.Pairs[i], kvPairs.Pairs[i])) + }) + } +} diff --git a/x/observer/simulation/operations.go b/x/observer/simulation/operations.go new file mode 100644 index 0000000000..7c12ced3c0 --- /dev/null +++ b/x/observer/simulation/operations.go @@ -0,0 +1,107 @@ +package simulation + +import ( + "math/rand" + + "github.com/cosmos/cosmos-sdk/baseapp" + "github.com/cosmos/cosmos-sdk/codec" + sdk "github.com/cosmos/cosmos-sdk/types" + moduletestutil "github.com/cosmos/cosmos-sdk/types/module/testutil" + simtypes "github.com/cosmos/cosmos-sdk/types/simulation" + "github.com/cosmos/cosmos-sdk/x/simulation" + + "github.com/zeta-chain/node/x/observer/keeper" + "github.com/zeta-chain/node/x/observer/types" +) + +// Simulation operation weights constants +// Operation weights are used by the `SimulateFromSeed` +// function to pick a random operation based on the weights.The functions with higher weights are more likely to be picked. + +// Therefore, this decides the percentage of a certain operation that is part of a block. + +// Based on the weights assigned in the cosmos sdk modules, +// 100 seems to the max weight used,and we should use relative weights +// to signify the number of each operation in a block. + +// TODO Add more details to comment based on what the number represents in terms of percentage of operations in a block +// https://github.com/zeta-chain/node/issues/3100 +const ( + // #nosec G101 not a hardcoded credential + OpWeightMsgTypeMsgEnableCCTX = "op_weight_msg_enable_crosschain_flags" + // DefaultWeightMsgTypeMsgEnableCCTX We ues a high weight for this operation + // to ensure that it is present in the block more number of times than any operation that changes the validator set + + // Arrived at this number based on the weights used in the cosmos sdk staking module and through some trial and error + DefaultWeightMsgTypeMsgEnableCCTX = 3650 +) + +// WeightedOperations for observer module +func WeightedOperations( + appParams simtypes.AppParams, cdc codec.JSONCodec, k keeper.Keeper, +) simulation.WeightedOperations { + var weightMsgTypeMsgEnableCCTX int + + appParams.GetOrGenerate(cdc, OpWeightMsgTypeMsgEnableCCTX, &weightMsgTypeMsgEnableCCTX, nil, + func(_ *rand.Rand) { + weightMsgTypeMsgEnableCCTX = DefaultWeightMsgTypeMsgEnableCCTX + }) + + return simulation.WeightedOperations{ + simulation.NewWeightedOperation( + weightMsgTypeMsgEnableCCTX, + SimulateMsgTypeMsgEnableCCTX(k), + ), + } +} + +// SimulateMsgTypeMsgEnableCCTX generates a MsgEnableCCTX and delivers it. +func SimulateMsgTypeMsgEnableCCTX(k keeper.Keeper) simtypes.Operation { + return func(r *rand.Rand, app *baseapp.BaseApp, ctx sdk.Context, accounts []simtypes.Account, _ string, + ) (OperationMsg simtypes.OperationMsg, futureOps []simtypes.FutureOperation, err error) { + policies, found := k.GetAuthorityKeeper().GetPolicies(ctx) + if !found { + return simtypes.NoOpMsg(types.ModuleName, types.TypeMsgEnableCCTX, "policies object not found"), nil, nil + } + if len(policies.Items) == 0 { + return simtypes.NoOpMsg(types.ModuleName, types.TypeMsgEnableCCTX, "no policies found"), nil, nil + } + + admin := policies.Items[0].Address + address, err := types.GetOperatorAddressFromAccAddress(admin) + if err != nil { + return simtypes.NoOpMsg(types.ModuleName, types.TypeMsgEnableCCTX, err.Error()), nil, err + } + simAccount, found := simtypes.FindAccount(accounts, address) + if !found { + return simtypes.NoOpMsg(types.ModuleName, types.TypeMsgEnableCCTX, "admin account not found"), nil, nil + } + + msg := types.MsgEnableCCTX{ + Creator: simAccount.Address.String(), + EnableInbound: true, + EnableOutbound: false, + } + + err = msg.ValidateBasic() + if err != nil { + return simtypes.NoOpMsg(types.ModuleName, msg.Type(), err.Error()), nil, err + } + + txCtx := simulation.OperationInput{ + R: r, + App: app, + TxGen: moduletestutil.MakeTestEncodingConfig().TxConfig, + Cdc: nil, + Msg: &msg, + MsgType: msg.Type(), + Context: ctx, + SimAccount: simAccount, + AccountKeeper: k.GetAuthKeeper(), + Bankkeeper: k.GetBankKeeper(), + ModuleName: types.ModuleName, + } + + return simulation.GenAndDeliverTxWithRandFees(txCtx) + } +} diff --git a/x/observer/simulation/simap.go b/x/observer/simulation/simap.go deleted file mode 100644 index 92c437c0d1..0000000000 --- a/x/observer/simulation/simap.go +++ /dev/null @@ -1,15 +0,0 @@ -package simulation - -import ( - sdk "github.com/cosmos/cosmos-sdk/types" - simtypes "github.com/cosmos/cosmos-sdk/types/simulation" -) - -// FindAccount find a specific address from an account list -func FindAccount(accs []simtypes.Account, address string) (simtypes.Account, bool) { - creator, err := sdk.AccAddressFromBech32(address) - if err != nil { - panic(err) - } - return simtypes.FindAccount(accs, creator) -} diff --git a/x/observer/types/expected_keepers.go b/x/observer/types/expected_keepers.go index 2cf2b9ac75..2788187c94 100644 --- a/x/observer/types/expected_keepers.go +++ b/x/observer/types/expected_keepers.go @@ -2,6 +2,7 @@ package types import ( sdk "github.com/cosmos/cosmos-sdk/types" + "github.com/cosmos/cosmos-sdk/x/auth/types" slashingtypes "github.com/cosmos/cosmos-sdk/x/slashing/types" stakingtypes "github.com/cosmos/cosmos-sdk/x/staking/types" @@ -38,6 +39,7 @@ type AuthorityKeeper interface { // SetPolicies is solely used for the migration of policies from observer to authority SetPolicies(ctx sdk.Context, policies authoritytypes.Policies) + GetPolicies(ctx sdk.Context) (val authoritytypes.Policies, found bool) } type LightclientKeeper interface { @@ -57,3 +59,12 @@ type LightclientKeeper interface { parentHash []byte, ) } + +type BankKeeper interface { + SpendableCoins(ctx sdk.Context, addr sdk.AccAddress) sdk.Coins +} + +// AccountKeeper defines the expected account keeper used for simulations (noalias) +type AccountKeeper interface { + GetAccount(ctx sdk.Context, addr sdk.AccAddress) types.AccountI +} diff --git a/x/observer/types/keys.go b/x/observer/types/keys.go index 539eab83c0..abdb2543d0 100644 --- a/x/observer/types/keys.go +++ b/x/observer/types/keys.go @@ -49,13 +49,15 @@ func ChainNoncesKeyPrefix(chainID int64) []byte { return []byte(strconv.FormatInt(chainID, 10)) } +// Address TODOS for name changes: https://github.com/zeta-chain/node/issues/3098 const ( BlameKey = "Blame-" - // TODO change identifier for VoterKey to something more descriptive + // TODO change identifier for VoterKey to BallotKey VoterKey = "Voter-value-" // AllChainParamsKey is the ke prefix for all chain params // NOTE: CoreParams is old name for AllChainParams we keep it as key value for backward compatibility + // TODO rename to ChainParamsListKey AllChainParamsKey = "CoreParams" ObserverSetKey = "ObserverSet-value-" @@ -64,20 +66,25 @@ const ( // NOTE: PermissionFlags is old name for CrosschainFlags we keep it as key value for backward compatibility CrosschainFlagsKey = "PermissionFlags-value-" + // TODO rename to LastObserverCountKey LastBlockObserverCountKey = "ObserverCount-value-" NodeAccountKey = "NodeAccount-value-" KeygenKey = "Keygen-value-" - BlockHeaderKey = "BlockHeader-value-" - BlockHeaderStateKey = "BlockHeaderState-value-" - BallotListKey = "BallotList-value-" - TSSKey = "TSS-value-" - TSSHistoryKey = "TSS-History-value-" + // TODO rename to BallotListForHeightKey + BallotListKey = "BallotList-value-" + TSSKey = "TSS-value-" + TSSHistoryKey = "TSS-History-value-" + + // TODO rename to TssFundMigratorInfoKey TssFundMigratorKey = "FundsMigrator-value-" + // TODO Rename to PendingNoncesListKey PendingNoncesKeyPrefix = "PendingNonces-value-" ChainNoncesKey = "ChainNonces-value-" - NonceToCctxKeyPrefix = "NonceToCctx-value-" + + // TODO rename to NonceToCctxListKey + NonceToCctxKeyPrefix = "NonceToCctx-value-" ParamsKey = "Params-value-" ) diff --git a/zetaclient/testutils/mocks/zetacore_client.go b/zetaclient/testutils/mocks/zetacore_client.go index fa5b34486b..fefd653874 100644 --- a/zetaclient/testutils/mocks/zetacore_client.go +++ b/zetaclient/testutils/mocks/zetacore_client.go @@ -22,8 +22,8 @@ import ( upgradetypes "github.com/cosmos/cosmos-sdk/x/upgrade/types" - zerolog "github.com/rs/zerolog" cometbfttypes "github.com/cometbft/cometbft/types" + zerolog "github.com/rs/zerolog" ) // ZetacoreClient is an autogenerated mock type for the ZetacoreClient type