diff --git a/.changelog/unreleased/features/2290-add-memo-to-ICS-rewards.md b/.changelog/unreleased/features/2290-add-memo-to-ICS-rewards.md new file mode 100644 index 0000000000..421967ddab --- /dev/null +++ b/.changelog/unreleased/features/2290-add-memo-to-ICS-rewards.md @@ -0,0 +1,4 @@ +- `[x/consumer]` Populate the memo on the IBC transfer packets used to send ICS rewards. +with the required consumer chain Id to identify the consumer to the provider. +- `[x/provider]` Identify the source of ICS rewards from the IBC transfer packet memo. + ([\#2290](https://github.com/cosmos/interchain-security/pull/2290)) \ No newline at end of file diff --git a/.changelog/unreleased/state-breaking/2290-add-memo-to-ICS-rewards.md b/.changelog/unreleased/state-breaking/2290-add-memo-to-ICS-rewards.md new file mode 100644 index 0000000000..421967ddab --- /dev/null +++ b/.changelog/unreleased/state-breaking/2290-add-memo-to-ICS-rewards.md @@ -0,0 +1,4 @@ +- `[x/consumer]` Populate the memo on the IBC transfer packets used to send ICS rewards. +with the required consumer chain Id to identify the consumer to the provider. +- `[x/provider]` Identify the source of ICS rewards from the IBC transfer packet memo. + ([\#2290](https://github.com/cosmos/interchain-security/pull/2290)) \ No newline at end of file diff --git a/UPGRADING.md b/UPGRADING.md index 5e40cc47e1..c0821da659 100644 --- a/UPGRADING.md +++ b/UPGRADING.md @@ -2,6 +2,26 @@ ## v5.2.x +### Consumer + +Upgrading a consumer from v4.4.x to v4.5.x and from v5.x or v6.1.x to v6.2.x requires state migrations. The following migrators should be added to the upgrade handler of the consumer chain: + + +```go +// InitializeConsumerId sets the consumer Id parameter in the consumer module, +// to the consumer id for which the consumer is registered on the provider chain. +// The consumer id can be obtained in by querying the provider, e.g. by using the +// QueryConsumerIdFromClientId query. +func InitializeConsumerId(ctx sdk.Context, consumerKeeper consumerkeeper.Keeper) error { + params, err := consumerKeeper.GetParams(ctx) + if err != nil { + return err + } + params.ConsumerId = ConsumerId + return consumerKeeper.SetParams(ctx, params) +} +``` + ### Provider Providers using versions `v5.1.x` can upgrade to `v5.2.x`. diff --git a/proto/interchain_security/ccv/v1/shared_consumer.proto b/proto/interchain_security/ccv/v1/shared_consumer.proto index f1535df010..cba300e99f 100644 --- a/proto/interchain_security/ccv/v1/shared_consumer.proto +++ b/proto/interchain_security/ccv/v1/shared_consumer.proto @@ -76,6 +76,10 @@ message ConsumerParams { // The period after which a consumer can retry sending a throttled packet. google.protobuf.Duration retry_delay_period = 13 [ (gogoproto.nullable) = false, (gogoproto.stdduration) = true ]; + + // The consumer ID of this consumer chain. Used by the consumer module to send + // ICS rewards. + string consumer_id = 14; } // ConsumerGenesisState defines shared genesis information between provider and diff --git a/tests/e2e/action_rapid_test.go b/tests/e2e/action_rapid_test.go index b720d3557c..1c002d84c1 100644 --- a/tests/e2e/action_rapid_test.go +++ b/tests/e2e/action_rapid_test.go @@ -101,7 +101,7 @@ func CreateSubmitChangeRewardDenomsProposalActionGen() *rapid.Generator[SubmitCh return SubmitChangeRewardDenomsProposalAction{ From: GetValidatorIDGen().Draw(t, "From"), Deposit: rapid.Uint().Draw(t, "Deposit"), - Denom: rapid.String().Draw(t, "Denom"), + Denoms: rapid.SliceOf(rapid.String()).Draw(t, "Denoms"), } }) } diff --git a/tests/e2e/actions.go b/tests/e2e/actions.go index 9a0ff5d647..3eea601cfd 100644 --- a/tests/e2e/actions.go +++ b/tests/e2e/actions.go @@ -265,6 +265,151 @@ type SubmitConsumerAdditionProposalAction struct { Denylist []string } +<<<<<<< HEAD +======= +func (tr Chain) UpdateConsumer(providerChain ChainID, validator ValidatorID, update types.MsgUpdateConsumer, verbose bool) { + content, err := json.Marshal(update) + if err != nil { + log.Fatal("failed marshalling MsgUpdateConsumer: ", err.Error()) + } + jsonFile := "/update-consumer.json" + bz, err := tr.target.ExecCommand( + "/bin/bash", "-c", fmt.Sprintf(`echo '%s' > %s`, content, jsonFile), + ).CombinedOutput() + if err != nil { + log.Fatal(err, "\n", string(bz)) + } + + // Send consumer chain update + cmd := tr.target.ExecCommand( + tr.testConfig.chainConfigs[providerChain].BinaryName, + "tx", "provider", "update-consumer", jsonFile, + `--from`, `validator`+fmt.Sprint(validator), + `--chain-id`, string(tr.testConfig.chainConfigs[providerChain].ChainId), + `--home`, tr.getValidatorHome(providerChain, validator), + `--gas`, `900000`, + `--node`, tr.getValidatorNode(providerChain, validator), + `--keyring-backend`, `test`, + "--output", "json", + `-y`, + ) + + bz, err = cmd.CombinedOutput() + if err != nil { + fmt.Println("command failed: ", cmd) + log.Fatalf("update consumer failed error: %s, output: %s", err, string(bz)) + } + + // Check transaction + txResponse := &TxResponse{} + err = json.Unmarshal(bz, txResponse) + if err != nil { + log.Fatalf("unmarshalling tx response on update-consumer: %s, json: %s", err.Error(), string(bz)) + } + + if txResponse.Code != 0 { + log.Fatalf("sending update-consumer transaction failed with error code %d, Log:'%s'", txResponse.Code, txResponse.RawLog) + } + + if verbose { + fmt.Println("running 'update-consumer' returned: ", txResponse) + } + + tr.waitBlocks(providerChain, 2, 10*time.Second) +} + +// CreateConsumer creates a consumer chain and returns its consumer-id +func (tr Chain) CreateConsumer(providerChain, consumerChain ChainID, validator ValidatorID, metadata types.ConsumerMetadata, initParams *types.ConsumerInitializationParameters, powerShapingParams *types.PowerShapingParameters) ConsumerID { + + msg := types.MsgCreateConsumer{ + ChainId: string(consumerChain), + Metadata: metadata, + InitializationParameters: initParams, + PowerShapingParameters: powerShapingParams, + } + + content, err := json.Marshal(msg) + if err != nil { + log.Fatalf("failed marshalling MsgCreateConsumer: %s", err.Error()) + } + jsonFile := "/create-consumer.json" + bz, err := tr.target.ExecCommand( + "/bin/bash", "-c", fmt.Sprintf(`echo '%s' > %s`, content, jsonFile), + ).CombinedOutput() + if err != nil { + log.Fatal(err, "\n", string(bz)) + } + + // Send consumer chain creation + cmd := tr.target.ExecCommand( + tr.testConfig.chainConfigs[providerChain].BinaryName, + "tx", "provider", "create-consumer", jsonFile, + `--from`, `validator`+fmt.Sprint(validator), + `--chain-id`, string(tr.testConfig.chainConfigs[providerChain].ChainId), + `--home`, tr.getValidatorHome(providerChain, validator), + `--gas`, `900000`, + `--node`, tr.getValidatorNode(providerChain, validator), + `--keyring-backend`, `test`, + "--output", "json", + `-y`, + ) + + bz, err = cmd.CombinedOutput() + if err != nil { + log.Fatal("create consumer failed ", "error: ", err, "output: ", string(bz)) + } + + txResponse := &TxResponse{} + err = json.Unmarshal(bz, txResponse) + if err != nil { + log.Fatalf("unmarshalling tx response on create-consumer: %s, json: %s", err.Error(), string(bz)) + } + + if txResponse.Code != 0 { + log.Fatalf("sending transaction failed with error code %d, Log:'%s'", txResponse.Code, txResponse.RawLog) + } + + // TODO: introduce waitForTx (see issue #2198) + tr.waitBlocks(providerChain, 2, 10*time.Second) + + // Get Consumer ID from transaction + cmd = tr.target.ExecCommand( + tr.testConfig.chainConfigs[providerChain].BinaryName, + "query", "tx", txResponse.TxHash, + `--node`, tr.getValidatorNode(providerChain, validator), + "--output", "json", + ) + bz, err = cmd.CombinedOutput() + if err != nil { + log.Fatalf("not able to query tx containing creation-consumer: tx: %s, err: %s, out: %s", + txResponse.TxHash, err.Error(), string(bz)) + } + + err = json.Unmarshal(bz, txResponse) + if err != nil { + log.Fatalf("unmarshalling tx containing create-consumer: %s, json: %s", err.Error(), string(bz)) + } + + consumerId := "" + for _, event := range txResponse.Events { + if event.Type != "create_consumer" { + continue + } + attr, exists := event.GetAttribute("consumer_id") + if !exists { + log.Fatalf("no event with consumer_id found in tx content of create_consumer: %v", event) + } + consumerId = attr.Value + } + if consumerId == "" { + log.Fatalf("no consumer-id found in consumer creation transaction events for chain '%s'. events: %v", consumerChain, txResponse.Events) + } + + return ConsumerID(consumerId) +} + +// submitConsumerAdditionProposal initializes a consumer chain and submits a governance proposal +>>>>>>> 0d782959 (feat!: add memo to IBC transfers of ICS rewards (#2290)) func (tr Chain) submitConsumerAdditionProposal( action SubmitConsumerAdditionProposalAction, verbose bool, @@ -1962,20 +2107,84 @@ func (tr Chain) registerRepresentative( } type SubmitChangeRewardDenomsProposalAction struct { +<<<<<<< HEAD Denom string +======= + Chain ChainID + Denoms []string +>>>>>>> 0d782959 (feat!: add memo to IBC transfers of ICS rewards (#2290)) Deposit uint From ValidatorID } func (tr Chain) submitChangeRewardDenomsProposal(action SubmitChangeRewardDenomsProposalAction, verbose bool) { +<<<<<<< HEAD providerChain := tr.testConfig.chainConfigs[ChainID("provi")] +======= + changeRewMsg := types.MsgChangeRewardDenoms{ + DenomsToAdd: action.Denoms, + DenomsToRemove: []string{"stake"}, + Authority: "cosmos10d07y265gmmuvt4z0w9aw880jnsr700j6zn9kn", + } + + // Generate proposal content + title := "change reward denoms" + description := "change reward denoms" + summary := "Proposal to change reward denoms" + expedited := false + metadata := "ipfs://CID" + deposit := fmt.Sprintf("%dstake", action.Deposit) + jsonStr := e2e.GenerateGovProposalContent(title, summary, metadata, deposit, description, expedited, &changeRewMsg) + + //#nosec G204 -- bypass unsafe quoting warning (no production code) + proposalFile := "/change-rewards-proposal.json" + bz, err := tr.target.ExecCommand( + "/bin/bash", "-c", fmt.Sprintf(`echo '%s' > %s`, jsonStr, proposalFile), + ).CombinedOutput() + if err != nil { + log.Fatal(err, "\n", string(bz)) + } + + // CHANGE REWARDS DENOM PROPOSAL + cmd := tr.target.ExecCommand( + tr.testConfig.chainConfigs[action.Chain].BinaryName, + "tx", "gov", "submit-proposal", proposalFile, + `--from`, `validator`+fmt.Sprint(action.From), + `--chain-id`, string(tr.testConfig.chainConfigs[action.Chain].ChainId), + `--home`, tr.getValidatorHome(action.Chain, action.From), + `--gas`, `900000`, + `--node`, tr.getValidatorNode(action.Chain, action.From), + `--keyring-backend`, `test`, + `-y`, + ) + + if verbose { + fmt.Println("change rewards denom props cmd:", cmd.String()) + fmt.Println("change rewards denom props json:", jsonStr) + } + bz, err = cmd.CombinedOutput() + if err != nil { + log.Fatal("submit-proposal failed:", err, "\n", string(bz)) + } + + if verbose { + fmt.Println("change rewards denom props output:", string(bz)) + } + + // wait for inclusion in a block -> '--broadcast-mode block' is deprecated + tr.waitBlocks(ChainID("provi"), 2, 30*time.Second) +} + +func (tr Chain) submitChangeRewardDenomsLegacyProposal(action SubmitChangeRewardDenomsProposalAction, verbose bool) { + providerChain := tr.testConfig.chainConfigs[action.Chain] +>>>>>>> 0d782959 (feat!: add memo to IBC transfers of ICS rewards (#2290)) prop := client.ChangeRewardDenomsProposalJSON{ Summary: "Change reward denoms", ChangeRewardDenomsProposal: types.ChangeRewardDenomsProposal{ Title: "Change reward denoms", Description: "Change reward denoms", - DenomsToAdd: []string{action.Denom}, + DenomsToAdd: action.Denoms, DenomsToRemove: []string{"stake"}, }, Deposit: fmt.Sprint(action.Deposit) + `stake`, @@ -2530,3 +2739,123 @@ func (tr Chain) AdvanceTimeForChain(chain ChainID, duration time.Duration) { // wait for 1 block of the chain to get a block with the advanced timestamp tr.waitBlocks(chain, 1, time.Minute) } +<<<<<<< HEAD +======= + +func (tr Commands) AssignConsumerPubKey(action e2e.AssignConsumerPubKeyAction, gas, home, node string, verbose bool) ([]byte, error) { + assignKey := fmt.Sprintf( + `%s tx provider assign-consensus-key %s '%s' --from validator%s --chain-id %s --home %s --node %s --gas %s --keyring-backend test -y -o json`, + tr.chainConfigs[ChainID("provi")].BinaryName, + string(tr.chainConfigs[action.Chain].ConsumerId), + action.ConsumerPubkey, + action.Validator, + tr.chainConfigs[ChainID("provi")].ChainId, + home, + node, + gas, + ) + + cmd := tr.target.ExecCommand( + "/bin/bash", "-c", + assignKey, + ) + + if verbose { + fmt.Println("assignConsumerPubKey cmd:", cmd.String()) + } + + return cmd.CombinedOutput() +} + +type CreateIbcClientAction struct { + ChainA ChainID + ChainB ChainID +} + +func (tr Chain) createIbcClientHermes( + action CreateIbcClientAction, + verbose bool, +) { + cmd := tr.target.ExecCommand("hermes", + "create", "client", + "--host-chain", string(tr.testConfig.chainConfigs[action.ChainA].ChainId), + "--reference-chain", string(tr.testConfig.chainConfigs[action.ChainB].ChainId), + "--trusting-period", "1200000s", + ) + + cmdReader, err := cmd.StdoutPipe() + if err != nil { + log.Fatal(err) + } + cmd.Stderr = cmd.Stdout + + if err := cmd.Start(); err != nil { + log.Fatal(err) + } + + scanner := bufio.NewScanner(cmdReader) + + for scanner.Scan() { + out := scanner.Text() + if verbose { + fmt.Println("createIbcClientHermes: " + out) + } + if out == done { + break + } + } + if err := scanner.Err(); err != nil { + log.Fatal(err) + } +} + +type TransferIbcTokenAction struct { + Chain ChainID + DstAddr string + From ValidatorID + Amount uint + Channel uint + Memo string +} + +func (tr Chain) transferIbcToken( + action TransferIbcTokenAction, + verbose bool, +) { + // Note: to get error response reported back from this command '--gas auto' needs to be set. + gas := "auto" + + transferCmd := fmt.Sprintf( + `%s tx ibc-transfer transfer transfer \ +%s %s %s --memo %q --from validator%s --chain-id %s \ +--home %s --node %s --gas %s --keyring-backend test -y -o json`, + tr.testConfig.chainConfigs[action.Chain].BinaryName, + "channel-"+fmt.Sprint(action.Channel), + action.DstAddr, + fmt.Sprint(action.Amount)+`stake`, + action.Memo, + action.From, + string(tr.testConfig.chainConfigs[action.Chain].ChainId), + tr.getValidatorHome(action.Chain, action.From), + tr.getValidatorNode(action.Chain, action.From), + gas, + ) + + cmd := tr.target.ExecCommand( + "/bin/bash", "-c", + transferCmd, + ) + + if verbose { + fmt.Println("transferIbcToken cmd:", cmd.String()) + } + + bz, err := cmd.CombinedOutput() + if err != nil { + log.Fatalf("unexpected error during IBC token transfer: %s: %s", string(bz), err) + } + + // wait for inclusion in a block -> '--broadcast-mode block' is deprecated + tr.waitBlocks(action.Chain, 2, 30*time.Second) +} +>>>>>>> 0d782959 (feat!: add memo to IBC transfers of ICS rewards (#2290)) diff --git a/tests/e2e/state.go b/tests/e2e/state.go index f38fde6814..4d874856e4 100644 --- a/tests/e2e/state.go +++ b/tests/e2e/state.go @@ -8,6 +8,7 @@ import ( "os/exec" "regexp" "strconv" + "strings" "time" clienttypes "github.com/cosmos/ibc-go/v8/modules/core/02-client/types" @@ -253,10 +254,10 @@ func (tr Chain) GetRewards(chain ChainID, modelState Rewards) Rewards { currentBlock = 1 } for k := range modelState.IsRewarded { - receivedRewards[k] = tr.target.GetReward(chain, k, nextBlock, modelState.IsNativeDenom) > tr.target.GetReward(chain, k, currentBlock, modelState.IsNativeDenom) + receivedRewards[k] = tr.target.GetReward(chain, k, nextBlock, modelState.Denom) > tr.target.GetReward(chain, k, currentBlock, modelState.Denom) } - return Rewards{IsRewarded: receivedRewards, IsIncrementalReward: modelState.IsIncrementalReward, IsNativeDenom: modelState.IsNativeDenom} + return Rewards{IsRewarded: receivedRewards, IsIncrementalReward: modelState.IsIncrementalReward, Denom: modelState.Denom} } func (tr Chain) GetConsumerAddresses(chain ChainID, modelState map[ValidatorID]string) map[ValidatorID]string { @@ -347,7 +348,7 @@ func (tr Commands) GetBlockHeight(chain ChainID) uint { return uint(blockHeight) } -func (tr Commands) GetReward(chain ChainID, validator ValidatorID, blockHeight uint, isNativeDenom bool) float64 { +func (tr Commands) GetReward(chain ChainID, validator ValidatorID, blockHeight uint, denom string) float64 { valCfg := tr.validatorConfigs[validator] delAddresss := valCfg.DelAddress if chain != ChainID("provi") { @@ -375,12 +376,23 @@ func (tr Commands) GetReward(chain ChainID, validator ValidatorID, blockHeight u log.Fatal("failed getting rewards: ", err, "\n", string(bz)) } - denomCondition := `total.#(denom!="stake").amount` - if isNativeDenom { - denomCondition = `total.#(denom=="stake").amount` + denomCondition := fmt.Sprintf(`total.#(%%"*%s*")`, denom) + amount := strings.Split(gjson.Get(string(bz), denomCondition).String(), denom)[0] + + fmt.Println("denomCondition:", denomCondition) + fmt.Println("json:", gjson.Parse(string(bz))) + + res := float64(0) + if amount != "" { + res, err = strconv.ParseFloat(amount, 64) + if err != nil { + log.Fatal("failed parsing consumer reward:", err) + } } - return gjson.Get(string(bz), denomCondition).Float() + fmt.Println("res", res) + + return res } // interchain-securityd query gov proposals diff --git a/tests/e2e/state_rapid_test.go b/tests/e2e/state_rapid_test.go index 1ca36d5578..204584c774 100644 --- a/tests/e2e/state_rapid_test.go +++ b/tests/e2e/state_rapid_test.go @@ -104,8 +104,8 @@ func GetRewardsGen() *rapid.Generator[Rewards] { return rapid.Custom(func(t *rapid.T) Rewards { return Rewards{ IsIncrementalReward: rapid.Bool().Draw(t, "IsIncrementalReward"), - IsNativeDenom: rapid.Bool().Draw(t, "IsNativeDenom"), - IsRewarded: rapid.MapOf(GetValidatorIDGen(), rapid.Bool()).Draw(t, "IsRewarded"), + // Denom: rapid.Str, + IsRewarded: rapid.MapOf(GetValidatorIDGen(), rapid.Bool()).Draw(t, "IsRewarded"), } }) } diff --git a/tests/e2e/steps_democracy.go b/tests/e2e/steps_democracy.go index 82e1d237af..6f1e430268 100644 --- a/tests/e2e/steps_democracy.go +++ b/tests/e2e/steps_democracy.go @@ -1,12 +1,19 @@ package main +<<<<<<< HEAD import ( "strconv" gov "github.com/cosmos/cosmos-sdk/x/gov/types/v1" ) +======= +import gov "github.com/cosmos/cosmos-sdk/x/gov/types/v1" +>>>>>>> 0d782959 (feat!: add memo to IBC transfers of ICS rewards (#2290)) -const consumerRewardDenom = "ibc/3C3D7B3BE4ECC85A0E5B52A3AEC3B7DFC2AA9CA47C37821E57020D6807043BE9" +var consumerRewardDenoms = []string{ + "ibc/3C3D7B3BE4ECC85A0E5B52A3AEC3B7DFC2AA9CA47C37821E57020D6807043BE9", // transfer channel-1 + "ibc/D549749C93524DA1831A4B3C850DFC1BA9060261BEDFB224B3B0B4744CD77A70", // transfer channel-2 +} func stepsDemocracy(consumerName string, expectRegisteredRewardDistribution bool) []Step { return []Step{ @@ -29,7 +36,7 @@ func stepsDemocracy(consumerName string, expectRegisteredRewardDistribution bool ValidatorID("carol"): false, }, IsIncrementalReward: true, - IsNativeDenom: true, + Denom: "stake", }, // Check that delegating on gov-consumer does not change validator powers ValPowers: &map[ValidatorID]uint{ @@ -68,7 +75,7 @@ func stepsDemocracy(consumerName string, expectRegisteredRewardDistribution bool ValidatorID("carol"): true, }, IsIncrementalReward: true, - IsNativeDenom: true, + Denom: "stake", }, }, }, @@ -143,7 +150,7 @@ func stepsDemocracy(consumerName string, expectRegisteredRewardDistribution bool ValidatorID("carol"): false, }, IsIncrementalReward: false, - IsNativeDenom: false, + Denom: consumerRewardDenoms[0], }, // Check that the denom is not registered on provider chain RegisteredConsumerRewardDenoms: &[]string{}, @@ -152,7 +159,12 @@ func stepsDemocracy(consumerName string, expectRegisteredRewardDistribution bool }, { Action: SubmitChangeRewardDenomsProposalAction{ +<<<<<<< HEAD Denom: consumerRewardDenom, +======= + Chain: ChainID("provi"), + Denoms: consumerRewardDenoms, +>>>>>>> 0d782959 (feat!: add memo to IBC transfers of ICS rewards (#2290)) Deposit: 10000001, From: ValidatorID("bob"), }, @@ -173,10 +185,11 @@ func stepsDemocracy(consumerName string, expectRegisteredRewardDistribution bool State: State{ ChainID("provi"): ChainState{ // Check that the denom is registered on provider chain - RegisteredConsumerRewardDenoms: &[]string{consumerRewardDenom}, + RegisteredConsumerRewardDenoms: &consumerRewardDenoms, }, }, }, + // Relay pending consumer rewards sent via the transfer channel-1 { Action: RelayRewardPacketsToProviderAction{ ConsumerChain: ChainID(consumerName), @@ -186,8 +199,115 @@ func stepsDemocracy(consumerName string, expectRegisteredRewardDistribution bool }, State: State{ ChainID("provi"): ChainState{ - // Check that ARE NOT minted and sent to provider chain and distributed to validators and their delegators on provider chain - // the tokens are not sent because the test configuration does not allow sending tokens + Rewards: &Rewards{ + // expectRegisteredRewardDistribution == true + // expect rewards to be distributed since IBC denoms are registered + // and transfer channel-1 is associated to the consumer id + IsRewarded: map[ValidatorID]bool{ + ValidatorID("alice"): expectRegisteredRewardDistribution, + ValidatorID("bob"): expectRegisteredRewardDistribution, + ValidatorID("carol"): expectRegisteredRewardDistribution, + }, + IsIncrementalReward: false, + Denom: consumerRewardDenoms[0], + }, + }, + }, + }, + // Create a second consumer client on the provider + { + Action: CreateIbcClientAction{ + ChainA: ChainID("provi"), + ChainB: ChainID(consumerName), + }, + State: State{}, + }, + // Create a new IBC connection between the 2nd consumer client + // and the existing provider client on the consumer + { + Action: AddIbcConnectionAction{ + ChainA: ChainID("provi"), + ChainB: ChainID(consumerName), + ClientA: 1, + ClientB: 0, // already created during the CCV handshake + }, + State: State{}, + }, + // Create IBC transfer channel-2 + { + Action: AddIbcChannelAction{ + ChainA: ChainID("provi"), + ChainB: ChainID(consumerName), + ConnectionA: 1, + PortA: "transfer", + PortB: "transfer", + Order: "unordered", + Version: "ics20-1", + }, + State: State{}, + }, + // Transfer tokens from the consumer to the consumer reward pool + // of the provider via the transfer channel-2 + { + Action: TransferIbcTokenAction{ + Chain: ChainID(consumerName), + From: ValidatorID("carol"), + DstAddr: "cosmos1ap0mh6xzfn8943urr84q6ae7zfnar48am2erhd", // consumer reward pool address + Amount: 1000000, + Channel: 2, + Memo: "consumer chain rewards distribution", // no consumer Id in memo + }, + State: State{}, + }, + // Relay the transfer packets from channel-2 + // and check that tokens are not distributed + // since the packet isn't associated to a consumer id + { + Action: RelayRewardPacketsToProviderAction{ + ConsumerChain: ChainID(consumerName), + ProviderChain: ChainID("provi"), + Port: "transfer", + Channel: 2, + }, + State: State{ + ChainID("provi"): ChainState{ + Rewards: &Rewards{ + IsRewarded: map[ValidatorID]bool{ + ValidatorID("alice"): false, + ValidatorID("bob"): false, + ValidatorID("carol"): false, + }, + IsIncrementalReward: true, + Denom: "stake", + }, + }, + }, + }, + // Transfer tokens from the consumer to the consumer reward pool + // of the provider via the transfer channel-2 using the correct memo + // to identify the consumer + { + Action: TransferIbcTokenAction{ + Chain: ChainID(consumerName), + From: ValidatorID("carol"), + DstAddr: "cosmos1ap0mh6xzfn8943urr84q6ae7zfnar48am2erhd", // consumer reward pool address + Amount: 1000000, + Channel: 2, + Memo: `{"provider":{"consumerId":"0","chainId":"democ","memo":"ICS rewards"}}`, + }, + State: State{}, + }, + // Relay the transfer packets from channel-2 + // and check that tokens are distributed + { + Action: RelayRewardPacketsToProviderAction{ + ConsumerChain: ChainID(consumerName), + ProviderChain: ChainID("provi"), + Port: "transfer", + Channel: 2, + }, + State: State{ + ChainID("provi"): ChainState{ Rewards: &Rewards{ IsRewarded: map[ValidatorID]bool{ ValidatorID("alice"): expectRegisteredRewardDistribution, @@ -195,7 +315,7 @@ func stepsDemocracy(consumerName string, expectRegisteredRewardDistribution bool ValidatorID("carol"): expectRegisteredRewardDistribution, }, IsIncrementalReward: false, - IsNativeDenom: false, + Denom: consumerRewardDenoms[1], }, }, }, diff --git a/tests/e2e/steps_inactive_vals.go b/tests/e2e/steps_inactive_vals.go new file mode 100644 index 0000000000..dab57b4eba --- /dev/null +++ b/tests/e2e/steps_inactive_vals.go @@ -0,0 +1,973 @@ +package main + +import ( + gov "github.com/cosmos/cosmos-sdk/x/gov/types/v1" + clienttypes "github.com/cosmos/ibc-go/v8/modules/core/02-client/types" +) + +// stepsInactiveValidatorsOnConsumer tests situations where validators that are *not* in the active set on the +// provider chain validate on the consumer chain. +// The provider chain is set to have at most *2* validators active in consensus, and there are 3 validators in total. +// high-level, this test does: +// - start the provider chain +// - start a consumer chain +// - check that non-consensus validators do not get slashed for downtime on the provider; and that they don't get rewards +// - check that active validators *do* get slashed for downtime on the provider, and don't get rewards while they are down +// - check that non-consensus validators *do* get jailed for consumer downtime on the provider +// - check that non-consensus validators *become* consensus validators when they have enough power +func stepsInactiveProviderValidators() []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{ + Denom: "stake", // check for rewards in the provider denom + IsIncrementalReward: true, // we need to get incremental rewards + // if we would look at total rewards, alice would trivially also get rewards, + // because she gets rewards in the first block 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, + }, + }, + }, + }, + // give carol more power so that she has enough power to validate if bob goes down + { + Action: DelegateTokensAction{ + Chain: ChainID("provi"), + From: ValidatorID("carol"), + To: ValidatorID("carol"), + Amount: 700000000, // carol needs to have more than 2/3rds of power(alice) + power(carol) + power(bob) to run both chains alone, so we stake some more to her + }, + State: State{ + ChainID("provi"): ChainState{ + ValPowers: &map[ValidatorID]uint{ + ValidatorID("alice"): 0, + ValidatorID("bob"): 200, + ValidatorID("carol"): 1000, + }, + StakedTokens: &map[ValidatorID]uint{ + ValidatorID("alice"): 100000000, + ValidatorID("bob"): 200000000, + ValidatorID("carol"): 1000000000, + }, + // check that bob and carol get rewards, but alice does not + Rewards: &Rewards{ + Denom: "stake", // 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, + }, + }, + }, + }, + }, + // 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"): 1000, + }, + StakedTokens: &map[ValidatorID]uint{ + ValidatorID("alice"): 100000000, + ValidatorID("bob"): 198000000, // 1% slash + ValidatorID("carol"): 1000000000, + }, + }, + }, + }, + { + // 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("provi"): ChainState{ + Rewards: &Rewards{ + Denom: "stake", // 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, + }, + }, + }, + ChainID("consu"): ChainState{ + ValPowers: &map[ValidatorID]uint{ + ValidatorID("alice"): 100, + ValidatorID("bob"): 0, + ValidatorID("carol"): 1000, + }, + }, + }, + }, + // 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"): 1000, + }, + // check that between two blocks now, alice does not get rewarded with the native denom + Rewards: &Rewards{ + Denom: "stake", // 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"): 1000, + }, + }, + }, + }, + // 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"): 1000, + }, + }, + }, + }, + // alice goes offline on the consumer chain + { + Action: DowntimeSlashAction{ + Chain: ChainID("consu"), + Validator: ValidatorID("alice"), + }, + State: State{ + ChainID("consu"): ChainState{ + ValPowers: &map[ValidatorID]uint{ + ValidatorID("alice"): 100, // power not affected yet + ValidatorID("bob"): 198, + ValidatorID("carol"): 1000, + }, + }, + ChainID("provi"): ChainState{ + ValPowers: &map[ValidatorID]uint{ + ValidatorID("alice"): 0, // alice is not consensus-active anyways, since we allow two vals at maximum + ValidatorID("bob"): 198, + ValidatorID("carol"): 1000, + }, + }, + }, + }, + // relay the packets so that the provider chain knows about alice's downtime + { + Action: RelayPacketsAction{ + ChainA: ChainID("consu"), + ChainB: ChainID("provi"), + Port: "consumer", + Channel: 0, + }, + State: State{ + ChainID("provi"): ChainState{ + ValPowers: &map[ValidatorID]uint{ + ValidatorID("alice"): 0, // alice is still not in the active set, and should now be jailed too. + // we cannot test directly whether alice is jailed, but we will test this below + ValidatorID("bob"): 198, + ValidatorID("carol"): 1000, + }, + }, + }, + }, + // we need to double-check that alice is actually jailed, so we get bob jailed, too, which usually would mean alice gets into power + { + Action: DowntimeSlashAction{ + Chain: ChainID("provi"), + Validator: ValidatorID("bob"), + }, + State: State{ + ChainID("provi"): ChainState{ + ValPowers: &map[ValidatorID]uint{ + ValidatorID("alice"): 0, // alice is jailed + ValidatorID("bob"): 0, // bob is jailed + ValidatorID("carol"): 1000, + }, + }, + }, + }, + // relay the packets so that the consumer chain is in sync again + { + Action: RelayPacketsAction{ + ChainA: ChainID("provi"), + ChainB: ChainID("consu"), + Port: "provider", + Channel: 0, + }, + State: State{ + ChainID("consu"): ChainState{ + ValPowers: &map[ValidatorID]uint{ + ValidatorID("alice"): 0, // alice is jailed + ValidatorID("bob"): 0, // bob is jailed + ValidatorID("carol"): 1000, + }, + }, + }, + }, + // unjail alice + { + Action: UnjailValidatorAction{ + Provider: ChainID("provi"), + Validator: ValidatorID("alice"), + }, + State: State{ + ChainID("provi"): ChainState{ + ValPowers: &map[ValidatorID]uint{ + // alice was not slashed because consumer downtime just jails without slashing tokens + ValidatorID("alice"): 100, // alice is back as an active consensus validator. + ValidatorID("bob"): 0, // bob is still jailed + ValidatorID("carol"): 1000, + }, + }, + }, + }, + // 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"): 196, // bob is back as an active consensus validator and lost 2 more power due to the second downtime + ValidatorID("carol"): 1000, + }, + }, + }, + }, + // relay the packets so that the consumer chain is in sync again + { + Action: RelayPacketsAction{ + ChainA: ChainID("provi"), + ChainB: ChainID("consu"), + Port: "provider", + Channel: 0, + }, + State: State{ + ChainID("consu"): ChainState{ + ValPowers: &map[ValidatorID]uint{ + ValidatorID("alice"): 100, // both alice and bob are validating the consumer + ValidatorID("bob"): 196, + ValidatorID("carol"): 1000, + }, + }, + }, + }, + }, + ) + + 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 concatSteps([]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, + AllowInactiveVals: true, + }, + 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: gov.ProposalStatus_PROPOSAL_STATUS_VOTING_PERIOD.String(), + }, + }, + HasToValidate: &map[ValidatorID][]ChainID{ + ValidatorID("alice"): {}, + ValidatorID("bob"): {}, + ValidatorID("carol"): {}, + }, + }, + }, + }, + }, + stepsOptInValidators("alice", "bob", "carol"), + []Step{ + { + 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: gov.ProposalStatus_PROPOSAL_STATUS_PASSED.String(), + }, + }, + }, + }, + }, + { + // 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, + ValidatorID("carol"): 300, + }, + }, + }, + }, + { + 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{}, + }, + }, + ) +} + +func stepsOptInValidators(validators ...ValidatorID) []Step { + s := make([]Step, 0) + for _, val := range validators { + // Οpt in all validators + s = append(s, Step{ + Action: OptInAction{ + Chain: ChainID("consu"), + Validator: val, + }, + State: State{ + ChainID("provi"): ChainState{}, + }, + }, + ) + } + return s +} + +// stepsInactiveProviderValidatorsGovernance validates that inactive validators +// are not included in the calculation of the quorum for governance proposals. +// It checks that when the quorum is met *among active validators*, +// the proposal can pass, even though the quorum would not be met if inactive validators +// would be counted. +func stepsInactiveProviderValidatorsGovernance() []Step { + s := concatSteps( + []Step{ + { + Action: StartChainAction{ + Chain: ChainID("provi"), + Validators: []StartChainValidator{ + {Id: ValidatorID("alice"), Stake: 290000000, Allocation: 10000000000}, + {Id: ValidatorID("bob"), Stake: 290000000, 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 1, so alice and bob should not be in power + ValidatorID("bob"): 0, + ValidatorID("carol"): 300, + }, + StakedTokens: &map[ValidatorID]uint{ + ValidatorID("alice"): 290000000, + ValidatorID("bob"): 290000000, + ValidatorID("carol"): 300000000, + }, + }, + }, + }, + }, + []Step{ + // create a governance proposal + { + Action: SubmitConsumerAdditionProposalAction{ + Chain: ChainID("provi"), + From: ValidatorID("alice"), + Deposit: 10000001, + ConsumerChain: ChainID("consu"), + SpawnTime: 0, + InitialHeight: clienttypes.Height{RevisionNumber: 0, RevisionHeight: 1}, + TopN: 51, + }, + 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: gov.ProposalStatus_PROPOSAL_STATUS_VOTING_PERIOD.String(), + }, + }, + }, + }, + }, + // vote for it with carol + { + Action: VoteGovProposalAction{ + Chain: ChainID("provi"), + From: []ValidatorID{ValidatorID("carol")}, + Vote: []string{"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}, + // the proposal should have passed because carol voted for it. + // carol alone is enough to pass the quorum, because stake of the other validators is not counted + Status: gov.ProposalStatus_PROPOSAL_STATUS_PASSED.String(), + }, + }, + }, + }, + }, + }, + ) + + return s +} + +// stepsInactiveProviderValidatorsGovernanceBasecase is a sanity check to go along with +// stepsInactiveProviderValidatorsGovernance. It tests that with all validators being active, +// the proposal does not pass if it does not meet the quorum among validators. +func stepsInactiveProviderValidatorsGovernanceBasecase() []Step { + s := concatSteps( + []Step{ + { + Action: StartChainAction{ + Chain: ChainID("provi"), + Validators: []StartChainValidator{ + {Id: ValidatorID("alice"), Stake: 290000000, Allocation: 10000000000}, + {Id: ValidatorID("bob"), Stake: 290000000, Allocation: 10000000000}, + {Id: ValidatorID("carol"), Stake: 300000000, Allocation: 10000000000}, + }, + }, + State: State{ + ChainID("provi"): ChainState{ + ValPowers: &map[ValidatorID]uint{ + ValidatorID("alice"): 290, + ValidatorID("bob"): 290, + ValidatorID("carol"): 300, + }, + StakedTokens: &map[ValidatorID]uint{ + ValidatorID("alice"): 290000000, + ValidatorID("bob"): 290000000, + ValidatorID("carol"): 300000000, + }, + }, + }, + }, + }, + []Step{ + // create a governance proposal + { + Action: SubmitConsumerAdditionProposalAction{ + Chain: ChainID("provi"), + From: ValidatorID("alice"), + Deposit: 10000001, + ConsumerChain: ChainID("consu"), + SpawnTime: 0, + InitialHeight: clienttypes.Height{RevisionNumber: 0, RevisionHeight: 1}, + TopN: 51, + }, + 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: gov.ProposalStatus_PROPOSAL_STATUS_VOTING_PERIOD.String(), + }, + }, + }, + }, + }, + // vote for it with carol + { + Action: VoteGovProposalAction{ + Chain: ChainID("provi"), + From: []ValidatorID{ValidatorID("carol")}, + Vote: []string{"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}, + // the proposal should *not* have passed because only carol voted for it, + // and carol is not enough to pass the quorum + Status: gov.ProposalStatus_PROPOSAL_STATUS_REJECTED.String(), + }, + }, + }, + }, + }, + }, + ) + + return s +} + +// stepsMinStake validates that a validator with less stake than the specified minStake parameter +// cannot validate the consumer chain. +func stepsMinStake() []Step { + return concatSteps( + []Step{ + { + Action: StartChainAction{ + Chain: ChainID("provi"), + Validators: []StartChainValidator{ + {Id: ValidatorID("alice"), Stake: 290000000, Allocation: 10000000000}, + {Id: ValidatorID("bob"), Stake: 290000000, Allocation: 10000000000}, + {Id: ValidatorID("carol"), Stake: 300000000, Allocation: 10000000000}, + }, + }, + State: State{ + ChainID("provi"): ChainState{ + ValPowers: &map[ValidatorID]uint{ + ValidatorID("alice"): 290, + ValidatorID("bob"): 290, + ValidatorID("carol"): 300, + }, + StakedTokens: &map[ValidatorID]uint{ + ValidatorID("alice"): 290000000, + ValidatorID("bob"): 290000000, + ValidatorID("carol"): 300000000, + }, + }, + }, + }, + // create a governance proposal + { + Action: SubmitConsumerAdditionProposalAction{ + Chain: ChainID("provi"), + From: ValidatorID("alice"), + Deposit: 10000001, + ConsumerChain: ChainID("consu"), + SpawnTime: 0, + InitialHeight: clienttypes.Height{RevisionNumber: 0, RevisionHeight: 1}, + TopN: 0, + MinStake: 300000000, + }, + 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: gov.ProposalStatus_PROPOSAL_STATUS_VOTING_PERIOD.String(), + }, + }, + }, + }, + }, + }, + stepsOptInValidators("alice", "bob", "carol"), + []Step{ + { + 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: gov.ProposalStatus_PROPOSAL_STATUS_PASSED.String(), + }, + }, + }, + }, + }, + { + // we start all the validators, but due to the min stake, only carol can validate + 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"): 0, + ValidatorID("bob"): 0, + ValidatorID("carol"): 300, // due to min stake of 300000000, only carol can validate + }, + }, + }, + }, + }, + ) +} + +// This test case validates that inactive validators are not included when computing +// the top N. +func stepsInactiveValsWithTopN() []Step { + return []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{ + Denom: "stake", // check for rewards in the provider denom + IsIncrementalReward: true, // we need to get incremental rewards + // if we would look at total rewards, alice would trivially also get rewards, + // because she gets rewards in the first block due to being in the genesis + IsRewarded: map[ValidatorID]bool{ + ValidatorID("alice"): false, + ValidatorID("bob"): true, + ValidatorID("carol"): true, + }, + }, + }, + }, + }, + { + Action: SubmitConsumerAdditionProposalAction{ + Chain: ChainID("provi"), + From: ValidatorID("alice"), + Deposit: 10000001, + ConsumerChain: ChainID("consu"), + SpawnTime: 0, + InitialHeight: clienttypes.Height{RevisionNumber: 0, RevisionHeight: 1}, + TopN: 51, + }, + 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: gov.ProposalStatus_PROPOSAL_STATUS_VOTING_PERIOD.String(), + }, + }, + }, + }, + }, + { + 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: gov.ProposalStatus_PROPOSAL_STATUS_PASSED.String(), + }, + }, + HasToValidate: &map[ValidatorID][]ChainID{ + ValidatorID("alice"): {}, + ValidatorID("bob"): {}, // bob doesn't have to validate because he is not in the top N + ValidatorID("carol"): {"consu"}, + }, + }, + }, + }, + { + 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"): 0, // alice and bob are not in the top N, so aren't in the validator set + ValidatorID("bob"): 0, + ValidatorID("carol"): 300, + }, + }, + }, + }, + } +} + +// stepsInactiveValsMint tests the minting of tokens with inactive validators +// It checks that inactive validators are not counted when computing whether the +// inflation rate should go up or down. +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 + }, + }, + }, + } +} + +// stepsMintBasecase tests the minting of tokens without inactive validators. +// This is done as a sanity check to complement stepsInactiveValsMint. +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"): 27, + ValidatorID("bob"): 28, + ValidatorID("carol"): 79, + }, + InflationRateChange: intPtr(-1), // inflation rate *still* goes down + }, + }, + }, + } +} diff --git a/tests/e2e/test_driver.go b/tests/e2e/test_driver.go index 18140a49f6..49ca7d3cc3 100644 --- a/tests/e2e/test_driver.go +++ b/tests/e2e/test_driver.go @@ -208,7 +208,22 @@ func (td *DefaultDriver) runAction(action interface{}) error { case OptOutAction: target.optOut(action, td.target, td.verbose) case SetConsumerCommissionRateAction: +<<<<<<< HEAD target.setConsumerCommissionRate(action, td.target, td.verbose) +======= + target := td.getTargetDriver("provider") + target.setConsumerCommissionRate(action, td.verbose) + case SubmitConsumerMisbehaviourAction: + target := td.getTargetDriver("provider") + target.submitConsumerMisbehaviour(action, td.verbose) + case CreateIbcClientAction: + // use default for hermes actions + target := td.getTargetDriver("") + target.createIbcClientHermes(action, td.verbose) + case TransferIbcTokenAction: + target := td.getTargetDriver(action.Chain) + target.transferIbcToken(action, td.verbose) +>>>>>>> 0d782959 (feat!: add memo to IBC transfers of ICS rewards (#2290)) default: log.Fatalf("unknown action in testRun %s: %#v", td.testCfg.name, action) } diff --git a/tests/e2e/testlib/types.go b/tests/e2e/testlib/types.go index 0fe6061f4a..0b36d56ef1 100644 --- a/tests/e2e/testlib/types.go +++ b/tests/e2e/testlib/types.go @@ -26,7 +26,7 @@ type ChainCommands interface { GetProposal(chain ChainID, proposal uint) Proposal GetParam(chain ChainID, param Param) string GetProviderAddressFromConsumer(consumerChain ChainID, validator ValidatorID) string - GetReward(chain ChainID, validator ValidatorID, blockHeight uint, isNativeDenom bool) float64 + GetReward(chain ChainID, validator ValidatorID, blockHeight uint, denom string) float64 GetRegisteredConsumerRewardDenoms(chain ChainID) []string GetSlashMeter() int64 GetPendingPacketQueueSize(chain ChainID) uint @@ -324,9 +324,9 @@ type Rewards struct { // if true it will calculate if the validator/delegator is rewarded between 2 successive blocks, // otherwise it will calculate if it received any rewards since the 1st block IsIncrementalReward bool - // if true checks rewards for "stake" token, otherwise checks rewards from - // other chains (e.g. false is used to check if provider received rewards from a consumer chain) - IsNativeDenom bool + // The reward denom to be checked. This can be either the native "stake" denom or + // a denom from other chains (e.g. if provider received rewards from a consumer chain) + Denom string } type ParamsProposal struct { diff --git a/tests/e2e/v4/state.go b/tests/e2e/v4/state.go index 9c8a0f2d58..fb1642adfe 100644 --- a/tests/e2e/v4/state.go +++ b/tests/e2e/v4/state.go @@ -7,7 +7,16 @@ import ( "os/exec" "regexp" "strconv" +<<<<<<< HEAD "time" +======= + "strings" + + clienttypes "github.com/cosmos/ibc-go/v8/modules/core/02-client/types" + "github.com/kylelemons/godebug/pretty" + "github.com/tidwall/gjson" + "gopkg.in/yaml.v2" +>>>>>>> 0d782959 (feat!: add memo to IBC transfers of ICS rewards (#2290)) gov "github.com/cosmos/cosmos-sdk/x/gov/types/v1" clienttypes "github.com/cosmos/ibc-go/v8/modules/core/02-client/types" @@ -167,7 +176,7 @@ func (tr Commands) GetValPower(chain ChainID, validator ValidatorID) uint { return 0 } -func (tr Commands) GetReward(chain ChainID, validator ValidatorID, blockHeight uint, isNativeDenom bool) float64 { +func (tr Commands) GetReward(chain ChainID, validator ValidatorID, blockHeight uint, denom string) float64 { valCfg := tr.ValidatorConfigs[validator] delAddresss := valCfg.DelAddress if chain != ChainID("provi") { @@ -181,25 +190,32 @@ func (tr Commands) GetReward(chain ChainID, validator ValidatorID, blockHeight u } binaryName := tr.ChainConfigs[chain].BinaryName - bz, err := tr.Target.ExecCommand(binaryName, - + cmd := tr.Target.ExecCommand(binaryName, "query", "distribution", "rewards", delAddresss, - `--height`, fmt.Sprint(blockHeight), `--node`, tr.GetQueryNode(chain), `-o`, `json`, - ).CombinedOutput() + ) + + bz, err := cmd.CombinedOutput() if err != nil { - log.Fatal(err, "\n", string(bz)) + log.Println("running cmd: ", cmd) + log.Fatal("failed getting rewards: ", err, "\n", string(bz)) } - denomCondition := `total.#(denom!="stake").amount` - if isNativeDenom { - denomCondition = `total.#(denom=="stake").amount` + denomCondition := fmt.Sprintf(`total.#(%%"*%s*")`, denom) + amount := strings.Split(gjson.Get(string(bz), denomCondition).String(), denom)[0] + + res := float64(0) + if amount != "" { + res, err = strconv.ParseFloat(amount, 64) + if err != nil { + log.Fatal("failed parsing consumer reward:", err) + } } - return gjson.Get(string(bz), denomCondition).Float() + return res } func (tr Commands) GetBalance(chain ChainID, validator ValidatorID) uint { diff --git a/tests/mbt/driver/setup.go b/tests/mbt/driver/setup.go index acdae7554b..4e4d38dd52 100644 --- a/tests/mbt/driver/setup.go +++ b/tests/mbt/driver/setup.go @@ -502,6 +502,7 @@ func createConsumerGenesis(modelParams ModelParams, providerChain *ibctesting.Te []string{}, []string{}, ccvtypes.DefaultRetryDelayPeriod, + "", ) return consumertypes.NewInitialGenesisState(consumerClientState, providerConsState, valUpdates, params) diff --git a/x/ccv/consumer/keeper/distribution.go b/x/ccv/consumer/keeper/distribution.go index 5c9bbe4a24..10e28c7d75 100644 --- a/x/ccv/consumer/keeper/distribution.go +++ b/x/ccv/consumer/keeper/distribution.go @@ -121,6 +121,11 @@ func (k Keeper) SendRewardsToProvider(ctx sdk.Context) error { sentCoins := sdk.NewCoins() var allBalances sdk.Coins + rewardMemo, err := ccv.CreateTransferMemo(k.GetConsumerId(ctx), ctx.ChainID()) + if err != nil { + return err + } + // iterate over all whitelisted reward denoms for _, denom := range k.AllowedRewardDenoms(ctx) { // get the balance of the denom in the toSendToProviderTokens address @@ -137,7 +142,7 @@ func (k Keeper) SendRewardsToProvider(ctx sdk.Context) error { Receiver: providerAddr, // provider fee pool address to send to TimeoutHeight: timeoutHeight, // timeout height disabled TimeoutTimestamp: timeoutTimestamp, - Memo: "consumer chain rewards distribution", + Memo: rewardMemo, } // validate MsgTransfer before calling Transfer() diff --git a/x/ccv/consumer/keeper/params.go b/x/ccv/consumer/keeper/params.go index 6a9f35dbfe..a698b3e82a 100644 --- a/x/ccv/consumer/keeper/params.go +++ b/x/ccv/consumer/keeper/params.go @@ -127,3 +127,8 @@ func (k Keeper) GetRetryDelayPeriod(ctx sdk.Context) time.Duration { params := k.GetConsumerParams(ctx) return params.RetryDelayPeriod } + +func (k Keeper) GetConsumerId(ctx sdk.Context) string { + params := k.GetConsumerParams(ctx) + return params.ConsumerId +} diff --git a/x/ccv/consumer/keeper/params_test.go b/x/ccv/consumer/keeper/params_test.go index a9f6cc10d9..603aaecc3a 100644 --- a/x/ccv/consumer/keeper/params_test.go +++ b/x/ccv/consumer/keeper/params_test.go @@ -31,6 +31,7 @@ func TestParams(t *testing.T) { rewardDenoms, provideRewardDenoms, ccv.DefaultRetryDelayPeriod, + "0", ) // these are the default params, IBC suite independently sets enabled=true params := consumerKeeper.GetConsumerParams(ctx) @@ -38,7 +39,7 @@ func TestParams(t *testing.T) { newParams := ccv.NewParams(false, 1000, "channel-2", "cosmos19pe9pg5dv9k5fzgzmsrgnw9rl9asf7ddwhu7lm", - 7*24*time.Hour, 25*time.Hour, "0.5", 500, 24*21*time.Hour, []string{"untrn"}, []string{"uatom"}, 2*time.Hour) + 7*24*time.Hour, 25*time.Hour, "0.5", 500, 24*21*time.Hour, []string{"untrn"}, []string{"uatom"}, 2*time.Hour, "1") consumerKeeper.SetParams(ctx, newParams) params = consumerKeeper.GetConsumerParams(ctx) require.Equal(t, newParams, params) diff --git a/x/ccv/consumer/migrations/v3/legacy_params.go b/x/ccv/consumer/migrations/v3/legacy_params.go index 8734cb4266..c1e4b282a4 100644 --- a/x/ccv/consumer/migrations/v3/legacy_params.go +++ b/x/ccv/consumer/migrations/v3/legacy_params.go @@ -24,6 +24,7 @@ func GetConsumerParamsLegacy(ctx sdk.Context, paramSpace ccvtypes.LegacyParamSub getRewardDenoms(ctx, paramSpace), getProviderRewardDenoms(ctx, paramSpace), getRetryDelayPeriod(ctx, paramSpace), + "0", ) } diff --git a/x/ccv/consumer/types/genesis_test.go b/x/ccv/consumer/types/genesis_test.go index d66bd924cb..83de1b619a 100644 --- a/x/ccv/consumer/types/genesis_test.go +++ b/x/ccv/consumer/types/genesis_test.go @@ -233,6 +233,7 @@ func TestValidateInitialGenesisState(t *testing.T) { []string{}, []string{}, ccv.DefaultRetryDelayPeriod, + "1", )), true, }, @@ -252,6 +253,7 @@ func TestValidateInitialGenesisState(t *testing.T) { []string{}, []string{}, ccv.DefaultRetryDelayPeriod, + "1", )), true, }, @@ -457,6 +459,7 @@ func TestValidateRestartConsumerGenesisState(t *testing.T) { []string{}, []string{}, ccv.DefaultRetryDelayPeriod, + "1", )), true, }, diff --git a/x/ccv/consumer/types/params_test.go b/x/ccv/consumer/types/params_test.go index a1cb9d651e..5ffd85c30f 100644 --- a/x/ccv/consumer/types/params_test.go +++ b/x/ccv/consumer/types/params_test.go @@ -11,6 +11,8 @@ import ( // Tests the validation of consumer params that happens at genesis func TestValidateParams(t *testing.T) { + consumerId := "13" + testCases := []struct { name string params ccvtypes.ConsumerParams @@ -19,59 +21,67 @@ func TestValidateParams(t *testing.T) { {"default params", ccvtypes.DefaultParams(), true}, { "custom valid params", - ccvtypes.NewParams(true, 5, "", "", 1004, 1005, "0.5", 1000, 24*21*time.Hour, []string{"untrn"}, []string{"uatom"}, 2*time.Hour), true, + ccvtypes.NewParams(true, 5, "", "", 1004, 1005, "0.5", 1000, 24*21*time.Hour, []string{"untrn"}, []string{"uatom"}, 2*time.Hour, consumerId), true, }, { "custom invalid params, block per dist transmission", - ccvtypes.NewParams(true, -5, "", "", 5, 1005, "0.5", 1000, 24*21*time.Hour, []string{"untrn"}, []string{"uatom"}, 2*time.Hour), false, + ccvtypes.NewParams(true, -5, "", "", 5, 1005, "0.5", 1000, 24*21*time.Hour, []string{"untrn"}, []string{"uatom"}, 2*time.Hour, consumerId), false, }, { "custom invalid params, dist transmission channel", - ccvtypes.NewParams(true, 5, "badchannel/", "", 5, 1005, "0.5", 1000, 24*21*time.Hour, []string{"untrn"}, []string{"uatom"}, 2*time.Hour), false, + ccvtypes.NewParams(true, 5, "badchannel/", "", 5, 1005, "0.5", 1000, 24*21*time.Hour, []string{"untrn"}, []string{"uatom"}, 2*time.Hour, consumerId), false, }, { "custom invalid params, ccv timeout", - ccvtypes.NewParams(true, 5, "", "", -5, 1005, "0.5", 1000, 24*21*time.Hour, []string{"untrn"}, []string{"uatom"}, 2*time.Hour), false, + ccvtypes.NewParams(true, 5, "", "", -5, 1005, "0.5", 1000, 24*21*time.Hour, []string{"untrn"}, []string{"uatom"}, 2*time.Hour, consumerId), false, }, { "custom invalid params, transfer timeout", - ccvtypes.NewParams(true, 5, "", "", 1004, -7, "0.5", 1000, 24*21*time.Hour, []string{"untrn"}, []string{"uatom"}, 2*time.Hour), false, + ccvtypes.NewParams(true, 5, "", "", 1004, -7, "0.5", 1000, 24*21*time.Hour, []string{"untrn"}, []string{"uatom"}, 2*time.Hour, consumerId), false, }, { "custom invalid params, consumer redist fraction is negative", - ccvtypes.NewParams(true, 5, "", "", 5, 1005, "-0.5", 1000, 24*21*time.Hour, []string{"untrn"}, []string{"uatom"}, 2*time.Hour), false, + ccvtypes.NewParams(true, 5, "", "", 5, 1005, "-0.5", 1000, 24*21*time.Hour, []string{"untrn"}, []string{"uatom"}, 2*time.Hour, consumerId), false, }, { "custom invalid params, consumer redist fraction is over 1", - ccvtypes.NewParams(true, 5, "", "", 5, 1005, "1.2", 1000, 24*21*time.Hour, []string{"untrn"}, []string{"uatom"}, 2*time.Hour), false, + ccvtypes.NewParams(true, 5, "", "", 5, 1005, "1.2", 1000, 24*21*time.Hour, []string{"untrn"}, []string{"uatom"}, 2*time.Hour, consumerId), false, }, { "custom invalid params, bad consumer redist fraction ", - ccvtypes.NewParams(true, 5, "", "", 5, 1005, "notFrac", 1000, 24*21*time.Hour, []string{"untrn"}, []string{"uatom"}, 2*time.Hour), false, + ccvtypes.NewParams(true, 5, "", "", 5, 1005, "notFrac", 1000, 24*21*time.Hour, []string{"untrn"}, []string{"uatom"}, 2*time.Hour, consumerId), false, }, { "custom invalid params, negative num historical entries", - ccvtypes.NewParams(true, 5, "", "", 5, 1005, "0.5", -100, 24*21*time.Hour, []string{"untrn"}, []string{"uatom"}, 2*time.Hour), false, + ccvtypes.NewParams(true, 5, "", "", 5, 1005, "0.5", -100, 24*21*time.Hour, []string{"untrn"}, []string{"uatom"}, 2*time.Hour, consumerId), false, }, { "custom invalid params, negative unbonding period", - ccvtypes.NewParams(true, 5, "", "", 5, 1005, "0.5", 1000, -24*21*time.Hour, []string{"untrn"}, []string{"uatom"}, 2*time.Hour), false, + ccvtypes.NewParams(true, 5, "", "", 5, 1005, "0.5", 1000, -24*21*time.Hour, []string{"untrn"}, []string{"uatom"}, 2*time.Hour, consumerId), false, }, { "custom invalid params, invalid reward denom", - ccvtypes.NewParams(true, 5, "", "", 5, 1005, "0.5", 1000, 24*21*time.Hour, []string{"u"}, []string{}, 2*time.Hour), false, + ccvtypes.NewParams(true, 5, "", "", 5, 1005, "0.5", 1000, 24*21*time.Hour, []string{"u"}, []string{}, 2*time.Hour, consumerId), false, }, { "custom invalid params, invalid provider reward denom", - ccvtypes.NewParams(true, 5, "", "", 5, 1005, "0.5", 1000, 24*21*time.Hour, []string{}, []string{"a"}, 2*time.Hour), false, + ccvtypes.NewParams(true, 5, "", "", 5, 1005, "0.5", 1000, 24*21*time.Hour, []string{}, []string{"a"}, 2*time.Hour, consumerId), false, }, { "custom invalid params, retry delay period is negative", - ccvtypes.NewParams(true, 5, "", "", 5, 1005, "0.5", 1000, 24*21*time.Hour, []string{}, []string{}, -2*time.Hour), false, + ccvtypes.NewParams(true, 5, "", "", 5, 1005, "0.5", 1000, 24*21*time.Hour, []string{}, []string{}, -2*time.Hour, consumerId), false, }, { "custom invalid params, retry delay period is zero", - ccvtypes.NewParams(true, 5, "", "", 5, 1005, "0.5", 1000, 24*21*time.Hour, []string{}, []string{}, 0), false, + ccvtypes.NewParams(true, 5, "", "", 5, 1005, "0.5", 1000, 24*21*time.Hour, []string{}, []string{}, 0, consumerId), false, + }, + { + "custom invalid params, consumer ID is blank", + ccvtypes.NewParams(true, 5, "", "", 5, 1005, "0.5", 1000, 24*21*time.Hour, []string{}, []string{}, time.Hour, ""), false, + }, + { + "custom invalid params, consumer ID is not a uint64", + ccvtypes.NewParams(true, 5, "", "", 5, 1005, "0.5", 1000, 24*21*time.Hour, []string{}, []string{}, time.Hour, "consumerId"), false, }, } diff --git a/x/ccv/provider/ibc_middleware.go b/x/ccv/provider/ibc_middleware.go index d9d816ab93..d90ad92f4c 100644 --- a/x/ccv/provider/ibc_middleware.go +++ b/x/ccv/provider/ibc_middleware.go @@ -12,8 +12,14 @@ import ( sdk "github.com/cosmos/cosmos-sdk/types" capabilitytypes "github.com/cosmos/ibc-go/modules/capability/types" +<<<<<<< HEAD "github.com/cosmos/interchain-security/v5/x/ccv/provider/keeper" "github.com/cosmos/interchain-security/v5/x/ccv/provider/types" +======= + "github.com/cosmos/interchain-security/v6/x/ccv/provider/keeper" + "github.com/cosmos/interchain-security/v6/x/ccv/provider/types" + ccvtypes "github.com/cosmos/interchain-security/v6/x/ccv/types" +>>>>>>> 0d782959 (feat!: add memo to IBC transfers of ICS rewards (#2290)) ) var _ porttypes.Middleware = &IBCMiddleware{} @@ -121,12 +127,15 @@ func (im IBCMiddleware) OnRecvPacket( // that the packet data is valid and can be safely // deserialized without checking errors. if ack.Success() { +<<<<<<< HEAD // execute the middleware logic only if the sender is a consumer chain consumerID, err := im.keeper.IdentifyConsumerChainIDFromIBCPacket(ctx, packet) if err != nil { return ack } +======= +>>>>>>> 0d782959 (feat!: add memo to IBC transfers of ICS rewards (#2290)) // extract the coin info received from the packet data var data ibctransfertypes.FungibleTokenPacketData _ = types.ModuleCdc.UnmarshalJSON(packet.GetData(), &data) @@ -137,8 +146,68 @@ func (im IBCMiddleware) OnRecvPacket( return ack } +<<<<<<< HEAD coinAmt, _ := math.NewIntFromString(data.Amount) coinDenom := GetProviderDenom(data.Denom, packet) +======= + consumerId := "" + // check if the transfer has the reward memo + if rewardMemo, err := ccvtypes.GetRewardMemoFromTransferMemo(data.Memo); err != nil { + // check if the transfer is on a channel with the same underlying + // client as the CCV channel + consumerId, err = im.keeper.IdentifyConsumerIdFromIBCPacket(ctx, packet) + if err != nil { + if data.Memo == "consumer chain rewards distribution" { + // log error message + logger.Error( + "received token transfer with ICS reward from unknown consumer", + "packet", packet.String(), + "fungibleTokenPacketData", data.String(), + "error", err.Error(), + ) + } + + return ack + } + } else { + logger.Info("transfer memo:%#+v", rewardMemo) + consumerId = rewardMemo.ConsumerId + } + + coinAmt, _ := math.NewIntFromString(data.Amount) + coinDenom := GetProviderDenom(data.Denom, packet) + chainId, err := im.keeper.GetConsumerChainId(ctx, consumerId) + if err != nil { + logger.Error( + "cannot get consumer chain id in transfer middleware", + "consumerId", consumerId, + "packet", packet.String(), + "fungibleTokenPacketData", data.String(), + "error", err.Error(), + ) + return ack + } + + logger.Info( + "received ICS rewards from consumer chain", + "consumerId", consumerId, + "chainId", chainId, + "denom", coinDenom, + "amount", data.Amount, + ) + + // initialize an empty slice to store event attributes + eventAttributes := []sdk.Attribute{} + + // add event attributes + eventAttributes = append(eventAttributes, []sdk.Attribute{ + sdk.NewAttribute(sdk.AttributeKeyModule, types.ModuleName), + sdk.NewAttribute(types.AttributeConsumerId, consumerId), + sdk.NewAttribute(types.AttributeConsumerChainId, chainId), + sdk.NewAttribute(types.AttributeRewardDenom, coinDenom), + sdk.NewAttribute(types.AttributeRewardAmount, data.Amount), + }...) +>>>>>>> 0d782959 (feat!: add memo to IBC transfers of ICS rewards (#2290)) // verify that the coin's denom is a whitelisted consumer denom, // and if so, adds it to the consumer chain rewards allocation, diff --git a/x/ccv/provider/keeper/consumer_lifecycle.go b/x/ccv/provider/keeper/consumer_lifecycle.go new file mode 100644 index 0000000000..c578df4ff2 --- /dev/null +++ b/x/ccv/provider/keeper/consumer_lifecycle.go @@ -0,0 +1,663 @@ +package keeper + +import ( + "fmt" + "time" + + clienttypes "github.com/cosmos/ibc-go/v8/modules/core/02-client/types" + channeltypes "github.com/cosmos/ibc-go/v8/modules/core/04-channel/types" + commitmenttypes "github.com/cosmos/ibc-go/v8/modules/core/23-commitment/types" + ibctmtypes "github.com/cosmos/ibc-go/v8/modules/light-clients/07-tendermint" + + errorsmod "cosmossdk.io/errors" + storetypes "cosmossdk.io/store/types" + + sdk "github.com/cosmos/cosmos-sdk/types" + sdkerrors "github.com/cosmos/cosmos-sdk/types/errors" + stakingtypes "github.com/cosmos/cosmos-sdk/x/staking/types" + + abci "github.com/cometbft/cometbft/abci/types" + tmtypes "github.com/cometbft/cometbft/types" + + "github.com/cosmos/interchain-security/v6/x/ccv/provider/types" + ccv "github.com/cosmos/interchain-security/v6/x/ccv/types" +) + +// PrepareConsumerForLaunch prepares to move the launch of a consumer chain from the previous spawn time to spawn time. +// Previous spawn time can correspond to its zero value if the validator was not previously set for launch. +func (k Keeper) PrepareConsumerForLaunch(ctx sdk.Context, consumerId string, previousSpawnTime, spawnTime time.Time) error { + if !previousSpawnTime.IsZero() { + // if this is not the first initialization and hence `previousSpawnTime` does not contain the zero value of `Time` + // remove the consumer id from the previous spawn time + err := k.RemoveConsumerToBeLaunched(ctx, consumerId, previousSpawnTime) + if err != nil { + return err + } + } + return k.AppendConsumerToBeLaunched(ctx, consumerId, spawnTime) +} + +// InitializeConsumer tries to move a consumer with `consumerId` to the initialized phase. +// If successful, it returns the spawn time and true. +func (k Keeper) InitializeConsumer(ctx sdk.Context, consumerId string) (time.Time, bool) { + // a chain needs to be in the registered or initialized phase + phase := k.GetConsumerPhase(ctx, consumerId) + if phase != types.CONSUMER_PHASE_REGISTERED && phase != types.CONSUMER_PHASE_INITIALIZED { + return time.Time{}, false + } + + initializationParameters, err := k.GetConsumerInitializationParameters(ctx, consumerId) + if err != nil { + return time.Time{}, false + } + + // the spawn time needs to be positive + if initializationParameters.SpawnTime.IsZero() { + return time.Time{}, false + } + + k.SetConsumerPhase(ctx, consumerId, types.CONSUMER_PHASE_INITIALIZED) + + return initializationParameters.SpawnTime, true +} + +// BeginBlockLaunchConsumers launches initialized consumers chains for which the spawn time has passed +func (k Keeper) BeginBlockLaunchConsumers(ctx sdk.Context) error { + bondedValidators := []stakingtypes.Validator{} + activeValidators := []stakingtypes.Validator{} + + consumerIds, err := k.ConsumeIdsFromTimeQueue( + ctx, + types.SpawnTimeToConsumerIdsKeyPrefix(), + k.GetConsumersToBeLaunched, + k.DeleteAllConsumersToBeLaunched, + k.AppendConsumerToBeLaunched, + 200, + ) + if err != nil { + return errorsmod.Wrapf(ccv.ErrInvalidConsumerState, "getting consumers ready to laumch: %s", err.Error()) + } + if len(consumerIds) > 0 { + // get the bonded validators from the staking module + bondedValidators, err = k.GetLastBondedValidators(ctx) + if err != nil { + return fmt.Errorf("getting last bonded validators: %w", err) + } + // get the provider active validators + activeValidators, err = k.GetLastProviderConsensusActiveValidators(ctx) + if err != nil { + return fmt.Errorf("getting last provider active validators: %w", err) + } + } + + for _, consumerId := range consumerIds { + cachedCtx, writeFn := ctx.CacheContext() + err = k.LaunchConsumer(cachedCtx, bondedValidators, activeValidators, consumerId) + if err != nil { + ctx.Logger().Error("could not launch chain", + "consumerId", consumerId, + "error", err) + + // reset spawn time to zero so that owner can try again later + initializationRecord, err := k.GetConsumerInitializationParameters(ctx, consumerId) + if err != nil { + return errorsmod.Wrapf(ccv.ErrInvalidConsumerState, + "getting initialization parameters, consumerId(%s): %s", consumerId, err.Error()) + } + initializationRecord.SpawnTime = time.Time{} + err = k.SetConsumerInitializationParameters(ctx, consumerId, initializationRecord) + if err != nil { + return fmt.Errorf("setting consumer initialization parameters, consumerId(%s): %w", consumerId, err) + } + // also set the phase to registered + k.SetConsumerPhase(ctx, consumerId, types.CONSUMER_PHASE_REGISTERED) + + continue + } + + writeFn() + } + return nil +} + +// ConsumeIdsFromTimeQueue returns from a time queue the consumer ids for which the associated time passed. +// The number of ids return is limited to 'limit'. The ids returned are removed from the time queue. +func (k Keeper) ConsumeIdsFromTimeQueue( + ctx sdk.Context, + timeQueueKeyPrefix byte, + getIds func(sdk.Context, time.Time) (types.ConsumerIds, error), + deleteAllIds func(sdk.Context, time.Time), + appendId func(sdk.Context, string, time.Time) error, + limit int, +) ([]string, error) { + store := ctx.KVStore(k.storeKey) + + result := []string{} + nextTime := []string{} + timestampsToDelete := []time.Time{} + + iterator := storetypes.KVStorePrefixIterator(store, []byte{timeQueueKeyPrefix}) + defer iterator.Close() + for ; iterator.Valid(); iterator.Next() { + if len(result) >= limit { + break + } + ts, err := types.ParseTime(timeQueueKeyPrefix, iterator.Key()) + if err != nil { + return result, fmt.Errorf("parsing removal time: %w", err) + } + if ts.After(ctx.BlockTime()) { + break + } + + consumerIds, err := getIds(ctx, ts) + if err != nil { + return result, + fmt.Errorf("getting consumers ids, ts(%s): %w", ts.String(), err) + } + + timestampsToDelete = append(timestampsToDelete, ts) + + availableSlots := limit - len(result) + if availableSlots >= len(consumerIds.Ids) { + // consumer all the ids + result = append(result, consumerIds.Ids...) + } else { + // consume only availableSlots + result = append(result, consumerIds.Ids[:availableSlots]...) + // and leave the others for next time + nextTime = consumerIds.Ids[availableSlots:] + break + } + } + + // remove consumers to prevent handling them twice + for i, ts := range timestampsToDelete { + deleteAllIds(ctx, ts) + if i == len(timestampsToDelete)-1 { + // for the last ts consumed, store back the ids for later + for _, consumerId := range nextTime { + err := appendId(ctx, consumerId, ts) + if err != nil { + return result, + fmt.Errorf("failed to append consumer id, consumerId(%s), ts(%s): %w", + consumerId, ts.String(), err) + } + } + } + } + + return result, nil +} + +// LaunchConsumer launches the chain with the provided consumer id by creating the consumer client and the respective +// consumer genesis file +// +// TODO add unit test for LaunchConsumer +func (k Keeper) LaunchConsumer( + ctx sdk.Context, + bondedValidators []stakingtypes.Validator, + activeValidators []stakingtypes.Validator, + consumerId string, +) error { + // compute consumer initial validator set + initialValUpdates, err := k.ComputeConsumerNextValSet(ctx, bondedValidators, activeValidators, consumerId, []types.ConsensusValidator{}) + if err != nil { + return fmt.Errorf("computing consumer next validator set, consumerId(%s): %w", consumerId, err) + } + if len(initialValUpdates) == 0 { + return fmt.Errorf("cannot launch consumer with no validator opted in, consumerId(%s)", consumerId) + } + + // create consumer genesis + genesisState, err := k.MakeConsumerGenesis(ctx, consumerId, initialValUpdates) + if err != nil { + return fmt.Errorf("creating consumer genesis state, consumerId(%s): %w", consumerId, err) + } + err = k.SetConsumerGenesis(ctx, consumerId, genesisState) + if err != nil { + return fmt.Errorf("setting consumer genesis state, consumerId(%s): %w", consumerId, err) + } + + // compute the hash of the consumer initial validator updates + updatesAsValSet, err := tmtypes.PB2TM.ValidatorUpdates(initialValUpdates) + if err != nil { + return fmt.Errorf("unable to create initial validator set from initial validator updates: %w", err) + } + valsetHash := tmtypes.NewValidatorSet(updatesAsValSet).Hash() + + // create the consumer client and the genesis + err = k.CreateConsumerClient(ctx, consumerId, valsetHash) + if err != nil { + return fmt.Errorf("crating consumer client, consumerId(%s): %w", consumerId, err) + } + + k.SetConsumerPhase(ctx, consumerId, types.CONSUMER_PHASE_LAUNCHED) + + k.Logger(ctx).Info("consumer successfully launched", + "consumerId", consumerId, + "valset size", len(initialValUpdates), + "valsetHash", string(valsetHash), + ) + + return nil +} + +// CreateConsumerClient will create the CCV client for the given consumer chain. The CCV channel must be built +// on top of the CCV client to ensure connection with the right consumer chain. +func (k Keeper) CreateConsumerClient( + ctx sdk.Context, + consumerId string, + valsetHash []byte, +) error { + initializationRecord, err := k.GetConsumerInitializationParameters(ctx, consumerId) + if err != nil { + return err + } + + phase := k.GetConsumerPhase(ctx, consumerId) + if phase != types.CONSUMER_PHASE_INITIALIZED { + return errorsmod.Wrapf(types.ErrInvalidPhase, + "cannot create client for consumer chain that is not in the Initialized phase but in phase %d: %s", phase, consumerId) + } + + chainId, err := k.GetConsumerChainId(ctx, consumerId) + if err != nil { + return err + } + + // Set minimum height for equivocation evidence from this consumer chain + k.SetEquivocationEvidenceMinHeight(ctx, consumerId, initializationRecord.InitialHeight.RevisionHeight) + + // Consumers start out with the unbonding period from the initialization parameters + consumerUnbondingPeriod := initializationRecord.UnbondingPeriod + + // Create client state by getting template client from initialization parameters + clientState := k.GetTemplateClient(ctx) + clientState.ChainId = chainId + clientState.LatestHeight = initializationRecord.InitialHeight + + trustPeriod, err := ccv.CalculateTrustPeriod(consumerUnbondingPeriod, k.GetTrustingPeriodFraction(ctx)) + if err != nil { + return err + } + clientState.TrustingPeriod = trustPeriod + clientState.UnbondingPeriod = consumerUnbondingPeriod + + // Create consensus state + consensusState := ibctmtypes.NewConsensusState( + ctx.BlockTime(), + commitmenttypes.NewMerkleRoot([]byte(ibctmtypes.SentinelRoot)), + valsetHash, + ) + + clientID, err := k.clientKeeper.CreateClient(ctx, clientState, consensusState) + if err != nil { + return err + } + k.SetConsumerClientId(ctx, consumerId, clientID) + + k.Logger(ctx).Info("consumer client created", + "consumer id", consumerId, + "client id", clientID, + ) + + ctx.EventManager().EmitEvent( + sdk.NewEvent( + types.EventTypeConsumerClientCreated, + sdk.NewAttribute(sdk.AttributeKeyModule, types.ModuleName), + sdk.NewAttribute(types.AttributeConsumerId, consumerId), + sdk.NewAttribute(types.AttributeConsumerChainId, chainId), + sdk.NewAttribute(clienttypes.AttributeKeyClientID, clientID), + sdk.NewAttribute(types.AttributeInitialHeight, initializationRecord.InitialHeight.String()), + sdk.NewAttribute(types.AttributeTrustingPeriod, clientState.TrustingPeriod.String()), + sdk.NewAttribute(types.AttributeUnbondingPeriod, clientState.UnbondingPeriod.String()), + sdk.NewAttribute(types.AttributeValsetHash, string(valsetHash)), + ), + ) + + return nil +} + +// MakeConsumerGenesis returns the created consumer genesis state for consumer chain `consumerId`, +// as well as the validator hash of the initial validator set of the consumer chain +func (k Keeper) MakeConsumerGenesis( + ctx sdk.Context, + consumerId string, + initialValidatorUpdates []abci.ValidatorUpdate, +) (gen ccv.ConsumerGenesisState, err error) { + initializationRecord, err := k.GetConsumerInitializationParameters(ctx, consumerId) + if err != nil { + return gen, errorsmod.Wrapf(ccv.ErrInvalidConsumerState, + "getting initialization parameters, consumerId(%s): %s", consumerId, err.Error()) + } + // note that providerFeePoolAddrStr is sent to the consumer during the IBC Channel handshake; + // see HandshakeMetadata in OnChanOpenTry on the provider-side, and OnChanOpenAck on the consumer-side + consumerGenesisParams := ccv.NewParams( + true, + initializationRecord.BlocksPerDistributionTransmission, + initializationRecord.DistributionTransmissionChannel, + "", // providerFeePoolAddrStr, + initializationRecord.CcvTimeoutPeriod, + initializationRecord.TransferTimeoutPeriod, + initializationRecord.ConsumerRedistributionFraction, + initializationRecord.HistoricalEntries, + initializationRecord.UnbondingPeriod, + []string{}, + []string{}, + ccv.DefaultRetryDelayPeriod, + consumerId, + ) + + // create provider client state and consensus state for the consumer to be able + // to create a provider client + + providerUnbondingPeriod, err := k.stakingKeeper.UnbondingTime(ctx) + if err != nil { + return gen, errorsmod.Wrapf(types.ErrNoUnbondingTime, "unbonding time not found: %s", err) + } + height := clienttypes.GetSelfHeight(ctx) + + clientState := k.GetTemplateClient(ctx) + // this is the counter party chain ID for the consumer + clientState.ChainId = ctx.ChainID() + // this is the latest height the client was updated at, i.e., + // the height of the latest consensus state (see below) + clientState.LatestHeight = height + trustPeriod, err := ccv.CalculateTrustPeriod(providerUnbondingPeriod, k.GetTrustingPeriodFraction(ctx)) + if err != nil { + return gen, errorsmod.Wrapf(sdkerrors.ErrInvalidHeight, "error %s calculating trusting_period for: %s", err, height) + } + clientState.TrustingPeriod = trustPeriod + clientState.UnbondingPeriod = providerUnbondingPeriod + + consState, err := k.clientKeeper.GetSelfConsensusState(ctx, height) + if err != nil { + return gen, errorsmod.Wrapf(clienttypes.ErrConsensusStateNotFound, "error %s getting self consensus state for: %s", err, height) + } + + gen = *ccv.NewInitialConsumerGenesisState( + clientState, + consState.(*ibctmtypes.ConsensusState), + initialValidatorUpdates, + consumerGenesisParams, + ) + return gen, nil +} + +// StopAndPrepareForConsumerRemoval sets the phase of the chain to stopped and prepares to get the state of the +// chain removed after unbonding period elapses +func (k Keeper) StopAndPrepareForConsumerRemoval(ctx sdk.Context, consumerId string) error { + // The phase of the chain is immediately set to stopped, albeit its state is removed later (see below). + // Setting the phase here helps in not considering this chain when we look at launched chains (e.g., in `QueueVSCPackets) + k.SetConsumerPhase(ctx, consumerId, types.CONSUMER_PHASE_STOPPED) + + // state of this chain is removed once UnbondingPeriod elapses + unbondingPeriod, err := k.stakingKeeper.UnbondingTime(ctx) + if err != nil { + return err + } + removalTime := ctx.BlockTime().Add(unbondingPeriod) + + if err := k.SetConsumerRemovalTime(ctx, consumerId, removalTime); err != nil { + return fmt.Errorf("cannot set removal time (%s): %s", removalTime.String(), err.Error()) + } + if err := k.AppendConsumerToBeRemoved(ctx, consumerId, removalTime); err != nil { + return errorsmod.Wrapf(ccv.ErrInvalidConsumerState, "cannot set consumer to be removed: %s", err.Error()) + } + + return nil +} + +// BeginBlockRemoveConsumers removes stopped consumer chain for which the removal time has passed +func (k Keeper) BeginBlockRemoveConsumers(ctx sdk.Context) error { + consumerIds, err := k.ConsumeIdsFromTimeQueue( + ctx, + types.RemovalTimeToConsumerIdsKeyPrefix(), + k.GetConsumersToBeRemoved, + k.DeleteAllConsumersToBeRemoved, + k.AppendConsumerToBeRemoved, + 200, + ) + if err != nil { + return errorsmod.Wrapf(ccv.ErrInvalidConsumerState, "getting consumers ready to stop: %s", err.Error()) + } + for _, consumerId := range consumerIds { + // delete consumer chain in a cached context to abort deletion in case of errors + cachedCtx, writeFn := ctx.CacheContext() + err = k.DeleteConsumerChain(cachedCtx, consumerId) + if err != nil { + k.Logger(ctx).Error("consumer chain could not be removed", + "consumerId", consumerId, + "error", err.Error()) + continue + } + + writeFn() + } + return nil +} + +// DeleteConsumerChain cleans up the state of the given consumer chain +func (k Keeper) DeleteConsumerChain(ctx sdk.Context, consumerId string) (err error) { + phase := k.GetConsumerPhase(ctx, consumerId) + if phase != types.CONSUMER_PHASE_STOPPED { + return fmt.Errorf("cannot delete non-stopped chain: %s", consumerId) + } + + // clean up states + k.DeleteConsumerClientId(ctx, consumerId) + k.DeleteConsumerGenesis(ctx, consumerId) + // Note: this call panics if the key assignment state is invalid + k.DeleteKeyAssignments(ctx, consumerId) + k.DeleteMinimumPowerInTopN(ctx, consumerId) + k.DeleteEquivocationEvidenceMinHeight(ctx, consumerId) + + // close channel and delete the mappings between chain ID and channel ID + if channelID, found := k.GetConsumerIdToChannelId(ctx, consumerId); found { + // Close the channel for the given channel ID on the condition + // that the channel exists and isn't already in the CLOSED state + channel, found := k.channelKeeper.GetChannel(ctx, ccv.ProviderPortID, channelID) + if found && channel.State != channeltypes.CLOSED { + err := k.chanCloseInit(ctx, channelID) + if err != nil { + k.Logger(ctx).Error("channel to consumer chain could not be closed", + "consumerId", consumerId, + "channelID", channelID, + "error", err.Error(), + ) + } + } + k.DeleteConsumerIdToChannelId(ctx, consumerId) + k.DeleteChannelIdToConsumerId(ctx, channelID) + } + + // delete consumer commission rate + provAddrs := k.GetAllCommissionRateValidators(ctx, consumerId) + for _, addr := range provAddrs { + k.DeleteConsumerCommissionRate(ctx, consumerId, addr) + } + + k.DeleteInitChainHeight(ctx, consumerId) + k.DeleteSlashAcks(ctx, consumerId) + k.DeletePendingVSCPackets(ctx, consumerId) + + k.DeleteAllowlist(ctx, consumerId) + k.DeleteDenylist(ctx, consumerId) + k.DeleteAllOptedIn(ctx, consumerId) + k.DeleteConsumerValSet(ctx, consumerId) + + k.DeleteConsumerRewardsAllocation(ctx, consumerId) + k.DeleteConsumerRemovalTime(ctx, consumerId) + + // TODO (PERMISSIONLESS) add newly-added state to be deleted + + // Note that we do not delete ConsumerIdToChainIdKey and ConsumerIdToPhase, as well + // as consumer metadata, initialization and power-shaping parameters. + // This is to enable block explorers and front ends to show information of + // consumer chains that were removed without needing an archive node. + + k.SetConsumerPhase(ctx, consumerId, types.CONSUMER_PHASE_DELETED) + k.Logger(ctx).Info("consumer chain deleted from provider", "consumerId", consumerId) + + return nil +} + +// +// Setters and Getters +// + +// GetConsumerRemovalTime returns the removal time associated with the to-be-removed chain with consumer id +func (k Keeper) GetConsumerRemovalTime(ctx sdk.Context, consumerId string) (time.Time, error) { + store := ctx.KVStore(k.storeKey) + buf := store.Get(types.ConsumerIdToRemovalTimeKey(consumerId)) + if buf == nil { + return time.Time{}, fmt.Errorf("failed to retrieve removal time for consumer id (%s)", consumerId) + } + var time time.Time + if err := time.UnmarshalBinary(buf); err != nil { + return time, fmt.Errorf("failed to unmarshal removal time for consumer id (%s): %w", consumerId, err) + } + return time, nil +} + +// SetConsumerRemovalTime sets the removal time associated with this consumer id +func (k Keeper) SetConsumerRemovalTime(ctx sdk.Context, consumerId string, removalTime time.Time) error { + store := ctx.KVStore(k.storeKey) + buf, err := removalTime.MarshalBinary() + if err != nil { + return fmt.Errorf("failed to marshal removal time (%+v) for consumer id (%s): %w", removalTime, consumerId, err) + } + store.Set(types.ConsumerIdToRemovalTimeKey(consumerId), buf) + return nil +} + +// DeleteConsumerRemovalTime deletes the removal time associated with this consumer id +func (k Keeper) DeleteConsumerRemovalTime(ctx sdk.Context, consumerId string) { + store := ctx.KVStore(k.storeKey) + store.Delete(types.ConsumerIdToRemovalTimeKey(consumerId)) +} + +// getConsumerIdsBasedOnTime returns all the consumer ids stored under this specific `key(time)` +func (k Keeper) getConsumerIdsBasedOnTime(ctx sdk.Context, key func(time.Time) []byte, time time.Time) (types.ConsumerIds, error) { + store := ctx.KVStore(k.storeKey) + bz := store.Get(key(time)) + if bz == nil { + return types.ConsumerIds{}, nil + } + + var consumerIds types.ConsumerIds + + if err := consumerIds.Unmarshal(bz); err != nil { + return types.ConsumerIds{}, fmt.Errorf("failed to unmarshal consumer ids: %w", err) + } + return consumerIds, nil +} + +// appendConsumerIdOnTime appends the consumer id on all the other consumer ids under `key(time)` +func (k Keeper) appendConsumerIdOnTime(ctx sdk.Context, consumerId string, key func(time.Time) []byte, time time.Time) error { + store := ctx.KVStore(k.storeKey) + + consumers, err := k.getConsumerIdsBasedOnTime(ctx, key, time) + if err != nil { + return err + } + + consumersWithAppend := types.ConsumerIds{ + Ids: append(consumers.Ids, consumerId), + } + + bz, err := consumersWithAppend.Marshal() + if err != nil { + return err + } + + store.Set(key(time), bz) + return nil +} + +// removeConsumerIdFromTime removes consumer id stored under `key(time)` +func (k Keeper) removeConsumerIdFromTime(ctx sdk.Context, consumerId string, key func(time.Time) []byte, time time.Time) error { + store := ctx.KVStore(k.storeKey) + + consumers, err := k.getConsumerIdsBasedOnTime(ctx, key, time) + if err != nil { + return err + } + + if len(consumers.Ids) == 0 { + return fmt.Errorf("no consumer ids found for this time: %s", time.String()) + } + + // find the index of the consumer we want to remove + index := -1 + for i := 0; i < len(consumers.Ids); i++ { + if consumers.Ids[i] == consumerId { + index = i + break + } + } + + if index == -1 { + return fmt.Errorf("failed to find consumer id (%s)", consumerId) + } + + if len(consumers.Ids) == 1 { + store.Delete(key(time)) + return nil + } + + consumersWithRemoval := types.ConsumerIds{ + Ids: append(consumers.Ids[:index], consumers.Ids[index+1:]...), + } + + bz, err := consumersWithRemoval.Marshal() + if err != nil { + return err + } + + store.Set(key(time), bz) + return nil +} + +// GetConsumersToBeLaunched returns all the consumer ids of chains stored under this spawn time +func (k Keeper) GetConsumersToBeLaunched(ctx sdk.Context, spawnTime time.Time) (types.ConsumerIds, error) { + return k.getConsumerIdsBasedOnTime(ctx, types.SpawnTimeToConsumerIdsKey, spawnTime) +} + +// AppendConsumerToBeLaunched appends the provider consumer id for the given spawn time +func (k Keeper) AppendConsumerToBeLaunched(ctx sdk.Context, consumerId string, spawnTime time.Time) error { + return k.appendConsumerIdOnTime(ctx, consumerId, types.SpawnTimeToConsumerIdsKey, spawnTime) +} + +// RemoveConsumerToBeLaunched removes consumer id from if stored for this specific spawn time +func (k Keeper) RemoveConsumerToBeLaunched(ctx sdk.Context, consumerId string, spawnTime time.Time) error { + return k.removeConsumerIdFromTime(ctx, consumerId, types.SpawnTimeToConsumerIdsKey, spawnTime) +} + +// DeleteAllConsumersToBeLaunched deletes all consumer to be launched at this specific spawn time +func (k Keeper) DeleteAllConsumersToBeLaunched(ctx sdk.Context, spawnTime time.Time) { + store := ctx.KVStore(k.storeKey) + store.Delete(types.SpawnTimeToConsumerIdsKey(spawnTime)) +} + +// GetConsumersToBeRemoved returns all the consumer ids of chains stored under this removal time +func (k Keeper) GetConsumersToBeRemoved(ctx sdk.Context, removalTime time.Time) (types.ConsumerIds, error) { + return k.getConsumerIdsBasedOnTime(ctx, types.RemovalTimeToConsumerIdsKey, removalTime) +} + +// AppendConsumerToBeRemoved appends the provider consumer id for the given removal time +func (k Keeper) AppendConsumerToBeRemoved(ctx sdk.Context, consumerId string, removalTime time.Time) error { + return k.appendConsumerIdOnTime(ctx, consumerId, types.RemovalTimeToConsumerIdsKey, removalTime) +} + +// RemoveConsumerToBeRemoved removes consumer id from the given removal time +func (k Keeper) RemoveConsumerToBeRemoved(ctx sdk.Context, consumerId string, removalTime time.Time) error { + return k.removeConsumerIdFromTime(ctx, consumerId, types.RemovalTimeToConsumerIdsKey, removalTime) +} + +// DeleteAllConsumersToBeRemoved deletes all consumer to be removed at this specific removal time +func (k Keeper) DeleteAllConsumersToBeRemoved(ctx sdk.Context, removalTime time.Time) { + store := ctx.KVStore(k.storeKey) + store.Delete(types.RemovalTimeToConsumerIdsKey(removalTime)) +} diff --git a/x/ccv/provider/keeper/consumer_lifecycle_test.go b/x/ccv/provider/keeper/consumer_lifecycle_test.go new file mode 100644 index 0000000000..6b258ae003 --- /dev/null +++ b/x/ccv/provider/keeper/consumer_lifecycle_test.go @@ -0,0 +1,1046 @@ +package keeper_test + +import ( + "encoding/json" + "fmt" + "testing" + "time" + + clienttypes "github.com/cosmos/ibc-go/v8/modules/core/02-client/types" + ibctmtypes "github.com/cosmos/ibc-go/v8/modules/light-clients/07-tendermint" + ibctesting "github.com/cosmos/ibc-go/v8/testing" + _go "github.com/cosmos/ics23/go" + "github.com/golang/mock/gomock" + "github.com/stretchr/testify/require" + + "cosmossdk.io/math" + + cryptocodec "github.com/cosmos/cosmos-sdk/crypto/codec" + sdk "github.com/cosmos/cosmos-sdk/types" + stakingtypes "github.com/cosmos/cosmos-sdk/x/staking/types" + + abci "github.com/cometbft/cometbft/abci/types" + tmprotocrypto "github.com/cometbft/cometbft/proto/tendermint/crypto" + + cryptotestutil "github.com/cosmos/interchain-security/v6/testutil/crypto" + testkeeper "github.com/cosmos/interchain-security/v6/testutil/keeper" + providerkeeper "github.com/cosmos/interchain-security/v6/x/ccv/provider/keeper" + providertypes "github.com/cosmos/interchain-security/v6/x/ccv/provider/types" + ccvtypes "github.com/cosmos/interchain-security/v6/x/ccv/types" +) + +func TestPrepareConsumerForLaunch(t *testing.T) { + providerKeeper, ctx, ctrl, _ := testkeeper.GetProviderKeeperAndCtx(t, testkeeper.NewInMemKeeperParams(t)) + defer ctrl.Finish() + + spawnTime := time.Now().UTC() + err := providerKeeper.PrepareConsumerForLaunch(ctx, CONSUMER_ID, time.Time{}, spawnTime) + require.NoError(t, err) + + consumers, err := providerKeeper.GetConsumersToBeLaunched(ctx, spawnTime) + require.NoError(t, err) + require.Equal(t, providertypes.ConsumerIds{Ids: []string{CONSUMER_ID}}, consumers) + + nextSpawnTime := spawnTime.Add(time.Hour) + err = providerKeeper.PrepareConsumerForLaunch(ctx, CONSUMER_ID, spawnTime, nextSpawnTime) + require.NoError(t, err) + + consumers, err = providerKeeper.GetConsumersToBeLaunched(ctx, spawnTime) + require.NoError(t, err) + require.Empty(t, consumers) + + consumers, err = providerKeeper.GetConsumersToBeLaunched(ctx, nextSpawnTime) + require.NoError(t, err) + require.Equal(t, providertypes.ConsumerIds{Ids: []string{CONSUMER_ID}}, consumers) +} + +func TestInitializeConsumer(t *testing.T) { + now := time.Now().UTC() + consumerId := "13" + + testCases := []struct { + name string + spawnTime time.Time + setup func(*providerkeeper.Keeper, sdk.Context, time.Time) + expInitialized bool + }{ + { + name: "valid", + spawnTime: now, + setup: func(pk *providerkeeper.Keeper, ctx sdk.Context, spawnTime time.Time) { + pk.SetConsumerPhase(ctx, consumerId, providertypes.CONSUMER_PHASE_REGISTERED) + err := pk.SetConsumerInitializationParameters(ctx, consumerId, + providertypes.ConsumerInitializationParameters{ + SpawnTime: spawnTime, + }) + require.NoError(t, err) + }, + expInitialized: true, + }, + { + name: "invalid: no phase", + spawnTime: now, + setup: func(pk *providerkeeper.Keeper, ctx sdk.Context, spawnTime time.Time) { + }, + expInitialized: false, + }, + { + name: "invalid: wrong phase", + spawnTime: now, + setup: func(pk *providerkeeper.Keeper, ctx sdk.Context, spawnTime time.Time) { + pk.SetConsumerPhase(ctx, consumerId, providertypes.CONSUMER_PHASE_LAUNCHED) + err := pk.SetConsumerInitializationParameters(ctx, consumerId, + providertypes.ConsumerInitializationParameters{ + SpawnTime: spawnTime, + }) + require.NoError(t, err) + }, + expInitialized: false, + }, + { + name: "invalid: no init params", + spawnTime: now, + setup: func(pk *providerkeeper.Keeper, ctx sdk.Context, spawnTime time.Time) { + pk.SetConsumerPhase(ctx, consumerId, providertypes.CONSUMER_PHASE_REGISTERED) + }, + expInitialized: false, + }, + { + name: "invalid: zero spawn time", + spawnTime: now, + setup: func(pk *providerkeeper.Keeper, ctx sdk.Context, spawnTime time.Time) { + pk.SetConsumerPhase(ctx, consumerId, providertypes.CONSUMER_PHASE_REGISTERED) + err := pk.SetConsumerInitializationParameters(ctx, consumerId, + providertypes.ConsumerInitializationParameters{ + SpawnTime: time.Time{}, + }) + require.NoError(t, err) + }, + expInitialized: false, + }, + } + + for _, tc := range testCases { + pk, ctx, ctrl, _ := testkeeper.GetProviderKeeperAndCtx(t, testkeeper.NewInMemKeeperParams(t)) + defer ctrl.Finish() + + tc.setup(&pk, ctx, tc.spawnTime) + + spawnTime, initialized := pk.InitializeConsumer(ctx, consumerId) + require.Equal(t, tc.expInitialized, initialized, tc.name) + if initialized { + require.Equal(t, tc.spawnTime, spawnTime, tc.name) + require.Equal(t, providertypes.CONSUMER_PHASE_INITIALIZED, pk.GetConsumerPhase(ctx, consumerId)) + } + } +} + +// TestBeginBlockInit directly tests BeginBlockLaunchConsumers against the spec using helpers defined above. +func TestBeginBlockLaunchConsumers(t *testing.T) { + now := time.Now().UTC() + + keeperParams := testkeeper.NewInMemKeeperParams(t) + providerKeeper, ctx, ctrl, mocks := testkeeper.GetProviderKeeperAndCtx(t, keeperParams) + providerKeeper.SetParams(ctx, providertypes.DefaultParams()) + defer ctrl.Finish() + ctx = ctx.WithBlockTime(now) + + // initialize registration, initialization, and update records + chainIds := []string{"chain0", "chain1", "chain2", "chain3", "chain4"} + + initializationParameters := []providertypes.ConsumerInitializationParameters{ + { + InitialHeight: clienttypes.NewHeight(3, 4), + GenesisHash: []byte{}, + BinaryHash: []byte{}, + SpawnTime: now.Add(-time.Hour * 2).UTC(), + UnbondingPeriod: time.Duration(100000000000), + CcvTimeoutPeriod: time.Duration(100000000000), + TransferTimeoutPeriod: time.Duration(100000000000), + ConsumerRedistributionFraction: "0.75", + BlocksPerDistributionTransmission: 10, + HistoricalEntries: 10000, + DistributionTransmissionChannel: "", + }, + { + InitialHeight: clienttypes.NewHeight(3, 4), + GenesisHash: []byte{}, + BinaryHash: []byte{}, + SpawnTime: now.Add(-time.Hour).UTC(), + UnbondingPeriod: time.Duration(100000000000), + CcvTimeoutPeriod: time.Duration(100000000000), + TransferTimeoutPeriod: time.Duration(100000000000), + ConsumerRedistributionFraction: "0.75", + BlocksPerDistributionTransmission: 10, + HistoricalEntries: 10000, + DistributionTransmissionChannel: "", + }, + { + InitialHeight: clienttypes.NewHeight(3, 4), + GenesisHash: []byte{}, + BinaryHash: []byte{}, + SpawnTime: now.Add(time.Hour).UTC(), + UnbondingPeriod: time.Duration(100000000000), + CcvTimeoutPeriod: time.Duration(100000000000), + TransferTimeoutPeriod: time.Duration(100000000000), + ConsumerRedistributionFraction: "0.75", + BlocksPerDistributionTransmission: 10, + HistoricalEntries: 10000, + DistributionTransmissionChannel: "", + }, + { + InitialHeight: clienttypes.NewHeight(3, 4), + GenesisHash: []byte{}, + BinaryHash: []byte{}, + SpawnTime: now.Add(-time.Hour).UTC(), + UnbondingPeriod: time.Duration(100000000000), + CcvTimeoutPeriod: time.Duration(100000000000), + TransferTimeoutPeriod: time.Duration(100000000000), + ConsumerRedistributionFraction: "0.75", + BlocksPerDistributionTransmission: 10, + HistoricalEntries: 10000, + DistributionTransmissionChannel: "", + }, + { + InitialHeight: clienttypes.NewHeight(3, 4), + GenesisHash: []byte{}, + BinaryHash: []byte{}, + SpawnTime: now.Add(-time.Minute).UTC(), + UnbondingPeriod: time.Duration(100000000000), + CcvTimeoutPeriod: time.Duration(100000000000), + TransferTimeoutPeriod: time.Duration(100000000000), + ConsumerRedistributionFraction: "0.75", + BlocksPerDistributionTransmission: 10, + HistoricalEntries: 10000, + DistributionTransmissionChannel: "", + }, + } + powerShapingParameters := []providertypes.PowerShapingParameters{ + { + Top_N: 50, + ValidatorsPowerCap: 0, + ValidatorSetCap: 0, + Allowlist: []string{}, + Denylist: []string{}, + }, + { + Top_N: 50, + ValidatorsPowerCap: 0, + ValidatorSetCap: 0, + Allowlist: []string{}, + Denylist: []string{}, + }, + { + Top_N: 50, + ValidatorsPowerCap: 0, + ValidatorSetCap: 0, + Allowlist: []string{}, + Denylist: []string{}, + }, + { + Top_N: 0, + ValidatorsPowerCap: 0, + ValidatorSetCap: 0, + Allowlist: []string{}, + Denylist: []string{}, + }, + { + Top_N: 0, + ValidatorsPowerCap: 0, + ValidatorSetCap: 0, + Allowlist: []string{}, + Denylist: []string{}, + }, + } + + // set up all the records + for i, chainId := range chainIds { + providerKeeper.SetConsumerChainId(ctx, fmt.Sprintf("%d", i), chainId) + } + + for i, r := range initializationParameters { + err := providerKeeper.SetConsumerInitializationParameters(ctx, fmt.Sprintf("%d", i), r) + require.NoError(t, err) + // set up the chains in their initialized phase, hence they could launch + providerKeeper.SetConsumerPhase(ctx, fmt.Sprintf("%d", i), providertypes.CONSUMER_PHASE_INITIALIZED) + err = providerKeeper.AppendConsumerToBeLaunched(ctx, fmt.Sprintf("%d", i), r.SpawnTime) + require.NoError(t, err) + } + for i, r := range powerShapingParameters { + err := providerKeeper.SetConsumerPowerShapingParameters(ctx, fmt.Sprintf("%d", i), r) + require.NoError(t, err) + } + + // opt in a sample validator so the chain's proposal can successfully execute + validator := cryptotestutil.NewCryptoIdentityFromIntSeed(0).SDKStakingValidator() + consAddr, _ := validator.GetConsAddr() + testkeeper.SetupMocksForLastBondedValidatorsExpectation(mocks.MockStakingKeeper, 1, []stakingtypes.Validator{validator}, -1) // -1 to allow any number of calls + + valAddr, _ := sdk.ValAddressFromBech32(validator.GetOperator()) + mocks.MockStakingKeeper.EXPECT().GetLastValidatorPower(gomock.Any(), valAddr).Return(int64(1), nil).AnyTimes() + + providerKeeper.SetOptedIn(ctx, "3", providertypes.NewProviderConsAddress(consAddr)) + + // Expect genesis and client creation for only the first, second, and fifth chains (spawn time already passed and valid) + expectedCalls := testkeeper.GetMocksForMakeConsumerGenesis(ctx, &mocks, time.Hour) + expectedCalls = append(expectedCalls, testkeeper.GetMocksForCreateConsumerClient(ctx, &mocks, "chain0", clienttypes.NewHeight(3, 4))...) + expectedCalls = append(expectedCalls, testkeeper.GetMocksForMakeConsumerGenesis(ctx, &mocks, time.Hour)...) + expectedCalls = append(expectedCalls, testkeeper.GetMocksForCreateConsumerClient(ctx, &mocks, "chain1", clienttypes.NewHeight(3, 4))...) + expectedCalls = append(expectedCalls, testkeeper.GetMocksForMakeConsumerGenesis(ctx, &mocks, time.Hour)...) + expectedCalls = append(expectedCalls, testkeeper.GetMocksForCreateConsumerClient(ctx, &mocks, "chain3", clienttypes.NewHeight(3, 4))...) + + gomock.InOrder(expectedCalls...) + + err := providerKeeper.BeginBlockLaunchConsumers(ctx) + require.NoError(t, err) + + // first chain was successfully launched + phase := providerKeeper.GetConsumerPhase(ctx, "0") + require.Equal(t, providertypes.CONSUMER_PHASE_LAUNCHED, phase) + _, found := providerKeeper.GetConsumerGenesis(ctx, "0") + require.True(t, found) + + // second chain was successfully launched + phase = providerKeeper.GetConsumerPhase(ctx, "1") + require.Equal(t, providertypes.CONSUMER_PHASE_LAUNCHED, phase) + _, found = providerKeeper.GetConsumerGenesis(ctx, "1") + require.True(t, found) + + // third chain was not launched because its spawn time has not passed + phase = providerKeeper.GetConsumerPhase(ctx, "2") + require.Equal(t, providertypes.CONSUMER_PHASE_INITIALIZED, phase) + _, found = providerKeeper.GetConsumerGenesis(ctx, "2") + require.False(t, found) + + // fourth chain corresponds to an Opt-In chain with one opted-in validator and hence the chain gets + // successfully executed + phase = providerKeeper.GetConsumerPhase(ctx, "3") + require.Equal(t, providertypes.CONSUMER_PHASE_LAUNCHED, phase) + _, found = providerKeeper.GetConsumerGenesis(ctx, "3") + require.True(t, found) + + // fifth chain corresponds to an Opt-In chain with no opted-in validators and hence the + // chain launch is NOT successful + phase = providerKeeper.GetConsumerPhase(ctx, "4") + require.Equal(t, providertypes.CONSUMER_PHASE_REGISTERED, phase) + _, found = providerKeeper.GetConsumerGenesis(ctx, "4") + require.False(t, found) +} + +func TestConsumeIdsFromTimeQueue(t *testing.T) { + expectedConsumerIds := []string{"1", "2", "3", "4"} + timestamps := []time.Time{time.Unix(10, 0), time.Unix(20, 0), time.Unix(30, 0)} + + testCases := []struct { + name string + ts time.Time + limit int + expOutcome func(sdk.Context, []string, func(sdk.Context, time.Time) (providertypes.ConsumerIds, error)) + }{ + { + name: "timestamp too early", + ts: time.Unix(9, 999999999), + limit: 3, + expOutcome: func(ctx sdk.Context, ids []string, getIds func(sdk.Context, time.Time) (providertypes.ConsumerIds, error)) { + require.Empty(t, ids) + }, + }, + { + name: "first timestamp", + ts: timestamps[0], + limit: 2, + expOutcome: func(ctx sdk.Context, ids []string, getIds func(sdk.Context, time.Time) (providertypes.ConsumerIds, error)) { + require.Equal(t, expectedConsumerIds[0:2], ids) + + // check that all consumers where removed + consumerIds, err := getIds(ctx, timestamps[0]) + require.NoError(t, err) + require.Empty(t, consumerIds) + }, + }, + { + name: "first timestamp, with limit", + ts: timestamps[0], + limit: 1, + expOutcome: func(ctx sdk.Context, ids []string, getIds func(sdk.Context, time.Time) (providertypes.ConsumerIds, error)) { + require.Equal(t, expectedConsumerIds[0:1], ids) + + // second consumer remained + ret, err := getIds(ctx, timestamps[0]) + require.NoError(t, err) + require.Equal(t, providertypes.ConsumerIds{ + Ids: []string{expectedConsumerIds[1]}, + }, ret) + }, + }, + { + name: "second timestamp", + ts: timestamps[1], + limit: 3, + expOutcome: func(ctx sdk.Context, ids []string, getIds func(sdk.Context, time.Time) (providertypes.ConsumerIds, error)) { + require.Equal(t, expectedConsumerIds[0:3], ids) + + // check that all consumers where removed + ret, err := getIds(ctx, timestamps[0]) + require.NoError(t, err) + require.Empty(t, ret) + ret, err = getIds(ctx, timestamps[1]) + require.NoError(t, err) + require.Empty(t, ret) + }, + }, + { + name: "third timestamp, with limit", + ts: timestamps[1], + limit: 3, + expOutcome: func(ctx sdk.Context, ids []string, getIds func(sdk.Context, time.Time) (providertypes.ConsumerIds, error)) { + require.Equal(t, expectedConsumerIds[0:3], ids) + + // 4th consumer remained + ret, err := getIds(ctx, timestamps[0]) + require.NoError(t, err) + require.Empty(t, ret) + ret, err = getIds(ctx, timestamps[1]) + require.NoError(t, err) + require.Empty(t, ret) + ret, err = getIds(ctx, timestamps[2]) + require.NoError(t, err) + require.Equal(t, providertypes.ConsumerIds{ + Ids: []string{expectedConsumerIds[3]}, + }, ret) + }, + }, + } + + // test for consumers to be launched + for _, tc := range testCases { + providerKeeper, ctx, ctrl, _ := testkeeper.GetProviderKeeperAndCtx(t, testkeeper.NewInMemKeeperParams(t)) + defer ctrl.Finish() + + callCases := []struct { + timeQueueKeyPrefix byte + getIds func(sdk.Context, time.Time) (providertypes.ConsumerIds, error) + deleteAllIds func(sdk.Context, time.Time) + appendId func(sdk.Context, string, time.Time) error + }{ + { + timeQueueKeyPrefix: providertypes.SpawnTimeToConsumerIdsKeyPrefix(), + getIds: providerKeeper.GetConsumersToBeLaunched, + deleteAllIds: providerKeeper.DeleteAllConsumersToBeLaunched, + appendId: providerKeeper.AppendConsumerToBeLaunched, + }, + { + timeQueueKeyPrefix: providertypes.RemovalTimeToConsumerIdsKeyPrefix(), + getIds: providerKeeper.GetConsumersToBeRemoved, + deleteAllIds: providerKeeper.DeleteAllConsumersToBeRemoved, + appendId: providerKeeper.AppendConsumerToBeRemoved, + }, + } + for _, cc := range callCases { + err := cc.appendId(ctx, expectedConsumerIds[0], timestamps[0]) + require.NoError(t, err) + err = cc.appendId(ctx, expectedConsumerIds[1], timestamps[0]) + require.NoError(t, err) + err = cc.appendId(ctx, expectedConsumerIds[2], timestamps[1]) + require.NoError(t, err) + err = cc.appendId(ctx, expectedConsumerIds[3], timestamps[2]) + require.NoError(t, err) + + ctx = ctx.WithBlockTime(tc.ts) + + consumerIds, err := providerKeeper.ConsumeIdsFromTimeQueue( + ctx, + cc.timeQueueKeyPrefix, + cc.getIds, + cc.deleteAllIds, + cc.appendId, + tc.limit, + ) + require.NoError(t, err) + + tc.expOutcome(ctx, consumerIds, cc.getIds) + } + } +} + +func TestCreateConsumerClient(t *testing.T) { + type testCase struct { + description string + // Any state-mutating setup on keeper and expected mock calls, specific to this test case + setup func(*providerkeeper.Keeper, sdk.Context, *testkeeper.MockedKeepers) + // Whether a client should be created + expClientCreated bool + } + tests := []testCase{ + { + description: "No state mutation, new client should be created", + setup: func(providerKeeper *providerkeeper.Keeper, ctx sdk.Context, mocks *testkeeper.MockedKeepers) { + providerKeeper.SetConsumerPhase(ctx, CONSUMER_ID, providertypes.CONSUMER_PHASE_INITIALIZED) + + // Valid client creation is asserted with mock expectations here + gomock.InOrder( + testkeeper.GetMocksForCreateConsumerClient(ctx, mocks, CONSUMER_CHAIN_ID, clienttypes.NewHeight(4, 5))..., + ) + }, + expClientCreated: true, + }, + { + description: "chain for this consumer id has already launched, and hence client was created, NO new one is created", + setup: func(providerKeeper *providerkeeper.Keeper, ctx sdk.Context, mocks *testkeeper.MockedKeepers) { + providerKeeper.SetConsumerPhase(ctx, CONSUMER_ID, providertypes.CONSUMER_PHASE_LAUNCHED) + + // Expect none of the client creation related calls to happen + mocks.MockClientKeeper.EXPECT().CreateClient(gomock.Any(), gomock.Any(), gomock.Any()).Times(0) + }, + expClientCreated: false, + }, + } + + for _, tc := range tests { + // Common setup + keeperParams := testkeeper.NewInMemKeeperParams(t) + providerKeeper, ctx, ctrl, mocks := testkeeper.GetProviderKeeperAndCtx(t, keeperParams) + providerKeeper.SetParams(ctx, providertypes.DefaultParams()) + + // Test specific setup + tc.setup(&providerKeeper, ctx, &mocks) + + // Call method with same arbitrary values as defined above in mock expectations. + providerKeeper.SetConsumerChainId(ctx, CONSUMER_ID, CONSUMER_CHAIN_ID) + err := providerKeeper.SetConsumerInitializationParameters(ctx, CONSUMER_ID, testkeeper.GetTestInitializationParameters()) + require.NoError(t, err) + + err = providerKeeper.CreateConsumerClient(ctx, CONSUMER_ID, []byte{}) + if tc.expClientCreated { + require.NoError(t, err) + clientId, found := providerKeeper.GetConsumerClientId(ctx, CONSUMER_ID) + require.True(t, found) + require.Equal(t, "clientID", clientId) + } else { + require.Error(t, err) + } + + // Assert mock calls from setup functions + ctrl.Finish() + } +} + +// TestMakeConsumerGenesis tests the MakeConsumerGenesis keeper method. +// An expected genesis state is hardcoded in json, unmarshaled, and compared +// against an actual consumer genesis state constructed by a provider keeper. +func TestMakeConsumerGenesis(t *testing.T) { + keeperParams := testkeeper.NewInMemKeeperParams(t) + providerKeeper, ctx, ctrl, mocks := testkeeper.GetProviderKeeperAndCtx(t, keeperParams) + defer ctrl.Finish() + + moduleParams := providertypes.Params{ + TemplateClient: &ibctmtypes.ClientState{ + TrustLevel: ibctmtypes.DefaultTrustLevel, + MaxClockDrift: 10000000000, + ProofSpecs: []*_go.ProofSpec{ + { + LeafSpec: &_go.LeafOp{ + Hash: _go.HashOp_SHA256, + PrehashKey: _go.HashOp_NO_HASH, + PrehashValue: _go.HashOp_SHA256, + Length: _go.LengthOp_VAR_PROTO, + Prefix: []byte{0x00}, + }, + InnerSpec: &_go.InnerSpec{ + ChildOrder: []int32{0, 1}, + ChildSize: 33, + MinPrefixLength: 4, + MaxPrefixLength: 12, + Hash: _go.HashOp_SHA256, + }, + MaxDepth: 0, + MinDepth: 0, + }, + { + LeafSpec: &_go.LeafOp{ + Hash: _go.HashOp_SHA256, + PrehashKey: _go.HashOp_NO_HASH, + PrehashValue: _go.HashOp_SHA256, + Length: _go.LengthOp_VAR_PROTO, + Prefix: []byte{0x00}, + }, + InnerSpec: &_go.InnerSpec{ + ChildOrder: []int32{0, 1}, + ChildSize: 32, + MinPrefixLength: 1, + MaxPrefixLength: 1, + Hash: _go.HashOp_SHA256, + }, + MaxDepth: 0, + }, + }, + UpgradePath: []string{"upgrade", "upgradedIBCState"}, + AllowUpdateAfterExpiry: true, + AllowUpdateAfterMisbehaviour: true, + }, + // Note these are unused provider parameters for this test, and not actually asserted against + // They must be populated with reasonable values to satisfy SetParams though. + TrustingPeriodFraction: providertypes.DefaultTrustingPeriodFraction, + CcvTimeoutPeriod: ccvtypes.DefaultCCVTimeoutPeriod, + SlashMeterReplenishPeriod: providertypes.DefaultSlashMeterReplenishPeriod, + SlashMeterReplenishFraction: providertypes.DefaultSlashMeterReplenishFraction, + ConsumerRewardDenomRegistrationFee: sdk.Coin{ + Denom: "stake", + Amount: math.NewInt(1000000), + }, + BlocksPerEpoch: 600, + NumberOfEpochsToStartReceivingRewards: 24, + } + providerKeeper.SetParams(ctx, moduleParams) + + // matches params from jsonString + ccvTimeoutPeriod := time.Duration(2419200000000000) + transferTimeoutPeriod := time.Duration(3600000000000) + consumerUnbondingPeriod := time.Duration(1728000000000000) + providerUnbondingPeriod := time.Duration(1814400000000000) + trustingPeriod := time.Duration(1197504000000000) + providerChainId := "provider-1" + providerRevisionNumber := uint64(1) + providerRevisionHeight := int64(5) + + initializationParameters := providertypes.ConsumerInitializationParameters{ + BlocksPerDistributionTransmission: 1000, + CcvTimeoutPeriod: ccvTimeoutPeriod, + TransferTimeoutPeriod: transferTimeoutPeriod, + ConsumerRedistributionFraction: "0.75", + HistoricalEntries: 10000, + UnbondingPeriod: consumerUnbondingPeriod, + } + + // + // Other setup not covered by custom template client state + // + ctx = ctx.WithChainID(providerChainId) // consumerId is obtained from ctx + ctx = ctx.WithBlockHeight(providerRevisionHeight) // RevisionHeight obtained from ctx + gomock.InOrder(testkeeper.GetMocksForMakeConsumerGenesis(ctx, &mocks, providerUnbondingPeriod)...) + + providerKeeper.SetConsumerChainId(ctx, CONSUMER_ID, CONSUMER_CHAIN_ID) + err := providerKeeper.SetConsumerInitializationParameters(ctx, CONSUMER_ID, initializationParameters) + require.NoError(t, err) + + _, pks, _ := ibctesting.GenerateKeys(t, 2) + var ppks [2]tmprotocrypto.PublicKey + for i, pk := range pks { + ppks[i], _ = cryptocodec.ToCmtProtoPublicKey(pk) + } + initialValUpdates := []abci.ValidatorUpdate{ + {PubKey: ppks[0], Power: 1}, + {PubKey: ppks[1], Power: 2}, + } + + actualGenesis, err := providerKeeper.MakeConsumerGenesis(ctx, CONSUMER_ID, initialValUpdates) + require.NoError(t, err) + + // JSON string with tabs, newlines and spaces for readability + jsonString := fmt.Sprintf(`{ + "params": { + "enabled": true, + "blocks_per_distribution_transmission": %d, + "ccv_timeout_period": %d, + "transfer_timeout_period": %d, + "consumer_redistribution_fraction": "%s", + "historical_entries": %d, + "unbonding_period": %d, + "soft_opt_out_threshold": "0", + "reward_denoms": [], + "provider_reward_denoms": [], + "retry_delay_period": %d, + "consumer_id": "%s" + }, + "new_chain": true, + "provider" : { + "client_state": { + "chain_id": "%s", + "trust_level": { + "numerator": 1, + "denominator": 3 + }, + "trusting_period": %d, + "unbonding_period": %d, + "max_clock_drift": %d, + "frozen_height": {}, + "latest_height": { + "revision_number": %d, + "revision_height": %d + }, + "proof_specs": [ + { + "leaf_spec": { + "hash": 1, + "prehash_value": 1, + "length": 1, + "prefix": "AA==" + }, + "inner_spec": { + "child_order": [0, 1], + "child_size": 33, + "min_prefix_length": 4, + "max_prefix_length": 12, + "hash": 1 + } + }, + { + "leaf_spec": { + "hash": 1, + "prehash_value": 1, + "length": 1, + "prefix": "AA==" + }, + "inner_spec": { + "child_order": [0, 1], + "child_size": 32, + "min_prefix_length": 1, + "max_prefix_length": 1, + "hash": 1 + } + } + ], + "upgrade_path": ["upgrade", "upgradedIBCState"], + "allow_update_after_expiry": true, + "allow_update_after_misbehaviour": true + }, + "consensus_state": { + "timestamp": "2020-01-02T00:00:10Z", + "root": { + "hash": "LpGpeyQVLUo9HpdsgJr12NP2eCICspcULiWa5u9udOA=" + }, + "next_validators_hash": "E30CE736441FB9101FADDAF7E578ABBE6DFDB67207112350A9A904D554E1F5BE" + }, + "initial_val_set": [{}] + } + }`, + initializationParameters.BlocksPerDistributionTransmission, + ccvTimeoutPeriod.Nanoseconds(), + transferTimeoutPeriod.Nanoseconds(), + initializationParameters.ConsumerRedistributionFraction, + initializationParameters.HistoricalEntries, + consumerUnbondingPeriod.Nanoseconds(), + ccvtypes.DefaultRetryDelayPeriod.Nanoseconds(), + CONSUMER_ID, + providerChainId, + trustingPeriod.Nanoseconds(), + providerUnbondingPeriod.Nanoseconds(), + providertypes.DefaultMaxClockDrift.Nanoseconds(), + providerRevisionNumber, + providerRevisionHeight, + ) + + var expectedGenesis ccvtypes.ConsumerGenesisState + err = json.Unmarshal([]byte(jsonString), &expectedGenesis) // ignores tabs, newlines and spaces + require.NoError(t, err) + expectedGenesis.Provider.InitialValSet = initialValUpdates + + // Zeroing out different fields that are challenging to mock + actualGenesis.Provider.ConsensusState = &ibctmtypes.ConsensusState{} + expectedGenesis.Provider.ConsensusState = &ibctmtypes.ConsensusState{} + + require.Equal(t, expectedGenesis, actualGenesis, "consumer chain genesis created incorrectly") +} + +func TestBeginBlockStopConsumers(t *testing.T) { + now := time.Now().UTC() + + keeperParams := testkeeper.NewInMemKeeperParams(t) + providerKeeper, ctx, ctrl, mocks := testkeeper.GetProviderKeeperAndCtx(t, keeperParams) + providerKeeper.SetParams(ctx, providertypes.DefaultParams()) + defer ctrl.Finish() + ctx = ctx.WithBlockTime(now) + + chainIds := []string{"chain1", "chain2", "chain3"} + consumerIds := []string{"consumerId1", "consumerId2", "consumerId3"} + err := providerKeeper.SetConsumerRemovalTime(ctx, consumerIds[0], now.Add(-time.Hour)) + require.NoError(t, err) + err = providerKeeper.AppendConsumerToBeRemoved(ctx, consumerIds[0], now.Add(-time.Hour)) + require.NoError(t, err) + err = providerKeeper.SetConsumerRemovalTime(ctx, consumerIds[1], now) + require.NoError(t, err) + err = providerKeeper.AppendConsumerToBeRemoved(ctx, consumerIds[1], now) + require.NoError(t, err) + err = providerKeeper.SetConsumerRemovalTime(ctx, consumerIds[2], now.Add(time.Hour)) + require.NoError(t, err) + err = providerKeeper.AppendConsumerToBeRemoved(ctx, consumerIds[2], now.Add(time.Hour)) + require.NoError(t, err) + + // + // Mock expectations + // + expectations := []*gomock.Call{} + for i := range consumerIds { + chainId := chainIds[i] + // A consumer chain is setup corresponding to each consumerId, making these mocks necessary + expectations = append(expectations, testkeeper.GetMocksForCreateConsumerClient(ctx, &mocks, + chainId, clienttypes.NewHeight(2, 3))...) + expectations = append(expectations, testkeeper.GetMocksForSetConsumerChain(ctx, &mocks, chainId)...) + } + // Only first two consumer chains should be stopped + expectations = append(expectations, testkeeper.GetMocksForDeleteConsumerChain(ctx, &mocks)...) + expectations = append(expectations, testkeeper.GetMocksForDeleteConsumerChain(ctx, &mocks)...) + + gomock.InOrder(expectations...) + + // + // Remaining setup + // + for i, consumerId := range consumerIds { + // Setup a valid consumer chain for each consumerId + initializationRecord := testkeeper.GetTestInitializationParameters() + initializationRecord.InitialHeight = clienttypes.NewHeight(2, 3) + registrationRecord := testkeeper.GetTestConsumerMetadata() + + providerKeeper.SetConsumerChainId(ctx, consumerId, chainIds[i]) + err = providerKeeper.SetConsumerMetadata(ctx, consumerId, registrationRecord) + require.NoError(t, err) + err = providerKeeper.SetConsumerInitializationParameters(ctx, consumerId, initializationRecord) + require.NoError(t, err) + err = providerKeeper.SetConsumerPowerShapingParameters(ctx, consumerId, testkeeper.GetTestPowerShapingParameters()) + require.NoError(t, err) + providerKeeper.SetConsumerPhase(ctx, consumerId, providertypes.CONSUMER_PHASE_INITIALIZED) + providerKeeper.SetConsumerClientId(ctx, consumerId, "clientID") + + err = providerKeeper.CreateConsumerClient(ctx, consumerId, []byte{}) + require.NoError(t, err) + err = providerKeeper.SetConsumerChain(ctx, "channelID") + require.NoError(t, err) + + // the chain is considered to be stopped and ready for deletion (i.e., `StopAndPrepareForConsumerRemoval` is called) + providerKeeper.SetConsumerPhase(ctx, consumerId, providertypes.CONSUMER_PHASE_STOPPED) + } + + // + // Test execution + // + + err = providerKeeper.BeginBlockRemoveConsumers(ctx) + require.NoError(t, err) + + // Only the 3rd (final) proposal is still stored as pending + phase := providerKeeper.GetConsumerPhase(ctx, consumerIds[0]) + require.Equal(t, providertypes.CONSUMER_PHASE_DELETED, phase) + phase = providerKeeper.GetConsumerPhase(ctx, consumerIds[1]) + require.Equal(t, providertypes.CONSUMER_PHASE_DELETED, phase) + // third chain had a removal time in the future and hence did not get deleted + phase = providerKeeper.GetConsumerPhase(ctx, consumerIds[2]) + require.Equal(t, providertypes.CONSUMER_PHASE_STOPPED, phase) +} + +// Tests the DeleteConsumerChain method against the spec, +// with more granularity than what's covered in TestHandleLegacyConsumerRemovalProposal, or integration tests. +// See: https://github.com/cosmos/ibc/blob/main/spec/app/ics-028-cross-chain-validation/methods.md#ccv-pcf-stcc1 +// Spec tag: [CCV-PCF-STCC.1] +func TestStopConsumerChain(t *testing.T) { + type testCase struct { + description string + // State-mutating setup specific to this test case + setup func(sdk.Context, *providerkeeper.Keeper, testkeeper.MockedKeepers) + // Whether we should expect the method to return an error + expErr bool + } + + consumerId := "0" + + tests := []testCase{ + { + description: "proposal dropped, client doesn't exist", + setup: func(ctx sdk.Context, providerKeeper *providerkeeper.Keeper, mocks testkeeper.MockedKeepers) { + // No mocks, meaning no external keeper methods are allowed to be called. + }, + expErr: true, + }, + { + description: "valid stop of consumer chain, all mock calls hit", + setup: func(ctx sdk.Context, providerKeeper *providerkeeper.Keeper, mocks testkeeper.MockedKeepers) { + testkeeper.SetupForDeleteConsumerChain(t, ctx, providerKeeper, mocks, consumerId) + + // set consumer minimum equivocation height + providerKeeper.SetEquivocationEvidenceMinHeight(ctx, consumerId, 1) + + // assert mocks for expected calls to `DeleteConsumerChain` when closing the underlying channel + gomock.InOrder(testkeeper.GetMocksForDeleteConsumerChain(ctx, &mocks)...) + }, + expErr: false, + }, + } + + for _, tc := range tests { + + // Common setup + keeperParams := testkeeper.NewInMemKeeperParams(t) + providerKeeper, ctx, ctrl, mocks := testkeeper.GetProviderKeeperAndCtx(t, keeperParams) + providerKeeper.SetParams(ctx, providertypes.DefaultParams()) + + // Setup specific to test case + tc.setup(ctx, &providerKeeper, mocks) + + err := providerKeeper.DeleteConsumerChain(ctx, consumerId) + + if tc.expErr { + require.Error(t, err, t) + } else { + require.NoError(t, err) + } + + testkeeper.TestProviderStateIsCleanedAfterConsumerChainIsDeleted(t, ctx, providerKeeper, consumerId, "channelID", tc.expErr) + + ctrl.Finish() + } +} + +// +// Setters and Getters +// + +// TestConsumerRemovalTime tests the getter, setter, and deletion of the consumer id to removal times methods +func TestConsumerRemovalTime(t *testing.T) { + providerKeeper, ctx, ctrl, _ := testkeeper.GetProviderKeeperAndCtx(t, testkeeper.NewInMemKeeperParams(t)) + defer ctrl.Finish() + + _, err := providerKeeper.GetConsumerRemovalTime(ctx, CONSUMER_ID) + require.Error(t, err) + + expectedRemovalTime := time.Unix(1234, 56789) + providerKeeper.SetConsumerRemovalTime(ctx, CONSUMER_ID, expectedRemovalTime) + actualRemovalTime, err := providerKeeper.GetConsumerRemovalTime(ctx, CONSUMER_ID) + require.NoError(t, err) + require.Equal(t, actualRemovalTime, expectedRemovalTime) + + providerKeeper.DeleteConsumerRemovalTime(ctx, CONSUMER_ID) + _, err = providerKeeper.GetConsumerRemovalTime(ctx, CONSUMER_ID) + require.Error(t, err) +} + +// TestConsumersToBeLaunched tests `AppendConsumerToBeLaunched`, `GetConsumersToBeLaunched`, and `RemoveConsumerToBeLaunched` +func TestConsumersToBeLaunched(t *testing.T) { + providerKeeper, ctx, ctrl, _ := testkeeper.GetProviderKeeperAndCtx(t, testkeeper.NewInMemKeeperParams(t)) + defer ctrl.Finish() + + spawnTime := time.Now() + err := providerKeeper.AppendConsumerToBeLaunched(ctx, "consumerId1", spawnTime) + require.NoError(t, err) + consumers, err := providerKeeper.GetConsumersToBeLaunched(ctx, spawnTime) + require.NoError(t, err) + require.Equal(t, []string{"consumerId1"}, consumers.Ids) + + err = providerKeeper.AppendConsumerToBeLaunched(ctx, "consumerId2", spawnTime) + require.NoError(t, err) + consumers, err = providerKeeper.GetConsumersToBeLaunched(ctx, spawnTime) + require.NoError(t, err) + require.Equal(t, []string{"consumerId1", "consumerId2"}, consumers.Ids) + + err = providerKeeper.AppendConsumerToBeLaunched(ctx, "consumerId3", spawnTime) + require.NoError(t, err) + consumers, err = providerKeeper.GetConsumersToBeLaunched(ctx, spawnTime) + require.NoError(t, err) + require.Equal(t, []string{"consumerId1", "consumerId2", "consumerId3"}, consumers.Ids) + + err = providerKeeper.RemoveConsumerToBeLaunched(ctx, "consumerId2", spawnTime) + require.NoError(t, err) + consumers, err = providerKeeper.GetConsumersToBeLaunched(ctx, spawnTime) + require.NoError(t, err) + require.Equal(t, []string{"consumerId1", "consumerId3"}, consumers.Ids) + + // also add consumer ids under a different spawn time and verify everything under the original spawn time is still there + spawnTimePlusOneHour := spawnTime.Add(time.Hour) + err = providerKeeper.AppendConsumerToBeLaunched(ctx, "consumerId4", spawnTimePlusOneHour) + require.NoError(t, err) + consumers, err = providerKeeper.GetConsumersToBeLaunched(ctx, spawnTimePlusOneHour) + require.NoError(t, err) + require.Equal(t, []string{"consumerId4"}, consumers.Ids) + + consumers, err = providerKeeper.GetConsumersToBeLaunched(ctx, spawnTime) + require.NoError(t, err) + require.Equal(t, []string{"consumerId1", "consumerId3"}, consumers.Ids) + + // start removing all consumers from `spawnTime` + err = providerKeeper.RemoveConsumerToBeLaunched(ctx, "consumerId3", spawnTime) + require.NoError(t, err) + err = providerKeeper.RemoveConsumerToBeLaunched(ctx, "consumerId1", spawnTime) + require.NoError(t, err) + consumers, err = providerKeeper.GetConsumersToBeLaunched(ctx, spawnTime) + require.NoError(t, err) + require.Empty(t, consumers.Ids) + + // remove from `spawnTimePlusOneHour` + err = providerKeeper.RemoveConsumerToBeLaunched(ctx, "consumerId4", spawnTimePlusOneHour) + require.NoError(t, err) + consumers, err = providerKeeper.GetConsumersToBeLaunched(ctx, spawnTimePlusOneHour) + require.NoError(t, err) + require.Empty(t, consumers.Ids) + + // add another consumer for `spawnTime` + err = providerKeeper.AppendConsumerToBeLaunched(ctx, "consumerId5", spawnTime) + require.NoError(t, err) + consumers, err = providerKeeper.GetConsumersToBeLaunched(ctx, spawnTime) + require.NoError(t, err) + require.Equal(t, []string{"consumerId5"}, consumers.Ids) +} + +// TestConsumersToBeRemoved tests `AppendConsumerToBeRemoved`, `GetConsumersToBeRemoved`, and `RemoveConsumerToBeRemoved` +func TestConsumersToBeRemoved(t *testing.T) { + providerKeeper, ctx, ctrl, _ := testkeeper.GetProviderKeeperAndCtx(t, testkeeper.NewInMemKeeperParams(t)) + defer ctrl.Finish() + + removalTime := time.Now() + err := providerKeeper.AppendConsumerToBeRemoved(ctx, "consumerId1", removalTime) + require.NoError(t, err) + consumers, err := providerKeeper.GetConsumersToBeRemoved(ctx, removalTime) + require.NoError(t, err) + require.Equal(t, []string{"consumerId1"}, consumers.Ids) + + err = providerKeeper.AppendConsumerToBeRemoved(ctx, "consumerId2", removalTime) + require.NoError(t, err) + consumers, err = providerKeeper.GetConsumersToBeRemoved(ctx, removalTime) + require.NoError(t, err) + require.Equal(t, []string{"consumerId1", "consumerId2"}, consumers.Ids) + + err = providerKeeper.AppendConsumerToBeRemoved(ctx, "consumerId3", removalTime) + require.NoError(t, err) + consumers, err = providerKeeper.GetConsumersToBeRemoved(ctx, removalTime) + require.NoError(t, err) + require.Equal(t, []string{"consumerId1", "consumerId2", "consumerId3"}, consumers.Ids) + + err = providerKeeper.RemoveConsumerToBeRemoved(ctx, "consumerId2", removalTime) + require.NoError(t, err) + consumers, err = providerKeeper.GetConsumersToBeRemoved(ctx, removalTime) + require.NoError(t, err) + require.Equal(t, []string{"consumerId1", "consumerId3"}, consumers.Ids) + + // also add consumer ids under a different removal time and verify everything under the original removal time is still there + removalTimePlusOneHour := removalTime.Add(time.Hour) + err = providerKeeper.AppendConsumerToBeRemoved(ctx, "consumerId4", removalTimePlusOneHour) + require.NoError(t, err) + consumers, err = providerKeeper.GetConsumersToBeRemoved(ctx, removalTimePlusOneHour) + require.NoError(t, err) + require.Equal(t, []string{"consumerId4"}, consumers.Ids) + + consumers, err = providerKeeper.GetConsumersToBeRemoved(ctx, removalTime) + require.NoError(t, err) + require.Equal(t, []string{"consumerId1", "consumerId3"}, consumers.Ids) + + // start removing all consumers from `removalTime` + err = providerKeeper.RemoveConsumerToBeRemoved(ctx, "consumerId3", removalTime) + require.NoError(t, err) + err = providerKeeper.RemoveConsumerToBeRemoved(ctx, "consumerId1", removalTime) + require.NoError(t, err) + consumers, err = providerKeeper.GetConsumersToBeRemoved(ctx, removalTime) + require.NoError(t, err) + require.Empty(t, consumers.Ids) + + // remove from `removalTimePlusOneHour` + err = providerKeeper.RemoveConsumerToBeRemoved(ctx, "consumerId4", removalTimePlusOneHour) + require.NoError(t, err) + consumers, err = providerKeeper.GetConsumersToBeRemoved(ctx, removalTimePlusOneHour) + require.NoError(t, err) + require.Empty(t, consumers.Ids) + + // add another consumer for `removalTime` + err = providerKeeper.AppendConsumerToBeRemoved(ctx, "consumerId5", removalTime) + require.NoError(t, err) + consumers, err = providerKeeper.GetConsumersToBeRemoved(ctx, removalTime) + require.NoError(t, err) + require.Equal(t, []string{"consumerId5"}, consumers.Ids) +} diff --git a/x/ccv/provider/keeper/grpc_query.go b/x/ccv/provider/keeper/grpc_query.go index d2a36c686d..6fc0c16afd 100644 --- a/x/ccv/provider/keeper/grpc_query.go +++ b/x/ccv/provider/keeper/grpc_query.go @@ -24,8 +24,14 @@ func (k Keeper) QueryConsumerGenesis(c context.Context, req *types.QueryConsumer return nil, status.Errorf(codes.InvalidArgument, "empty request") } +<<<<<<< HEAD if req.ChainId == "" { return nil, status.Errorf(codes.InvalidArgument, "invalid request: chain id cannot be empty") +======= + consumerId := req.ConsumerId + if err := ccvtypes.ValidateConsumerId(consumerId); err != nil { + return nil, status.Error(codes.InvalidArgument, err.Error()) +>>>>>>> 0d782959 (feat!: add memo to IBC transfers of ICS rewards (#2290)) } gen, ok := k.GetConsumerGenesis(ctx, req.ChainId) @@ -146,6 +152,14 @@ func (k Keeper) QueryValidatorConsumerAddr(goCtx context.Context, req *types.Que ctx := sdk.UnwrapSDKContext(goCtx) +<<<<<<< HEAD +======= + consumerId := req.ConsumerId + if err := ccvtypes.ValidateConsumerId(consumerId); err != nil { + return nil, status.Error(codes.InvalidArgument, err.Error()) + } + +>>>>>>> 0d782959 (feat!: add memo to IBC transfers of ICS rewards (#2290)) providerAddrTmp, err := sdk.ConsAddressFromBech32(req.ProviderAddress) if err != nil { return nil, err @@ -227,6 +241,7 @@ func (k Keeper) QueryProposedConsumerChainIDs(goCtx context.Context, req *types. return nil, status.Error(codes.InvalidArgument, "empty request") } +<<<<<<< HEAD ctx := sdk.UnwrapSDKContext(goCtx) chains := k.GetAllProposedConsumerChainIDs(ctx) @@ -243,6 +258,11 @@ func (k Keeper) QueryAllPairsValConAddrByConsumerChainID(goCtx context.Context, if req.ChainId == "" { return nil, status.Error(codes.InvalidArgument, "empty chainId") +======= + consumerId := req.ConsumerId + if err := ccvtypes.ValidateConsumerId(consumerId); err != nil { + return nil, status.Error(codes.InvalidArgument, err.Error()) +>>>>>>> 0d782959 (feat!: add memo to IBC transfers of ICS rewards (#2290)) } // list of pairs valconsensus addr @@ -285,9 +305,15 @@ func (k Keeper) QueryConsumerChainOptedInValidators(goCtx context.Context, req * return nil, status.Error(codes.InvalidArgument, "empty request") } +<<<<<<< HEAD consumerChainID := req.ChainId if consumerChainID == "" { return nil, status.Error(codes.InvalidArgument, "empty chainId") +======= + consumerId := req.ConsumerId + if err := ccvtypes.ValidateConsumerId(consumerId); err != nil { + return nil, status.Error(codes.InvalidArgument, err.Error()) +>>>>>>> 0d782959 (feat!: add memo to IBC transfers of ICS rewards (#2290)) } optedInVals := []string{} @@ -312,9 +338,15 @@ func (k Keeper) QueryConsumerValidators(goCtx context.Context, req *types.QueryC return nil, status.Error(codes.InvalidArgument, "empty request") } +<<<<<<< HEAD consumerChainID := req.ChainId if consumerChainID == "" { return nil, status.Error(codes.InvalidArgument, "empty chainId") +======= + consumerId := req.ConsumerId + if err := ccvtypes.ValidateConsumerId(consumerId); err != nil { + return nil, status.Error(codes.InvalidArgument, err.Error()) +>>>>>>> 0d782959 (feat!: add memo to IBC transfers of ICS rewards (#2290)) } ctx := sdk.UnwrapSDKContext(goCtx) @@ -424,9 +456,15 @@ func (k Keeper) QueryValidatorConsumerCommissionRate(goCtx context.Context, req return nil, status.Error(codes.InvalidArgument, "empty request") } +<<<<<<< HEAD consumerChainID := req.ChainId if consumerChainID == "" { return nil, status.Error(codes.InvalidArgument, "empty chainId") +======= + consumerId := req.ConsumerId + if err := ccvtypes.ValidateConsumerId(consumerId); err != nil { + return nil, status.Error(codes.InvalidArgument, err.Error()) +>>>>>>> 0d782959 (feat!: add memo to IBC transfers of ICS rewards (#2290)) } consAddr, err := sdk.ConsAddressFromBech32(req.ProviderAddress) @@ -465,8 +503,20 @@ func (k Keeper) QueryOldestUnconfirmedVsc(goCtx context.Context, req *types.Quer return nil, status.Errorf(codes.InvalidArgument, "empty request") } +<<<<<<< HEAD if req.ChainId == "" { return nil, status.Errorf(codes.InvalidArgument, "invalid request: chain id cannot be empty") +======= + consumerId := req.ConsumerId + if err := ccvtypes.ValidateConsumerId(consumerId); err != nil { + return nil, status.Error(codes.InvalidArgument, err.Error()) + } + ctx := sdk.UnwrapSDKContext(goCtx) + + chainId, err := k.GetConsumerChainId(ctx, consumerId) + if err != nil { + return nil, status.Errorf(codes.InvalidArgument, "cannot retrieve chain id for consumer id: %s", consumerId) +>>>>>>> 0d782959 (feat!: add memo to IBC transfers of ICS rewards (#2290)) } if _, consumerRegistered := k.GetConsumerClientId(ctx, req.ChainId); !consumerRegistered { diff --git a/x/ccv/provider/types/errors.go b/x/ccv/provider/types/errors.go index 5109489d76..11e287dade 100644 --- a/x/ccv/provider/types/errors.go +++ b/x/ccv/provider/types/errors.go @@ -6,6 +6,7 @@ import ( // Provider sentinel errors var ( +<<<<<<< HEAD ErrInvalidConsumerAdditionProposal = errorsmod.Register(ModuleName, 1, "invalid consumer addition proposal") ErrInvalidConsumerRemovalProp = errorsmod.Register(ModuleName, 2, "invalid consumer removal proposal") ErrUnknownConsumerChainId = errorsmod.Register(ModuleName, 3, "no consumer chain with this chain id") @@ -32,4 +33,39 @@ var ( ErrInvalidAddress = errorsmod.Register(ModuleName, 24, "invalid address") ErrUnauthorized = errorsmod.Register(ModuleName, 25, "unauthorized") ErrBlankConsumerChainID = errorsmod.Register(ModuleName, 26, "consumer chain id must not be blank") +======= + ErrUnknownConsumerId = errorsmod.Register(ModuleName, 3, "no consumer chain with this consumer id") + ErrUnknownConsumerChannelId = errorsmod.Register(ModuleName, 4, "no consumer chain with this channel id") + ErrConsumerKeyInUse = errorsmod.Register(ModuleName, 10, "consumer key is already in use by a validator") + ErrCannotAssignDefaultKeyAssignment = errorsmod.Register(ModuleName, 11, "cannot re-assign default key assignment") + ErrInvalidConsumerRewardDenom = errorsmod.Register(ModuleName, 14, "invalid consumer reward denom") + ErrInvalidConsumerClient = errorsmod.Register(ModuleName, 16, "ccv channel is not built on correct client") + ErrCannotOptOutFromTopN = errorsmod.Register(ModuleName, 20, "cannot opt out from a Top N chain") + ErrNoUnbondingTime = errorsmod.Register(ModuleName, 23, "provider unbonding time not found") + ErrUnauthorized = errorsmod.Register(ModuleName, 25, "unauthorized") + ErrInvalidPhase = errorsmod.Register(ModuleName, 27, "cannot perform action in the current phase of consumer chain") + ErrInvalidConsumerMetadata = errorsmod.Register(ModuleName, 28, "invalid consumer metadata") + ErrInvalidPowerShapingParameters = errorsmod.Register(ModuleName, 29, "invalid power shaping parameters") + ErrInvalidConsumerInitializationParameters = errorsmod.Register(ModuleName, 30, "invalid consumer initialization parameters") + ErrCannotUpdateMinimumPowerInTopN = errorsmod.Register(ModuleName, 31, "cannot update minimum power in Top N") + ErrNoConsumerGenesis = errorsmod.Register(ModuleName, 33, "missing consumer genesis") + ErrInvalidConsumerGenesis = errorsmod.Register(ModuleName, 34, "invalid consumer genesis") + ErrNoConsumerId = errorsmod.Register(ModuleName, 35, "missing consumer id") + ErrAlreadyOptedIn = errorsmod.Register(ModuleName, 36, "already opted in to a chain with the same chain id") + ErrNoOwnerAddress = errorsmod.Register(ModuleName, 37, "missing owner address") + ErrInvalidNewOwnerAddress = errorsmod.Register(ModuleName, 38, "invalid new owner address") + ErrInvalidTransformToTopN = errorsmod.Register(ModuleName, 39, "invalid transform to Top N chain") + ErrInvalidTransformToOptIn = errorsmod.Register(ModuleName, 40, "invalid transform to Opt In chain") + ErrCannotCreateTopNChain = errorsmod.Register(ModuleName, 41, "cannot create Top N chain outside permissionlessly") + ErrInvalidRemovalTime = errorsmod.Register(ModuleName, 43, "invalid removal time") + ErrInvalidMsgCreateConsumer = errorsmod.Register(ModuleName, 44, "invalid create consumer message") + ErrInvalidMsgUpdateConsumer = errorsmod.Register(ModuleName, 45, "invalid update consumer message") + ErrInvalidMsgAssignConsumerKey = errorsmod.Register(ModuleName, 46, "invalid assign consumer key message") + ErrInvalidMsgSubmitConsumerMisbehaviour = errorsmod.Register(ModuleName, 47, "invalid submit consumer misbehaviour message") + ErrInvalidMsgSubmitConsumerDoubleVoting = errorsmod.Register(ModuleName, 48, "invalid submit consumer double voting message") + ErrInvalidMsgOptIn = errorsmod.Register(ModuleName, 49, "invalid opt in message") + ErrInvalidMsgOptOut = errorsmod.Register(ModuleName, 50, "invalid opt out message") + ErrInvalidMsgSetConsumerCommissionRate = errorsmod.Register(ModuleName, 51, "invalid set consumer commission rate message") + ErrInvalidMsgChangeRewardDenoms = errorsmod.Register(ModuleName, 52, "invalid change reward denoms message") +>>>>>>> 0d782959 (feat!: add memo to IBC transfers of ICS rewards (#2290)) ) diff --git a/x/ccv/provider/types/msg.go b/x/ccv/provider/types/msg.go index 59545dc1bb..f3a1164ecc 100644 --- a/x/ccv/provider/types/msg.go +++ b/x/ccv/provider/types/msg.go @@ -97,11 +97,17 @@ func (msg MsgAssignConsumerKey) ValidateBasic() error { if strings.TrimSpace(msg.ChainId) == "" { return errorsmod.Wrapf(ErrInvalidConsumerChainID, "chainId cannot be blank") } +<<<<<<< HEAD // It is possible to assign keys for consumer chains that are not yet approved. // This can only be done by a signing validator, but it is still sensible // to limit the chainID size to prevent abuse. if 128 < len(msg.ChainId) { return errorsmod.Wrapf(ErrInvalidConsumerChainID, "chainId cannot exceed 128 length") +======= + + if err := ccvtypes.ValidateConsumerId(msg.ConsumerId); err != nil { + return errorsmod.Wrapf(ErrInvalidMsgAssignConsumerKey, "ConsumerId: %s", err.Error()) +>>>>>>> 0d782959 (feat!: add memo to IBC transfers of ICS rewards (#2290)) } valAddr, err := sdk.ValAddressFromBech32(msg.ProviderAddr) if err != nil { @@ -115,11 +121,319 @@ func (msg MsgAssignConsumerKey) ValidateBasic() error { return ErrInvalidConsumerConsensusPubKey } if _, _, err := ParseConsumerKeyFromJson(msg.ConsumerKey); err != nil { +<<<<<<< HEAD return ErrInvalidConsumerConsensusPubKey +======= + return errorsmod.Wrapf(ErrInvalidMsgAssignConsumerKey, "ConsumerKey: %s", err.Error()) + } + + return nil +} + +// ValidateBasic implements the sdk.HasValidateBasic interface. +func (msg *MsgChangeRewardDenoms) ValidateBasic() error { + emptyDenomsToAdd := len(msg.DenomsToAdd) == 0 + emptyDenomsToRemove := len(msg.DenomsToRemove) == 0 + // Return error if both sets are empty or nil + if emptyDenomsToAdd && emptyDenomsToRemove { + return errorsmod.Wrapf(ErrInvalidMsgChangeRewardDenoms, "both DenomsToAdd and DenomsToRemove are empty") + } + + denomMap := map[string]struct{}{} + for _, denom := range msg.DenomsToAdd { + // validate the denom + if !sdk.NewCoin(denom, math.NewInt(1)).IsValid() { + return errorsmod.Wrapf(ErrInvalidMsgChangeRewardDenoms, "DenomsToAdd: invalid denom(%s)", denom) + } + denomMap[denom] = struct{}{} + } + for _, denom := range msg.DenomsToRemove { + // validate the denom + if !sdk.NewCoin(denom, math.NewInt(1)).IsValid() { + return errorsmod.Wrapf(ErrInvalidMsgChangeRewardDenoms, "DenomsToRemove: invalid denom(%s)", denom) + } + // denom cannot be in both sets + if _, found := denomMap[denom]; found { + return errorsmod.Wrapf(ErrInvalidMsgChangeRewardDenoms, + "denom(%s) cannot be both added and removed", denom) + } + } + + return nil +} + +func NewMsgSubmitConsumerMisbehaviour( + consumerId string, + submitter sdk.AccAddress, + misbehaviour *ibctmtypes.Misbehaviour, +) (*MsgSubmitConsumerMisbehaviour, error) { + return &MsgSubmitConsumerMisbehaviour{ + Submitter: submitter.String(), + Misbehaviour: misbehaviour, + ConsumerId: consumerId, + }, nil +} + +// ValidateBasic implements the sdk.HasValidateBasic interface. +func (msg MsgSubmitConsumerMisbehaviour) ValidateBasic() error { + if err := ccvtypes.ValidateConsumerId(msg.ConsumerId); err != nil { + return errorsmod.Wrapf(ErrInvalidMsgSubmitConsumerMisbehaviour, "ConsumerId: %s", err.Error()) + } + + if err := msg.Misbehaviour.ValidateBasic(); err != nil { + return errorsmod.Wrapf(ErrInvalidMsgSubmitConsumerMisbehaviour, "Misbehaviour: %s", err.Error()) +>>>>>>> 0d782959 (feat!: add memo to IBC transfers of ICS rewards (#2290)) + } + return nil +} + +<<<<<<< HEAD +======= +func NewMsgSubmitConsumerDoubleVoting( + consumerId string, + submitter sdk.AccAddress, + ev *tmtypes.DuplicateVoteEvidence, + header *ibctmtypes.Header, +) (*MsgSubmitConsumerDoubleVoting, error) { + return &MsgSubmitConsumerDoubleVoting{ + Submitter: submitter.String(), + DuplicateVoteEvidence: ev, + InfractionBlockHeader: header, + ConsumerId: consumerId, + }, nil +} + +// ValidateBasic implements the sdk.HasValidateBasic interface. +func (msg MsgSubmitConsumerDoubleVoting) ValidateBasic() error { + if dve, err := cmttypes.DuplicateVoteEvidenceFromProto(msg.DuplicateVoteEvidence); err != nil { + return errorsmod.Wrapf(ErrInvalidMsgSubmitConsumerDoubleVoting, "DuplicateVoteEvidence: %s", err.Error()) + } else { + if err = dve.ValidateBasic(); err != nil { + return errorsmod.Wrapf(ErrInvalidMsgSubmitConsumerDoubleVoting, "DuplicateVoteEvidence: %s", err.Error()) + } + } + + if err := ValidateHeaderForConsumerDoubleVoting(msg.InfractionBlockHeader); err != nil { + return errorsmod.Wrapf(ErrInvalidMsgSubmitConsumerDoubleVoting, "ValidateTendermintHeader: %s", err.Error()) + } + + if err := ccvtypes.ValidateConsumerId(msg.ConsumerId); err != nil { + return errorsmod.Wrapf(ErrInvalidMsgSubmitConsumerDoubleVoting, "ConsumerId: %s", err.Error()) + } + + return nil +} + +// NewMsgOptIn creates a new NewMsgOptIn instance. +func NewMsgOptIn(consumerId string, providerValidatorAddress sdk.ValAddress, consumerConsensusPubKey, signer string) (*MsgOptIn, error) { + return &MsgOptIn{ + ConsumerId: consumerId, + ProviderAddr: providerValidatorAddress.String(), + ConsumerKey: consumerConsensusPubKey, + Signer: signer, + }, nil +} + +// ValidateBasic implements the sdk.HasValidateBasic interface. +func (msg MsgOptIn) ValidateBasic() error { + if err := validateDeprecatedChainId(msg.ChainId); err != nil { + return errorsmod.Wrapf(ErrInvalidMsgOptIn, "ChainId: %s", err.Error()) + } + + if err := ccvtypes.ValidateConsumerId(msg.ConsumerId); err != nil { + return errorsmod.Wrapf(ErrInvalidMsgOptIn, "ConsumerId: %s", err.Error()) + } + + if err := validateProviderAddress(msg.ProviderAddr, msg.Signer); err != nil { + return errorsmod.Wrapf(ErrInvalidMsgOptIn, "ProviderAddr: %s", err.Error()) + } + + if msg.ConsumerKey != "" { + if _, _, err := ParseConsumerKeyFromJson(msg.ConsumerKey); err != nil { + return errorsmod.Wrapf(ErrInvalidMsgOptIn, "ConsumerKey: %s", err.Error()) + } + } + return nil +} + +// NewMsgOptOut creates a new NewMsgOptIn instance. +func NewMsgOptOut(consumerId string, providerValidatorAddress sdk.ValAddress, signer string) (*MsgOptOut, error) { + return &MsgOptOut{ + ConsumerId: consumerId, + ProviderAddr: providerValidatorAddress.String(), + Signer: signer, + }, nil +} + +// ValidateBasic implements the sdk.HasValidateBasic interface. +func (msg MsgOptOut) ValidateBasic() error { + if err := validateDeprecatedChainId(msg.ChainId); err != nil { + return errorsmod.Wrapf(ErrInvalidMsgOptOut, "ChainId: %s", err.Error()) + } + + if err := ccvtypes.ValidateConsumerId(msg.ConsumerId); err != nil { + return errorsmod.Wrapf(ErrInvalidMsgOptOut, "ConsumerId: %s", err.Error()) + } + + if err := validateProviderAddress(msg.ProviderAddr, msg.Signer); err != nil { + return errorsmod.Wrapf(ErrInvalidMsgOptOut, "ProviderAddr: %s", err.Error()) + } + + return nil +} + +// NewMsgSetConsumerCommissionRate creates a new MsgSetConsumerCommissionRate msg instance. +func NewMsgSetConsumerCommissionRate( + consumerId string, + commission math.LegacyDec, + providerValidatorAddress sdk.ValAddress, + signer string, +) *MsgSetConsumerCommissionRate { + return &MsgSetConsumerCommissionRate{ + ConsumerId: consumerId, + Rate: commission, + ProviderAddr: providerValidatorAddress.String(), + Signer: signer, + } +} + +// ValidateBasic implements the sdk.HasValidateBasic interface. +func (msg MsgSetConsumerCommissionRate) ValidateBasic() error { + if err := validateDeprecatedChainId(msg.ChainId); err != nil { + return errorsmod.Wrapf(ErrInvalidMsgSetConsumerCommissionRate, "ChainId: %s", err.Error()) + } + + if err := ccvtypes.ValidateConsumerId(msg.ConsumerId); err != nil { + return errorsmod.Wrapf(ErrInvalidMsgSetConsumerCommissionRate, "ConsumerId: %s", err.Error()) + } + + if err := validateProviderAddress(msg.ProviderAddr, msg.Signer); err != nil { + return errorsmod.Wrapf(ErrInvalidMsgSetConsumerCommissionRate, "ProviderAddr: %s", err.Error()) + } + + if msg.Rate.IsNegative() || msg.Rate.GT(math.LegacyOneDec()) { + return errorsmod.Wrapf(ErrInvalidMsgSetConsumerCommissionRate, "consumer commission rate should be in the range [0, 1]") + } + + return nil +} + +// NewMsgCreateConsumer creates a new MsgCreateConsumer instance +func NewMsgCreateConsumer(submitter, chainId string, metadata ConsumerMetadata, + initializationParameters *ConsumerInitializationParameters, powerShapingParameters *PowerShapingParameters, +) (*MsgCreateConsumer, error) { + return &MsgCreateConsumer{ + Submitter: submitter, + ChainId: chainId, + Metadata: metadata, + InitializationParameters: initializationParameters, + PowerShapingParameters: powerShapingParameters, + }, nil +} + +// ValidateBasic implements the sdk.HasValidateBasic interface. +func (msg MsgCreateConsumer) ValidateBasic() error { + if err := ValidateStringField("ChainId", msg.ChainId, cmttypes.MaxChainIDLen); err != nil { + return errorsmod.Wrapf(ErrInvalidMsgCreateConsumer, "ChainId: %s", err.Error()) + } + + // With permissionless ICS, we can have multiple consumer chains with the exact same chain id. + // However, as we already have the Neutron and Stride Top N chains running, as a first step we would like to + // prevent permissionless chains from re-using the chain ids of Neutron and Stride. Note that this is just a + // preliminary measure that will be removed later on as part of: + // TODO (#2242): find a better way of ignoring past misbehaviors + if msg.ChainId == "neutron-1" || msg.ChainId == "stride-1" { + return errorsmod.Wrapf(ErrInvalidMsgCreateConsumer, + "cannot reuse chain ids of existing Neutron and Stride Top N consumer chains") + } + + if err := ValidateConsumerMetadata(msg.Metadata); err != nil { + return errorsmod.Wrapf(ErrInvalidMsgCreateConsumer, "Metadata: %s", err.Error()) + } + + if msg.InitializationParameters != nil { + if err := ValidateInitializationParameters(*msg.InitializationParameters); err != nil { + return errorsmod.Wrapf(ErrInvalidMsgCreateConsumer, "InitializationParameters: %s", err.Error()) + } + } + + if msg.PowerShapingParameters != nil { + if msg.PowerShapingParameters.Top_N != 0 { + return fmt.Errorf("cannot create a Top N chain through `MsgCreateConsumer`; " + + "first create the chain and then use `MsgUpdateConsumer` to make the chain Top N") + } + if err := ValidatePowerShapingParameters(*msg.PowerShapingParameters); err != nil { + return errorsmod.Wrapf(ErrInvalidMsgCreateConsumer, "PowerShapingParameters: %s", err.Error()) + } } + return nil } +// NewMsgUpdateConsumer creates a new MsgUpdateConsumer instance +func NewMsgUpdateConsumer(owner, consumerId, ownerAddress string, metadata *ConsumerMetadata, + initializationParameters *ConsumerInitializationParameters, powerShapingParameters *PowerShapingParameters, +) (*MsgUpdateConsumer, error) { + return &MsgUpdateConsumer{ + Owner: owner, + ConsumerId: consumerId, + NewOwnerAddress: ownerAddress, + Metadata: metadata, + InitializationParameters: initializationParameters, + PowerShapingParameters: powerShapingParameters, + }, nil +} + +// ValidateBasic implements the sdk.HasValidateBasic interface. +func (msg MsgUpdateConsumer) ValidateBasic() error { + if err := ccvtypes.ValidateConsumerId(msg.ConsumerId); err != nil { + return errorsmod.Wrapf(ErrInvalidMsgUpdateConsumer, "ConsumerId: %s", err.Error()) + } + + // Note that NewOwnerAddress is validated when handling the message in UpdateConsumer + + if msg.Metadata != nil { + if err := ValidateConsumerMetadata(*msg.Metadata); err != nil { + return errorsmod.Wrapf(ErrInvalidMsgUpdateConsumer, "Metadata: %s", err.Error()) + } + } + + if msg.InitializationParameters != nil { + if err := ValidateInitializationParameters(*msg.InitializationParameters); err != nil { + return errorsmod.Wrapf(ErrInvalidMsgUpdateConsumer, "InitializationParameters: %s", err.Error()) + } + } + + if msg.PowerShapingParameters != nil { + if err := ValidatePowerShapingParameters(*msg.PowerShapingParameters); err != nil { + return errorsmod.Wrapf(ErrInvalidMsgUpdateConsumer, "PowerShapingParameters: %s", err.Error()) + } + } + + return nil +} + +// NewMsgRemoveConsumer creates a new MsgRemoveConsumer instance +func NewMsgRemoveConsumer(owner, consumerId string) (*MsgRemoveConsumer, error) { + return &MsgRemoveConsumer{ + Owner: owner, + ConsumerId: consumerId, + }, nil +} + +// ValidateBasic implements the sdk.HasValidateBasic interface. +func (msg MsgRemoveConsumer) ValidateBasic() error { + if err := ccvtypes.ValidateConsumerId(msg.ConsumerId); err != nil { + return err + } + return nil +} + +// +// Validation methods +// + +>>>>>>> 0d782959 (feat!: add memo to IBC transfers of ICS rewards (#2290)) // ParseConsumerKeyFromJson parses the consumer key from a JSON string, // this replaces deserializing a protobuf any. func ParseConsumerKeyFromJson(jsonStr string) (pkType, key string, err error) { @@ -215,6 +529,7 @@ func (msg MsgSubmitConsumerDoubleVoting) ValidateBasic() error { return nil } +<<<<<<< HEAD // Type implements the sdk.Msg interface. func (msg MsgSubmitConsumerDoubleVoting) GetSignBytes() []byte { bz := ccvtypes.ModuleCdc.MustMarshalJSON(&msg) @@ -237,6 +552,154 @@ func (msg MsgSubmitConsumerDoubleVoting) GetSigners() []sdk.AccAddress { // sign the msg as well. func (msg *MsgConsumerAddition) GetSigners() []sdk.AccAddress { valAddr, err := sdk.ValAddressFromBech32(msg.Authority) +======= +// ValidateStringField validates that a string `field` satisfies the following properties: +// - is not empty +// - has at most `maxLength` characters +func ValidateStringField(nameOfTheField, field string, maxLength int) error { + if strings.TrimSpace(field) == "" { + return fmt.Errorf("%s cannot be empty", nameOfTheField) + } else if len(field) > maxLength { + return fmt.Errorf("%s is too long; got: %d, max: %d", nameOfTheField, len(field), maxLength) + } + return nil +} + +// TruncateString truncates a string to maximum length characters +func TruncateString(str string, maxLength int) string { + if maxLength <= 0 { + return "" + } + + truncated := "" + count := 0 + for _, char := range str { + truncated += string(char) + count++ + if count >= maxLength { + break + } + } + return truncated +} + +// ValidateConsumerMetadata validates that all the provided metadata are in the expected range +func ValidateConsumerMetadata(metadata ConsumerMetadata) error { + if err := ValidateStringField("name", metadata.Name, MaxNameLength); err != nil { + return errorsmod.Wrapf(ErrInvalidConsumerMetadata, "Name: %s", err.Error()) + } + + if err := ValidateStringField("description", metadata.Description, MaxDescriptionLength); err != nil { + return errorsmod.Wrapf(ErrInvalidConsumerMetadata, "Description: %s", err.Error()) + } + + if err := ValidateStringField("metadata", metadata.Metadata, MaxMetadataLength); err != nil { + return errorsmod.Wrapf(ErrInvalidConsumerMetadata, "Metadata: %s", err.Error()) + } + + return nil +} + +// ValidateConsAddressList validates a list of consensus addresses +func ValidateConsAddressList(list []string, maxLength int) error { + if len(list) > maxLength { + return fmt.Errorf("consensus address list too long; got: %d, max: %d", len(list), maxLength) + } + for _, address := range list { + _, err := sdk.ConsAddressFromBech32(address) + if err != nil { + return fmt.Errorf("invalid address %s: %s", address, err.Error()) + } + } + return nil +} + +// ValidatePowerShapingParameters validates that all the provided power-shaping parameters are in the expected range +func ValidatePowerShapingParameters(powerShapingParameters PowerShapingParameters) error { + // Top N corresponds to the top N% of validators that have to validate the consumer chain and can only be 0 (for an + // Opt In chain) or in the range [50, 100] (for a Top N chain). + if powerShapingParameters.Top_N != 0 && (powerShapingParameters.Top_N < 50 || powerShapingParameters.Top_N > 100) { + return errorsmod.Wrap(ErrInvalidPowerShapingParameters, "Top N can either be 0 or in the range [50, 100]") + } + + if powerShapingParameters.ValidatorsPowerCap > 100 { + return errorsmod.Wrap(ErrInvalidPowerShapingParameters, "ValidatorsPowerCap has to be in the range [0, 100]") + } + + if err := ValidateConsAddressList(powerShapingParameters.Allowlist, MaxValidatorCount); err != nil { + return errorsmod.Wrapf(ErrInvalidPowerShapingParameters, "Allowlist: %s", err.Error()) + } + if err := ValidateConsAddressList(powerShapingParameters.Denylist, MaxValidatorCount); err != nil { + return errorsmod.Wrapf(ErrInvalidPowerShapingParameters, "Denylist: %s", err.Error()) + } + + return nil +} + +// ValidateInitializationParameters validates that all the provided parameters are in the expected range +func ValidateInitializationParameters(initializationParameters ConsumerInitializationParameters) error { + if initializationParameters.InitialHeight.IsZero() { + return errorsmod.Wrap(ErrInvalidConsumerInitializationParameters, "InitialHeight cannot be zero") + } + + if err := ValidateByteSlice(initializationParameters.GenesisHash, MaxHashLength); err != nil { + return errorsmod.Wrapf(ErrInvalidConsumerInitializationParameters, "GenesisHash: %s", err.Error()) + } + + if err := ValidateByteSlice(initializationParameters.BinaryHash, MaxHashLength); err != nil { + return errorsmod.Wrapf(ErrInvalidConsumerInitializationParameters, "BinaryHash: %s", err.Error()) + } + + if err := ccvtypes.ValidateStringFraction(initializationParameters.ConsumerRedistributionFraction); err != nil { + return errorsmod.Wrapf(ErrInvalidConsumerInitializationParameters, "ConsumerRedistributionFraction: %s", err.Error()) + } + + if err := ccvtypes.ValidatePositiveInt64(initializationParameters.BlocksPerDistributionTransmission); err != nil { + return errorsmod.Wrapf(ErrInvalidConsumerInitializationParameters, "BlocksPerDistributionTransmission: %s", err.Error()) + } + + if err := ccvtypes.ValidateDistributionTransmissionChannel(initializationParameters.DistributionTransmissionChannel); err != nil { + return errorsmod.Wrapf(ErrInvalidConsumerInitializationParameters, "DistributionTransmissionChannel: %s", err.Error()) + } + + if err := ccvtypes.ValidatePositiveInt64(initializationParameters.HistoricalEntries); err != nil { + return errorsmod.Wrapf(ErrInvalidConsumerInitializationParameters, "HistoricalEntries: %s", err.Error()) + } + + if err := ccvtypes.ValidateDuration(initializationParameters.CcvTimeoutPeriod); err != nil { + return errorsmod.Wrapf(ErrInvalidConsumerInitializationParameters, "CcvTimeoutPeriod: %s", err.Error()) + } + + if err := ccvtypes.ValidateDuration(initializationParameters.TransferTimeoutPeriod); err != nil { + return errorsmod.Wrapf(ErrInvalidConsumerInitializationParameters, "TransferTimeoutPeriod: %s", err.Error()) + } + + if err := ccvtypes.ValidateDuration(initializationParameters.UnbondingPeriod); err != nil { + return errorsmod.Wrapf(ErrInvalidConsumerInitializationParameters, "UnbondingPeriod: %s", err.Error()) + } + + return nil +} + +func ValidateByteSlice(hash []byte, maxLength int) error { + if len(hash) > maxLength { + return fmt.Errorf("hash is too long; got: %d, max: %d", len(hash), maxLength) + } + return nil +} + +func validateDeprecatedChainId(chainId string) error { + if strings.TrimSpace(chainId) != "" { + return fmt.Errorf("found non-empty chainId(%s); chainId is deprecated, use consumerId instead", chainId) + } + + return nil +} + +// validateProviderAddress validates that the address is a sdk.ValAddress in Bech32 string format +func validateProviderAddress(addr, signer string) error { + valAddr, err := sdk.ValAddressFromBech32(addr) +>>>>>>> 0d782959 (feat!: add memo to IBC transfers of ICS rewards (#2290)) if err != nil { // same behavior as in cosmos-sdk panic(err) diff --git a/x/ccv/provider/types/msg_test.go b/x/ccv/provider/types/msg_test.go index 4ef2a7efc8..eed4e9cd1d 100644 --- a/x/ccv/provider/types/msg_test.go +++ b/x/ccv/provider/types/msg_test.go @@ -9,6 +9,552 @@ import ( "github.com/stretchr/testify/require" ) +<<<<<<< HEAD +======= +func TestValidateStringField(t *testing.T) { + testCases := []struct { + name string + field string + maxLength int + valid bool + }{ + { + name: "invalid: empty", + field: "", + maxLength: 5, + valid: false, + }, + { + name: "invalid: too long", + field: "this field is too long", + maxLength: 5, + valid: false, + }, + { + name: "valid", + field: "valid", + maxLength: 5, + valid: true, + }, + } + + for _, tc := range testCases { + err := types.ValidateStringField(tc.name, tc.field, tc.maxLength) + if tc.valid { + require.NoError(t, err, tc.name) + } else { + require.Error(t, err, tc.name) + } + } +} + +func TestTruncateString(t *testing.T) { + testCases := []struct { + str string + maxLength int + expStr string + }{ + {"drink", 3, "dri"}, + {"drink", 6, "drink"}, + {"drink", 0, ""}, + {"drink", -1, ""}, + {"drink", 100, "drink"}, + {"pub", 100, "pub"}, + {"こんにちは", 3, "こんに"}, + } + + for _, tc := range testCases { + truncated := types.TruncateString(tc.str, tc.maxLength) + require.Equal(t, tc.expStr, truncated) + } +} + +func TestValidateConsumerMetadata(t *testing.T) { + generateLongString := func(length int) string { + result := make([]byte, length) + for i := range result { + result[i] = byte('a') + } + return string(result) + } + + testCases := []struct { + name string + metadata types.ConsumerMetadata + valid bool + }{ + { + name: "valid", + metadata: types.ConsumerMetadata{ + Name: "name", + Description: "description", + Metadata: "metadata", + }, + valid: true, + }, + { + name: "valid with long strings", + metadata: types.ConsumerMetadata{ + Name: generateLongString(types.MaxNameLength), + Description: generateLongString(types.MaxDescriptionLength), + Metadata: generateLongString(types.MaxMetadataLength), + }, + valid: true, + }, + { + name: "invalid name", + metadata: types.ConsumerMetadata{ + Name: generateLongString(types.MaxNameLength + 1), + Description: "description", + Metadata: "metadata", + }, + valid: false, + }, + { + name: "invalid description", + metadata: types.ConsumerMetadata{ + Name: "name", + Description: generateLongString(types.MaxDescriptionLength + 1), + Metadata: "metadata", + }, + valid: false, + }, + { + name: "invalid metadata", + metadata: types.ConsumerMetadata{ + Name: "name", + Description: "description", + Metadata: generateLongString(types.MaxMetadataLength + 1), + }, + valid: false, + }, + } + + for _, tc := range testCases { + err := types.ValidateConsumerMetadata(tc.metadata) + if tc.valid { + require.NoError(t, err, tc.name) + } else { + require.Error(t, err, tc.name) + } + } +} + +func TestValidateInitializationParameters(t *testing.T) { + now := time.Now().UTC() + coolStr := "Cosmos Hub is the best place to launch a chain. Interchain Security is awesome." + tooLongHash := []byte(coolStr) + + testCases := []struct { + name string + params types.ConsumerInitializationParameters + valid bool + }{ + { + name: "valid", + params: types.ConsumerInitializationParameters{ + InitialHeight: clienttypes.NewHeight(3, 4), + GenesisHash: []byte{0x01}, + BinaryHash: []byte{0x01}, + SpawnTime: now, + UnbondingPeriod: time.Duration(100000000000), + CcvTimeoutPeriod: time.Duration(100000000000), + TransferTimeoutPeriod: time.Duration(100000000000), + ConsumerRedistributionFraction: "0.75", + BlocksPerDistributionTransmission: 10, + HistoricalEntries: 10000, + DistributionTransmissionChannel: "", + }, + valid: true, + }, + { + name: "invalid - zero height", + params: types.ConsumerInitializationParameters{ + InitialHeight: clienttypes.ZeroHeight(), + GenesisHash: []byte{0x01}, + BinaryHash: []byte{0x01}, + SpawnTime: now, + UnbondingPeriod: time.Duration(100000000000), + CcvTimeoutPeriod: time.Duration(100000000000), + TransferTimeoutPeriod: time.Duration(100000000000), + ConsumerRedistributionFraction: "0.75", + BlocksPerDistributionTransmission: 10, + HistoricalEntries: 10000, + DistributionTransmissionChannel: "", + }, + valid: false, + }, + { + name: "invalid - hash too long", + params: types.ConsumerInitializationParameters{ + InitialHeight: clienttypes.NewHeight(3, 4), + GenesisHash: tooLongHash, + BinaryHash: []byte{0x01}, + SpawnTime: now, + UnbondingPeriod: time.Duration(100000000000), + CcvTimeoutPeriod: time.Duration(100000000000), + TransferTimeoutPeriod: time.Duration(100000000000), + ConsumerRedistributionFraction: "0.75", + BlocksPerDistributionTransmission: 10, + HistoricalEntries: 10000, + DistributionTransmissionChannel: "", + }, + valid: false, + }, + { + name: "invalid - zero spawn time", + params: types.ConsumerInitializationParameters{ + InitialHeight: clienttypes.NewHeight(3, 4), + GenesisHash: []byte{0x01}, + BinaryHash: []byte{0x01}, + SpawnTime: time.Time{}, + UnbondingPeriod: time.Duration(100000000000), + CcvTimeoutPeriod: time.Duration(100000000000), + TransferTimeoutPeriod: time.Duration(100000000000), + ConsumerRedistributionFraction: "0.75", + BlocksPerDistributionTransmission: 10, + HistoricalEntries: 10000, + DistributionTransmissionChannel: "", + }, + valid: true, + }, + { + name: "invalid - zero duration", + params: types.ConsumerInitializationParameters{ + InitialHeight: clienttypes.NewHeight(3, 4), + GenesisHash: []byte{0x01}, + BinaryHash: []byte{0x01}, + SpawnTime: now, + UnbondingPeriod: 0, + CcvTimeoutPeriod: time.Duration(100000000000), + TransferTimeoutPeriod: time.Duration(100000000000), + ConsumerRedistributionFraction: "0.75", + BlocksPerDistributionTransmission: 10, + HistoricalEntries: 10000, + DistributionTransmissionChannel: "", + }, + valid: false, + }, + { + name: "invalid -- ConsumerRedistributionFraction > 1", + params: types.ConsumerInitializationParameters{ + InitialHeight: clienttypes.NewHeight(3, 4), + GenesisHash: []byte{0x01}, + BinaryHash: []byte{0x01}, + SpawnTime: now, + UnbondingPeriod: time.Duration(100000000000), + CcvTimeoutPeriod: time.Duration(100000000000), + TransferTimeoutPeriod: time.Duration(100000000000), + ConsumerRedistributionFraction: "1.75", + BlocksPerDistributionTransmission: 10, + HistoricalEntries: 10000, + DistributionTransmissionChannel: "", + }, + valid: false, + }, + { + name: "invalid -- ConsumerRedistributionFraction wrong format", + params: types.ConsumerInitializationParameters{ + InitialHeight: clienttypes.NewHeight(3, 4), + GenesisHash: []byte{0x01}, + BinaryHash: []byte{0x01}, + SpawnTime: now, + UnbondingPeriod: time.Duration(100000000000), + CcvTimeoutPeriod: time.Duration(100000000000), + TransferTimeoutPeriod: time.Duration(100000000000), + ConsumerRedistributionFraction: coolStr, + BlocksPerDistributionTransmission: 10, + HistoricalEntries: 10000, + DistributionTransmissionChannel: "", + }, + valid: false, + }, + { + name: "invalid - BlocksPerDistributionTransmission zero", + params: types.ConsumerInitializationParameters{ + InitialHeight: clienttypes.NewHeight(3, 4), + GenesisHash: []byte{0x01}, + BinaryHash: []byte{0x01}, + SpawnTime: now, + UnbondingPeriod: time.Duration(100000000000), + CcvTimeoutPeriod: time.Duration(100000000000), + TransferTimeoutPeriod: time.Duration(100000000000), + ConsumerRedistributionFraction: "0.75", + BlocksPerDistributionTransmission: 0, + HistoricalEntries: 10000, + DistributionTransmissionChannel: "", + }, + valid: false, + }, + { + name: "invalid - HistoricalEntries zero", + params: types.ConsumerInitializationParameters{ + InitialHeight: clienttypes.NewHeight(3, 4), + GenesisHash: []byte{0x01}, + BinaryHash: []byte{0x01}, + SpawnTime: now, + UnbondingPeriod: time.Duration(100000000000), + CcvTimeoutPeriod: time.Duration(100000000000), + TransferTimeoutPeriod: time.Duration(100000000000), + ConsumerRedistributionFraction: "0.75", + BlocksPerDistributionTransmission: 10, + HistoricalEntries: 0, + DistributionTransmissionChannel: "", + }, + valid: false, + }, + { + name: "invalid - DistributionTransmissionChannel too long", + params: types.ConsumerInitializationParameters{ + InitialHeight: clienttypes.NewHeight(3, 4), + GenesisHash: []byte{0x01}, + BinaryHash: []byte{0x01}, + SpawnTime: now, + UnbondingPeriod: time.Duration(100000000000), + CcvTimeoutPeriod: time.Duration(100000000000), + TransferTimeoutPeriod: time.Duration(100000000000), + ConsumerRedistributionFraction: "0.75", + BlocksPerDistributionTransmission: 10, + HistoricalEntries: 10000, + DistributionTransmissionChannel: coolStr, + }, + valid: false, + }, + } + + for _, tc := range testCases { + err := types.ValidateInitializationParameters(tc.params) + if tc.valid { + require.NoError(t, err, tc.name) + } else { + require.Error(t, err, tc.name) + } + } +} + +func TestValidateConsAddressList(t *testing.T) { + consAddr1 := "cosmosvalcons1qmq08eruchr5sf5s3rwz7djpr5a25f7xw4mceq" + consAddr2 := "cosmosvalcons1nx7n5uh0ztxsynn4sje6eyq2ud6rc6klc96w39" + invalidConsAddr := "cosmosvalcons1nx7n5uh0ztxsynn4sje6ey" + + testCases := []struct { + name string + list []string + maxLength int + valid bool + }{ + { + name: "valid - empty list", + list: []string{}, + maxLength: 10, + valid: true, + }, + { + name: "valid - non-empty list", + list: []string{consAddr1, consAddr2}, + maxLength: 10, + valid: true, + }, + { + name: "invalid - address with wrong format", + list: []string{invalidConsAddr}, + maxLength: 10, + valid: false, + }, + { + name: "invalid - empty address", + list: []string{""}, + maxLength: 10, + valid: false, + }, + { + name: "invalid - list length", + list: []string{consAddr1, consAddr2}, + maxLength: 1, + valid: false, + }, + } + + for _, tc := range testCases { + err := types.ValidateConsAddressList(tc.list, tc.maxLength) + if tc.valid { + require.NoError(t, err, tc.name) + } else { + require.Error(t, err, tc.name) + } + } +} + +func TestValidateByteSlice(t *testing.T) { + testCases := []struct { + name string + slice []byte + maxLength int + valid bool + }{ + { + name: "valid: empty", + slice: []byte{}, + maxLength: 5, + valid: true, + }, + { + name: "invalid: too long", + slice: []byte{0x01, 0x02}, + maxLength: 1, + valid: false, + }, + { + name: "valid", + slice: []byte{0x01, 0x02}, + maxLength: 5, + valid: true, + }, + } + + for _, tc := range testCases { + err := types.ValidateByteSlice(tc.slice, tc.maxLength) + if tc.valid { + require.NoError(t, err, tc.name) + } else { + require.Error(t, err, tc.name) + } + } +} + +func TestMsgCreateConsumerValidateBasic(t *testing.T) { + testCases := []struct { + name string + chainId string + powerShapingParameters *types.PowerShapingParameters + expPass bool + }{ + { + "empty chain id", + "", + nil, // no power-shaping parameters + false, + }, + { + "empty chain id after trimming", + " ", + nil, // no power-shaping parameters + false, + }, + { + "neutron chain id that cannot be reused", + "neutron-1", + nil, // no power-shaping parameters + false, + }, + { + "stride chain id that cannot be reused", + "stride-1", + nil, // no power-shaping parameters + false, + }, + { + "valid chain id", + "somechain-1", + nil, // no power-shaping parameters + true, + }, + { + "valid chain id and invalid power-shaping parameters", + "somechain-1", + &types.PowerShapingParameters{Top_N: 51}, // TopN cannot be > 0 in MsgCreateConsumer + false, + }, + } + + for _, tc := range testCases { + validConsumerMetadata := types.ConsumerMetadata{Name: "name", Description: "description", Metadata: "metadata"} + msg, err := types.NewMsgCreateConsumer("submitter", tc.chainId, validConsumerMetadata, nil, tc.powerShapingParameters) + require.NoError(t, err) + err = msg.ValidateBasic() + if tc.expPass { + require.NoError(t, err, "valid case: %s should not return error. got %w", tc.name, err) + } else { + require.Error(t, err, "invalid case: '%s' must return error but got none", tc.name) + } + } +} + +func TestMsgUpdateConsumerValidateBasic(t *testing.T) { + consAddr1 := "cosmosvalcons1qmq08eruchr5sf5s3rwz7djpr5a25f7xw4mceq" + consAddr2 := "cosmosvalcons1nx7n5uh0ztxsynn4sje6eyq2ud6rc6klc96w39" + consAddr3 := "cosmosvalcons1muys5jyqk4xd27e208nym85kn0t4zjcfeu63fe" + + testCases := []struct { + name string + powerShapingParameters types.PowerShapingParameters + expPass bool + }{ + { + "success", + types.PowerShapingParameters{ + Top_N: 50, + ValidatorsPowerCap: 100, + ValidatorSetCap: 34, + Allowlist: []string{consAddr1}, + Denylist: nil, + MinStake: 0, + AllowInactiveVals: false, + }, + true, + }, + { + "top N is invalid", + types.PowerShapingParameters{ + Top_N: 10, + ValidatorsPowerCap: 0, + ValidatorSetCap: 0, + Allowlist: nil, + Denylist: nil, + }, + false, + }, + { + "validators power cap is invalid", + types.PowerShapingParameters{ + Top_N: 50, + ValidatorsPowerCap: 101, + ValidatorSetCap: 0, + Allowlist: nil, + Denylist: nil, + MinStake: 0, + AllowInactiveVals: false, + }, + false, + }, + { + "valid proposal", + types.PowerShapingParameters{ + Top_N: 54, + ValidatorsPowerCap: 92, + ValidatorSetCap: 0, + Allowlist: []string{consAddr1}, + Denylist: []string{consAddr2, consAddr3}, + MinStake: 0, + AllowInactiveVals: false, + }, + true, + }, + } + + for _, tc := range testCases { + // TODO (PERMISSIONLESS) add more tests + msg, _ := types.NewMsgUpdateConsumer("", "0", "cosmos1p3ucd3ptpw902fluyjzhq3ffgq4ntddac9sa3s", nil, nil, &tc.powerShapingParameters) + err := msg.ValidateBasic() + if tc.expPass { + require.NoError(t, err, "valid case: %s should not return error. got %w", tc.name, err) + } else { + require.Error(t, err, "invalid case: '%s' must return error but got none", tc.name) + } + } +} + +>>>>>>> 0d782959 (feat!: add memo to IBC transfers of ICS rewards (#2290)) func TestMsgAssignConsumerKeyValidateBasic(t *testing.T) { cId1 := cryptoutil.NewCryptoIdentityFromIntSeed(35443543534) cId2 := cryptoutil.NewCryptoIdentityFromIntSeed(65465464564) diff --git a/x/ccv/types/errors.go b/x/ccv/types/errors.go index e492984e48..d155c17c1a 100644 --- a/x/ccv/types/errors.go +++ b/x/ccv/types/errors.go @@ -22,4 +22,10 @@ var ( ErrDuplicateConsumerChain = errorsmod.Register(ModuleName, 14, "consumer chain already exists") ErrConsumerChainNotFound = errorsmod.Register(ModuleName, 15, "consumer chain not found") ErrInvalidDoubleVotingEvidence = errorsmod.Register(ModuleName, 16, "invalid consumer double voting evidence") +<<<<<<< HEAD +======= + ErrStoreKeyNotFound = errorsmod.Register(ModuleName, 17, "store key not found") + ErrStoreUnmarshal = errorsmod.Register(ModuleName, 18, "cannot unmarshal value from store") + ErrInvalidConsumerId = errorsmod.Register(ModuleName, 19, "invalid consumer id") +>>>>>>> 0d782959 (feat!: add memo to IBC transfers of ICS rewards (#2290)) ) diff --git a/x/ccv/types/params.go b/x/ccv/types/params.go index c5b2c800a0..897f5051d5 100644 --- a/x/ccv/types/params.go +++ b/x/ccv/types/params.go @@ -77,6 +77,7 @@ func NewParams(enabled bool, blocksPerDistributionTransmission int64, consumerRedistributionFraction string, historicalEntries int64, consumerUnbondingPeriod time.Duration, rewardDenoms, providerRewardDenoms []string, retryDelayPeriod time.Duration, + consumerId string, ) ConsumerParams { return ConsumerParams{ Enabled: enabled, @@ -93,6 +94,7 @@ func NewParams(enabled bool, blocksPerDistributionTransmission int64, RewardDenoms: rewardDenoms, ProviderRewardDenoms: providerRewardDenoms, RetryDelayPeriod: retryDelayPeriod, + ConsumerId: consumerId, } } @@ -113,6 +115,7 @@ func DefaultParams() ConsumerParams { rewardDenoms, provideRewardDenoms, DefaultRetryDelayPeriod, + "0", ) } @@ -154,6 +157,9 @@ func (p ConsumerParams) Validate() error { if err := ValidateDuration(p.RetryDelayPeriod); err != nil { return err } + if err := ValidateConsumerId(p.ConsumerId); err != nil { + return err + } return nil } diff --git a/x/ccv/types/shared_consumer.pb.go b/x/ccv/types/shared_consumer.pb.go index 92d0d82e6a..5fffae9f37 100644 --- a/x/ccv/types/shared_consumer.pb.go +++ b/x/ccv/types/shared_consumer.pb.go @@ -75,6 +75,8 @@ type ConsumerParams struct { ProviderRewardDenoms []string `protobuf:"bytes,12,rep,name=provider_reward_denoms,json=providerRewardDenoms,proto3" json:"provider_reward_denoms,omitempty"` // The period after which a consumer can retry sending a throttled packet. RetryDelayPeriod time.Duration `protobuf:"bytes,13,opt,name=retry_delay_period,json=retryDelayPeriod,proto3,stdduration" json:"retry_delay_period"` + // The consumer ID of this consumer chain + ConsumerId string `protobuf:"bytes,14,opt,name=consumer_id,json=consumerId,proto3" json:"consumer_id,omitempty"` } func (m *ConsumerParams) Reset() { *m = ConsumerParams{} } @@ -202,6 +204,13 @@ func (m *ConsumerParams) GetRetryDelayPeriod() time.Duration { return 0 } +func (m *ConsumerParams) GetConsumerId() string { + if m != nil { + return m.ConsumerId + } + return "" +} + // ConsumerGenesisState defines shared genesis information between provider and // consumer type ConsumerGenesisState struct { @@ -341,6 +350,7 @@ func init() { } var fileDescriptor_d0a8be0efc64dfbc = []byte{ +<<<<<<< HEAD // 817 bytes of a gzipped FileDescriptorProto 0x1f, 0x8b, 0x08, 0x00, 0x00, 0x00, 0x00, 0x00, 0x02, 0xff, 0x94, 0x55, 0x41, 0x73, 0xdc, 0x34, 0x14, 0x8e, 0xb3, 0x25, 0xdd, 0x68, 0x93, 0xa6, 0x88, 0x50, 0x4c, 0x3a, 0xb3, 0x71, 0x03, 0x87, @@ -393,6 +403,61 @@ var fileDescriptor_d0a8be0efc64dfbc = []byte{ 0x7d, 0xff, 0x64, 0x2e, 0x6c, 0x5a, 0x26, 0x21, 0x53, 0x79, 0xc4, 0x94, 0xc9, 0x95, 0x89, 0xae, 0xcf, 0xe2, 0xf1, 0xd5, 0xcb, 0x51, 0x7d, 0x16, 0xfd, 0xe4, 0x9e, 0x0f, 0x37, 0xf8, 0x93, 0x15, 0x77, 0xa9, 0x3e, 0xfd, 0x3b, 0x00, 0x00, 0xff, 0xff, 0x00, 0xe4, 0xb0, 0x69, 0x66, 0x06, 0x00, +======= + // 833 bytes of a gzipped FileDescriptorProto + 0x1f, 0x8b, 0x08, 0x00, 0x00, 0x00, 0x00, 0x00, 0x02, 0xff, 0x94, 0x55, 0xcf, 0x73, 0xdc, 0x34, + 0x14, 0x8e, 0xb3, 0x25, 0xd9, 0x68, 0xf3, 0xa3, 0x88, 0x50, 0x4c, 0x3a, 0xb3, 0xd9, 0x06, 0x0e, + 0x3b, 0x30, 0xb5, 0x49, 0xe8, 0xc0, 0x0c, 0x37, 0x92, 0x50, 0xda, 0x1e, 0x92, 0xad, 0x13, 0xca, + 0x0c, 0x1c, 0x34, 0xb2, 0xf4, 0x76, 0xad, 0xc1, 0x96, 0x3c, 0x92, 0xec, 0x90, 0x3b, 0x33, 0x5c, + 0x39, 0xf2, 0x27, 0x95, 0x5b, 0x8f, 0x9c, 0x80, 0x49, 0xfe, 0x11, 0xc6, 0xb2, 0xbd, 0xf1, 0x32, + 0x04, 0xda, 0x9b, 0x9f, 0xf4, 0x7d, 0x9f, 0xf5, 0xbd, 0xa7, 0xf7, 0x84, 0x3e, 0x11, 0xd2, 0x82, + 0x66, 0x09, 0x15, 0x92, 0x18, 0x60, 0x85, 0x16, 0xf6, 0x32, 0x64, 0xac, 0x0c, 0xcb, 0xfd, 0xd0, + 0x24, 0x54, 0x03, 0x27, 0x4c, 0x49, 0x53, 0x64, 0xa0, 0x83, 0x5c, 0x2b, 0xab, 0xf0, 0xce, 0xbf, + 0x30, 0x02, 0xc6, 0xca, 0xa0, 0xdc, 0xdf, 0xb9, 0x6f, 0x41, 0x72, 0xd0, 0x99, 0x90, 0x36, 0xa4, + 0x31, 0x13, 0xa1, 0xbd, 0xcc, 0xc1, 0xd4, 0xc4, 0x9d, 0x50, 0xc4, 0x2c, 0x4c, 0xc5, 0x2c, 0xb1, + 0x2c, 0x15, 0x20, 0xad, 0x09, 0x3b, 0xe8, 0x72, 0xbf, 0x13, 0x35, 0x84, 0xe1, 0x4c, 0xa9, 0x59, + 0x0a, 0xa1, 0x8b, 0xe2, 0x62, 0x1a, 0xf2, 0x42, 0x53, 0x2b, 0x94, 0x6c, 0xf6, 0xb7, 0x67, 0x6a, + 0xa6, 0xdc, 0x67, 0x58, 0x7d, 0xd5, 0xab, 0x7b, 0x3f, 0xad, 0xa2, 0xcd, 0xa3, 0xe6, 0xc8, 0x13, + 0xaa, 0x69, 0x66, 0xb0, 0x8f, 0x56, 0x41, 0xd2, 0x38, 0x05, 0xee, 0x7b, 0x23, 0x6f, 0xdc, 0x8f, + 0xda, 0x10, 0x9f, 0xa2, 0x0f, 0xe3, 0x54, 0xb1, 0x1f, 0x0c, 0xc9, 0x41, 0x13, 0x2e, 0x8c, 0xd5, + 0x22, 0x2e, 0xaa, 0x7f, 0x10, 0xab, 0xa9, 0x34, 0x99, 0x30, 0x46, 0x28, 0xe9, 0x2f, 0x8f, 0xbc, + 0x71, 0x2f, 0x7a, 0x50, 0x63, 0x27, 0xa0, 0x8f, 0x3b, 0xc8, 0xf3, 0x0e, 0x10, 0x3f, 0x43, 0x0f, + 0x6e, 0x55, 0x21, 0x2c, 0xa1, 0x52, 0x42, 0xea, 0xf7, 0x46, 0xde, 0x78, 0x2d, 0xda, 0xe5, 0xb7, + 0x88, 0x1c, 0xd5, 0x30, 0xfc, 0x05, 0xda, 0xc9, 0xb5, 0x2a, 0x05, 0x07, 0x4d, 0xa6, 0x00, 0x24, + 0x57, 0x2a, 0x25, 0x94, 0x73, 0x4d, 0x8c, 0xd5, 0xfe, 0x1d, 0x27, 0x72, 0xaf, 0x45, 0x3c, 0x06, + 0x98, 0x28, 0x95, 0x7e, 0xc9, 0xb9, 0x3e, 0xb3, 0x1a, 0x3f, 0x47, 0x98, 0xb1, 0x92, 0x58, 0x91, + 0x81, 0x2a, 0x6c, 0xe5, 0x4e, 0x28, 0xee, 0xbf, 0x35, 0xf2, 0xc6, 0x83, 0x83, 0xf7, 0x83, 0x3a, + 0xb1, 0x41, 0x9b, 0xd8, 0xe0, 0xb8, 0x49, 0xec, 0x61, 0xff, 0xe5, 0x1f, 0xbb, 0x4b, 0xbf, 0xfe, + 0xb9, 0xeb, 0x45, 0x77, 0x19, 0x2b, 0xcf, 0x6b, 0xf6, 0xc4, 0x91, 0xf1, 0xf7, 0xe8, 0x3d, 0xe7, + 0x66, 0x0a, 0xfa, 0x9f, 0xba, 0x2b, 0xaf, 0xaf, 0xfb, 0x6e, 0xab, 0xb1, 0x28, 0xfe, 0x04, 0x8d, + 0xda, 0x7b, 0x46, 0x34, 0x2c, 0xa4, 0x70, 0xaa, 0x29, 0xab, 0x3e, 0xfc, 0x55, 0xe7, 0x78, 0xd8, + 0xe2, 0xa2, 0x05, 0xd8, 0xe3, 0x06, 0x85, 0x1f, 0x22, 0x9c, 0x08, 0x63, 0x95, 0x16, 0x8c, 0xa6, + 0x04, 0xa4, 0xd5, 0x02, 0x8c, 0xdf, 0x77, 0x05, 0x7c, 0xfb, 0x66, 0xe7, 0xab, 0x7a, 0x03, 0x9f, + 0xa0, 0xbb, 0x85, 0x8c, 0x95, 0xe4, 0x42, 0xce, 0x5a, 0x3b, 0x6b, 0xaf, 0x6f, 0x67, 0x6b, 0x4e, + 0x6e, 0x8c, 0x7c, 0x8e, 0xee, 0x19, 0x35, 0xb5, 0x44, 0xe5, 0x96, 0x54, 0x19, 0xb2, 0x89, 0x06, + 0x93, 0xa8, 0x94, 0xfb, 0xa8, 0x3a, 0xfe, 0xe1, 0xb2, 0xef, 0x45, 0xef, 0x54, 0x88, 0xd3, 0xdc, + 0x9e, 0x16, 0xf6, 0xbc, 0xdd, 0xc6, 0x1f, 0xa0, 0x0d, 0x0d, 0x17, 0x54, 0x73, 0xc2, 0x41, 0xaa, + 0xcc, 0xf8, 0x83, 0x51, 0x6f, 0xbc, 0x16, 0xad, 0xd7, 0x8b, 0xc7, 0x6e, 0x0d, 0x3f, 0x42, 0xf3, + 0x82, 0x93, 0x45, 0xf4, 0xba, 0x43, 0x6f, 0xb7, 0xbb, 0x51, 0x97, 0xf5, 0x1c, 0x61, 0x0d, 0x56, + 0x5f, 0x12, 0x0e, 0x29, 0xbd, 0x6c, 0x5d, 0x6e, 0xbc, 0xc1, 0x65, 0x70, 0xf4, 0xe3, 0x8a, 0xdd, + 0xd8, 0xdc, 0x45, 0x83, 0x79, 0xbd, 0x04, 0xf7, 0x37, 0x5d, 0x69, 0x50, 0xbb, 0xf4, 0x94, 0xef, + 0xfd, 0xe6, 0xa1, 0xed, 0xb6, 0x0d, 0xbf, 0x06, 0x09, 0x46, 0x98, 0x33, 0x4b, 0x2d, 0xe0, 0x27, + 0x68, 0x25, 0x77, 0x6d, 0xe9, 0x7a, 0x71, 0x70, 0xf0, 0x51, 0x70, 0xfb, 0x40, 0x09, 0x16, 0x1b, + 0xf9, 0xf0, 0x4e, 0x75, 0xa2, 0xa8, 0xe1, 0xe3, 0x67, 0xa8, 0xdf, 0xda, 0x75, 0x0d, 0x3a, 0x38, + 0x18, 0xff, 0x97, 0xd6, 0xa4, 0xc1, 0x3e, 0x95, 0x53, 0xd5, 0x28, 0xcd, 0xf9, 0xf8, 0x3e, 0x5a, + 0x93, 0x70, 0x41, 0x1c, 0xd3, 0xf5, 0x67, 0x3f, 0xea, 0x4b, 0xb8, 0x38, 0xaa, 0xe2, 0xbd, 0x9f, + 0x97, 0xd1, 0x7a, 0x97, 0x8d, 0x4f, 0xd0, 0x7a, 0x3d, 0xc3, 0x88, 0xa9, 0x3c, 0x35, 0x4e, 0x3e, + 0x0e, 0x44, 0xcc, 0x82, 0xee, 0x84, 0x0b, 0x3a, 0x33, 0xad, 0x72, 0xe3, 0x56, 0x5d, 0x1a, 0xa2, + 0x01, 0xbb, 0x09, 0xf0, 0xb7, 0x68, 0xab, 0x4a, 0x1d, 0x48, 0x53, 0x98, 0x46, 0xb2, 0x36, 0x14, + 0xfc, 0xaf, 0x64, 0x4b, 0xab, 0x55, 0x37, 0xd9, 0x42, 0x8c, 0x4f, 0xd0, 0x96, 0x90, 0xc2, 0x0a, + 0x9a, 0x92, 0x92, 0xa6, 0xc4, 0x80, 0xf5, 0x7b, 0xa3, 0xde, 0x78, 0x70, 0x30, 0xea, 0xea, 0x54, + 0xa3, 0x3a, 0x78, 0x41, 0x53, 0xc1, 0xa9, 0x55, 0xfa, 0x9b, 0x9c, 0x53, 0x0b, 0x4d, 0x86, 0x36, + 0x1a, 0xfa, 0x0b, 0x9a, 0x9e, 0x81, 0x3d, 0x3c, 0x79, 0x79, 0x35, 0xf4, 0x5e, 0x5d, 0x0d, 0xbd, + 0xbf, 0xae, 0x86, 0xde, 0x2f, 0xd7, 0xc3, 0xa5, 0x57, 0xd7, 0xc3, 0xa5, 0xdf, 0xaf, 0x87, 0x4b, + 0xdf, 0x3d, 0x9a, 0x09, 0x9b, 0x14, 0x71, 0xc0, 0x54, 0x16, 0x32, 0x65, 0x32, 0x65, 0xc2, 0x9b, + 0x5a, 0x3c, 0x9c, 0x3f, 0x2d, 0xe5, 0x67, 0xe1, 0x8f, 0xee, 0x7d, 0x71, 0x2f, 0x43, 0xbc, 0xe2, + 0x6e, 0xdd, 0xa7, 0x7f, 0x07, 0x00, 0x00, 0xff, 0xff, 0xbc, 0xf0, 0xde, 0x5e, 0x87, 0x06, 0x00, +>>>>>>> 0d782959 (feat!: add memo to IBC transfers of ICS rewards (#2290)) 0x00, } @@ -416,6 +481,13 @@ func (m *ConsumerParams) MarshalToSizedBuffer(dAtA []byte) (int, error) { _ = i var l int _ = l + if len(m.ConsumerId) > 0 { + i -= len(m.ConsumerId) + copy(dAtA[i:], m.ConsumerId) + i = encodeVarintSharedConsumer(dAtA, i, uint64(len(m.ConsumerId))) + i-- + dAtA[i] = 0x72 + } n1, err1 := github_com_cosmos_gogoproto_types.StdDurationMarshalTo(m.RetryDelayPeriod, dAtA[i-github_com_cosmos_gogoproto_types.SizeOfStdDuration(m.RetryDelayPeriod):]) if err1 != nil { return 0, err1 @@ -693,6 +765,10 @@ func (m *ConsumerParams) Size() (n int) { } l = github_com_cosmos_gogoproto_types.SizeOfStdDuration(m.RetryDelayPeriod) n += 1 + l + sovSharedConsumer(uint64(l)) + l = len(m.ConsumerId) + if l > 0 { + n += 1 + l + sovSharedConsumer(uint64(l)) + } return n } @@ -1152,6 +1228,38 @@ func (m *ConsumerParams) Unmarshal(dAtA []byte) error { return err } iNdEx = postIndex + case 14: + if wireType != 2 { + return fmt.Errorf("proto: wrong wireType = %d for field ConsumerId", wireType) + } + var stringLen uint64 + for shift := uint(0); ; shift += 7 { + if shift >= 64 { + return ErrIntOverflowSharedConsumer + } + if iNdEx >= l { + return io.ErrUnexpectedEOF + } + b := dAtA[iNdEx] + iNdEx++ + stringLen |= uint64(b&0x7F) << shift + if b < 0x80 { + break + } + } + intStringLen := int(stringLen) + if intStringLen < 0 { + return ErrInvalidLengthSharedConsumer + } + postIndex := iNdEx + intStringLen + if postIndex < 0 { + return ErrInvalidLengthSharedConsumer + } + if postIndex > l { + return io.ErrUnexpectedEOF + } + m.ConsumerId = string(dAtA[iNdEx:postIndex]) + iNdEx = postIndex default: iNdEx = preIndex skippy, err := skipSharedConsumer(dAtA[iNdEx:]) diff --git a/x/ccv/types/shared_params.go b/x/ccv/types/shared_params.go index fa2e28c81c..3f7eb9240f 100644 --- a/x/ccv/types/shared_params.go +++ b/x/ccv/types/shared_params.go @@ -2,8 +2,16 @@ package types import ( fmt "fmt" + "strconv" + "strings" "time" +<<<<<<< HEAD +======= + ibchost "github.com/cosmos/ibc-go/v8/modules/core/24-host" + + errorsmod "cosmossdk.io/errors" +>>>>>>> 0d782959 (feat!: add memo to IBC transfers of ICS rewards (#2290)) "cosmossdk.io/math" ibchost "github.com/cosmos/ibc-go/v8/modules/core/24-host" @@ -113,3 +121,18 @@ func CalculateTrustPeriod(unbondingPeriod time.Duration, defaultTrustPeriodFract return trustPeriod, nil } + +// ValidateConsumerId validates the provided consumer id and returns an error if it is not valid +func ValidateConsumerId(consumerId string) error { + if strings.TrimSpace(consumerId) == "" { + return errorsmod.Wrapf(ErrInvalidConsumerId, "consumer id cannot be blank") + } + + // check that `consumerId` corresponds to a `uint64` + _, err := strconv.ParseUint(consumerId, 10, 64) + if err != nil { + return errorsmod.Wrapf(ErrInvalidConsumerId, "consumer id (%s) cannot be parsed: %s", consumerId, err.Error()) + } + + return nil +} diff --git a/x/ccv/types/shared_params_test.go b/x/ccv/types/shared_params_test.go new file mode 100644 index 0000000000..9d2bbfce00 --- /dev/null +++ b/x/ccv/types/shared_params_test.go @@ -0,0 +1,23 @@ +package types_test + +import ( + "testing" + + "github.com/stretchr/testify/require" + + "github.com/cosmos/interchain-security/v6/x/ccv/types" +) + +func TestValidateConsumerId(t *testing.T) { + // empty consumer id + require.Error(t, types.ValidateConsumerId("")) + + // not a `uint64` where `uint64` is in the range [0, 2^64) + require.Error(t, types.ValidateConsumerId("a")) + require.Error(t, types.ValidateConsumerId("-2545")) + require.Error(t, types.ValidateConsumerId("18446744073709551616")) // 2^64 + + // valid consumer id + require.NoError(t, types.ValidateConsumerId("0")) + require.NoError(t, types.ValidateConsumerId("18446744073709551615")) // 2^64 - 1 +} diff --git a/x/ccv/types/wire.go b/x/ccv/types/wire.go index 9c22522b74..ec04399380 100644 --- a/x/ccv/types/wire.go +++ b/x/ccv/types/wire.go @@ -1,6 +1,7 @@ package types import ( + "encoding/json" "fmt" errorsmod "cosmossdk.io/errors" @@ -194,3 +195,54 @@ func NewConsumerPacketData(cpdType ConsumerPacketDataType, data isConsumerPacket Data: data, } } + +type RewardMemo struct { + ConsumerId string `json:"consumerId"` + ChainId string `json:"chainId"` + Memo string `json:"memo"` +} + +func NewRewardMemo(consumerId, chainId, memo string) RewardMemo { + return RewardMemo{ + ConsumerId: consumerId, + ChainId: chainId, + Memo: memo, + } +} + +// CreateTransferMemo creates a memo for the IBC transfer of ICS rewards. +// Note that the memo follows the Fungible Token Transfer v2 standard +// https://github.com/cosmos/ibc/blob/main/spec/app/ics-020-fungible-token-transfer/README.md#using-the-memo-field +func CreateTransferMemo(consumerId, chainId string) (string, error) { + memo := NewRewardMemo(consumerId, chainId, "ICS rewards") + memoBytes, err := json.Marshal(memo) + if err != nil { + return "", err + } + return fmt.Sprintf(`{ + "provider": %s + }`, + string(memoBytes), + ), nil +} + +func GetRewardMemoFromTransferMemo(memo string) (RewardMemo, error) { + memoData := map[string]json.RawMessage{} + err := json.Unmarshal([]byte(memo), &memoData) + if err != nil { + return RewardMemo{}, err + } + + providerMemo, ok := memoData["provider"] + if !ok { + return RewardMemo{}, err + } + + rewardMemo := RewardMemo{} + err = json.Unmarshal([]byte(providerMemo), &rewardMemo) + if err != nil { + return RewardMemo{}, err + } + + return rewardMemo, nil +} diff --git a/x/ccv/types/wire_test.go b/x/ccv/types/wire_test.go index 300ed7071f..3bb0e7b28a 100644 --- a/x/ccv/types/wire_test.go +++ b/x/ccv/types/wire_test.go @@ -223,3 +223,17 @@ func TestVSCMaturedPacketDataWireBytes(t *testing.T) { require.Equal(t, expectedStr, str) } + +func TestCreateTransferMemo(t *testing.T) { + consumerId := "13" + chainId := "chain-13" + + transferMemo, err := types.CreateTransferMemo(consumerId, chainId) + require.NoError(t, err) + + rewardMemo, err := types.GetRewardMemoFromTransferMemo(transferMemo) + require.NoError(t, err) + require.Equal(t, consumerId, rewardMemo.ConsumerId) + require.Equal(t, chainId, rewardMemo.ChainId) + require.Equal(t, "ICS rewards", rewardMemo.Memo) +}