From 028503949e93227bb48ea8c5642bcc7500615085 Mon Sep 17 00:00:00 2001 From: Philip Offtermatt Date: Thu, 23 May 2024 13:51:12 +0200 Subject: [PATCH] Start adding e2e test --- tests/e2e/config.go | 14 ++ tests/e2e/main.go | 6 + tests/e2e/state.go | 13 +- tests/e2e/steps_inactive_vals.go | 405 +++++++++++++++++++++++++++++++ testutil/keeper/mocks.go | 14 ++ x/ccv/provider/keeper/relay.go | 4 +- x/ccv/types/expected_keepers.go | 1 + 7 files changed, 451 insertions(+), 6 deletions(-) create mode 100644 tests/e2e/steps_inactive_vals.go diff --git a/tests/e2e/config.go b/tests/e2e/config.go index a5b86e85f9..36629f57d9 100644 --- a/tests/e2e/config.go +++ b/tests/e2e/config.go @@ -87,6 +87,7 @@ const ( MulticonsumerTestCfg TestConfigType = "multi-consumer" ConsumerMisbehaviourTestCfg TestConfigType = "consumer-misbehaviour" CompatibilityTestCfg TestConfigType = "compatibility" + InactiveValsCfg TestConfigType = "inactive-vals" ) // Attributes that are unique to a validator. Allows us to map (part of) @@ -264,6 +265,8 @@ func GetTestConfig(cfgType TestConfigType, providerVersion, consumerVersion stri testCfg = ConsumerMisbehaviourTestConfig() case CompatibilityTestCfg: testCfg = CompatibilityTestConfig(pv, cv) + case InactiveValsCfg: + testCfg = InactiveValsConfig() default: panic(fmt.Sprintf("Invalid test config: %s", cfgType)) } @@ -500,6 +503,17 @@ func CompatibilityTestConfig(providerVersion, consumerVersion string) TestConfig return testCfg } +func InactiveValsConfig() TestConfig { + tr := DefaultTestConfig() + tr.name = "InactiveValsConfig" + // set the MaxProviderConsensusValidators param to 2 + proviConfig := tr.chainConfigs[ChainID("provi")] + proviConfig.GenesisChanges += " | .app_state.provider.params.max_provider_consensus_validators = \"2\"" + tr.chainConfigs[ChainID("provi")] = proviConfig + + return tr +} + func DefaultTestConfig() TestConfig { tr := TestConfig{ name: string(DefaultTestCfg), diff --git a/tests/e2e/main.go b/tests/e2e/main.go index c1766ccf37..09e963d84f 100644 --- a/tests/e2e/main.go +++ b/tests/e2e/main.go @@ -168,6 +168,12 @@ var stepChoices = map[string]StepChoice{ description: "test partial set security for a Top-N chain", testConfig: DefaultTestCfg, }, + "inactive-validators-on-consumer": { + name: "inactive-validators-on-consumer", + steps: stepsInactiveValidatorsOnConsumer(), + description: "test inactive validators on consumer", + testConfig: InactiveValsCfg, + }, } func getTestCaseUsageString() string { diff --git a/tests/e2e/state.go b/tests/e2e/state.go index 7b674b0df8..ab93240723 100644 --- a/tests/e2e/state.go +++ b/tests/e2e/state.go @@ -328,14 +328,14 @@ func (tr TestConfig) getRewards(chain ChainID, modelState Rewards) Rewards { func (tr TestConfig) getReward(chain ChainID, validator ValidatorID, blockHeight uint, isNativeDenom bool) float64 { valCfg := tr.validatorConfigs[validator] - delAddresss := valCfg.DelAddress + delAddress := valCfg.DelAddress if chain != ChainID("provi") { // use binary with Bech32Prefix set to ConsumerAccountPrefix if valCfg.UseConsumerKey { - delAddresss = valCfg.ConsumerDelAddress + delAddress = valCfg.ConsumerDelAddress } else { // use the same address as on the provider but with different prefix - delAddresss = valCfg.DelAddressOnConsumer + delAddress = valCfg.DelAddressOnConsumer } } @@ -343,7 +343,7 @@ func (tr TestConfig) getReward(chain ChainID, validator ValidatorID, blockHeight bz, err := exec.Command("docker", "exec", tr.containerConfig.InstanceName, tr.chainConfigs[chain].BinaryName, "query", "distribution", "rewards", - delAddresss, + delAddress, `--height`, fmt.Sprint(blockHeight), `--node`, tr.getQueryNode(chain), @@ -358,6 +358,11 @@ func (tr TestConfig) getReward(chain ChainID, validator ValidatorID, blockHeight denomCondition = `total.#(denom=="stake").amount` } + if *verbose { + log.Println("Getting reward for chain: ", chain, " validator: ", validator, " block: ", blockHeight) + log.Println("Reward response: ", string(bz)) + } + return gjson.Get(string(bz), denomCondition).Float() } diff --git a/tests/e2e/steps_inactive_vals.go b/tests/e2e/steps_inactive_vals.go new file mode 100644 index 0000000000..01b647d06b --- /dev/null +++ b/tests/e2e/steps_inactive_vals.go @@ -0,0 +1,405 @@ +package main + +import clienttypes "github.com/cosmos/ibc-go/v7/modules/core/02-client/types" + +// stepsOptInChain starts a provider chain and an Opt-In chain and opts in and out validators +func stepsInactiveValidatorsOnConsumer() []Step { + s := concatSteps( + []Step{ + { + Action: StartChainAction{ + Chain: ChainID("provi"), + Validators: []StartChainValidator{ + {Id: ValidatorID("alice"), Stake: 100000000, Allocation: 10000000000}, + {Id: ValidatorID("bob"), Stake: 200000000, Allocation: 10000000000}, + {Id: ValidatorID("carol"), Stake: 300000000, Allocation: 10000000000}, + }, + }, + State: State{ + ChainID("provi"): ChainState{ + ValPowers: &map[ValidatorID]uint{ + ValidatorID("alice"): 0, // max consensus validators is 2, so alice should not be in power + ValidatorID("bob"): 200, + ValidatorID("carol"): 300, + }, + StakedTokens: &map[ValidatorID]uint{ + ValidatorID("alice"): 100000000, + ValidatorID("bob"): 200000000, + ValidatorID("carol"): 300000000, + }, + Rewards: &Rewards{ + IsNativeDenom: true, // check for rewards in the provider denom + IsIncrementalReward: true, // check current rewards, because alice gets one block of rewards due to being in the genesis + IsRewarded: map[ValidatorID]bool{ + ValidatorID("alice"): false, + ValidatorID("bob"): true, + ValidatorID("carol"): true, + }, + }, + }, + }, + }, + }, + setupOptInChain(), + []Step{ + // check that active-but-not-consensus validators do not get slashed for downtime + { + // alices provider node goes offline + Action: DowntimeSlashAction{ + Chain: ChainID("provi"), + Validator: ValidatorID("alice"), + }, + State: State{ + ChainID("provi"): ChainState{ + ValPowers: &map[ValidatorID]uint{ + ValidatorID("alice"): 0, // still 0 consensus power + ValidatorID("bob"): 200, + ValidatorID("carol"): 300, + }, + StakedTokens: &map[ValidatorID]uint{ + ValidatorID("alice"): 100000000, // but alice does not get jailed or slashed + ValidatorID("bob"): 200000000, + ValidatorID("carol"): 300000000, + }, + // check that bob and carol get rewards, but alice does not + Rewards: &Rewards{ + IsNativeDenom: true, // check for rewards in the provider denom + IsIncrementalReward: true, // check rewards since block 1 + IsRewarded: map[ValidatorID]bool{ + ValidatorID("alice"): false, + ValidatorID("bob"): true, + ValidatorID("carol"): true, + }, + }, + }, + }, + }, + // give carol more power so that she has enough power to validate if bob goes down + { + Action: DelegateTokensAction{ + Chain: ChainID("provi"), + From: ValidatorID("bob"), + To: ValidatorID("carol"), + Amount: 200000000, // carol needs to have more than 2/3rds of power(carol) + power(bob), so if bob has 200 power, carol needs at least 401, so we just go for 500 + }, + State: State{ + ChainID("provi"): ChainState{ + ValPowers: &map[ValidatorID]uint{ + ValidatorID("alice"): 0, + ValidatorID("bob"): 200, + ValidatorID("carol"): 500, + }, + StakedTokens: &map[ValidatorID]uint{ + ValidatorID("alice"): 100000000, + ValidatorID("bob"): 200000000, + ValidatorID("carol"): 500000000, + }, + }, + }, + }, + // bob goes offline + { + Action: DowntimeSlashAction{ + Chain: ChainID("provi"), + Validator: ValidatorID("bob"), + }, + State: State{ + ChainID("provi"): ChainState{ + ValPowers: &map[ValidatorID]uint{ + ValidatorID("alice"): 100, // alice gets into the active set + ValidatorID("bob"): 0, // bob is jailed + ValidatorID("carol"): 500, + }, + StakedTokens: &map[ValidatorID]uint{ + ValidatorID("alice"): 100000000, + ValidatorID("bob"): 198000000, // 1% slash + ValidatorID("carol"): 500000000, + }, + // check that now every validator got rewarded since the first block + Rewards: &Rewards{ + IsNativeDenom: true, // check for rewards in the provider denom + IsIncrementalReward: true, // check rewards for currently produced blocks only + IsRewarded: map[ValidatorID]bool{ + ValidatorID("alice"): true, // alice is participating right now, so gets rewards + ValidatorID("bob"): false, // bob does not get rewards since he is not participating in consensus + ValidatorID("carol"): true, + }, + }, + }, + }, + }, + { + // relay packets so that the consumer gets up to date with the provider + Action: RelayPacketsAction{ + ChainA: ChainID("provi"), + ChainB: ChainID("consu"), + Port: "provider", + Channel: 0, + }, + State: State{ + ChainID("consu"): ChainState{ + ValPowers: &map[ValidatorID]uint{ + ValidatorID("alice"): 100, + ValidatorID("bob"): 0, + ValidatorID("carol"): 500, + }, + }, + }, + }, + // unjail bob + { + Action: UnjailValidatorAction{ + Provider: ChainID("provi"), + Validator: ValidatorID("bob"), + }, + State: State{ + ChainID("provi"): ChainState{ + ValPowers: &map[ValidatorID]uint{ + ValidatorID("alice"): 0, // alice is back out because only 2 validators can be active in consensus + ValidatorID("bob"): 198, // bob was slashed 1% + ValidatorID("carol"): 500, + }, + // check that between two blocks now, alice does not get rewarded with the native denom + Rewards: &Rewards{ + IsNativeDenom: true, // check for rewards in the provider denom + IsIncrementalReward: true, // check rewards for currently produced blocks only + IsRewarded: map[ValidatorID]bool{ + ValidatorID("alice"): false, + ValidatorID("bob"): true, + ValidatorID("carol"): true, + }, + }, + }, + // bob is still at 0 power on the consumer chain + ChainID("consu"): ChainState{ + ValPowers: &map[ValidatorID]uint{ + ValidatorID("alice"): 100, + ValidatorID("bob"): 0, + ValidatorID("carol"): 500, + }, + }, + }, + }, + // relay packets so that the consumer gets up to date with the provider + { + Action: RelayPacketsAction{ + ChainA: ChainID("provi"), + ChainB: ChainID("consu"), + Port: "provider", + Channel: 0, + }, + State: State{ + ChainID("consu"): ChainState{ + ValPowers: &map[ValidatorID]uint{ + ValidatorID("alice"): 100, + ValidatorID("bob"): 198, + ValidatorID("carol"): 500, + }, + }, + }, + }, + }, + ) + + return s +} + +// Precondition: The provider chain is running. +// Postcondition: A consumer chain with Top N = 0 is running, including an up-and-running IBC connection to the provider. +// "alice", "bob", "carol" have opted in and are validating. +func setupOptInChain() []Step { + return []Step{ + { + Action: SubmitConsumerAdditionProposalAction{ + Chain: ChainID("provi"), + From: ValidatorID("alice"), + Deposit: 10000001, + ConsumerChain: ChainID("consu"), + SpawnTime: 0, + InitialHeight: clienttypes.Height{RevisionNumber: 0, RevisionHeight: 1}, + TopN: 0, + }, + State: State{ + ChainID("provi"): ChainState{ + Proposals: &map[uint]Proposal{ + 1: ConsumerAdditionProposal{ + Deposit: 10000001, + Chain: ChainID("consu"), + SpawnTime: 0, + InitialHeight: clienttypes.Height{RevisionNumber: 0, RevisionHeight: 1}, + Status: "PROPOSAL_STATUS_VOTING_PERIOD", + }, + }, + HasToValidate: &map[ValidatorID][]ChainID{ + ValidatorID("alice"): {}, + ValidatorID("bob"): {}, + ValidatorID("carol"): {}, + }, + }, + }, + }, + // Οpt in "alice" and "bob" so the chain is not empty when it is about to start. Note, that "alice" and "bob" use + // the provider's public key (see `UseConsumerKey` is set to `false` in `getDefaultValidators`) and hence do not + // need a consumer-key assignment. + { + Action: OptInAction{ + Chain: ChainID("consu"), + Validator: ValidatorID("alice"), + }, + State: State{ + ChainID("provi"): ChainState{ + HasToValidate: &map[ValidatorID][]ChainID{ + ValidatorID("alice"): {}, // chain is not running yet + ValidatorID("bob"): {}, + ValidatorID("carol"): {}, + }, + }, + }, + }, + { + Action: OptInAction{ + Chain: ChainID("consu"), + Validator: ValidatorID("bob"), + }, + State: State{ + ChainID("provi"): ChainState{ + HasToValidate: &map[ValidatorID][]ChainID{ + ValidatorID("alice"): {}, + ValidatorID("bob"): {}, + ValidatorID("carol"): {}, + }, + }, + }, + }, + { + Action: VoteGovProposalAction{ + Chain: ChainID("provi"), + From: []ValidatorID{ValidatorID("alice"), ValidatorID("bob")}, + Vote: []string{"yes", "yes"}, + PropNumber: 1, + }, + State: State{ + ChainID("provi"): ChainState{ + Proposals: &map[uint]Proposal{ + 1: ConsumerAdditionProposal{ + Deposit: 10000001, + Chain: ChainID("consu"), + SpawnTime: 0, + InitialHeight: clienttypes.Height{RevisionNumber: 0, RevisionHeight: 1}, + Status: "PROPOSAL_STATUS_PASSED", + }, + }, + }, + }, + }, + { + // we start all the validators but only "alice" and "bob" have opted in and hence + // only "alice" and "bob" are validating blocks + Action: StartConsumerChainAction{ + ConsumerChain: ChainID("consu"), + ProviderChain: ChainID("provi"), + Validators: []StartChainValidator{ + {Id: ValidatorID("alice"), Stake: 100000000, Allocation: 10000000000}, + {Id: ValidatorID("bob"), Stake: 200000000, Allocation: 10000000000}, + {Id: ValidatorID("carol"), Stake: 300000000, Allocation: 10000000000}, + }, + // For consumers that're launching with the provider being on an earlier version + // of ICS before the soft opt-out threshold was introduced, we need to set the + // soft opt-out threshold to 0.05 in the consumer genesis to ensure that the + // consumer binary doesn't panic. Sdk requires that all params are set to valid + // values from the genesis file. + GenesisChanges: ".app_state.ccvconsumer.params.soft_opt_out_threshold = \"0.05\"", + }, + State: State{ + ChainID("consu"): ChainState{ + ValPowers: &map[ValidatorID]uint{ + ValidatorID("alice"): 100, + ValidatorID("bob"): 200, + // carol has not yet opted in + ValidatorID("carol"): 0, + }, + }, + }, + }, + { + Action: AddIbcConnectionAction{ + ChainA: ChainID("consu"), + ChainB: ChainID("provi"), + ClientA: 0, + ClientB: 0, + }, + State: State{}, + }, + { + Action: AddIbcChannelAction{ + ChainA: ChainID("consu"), + ChainB: ChainID("provi"), + ConnectionA: 0, + PortA: "consumer", + PortB: "provider", + Order: "ordered", + }, + State: State{}, + }, + { + Action: OptInAction{ + Chain: ChainID("consu"), + Validator: ValidatorID("carol"), + }, + State: State{ + ChainID("consu"): ChainState{ + ValPowers: &map[ValidatorID]uint{ + ValidatorID("alice"): 100, + ValidatorID("bob"): 200, + // "carol" has opted in, but the VSCPacket capturing the opt-in was not relayed yet + ValidatorID("carol"): 0, + }, + }, + ChainID("provi"): ChainState{ + HasToValidate: &map[ValidatorID][]ChainID{ + ValidatorID("alice"): {"consu"}, + ValidatorID("bob"): {"consu"}, + ValidatorID("carol"): {"consu"}, + }, + }, + }, + }, + { + // assign the consumer key "carol" is using on the consumer chain to be the one "carol" uses when opting in + Action: AssignConsumerPubKeyAction{ + Chain: ChainID("consu"), + Validator: ValidatorID("carol"), + // reconfigure the node -> validator was using provider key + // until this point -> key matches config.consumerValPubKey for "carol" + ConsumerPubkey: getDefaultValidators()[ValidatorID("carol")].ConsumerValPubKey, + ReconfigureNode: true, + }, + State: State{}, + }, + { + Action: RelayPacketsAction{ + ChainA: ChainID("provi"), + ChainB: ChainID("consu"), + Port: "provider", + Channel: 0, + }, + State: State{ + ChainID("consu"): ChainState{ + ValPowers: &map[ValidatorID]uint{ + ValidatorID("alice"): 100, + ValidatorID("bob"): 200, + // carol has now opted in + ValidatorID("carol"): 300, + }, + }, + ChainID("provi"): ChainState{ + HasToValidate: &map[ValidatorID][]ChainID{ + ValidatorID("alice"): {"consu"}, + ValidatorID("bob"): {"consu"}, + ValidatorID("carol"): {"consu"}, + }, + }, + }, + }, + } +} diff --git a/testutil/keeper/mocks.go b/testutil/keeper/mocks.go index ffa76ad40c..4998da9bdf 100644 --- a/testutil/keeper/mocks.go +++ b/testutil/keeper/mocks.go @@ -77,6 +77,20 @@ func (mr *MockStakingKeeperMockRecorder) Delegation(ctx, addr, valAddr interface return mr.mock.ctrl.RecordCallWithMethodType(mr.mock, "Delegation", reflect.TypeOf((*MockStakingKeeper)(nil).Delegation), ctx, addr, valAddr) } +// GetBondedValidatorsByPower mocks base method. +func (m *MockStakingKeeper) GetBondedValidatorsByPower(ctx types0.Context) []types5.Validator { + m.ctrl.T.Helper() + ret := m.ctrl.Call(m, "GetBondedValidatorsByPower", ctx) + ret0, _ := ret[0].([]types5.Validator) + return ret0 +} + +// GetBondedValidatorsByPower indicates an expected call of GetBondedValidatorsByPower. +func (mr *MockStakingKeeperMockRecorder) GetBondedValidatorsByPower(ctx interface{}) *gomock.Call { + mr.mock.ctrl.T.Helper() + return mr.mock.ctrl.RecordCallWithMethodType(mr.mock, "GetBondedValidatorsByPower", reflect.TypeOf((*MockStakingKeeper)(nil).GetBondedValidatorsByPower), ctx) +} + // GetLastTotalPower mocks base method. func (m *MockStakingKeeper) GetLastTotalPower(ctx types0.Context) math.Int { m.ctrl.T.Helper() diff --git a/x/ccv/provider/keeper/relay.go b/x/ccv/provider/keeper/relay.go index 3c6a98a945..ae1cbdb8dc 100644 --- a/x/ccv/provider/keeper/relay.go +++ b/x/ccv/provider/keeper/relay.go @@ -260,7 +260,7 @@ func (k Keeper) QueueVSCPackets(ctx sdk.Context) { func (k Keeper) ProviderValidatorUpdates(ctx sdk.Context) []abci.ValidatorUpdate { // get the bonded validators from the staking module - bondedValidators := k.stakingKeeper.GetLastValidators(ctx) + bondedValidators := k.stakingKeeper.GetBondedValidatorsByPower(ctx) // get the last validator set sent to consensus currentValidators := k.GetLastProviderConsensusValSet(ctx) @@ -275,7 +275,7 @@ func (k Keeper) ProviderValidatorUpdates(ctx sdk.Context) []abci.ValidatorUpdate // create the validator from the staking validator consAddr, err := val.GetConsAddr() if err != nil { - k.Logger(ctx).Error("could not create consumer validator", + k.Logger(ctx).Error("could not create validator", "validator", val.GetOperator().String(), "error", err) continue diff --git a/x/ccv/types/expected_keepers.go b/x/ccv/types/expected_keepers.go index b3815b6d65..1334f1b143 100644 --- a/x/ccv/types/expected_keepers.go +++ b/x/ccv/types/expected_keepers.go @@ -58,6 +58,7 @@ type StakingKeeper interface { GetRedelegationsFromSrcValidator(ctx sdk.Context, valAddr sdk.ValAddress) (reds []stakingtypes.Redelegation) GetUnbondingType(ctx sdk.Context, id uint64) (unbondingType stakingtypes.UnbondingType, found bool) MinCommissionRate(ctx sdk.Context) math.LegacyDec + GetBondedValidatorsByPower(ctx sdk.Context) (validators []stakingtypes.Validator) } // SlashingKeeper defines the contract expected to perform ccv slashing