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