diff --git a/docs/docs/adrs/adr-017-allowing-inactive-validators.md b/docs/docs/adrs/adr-017-allowing-inactive-validators.md index aa8e3c117c..25ee270827 100644 --- a/docs/docs/adrs/adr-017-allowing-inactive-validators.md +++ b/docs/docs/adrs/adr-017-allowing-inactive-validators.md @@ -143,8 +143,17 @@ Checked as part of the e2e test `inactive-vals-topN`. To compute the inflation rate, only the active validators should be considered. -We can check this by querying the inflation rate from the mint module twice: -Once with all validators active, and once with a lot of stake inactive. +We can check this by querying the inflation rate change over subsequent blocks. + +We start a provider chain with these arguments +* 3 validators with powers alice=290, bob=280, charlie=270 +* either 1 or 3 active validators +* a bonded goal of 300 tokens (this is given in percent, but we simplify here) + +If we have 3 validators active, then the inflation rate should *decrease* between blocks, because the bonded goal is exceeded as all validators are bonded. +If we have only 1 validator active, then the inflation rate should *increase* between blocks, because the bonded goal is not met. + +Checked as part of the e2e tests `inactive-vals-mint` (scenario with 1 active val) and `mint-basecase` (scenario with 3 active vals). ### Scenarios 8: Inactive validators can validate on consumer chains diff --git a/tests/e2e/config.go b/tests/e2e/config.go index e27f8a42cf..2296a07b8e 100644 --- a/tests/e2e/config.go +++ b/tests/e2e/config.go @@ -95,6 +95,8 @@ const ( InactiveProviderValsTestCfg TestConfigType = "inactive-provider-vals" GovTestCfg TestConfigType = "gov" InactiveValsGovTestCfg TestConfigType = "inactive-vals-gov" + InactiveValsMintTestCfg TestConfigType = "inactive-vals-mint" + MintTestCfg TestConfigType = "mint" ) type TestConfig struct { @@ -189,6 +191,10 @@ func GetTestConfig(cfgType TestConfigType, providerVersion, consumerVersion stri testCfg = GovTestConfig() case InactiveValsGovTestCfg: testCfg = InactiveValsGovTestConfig() + case InactiveValsMintTestCfg: + testCfg = InactiveValsMintTestConfig() + case MintTestCfg: + testCfg = MintTestConfig() default: panic(fmt.Sprintf("Invalid test config: %s", cfgType)) } @@ -630,6 +636,33 @@ func InactiveValsGovTestConfig() TestConfig { return cfg } +func MintTestConfig() TestConfig { + cfg := GovTestConfig() + AdjustMint(cfg) + + return cfg +} + +func InactiveValsMintTestConfig() TestConfig { + cfg := InactiveValsGovTestConfig() + AdjustMint(cfg) + + return cfg +} + +// AdjustMint adjusts the mint parameters to have a very low goal bonded amount +// and a high inflation rate change +func AdjustMint(cfg TestConfig) { + proviConfig := cfg.chainConfigs[ChainID("provi")] + // total supply is 30000000000stake; we want to set the mint bonded goal to + // a small fraction of that + proviConfig.GenesisChanges += "| .app_state.mint.params.goal_bonded = \"0.001\"" + + "| .app_state.mint.params.inflation_rate_change = \"1\"" + + "| .app_state.mint.params.inflation_max = \"0.5\"" + + "| .app_state.mint.params.inflation_min = \"0.1\"" + cfg.chainConfigs[ChainID("provi")] = proviConfig +} + func MultiConsumerTestConfig() TestConfig { tr := TestConfig{ name: string(MulticonsumerTestCfg), diff --git a/tests/e2e/main.go b/tests/e2e/main.go index bde83d27a3..508e018604 100644 --- a/tests/e2e/main.go +++ b/tests/e2e/main.go @@ -240,6 +240,18 @@ var stepChoices = map[string]StepChoice{ description: "checks that the min stake parameter for consumer chains is respected", testConfig: GovTestCfg, // see above: we reuse the GovTestCfg for convenience }, + "inactive-vals-mint": { + name: "inactive-vals-mint", + steps: stepsInactiveValsMint(), + description: "test minting with inactive validators", + testConfig: InactiveValsMintTestCfg, + }, + "mint-basecase": { + name: "mint-basecase", + steps: stepsMintBasecase(), + description: "test minting without inactive validators as a sanity check", + testConfig: MintTestCfg, + }, } func getTestCaseUsageString() string { @@ -330,6 +342,8 @@ func getTestCases(selectedPredefinedTests, selectedTestFiles TestSet, providerVe "partial-set-security-validators-allowlisted", "partial-set-security-validators-denylisted", "partial-set-security-modification-proposal", "active-set-changes", "inactive-vals-topN", + "inactive-provider-validators-on-consumer", "inactive-provider-validators-governance", + "max-rank", "min-stake", "inactive-vals-mint", } if includeMultiConsumer != nil && *includeMultiConsumer { selectedPredefinedTests = append(selectedPredefinedTests, "multiconsumer") diff --git a/tests/e2e/state.go b/tests/e2e/state.go index 4e5959898c..3caee9ce5c 100644 --- a/tests/e2e/state.go +++ b/tests/e2e/state.go @@ -116,6 +116,29 @@ func (tr Chain) GetChainState(chain ChainID, modelState ChainState) ChainState { chainState.HasToValidate = &hasToValidate } + if modelState.InflationRateChange != nil { + // get the inflation rate now + inflationRateNow := tr.target.GetInflationRate(chain) + + // wait a block + tr.waitBlocks(chain, 1, 10*time.Second) + + // get the new inflation rate + inflationRateAfter := tr.target.GetInflationRate(chain) + + // calculate the change + inflationRateChange := inflationRateAfter - inflationRateNow + var inflationRateChangeDirection int + if inflationRateChange > 0 { + inflationRateChangeDirection = 1 + } else if inflationRateChange < 0 { + inflationRateChangeDirection = -1 + } else { + inflationRateChangeDirection = 0 + } + chainState.InflationRateChange = &inflationRateChangeDirection + } + if modelState.ConsumerPendingPacketQueueSize != nil { pendingPacketQueueSize := tr.target.GetPendingPacketQueueSize(chain) chainState.ConsumerPendingPacketQueueSize = &pendingPacketQueueSize @@ -302,6 +325,10 @@ func uintPtr(i uint) *uint { return &i } +func intPtr(i int) *int { + return &i +} + type Commands struct { containerConfig ContainerConfig // FIXME only needed for 'Now' time tracking validatorConfigs map[ValidatorID]ValidatorConfig @@ -867,6 +894,23 @@ func (tr Commands) GetHasToValidate( return chains } +func (tr Commands) GetInflationRate( + chain ChainID, +) float64 { + binaryName := tr.chainConfigs[chain].BinaryName + bz, err := tr.target.ExecCommand(binaryName, + "query", "mint", "inflation", + `--node`, tr.GetQueryNode(chain), + `-o`, `json`, + ).CombinedOutput() + if err != nil { + log.Fatal(err, "\n", string(bz)) + } + + inflationRate := gjson.Get(string(bz), "inflation").Float() + return inflationRate +} + func (tr Commands) GetTrustedHeight( chain ChainID, clientID string, diff --git a/tests/e2e/steps_inactive_vals.go b/tests/e2e/steps_inactive_vals.go index 2153e0105f..4e51ee19a1 100644 --- a/tests/e2e/steps_inactive_vals.go +++ b/tests/e2e/steps_inactive_vals.go @@ -976,3 +976,91 @@ func stepsInactiveValsWithTopN() []Step { }, } } + +func stepsInactiveValsMint() []Step { + // total supply is 30000000000, bonded goal ratio makes it so we want 30000000 tokens bonded + return []Step{ + { + Action: StartChainAction{ + Chain: ChainID("provi"), + Validators: []StartChainValidator{ + {Id: ValidatorID("alice"), Stake: 27000000, Allocation: 10000000000}, + {Id: ValidatorID("bob"), Stake: 28000000, Allocation: 10000000000}, + {Id: ValidatorID("carol"), Stake: 29000000, Allocation: 10000000000}, + }, + }, + State: State{ + ChainID("provi"): ChainState{ + ValPowers: &map[ValidatorID]uint{ + ValidatorID("alice"): 0, + ValidatorID("bob"): 0, + ValidatorID("carol"): 29, // other validators are not in power since only 1 can be active + }, + InflationRateChange: intPtr(1), // inflation rate goes up because less than the goal is bonded, since only carol is active + }, + }, + }, + { + Action: DelegateTokensAction{ + Chain: ChainID("provi"), + From: ValidatorID("carol"), + To: ValidatorID("carol"), + Amount: 50000000, + }, + State: State{ + ChainID("provi"): ChainState{ + ValPowers: &map[ValidatorID]uint{ + ValidatorID("alice"): 0, + ValidatorID("bob"): 0, + ValidatorID("carol"): 79, + }, + InflationRateChange: intPtr(-1), // inflation rate goes down now, because carol has more bonded than the goal + }, + }, + }, + } +} + +func stepsMintBasecase() []Step { + // total supply is 30000000000, bonded goal ratio makes it so we want 30000000 tokens bonded + return []Step{ + { + Action: StartChainAction{ + Chain: ChainID("provi"), + Validators: []StartChainValidator{ + {Id: ValidatorID("alice"), Stake: 27000000, Allocation: 10000000000}, + {Id: ValidatorID("bob"), Stake: 28000000, Allocation: 10000000000}, + {Id: ValidatorID("carol"), Stake: 29000000, Allocation: 10000000000}, + }, + }, + State: State{ + ChainID("provi"): ChainState{ + ValPowers: &map[ValidatorID]uint{ + ValidatorID("alice"): 27, + ValidatorID("bob"): 28, + ValidatorID("carol"): 29, + }, + InflationRateChange: intPtr(-1), // inflation rate goes down because more than the goal is bonded + }, + }, + }, + { + Action: DelegateTokensAction{ + Chain: ChainID("provi"), + From: ValidatorID("carol"), + To: ValidatorID("carol"), + Amount: 50000000, + }, + State: State{ + ChainID("provi"): ChainState{ + ValPowers: &map[ValidatorID]uint{ + ValidatorID("alice"): 28, + ValidatorID("bob"): 29, + ValidatorID("carol"): 79, + }, + InflationRateChange: intPtr(-1), // inflation rate *still* goes down + }, + }, + }, + } +} diff --git a/tests/e2e/testlib/types.go b/tests/e2e/testlib/types.go index 135f07a6a8..725c4e6951 100644 --- a/tests/e2e/testlib/types.go +++ b/tests/e2e/testlib/types.go @@ -37,6 +37,7 @@ type ChainCommands interface { GetValPower(chain ChainID, validator ValidatorID) uint GetValStakedTokens(chain ChainID, validatorAddress string) uint GetQueryNodeIP(chain ChainID) string + GetInflationRate(chain ChainID) float64 } // TODO: replace ExecutionTarget with new TargetDriver interface @@ -153,7 +154,6 @@ type ProposalAndType struct { RawProposal json.RawMessage Type string } - type ChainState struct { ValBalances *map[ValidatorID]uint Proposals *map[uint]Proposal @@ -170,6 +170,7 @@ type ChainState struct { RegisteredConsumerRewardDenoms *[]string ClientsFrozenHeights *map[string]clienttypes.Height HasToValidate *map[ValidatorID][]ChainID // only relevant to provider chain + InflationRateChange *int // whether the inflation rate between two blocks changes negatively (any negative number), is equal (0), or changes positively (any positive number) } // custom marshal and unmarshal functions for the chainstate that convert proposals to/from the auxiliary type with type info diff --git a/tests/e2e/v4/state.go b/tests/e2e/v4/state.go index 70ca8afe7c..ab8fc58267 100644 --- a/tests/e2e/v4/state.go +++ b/tests/e2e/v4/state.go @@ -236,7 +236,7 @@ func (tr Commands) GetBalance(chain ChainID, validator ValidatorID) uint { // interchain-securityd query gov proposals func (tr Commands) GetProposal(chain ChainID, proposal uint) Proposal { - var noProposalRegex = regexp.MustCompile(`doesn't exist: key not found`) + noProposalRegex := regexp.MustCompile(`doesn't exist: key not found`) binaryName := tr.ChainConfigs[chain].BinaryName bz, err := tr.Target.ExecCommand(binaryName, @@ -411,6 +411,7 @@ func (tr Commands) GetConsumerChains(chain ChainID) map[ChainID]bool { return chains } + func (tr Commands) GetConsumerAddress(consumerChain ChainID, validator ValidatorID) string { binaryName := tr.ChainConfigs[ChainID("provi")].BinaryName cmd := tr.Target.ExecCommand(binaryName, @@ -656,3 +657,20 @@ func (tr Commands) GetHasToValidate(validator ValidatorID) []ChainID { func uintPtr(i uint) *uint { return &i } + +func (tr Commands) GetInflationRate( + chain ChainID, +) float64 { + binaryName := tr.ChainConfigs[chain].BinaryName + bz, err := tr.Target.ExecCommand(binaryName, + "query", "mint", "inflation", + `--node`, tr.GetQueryNode(chain), + `-o`, `json`, + ).CombinedOutput() + if err != nil { + log.Fatal(err, "\n", string(bz)) + } + + inflationRate := gjson.Get(string(bz), "inflation").Float() + return inflationRate +}