diff --git a/app/provider/app.go b/app/provider/app.go index 25ec012836..423f96fba9 100644 --- a/app/provider/app.go +++ b/app/provider/app.go @@ -144,7 +144,8 @@ var ( upgradeclient.CancelProposalHandler, ibcclientclient.UpdateClientProposalHandler, ibcclientclient.UpgradeProposalHandler, - ibcproviderclient.ProposalHandler, + ibcproviderclient.ConsumerAdditionProposalHandler, + ibcproviderclient.ConsumerRemovalProposalHandler, ), params.AppModuleBasic{}, crisis.AppModuleBasic{}, diff --git a/tests/integration/actions.go b/tests/integration/actions.go index aeb175af9f..54bca82601 100644 --- a/tests/integration/actions.go +++ b/tests/integration/actions.go @@ -184,7 +184,7 @@ func (tr TestRun) submitTextProposal( } } -type submitConsumerProposalAction struct { +type submitConsumerAdditionProposalAction struct { chain chainID from validatorID deposit uint @@ -194,7 +194,7 @@ type submitConsumerProposalAction struct { } func (tr TestRun) submitConsumerAdditionProposal( - action submitConsumerProposalAction, + action submitConsumerAdditionProposalAction, verbose bool, ) { spawnTime := tr.containerConfig.now.Add(time.Duration(action.spawnTime) * time.Millisecond) @@ -247,6 +247,65 @@ func (tr TestRun) submitConsumerAdditionProposal( } } +type submitConsumerRemovalProposalAction struct { + chain chainID + from validatorID + deposit uint + consumerChain chainID + stopTimeOffset time.Duration // offset from time.Now() +} + +func (tr TestRun) submitConsumerRemovalProposal( + action submitConsumerRemovalProposalAction, + verbose bool, +) { + stopTime := tr.containerConfig.now.Add(action.stopTimeOffset) + prop := client.ConsumerRemovalProposalJSON{ + Title: fmt.Sprintf("Stop the %v chain", action.consumerChain), + Description: "It was a great chain", + ChainId: string(tr.chainConfigs[action.consumerChain].chainId), + StopTime: stopTime, + Deposit: fmt.Sprint(action.deposit) + `stake`, + } + + bz, err := json.Marshal(prop) + if err != nil { + log.Fatal(err) + } + + jsonStr := string(bz) + if strings.Contains(jsonStr, "'") { + log.Fatal("prop json contains single quote") + } + + //#nosec G204 -- Bypass linter warning for spawning subprocess with cmd arguments. + bz, err = exec.Command("docker", "exec", tr.containerConfig.instanceName, + "/bin/bash", "-c", fmt.Sprintf(`echo '%s' > %s`, jsonStr, "/temp-proposal.json")).CombinedOutput() + + if err != nil { + log.Fatal(err, "\n", string(bz)) + } + + //#nosec G204 -- Bypass linter warning for spawning subprocess with cmd arguments. + bz, err = exec.Command("docker", "exec", tr.containerConfig.instanceName, tr.chainConfigs[action.chain].binaryName, + + "tx", "gov", "submit-proposal", "consumer-removal", + "/temp-proposal.json", + + `--from`, `validator`+fmt.Sprint(action.from), + `--chain-id`, string(tr.chainConfigs[action.chain].chainId), + `--home`, tr.getValidatorHome(action.chain, action.from), + `--node`, tr.getValidatorNode(action.chain, action.from), + `--keyring-backend`, `test`, + `-b`, `block`, + `-y`, + ).CombinedOutput() + + if err != nil { + log.Fatal(err, "\n", string(bz)) + } +} + type submitParamChangeProposalAction struct { chain chainID from validatorID diff --git a/tests/integration/main.go b/tests/integration/main.go index 149e894241..bedf0d65b1 100644 --- a/tests/integration/main.go +++ b/tests/integration/main.go @@ -63,8 +63,10 @@ func (tr *TestRun) runStep(step Step, verbose bool) { tr.sendTokens(action, verbose) case submitTextProposalAction: tr.submitTextProposal(action, verbose) - case submitConsumerProposalAction: + case submitConsumerAdditionProposalAction: tr.submitConsumerAdditionProposal(action, verbose) + case submitConsumerRemovalProposalAction: + tr.submitConsumerRemovalProposal(action, verbose) case submitParamChangeProposalAction: tr.submitParamChangeProposal(action, verbose) case voteGovProposalAction: diff --git a/tests/integration/state.go b/tests/integration/state.go index 3e0785cd78..bfd16ca38e 100644 --- a/tests/integration/state.go +++ b/tests/integration/state.go @@ -22,6 +22,7 @@ type ChainState struct { RepresentativePowers *map[validatorID]uint Params *[]Param Rewards *Rewards + ConsumerChains *map[chainID]bool } type Proposal interface { @@ -36,7 +37,7 @@ type TextProposal struct { func (p TextProposal) isProposal() {} -type ConsumerProposal struct { +type ConsumerAdditionProposal struct { Deposit uint Chain chainID SpawnTime int @@ -44,6 +45,17 @@ type ConsumerProposal struct { Status string } +func (p ConsumerAdditionProposal) isProposal() {} + +type ConsumerRemovalProposal struct { + Deposit uint + Chain chainID + StopTime int + Status string +} + +func (p ConsumerRemovalProposal) isProposal() {} + type Rewards struct { IsRewarded map[validatorID]bool //if true it will calculate if the validator/delegator is rewarded between 2 successive blocks, @@ -54,8 +66,6 @@ type Rewards struct { IsNativeDenom bool } -func (p ConsumerProposal) isProposal() {} - type ParamsProposal struct { Deposit uint Status string @@ -115,6 +125,11 @@ func (tr TestRun) getChainState(chain chainID, modelState ChainState) ChainState chainState.Rewards = &rewards } + if modelState.ConsumerChains != nil { + chains := tr.getConsumerChains(chain) + chainState.ConsumerChains = &chains + } + return chainState } @@ -315,7 +330,7 @@ func (tr TestRun) getProposal(chain chainID, proposal uint) Proposal { } } - return ConsumerProposal{ + return ConsumerAdditionProposal{ Deposit: uint(deposit), Status: status, Chain: chain, @@ -325,6 +340,25 @@ func (tr TestRun) getProposal(chain chainID, proposal uint) Proposal { RevisionHeight: gjson.Get(string(bz), `content.initial_height.revision_height`).Uint(), }, } + case "/interchain_security.ccv.provider.v1.ConsumerRemovalProposal": + chainId := gjson.Get(string(bz), `content.chain_id`).String() + stopTime := gjson.Get(string(bz), `content.stop_time`).Time().Sub(tr.containerConfig.now) + + var chain chainID + for i, conf := range tr.chainConfigs { + if string(conf.chainId) == chainId { + chain = i + break + } + } + + return ConsumerRemovalProposal{ + Deposit: uint(deposit), + Status: status, + Chain: chain, + StopTime: int(stopTime.Milliseconds()), + } + case "/cosmos.params.v1beta1.ParameterChangeProposal": return ParamsProposal{ Deposit: uint(deposit), @@ -439,6 +473,32 @@ func (tr TestRun) getParam(chain chainID, param Param) string { return value.String() } +// getConsumerChains returns a list of consumer chains that're being secured by the provider chain, +// determined by querying the provider chain. +func (tr TestRun) getConsumerChains(chain chainID) map[chainID]bool { + //#nosec G204 -- Bypass linter warning for spawning subprocess with cmd arguments. + cmd := exec.Command("docker", "exec", tr.containerConfig.instanceName, tr.chainConfigs[chain].binaryName, + + "query", "provider", "list-consumer-chains", + `--node`, tr.getQueryNode(chain), + `-o`, `json`, + ) + + bz, err := cmd.CombinedOutput() + if err != nil { + log.Fatal(err, "\n", string(bz)) + } + + arr := gjson.Get(string(bz), "chains").Array() + chains := make(map[chainID]bool) + for _, c := range arr { + id := c.Get("chain_id").String() + chains[chainID(id)] = true + } + + return chains +} + func (tr TestRun) getValidatorNode(chain chainID, validator validatorID) string { return "tcp://" + tr.getValidatorIP(chain, validator) + ":26658" } diff --git a/tests/integration/steps.go b/tests/integration/steps.go index d1eb5f8f00..b1c68ee008 100644 --- a/tests/integration/steps.go +++ b/tests/integration/steps.go @@ -18,6 +18,7 @@ var happyPathSteps = concatSteps( stepsDelegate("consu"), stepsUnbondRedelegate("consu"), stepsDowntime("consu"), + stepsStopChain("consu"), ) var democracySteps = concatSteps( diff --git a/tests/integration/steps_start_chains.go b/tests/integration/steps_start_chains.go index fca51fa254..7356f9fe5c 100644 --- a/tests/integration/steps_start_chains.go +++ b/tests/integration/steps_start_chains.go @@ -46,7 +46,7 @@ func stepsStartChains(consumerName string, setupTransferChan bool) []Step { }, }, { - action: submitConsumerProposalAction{ + action: submitConsumerAdditionProposalAction{ chain: chainID("provi"), from: validatorID("alice"), deposit: 10000001, @@ -61,7 +61,7 @@ func stepsStartChains(consumerName string, setupTransferChan bool) []Step { validatorID("bob"): 9500000002, }, Proposals: &map[uint]Proposal{ - 1: ConsumerProposal{ + 1: ConsumerAdditionProposal{ Deposit: 10000001, Chain: chainID(consumerName), SpawnTime: 0, @@ -82,7 +82,7 @@ func stepsStartChains(consumerName string, setupTransferChan bool) []Step { state: State{ chainID("provi"): ChainState{ Proposals: &map[uint]Proposal{ - 1: ConsumerProposal{ + 1: ConsumerAdditionProposal{ Deposit: 10000001, Chain: chainID(consumerName), SpawnTime: 0, diff --git a/tests/integration/steps_stop_chain.go b/tests/integration/steps_stop_chain.go new file mode 100644 index 0000000000..c35a7f28f1 --- /dev/null +++ b/tests/integration/steps_stop_chain.go @@ -0,0 +1,60 @@ +package main + +import "time" + +// submits a consumer-removal proposal and removes the chain +func stepsStopChain(consumerName string) []Step { + s := []Step{ + { + action: submitConsumerRemovalProposalAction{ + chain: chainID("provi"), + from: validatorID("bob"), + deposit: 10000001, + consumerChain: chainID(consumerName), + stopTimeOffset: 0 * time.Millisecond, + }, + state: State{ + chainID("provi"): ChainState{ + ValBalances: &map[validatorID]uint{ + validatorID("bob"): 9490000001, + }, + Proposals: &map[uint]Proposal{ + 2: ConsumerRemovalProposal{ + Deposit: 10000001, + Chain: chainID(consumerName), + StopTime: 0, + Status: "PROPOSAL_STATUS_VOTING_PERIOD", + }, + }, + ConsumerChains: &map[chainID]bool{"consu": true}, // consumer chain not yet removed + }, + }, + }, + { + action: voteGovProposalAction{ + chain: chainID("provi"), + from: []validatorID{validatorID("alice"), validatorID("bob"), validatorID("carol")}, + vote: []string{"yes", "yes", "yes"}, + propNumber: 2, + }, + state: State{ + chainID("provi"): ChainState{ + Proposals: &map[uint]Proposal{ + 2: ConsumerRemovalProposal{ + Deposit: 10000001, + Chain: chainID(consumerName), + StopTime: 0, + Status: "PROPOSAL_STATUS_PASSED", + }, + }, + ValBalances: &map[validatorID]uint{ + validatorID("bob"): 9500000002, + }, + ConsumerChains: &map[chainID]bool{}, // Consumer chain is now removed + }, + }, + }, + } + + return s +} diff --git a/x/ccv/provider/client/proposal_handler.go b/x/ccv/provider/client/proposal_handler.go index 345001c64a..66c20ef233 100644 --- a/x/ccv/provider/client/proposal_handler.go +++ b/x/ccv/provider/client/proposal_handler.go @@ -20,8 +20,10 @@ import ( "github.com/spf13/cobra" ) -// ProposalHandler is the param change proposal handler. -var ProposalHandler = govclient.NewProposalHandler(SubmitConsumerAdditionPropTxCmd, ProposalRESTHandler) +var ( + ConsumerAdditionProposalHandler = govclient.NewProposalHandler(SubmitConsumerAdditionPropTxCmd, ConsumerAdditionProposalRESTHandler) + ConsumerRemovalProposalHandler = govclient.NewProposalHandler(SubmitConsumerRemovalProposalTxCmd, ConsumerRemovalProposalRESTHandler) +) // SubmitConsumerAdditionPropTxCmd returns a CLI command handler for submitting // a consumer addition proposal via a transaction. @@ -35,7 +37,7 @@ Submit a consumer addition proposal along with an initial deposit. The proposal details must be supplied via a JSON file. Example: -$ %s tx gov submit-proposal consumer-addition --from= +$ tx gov submit-proposal consumer-addition --from= Where proposal.json contains: @@ -85,6 +87,59 @@ Where proposal.json contains: } } +// SubmitConsumerRemovalPropTxCmd returns a CLI command handler for submitting +// a consumer addition proposal via a transaction. +func SubmitConsumerRemovalProposalTxCmd() *cobra.Command { + return &cobra.Command{ + Use: "consumer-removal [proposal-file]", + Args: cobra.ExactArgs(1), + Short: "Submit a consumer chain removal proposal", + Long: ` +Submit a consumer chain removal proposal along with an initial deposit. +The proposal details must be supplied via a JSON file. + +Example: +$ tx gov submit-proposal consumer-removal --from= + +Where proposal.json contains: +{ + "title": "Stop the FooChain", + "description": "It was a great chain", + "chain_id": "foochain", + "stop_time": "2022-01-27T15:59:50.121607-08:00", + "deposit": "10000stake" +} + `, RunE: func(cmd *cobra.Command, args []string) error { + clientCtx, err := client.GetClientTxContext(cmd) + if err != nil { + return err + } + + proposal, err := ParseConsumerRemovalProposalJSON(args[0]) + if err != nil { + return err + } + + content := types.NewConsumerRemovalProposal( + proposal.Title, proposal.Description, proposal.ChainId, proposal.StopTime) + + from := clientCtx.GetFromAddress() + + deposit, err := sdk.ParseCoinsNormalized(proposal.Deposit) + if err != nil { + return err + } + + msg, err := govtypes.NewMsgSubmitProposal(content, deposit, from) + if err != nil { + return err + } + + return tx.GenerateOrBroadcastTxCLI(clientCtx, cmd.Flags(), msg) + }, + } +} + type ConsumerAdditionProposalJSON struct { Title string `json:"title"` Description string `json:"description"` @@ -125,16 +180,58 @@ func ParseConsumerAdditionProposalJSON(proposalFile string) (ConsumerAdditionPro return proposal, nil } -// ProposalRESTHandler returns a ProposalRESTHandler that exposes the param -// change REST handler with a given sub-route. -func ProposalRESTHandler(clientCtx client.Context) govrest.ProposalRESTHandler { +type ConsumerRemovalProposalJSON struct { + Title string `json:"title"` + Description string `json:"description"` + ChainId string `json:"chain_id"` + StopTime time.Time `json:"stop_time"` + Deposit string `json:"deposit"` +} + +type ConsumerRemovalProposalReq struct { + BaseReq rest.BaseReq `json:"base_req"` + Proposer sdk.AccAddress `json:"proposer"` + + Title string `json:"title"` + Description string `json:"description"` + ChainId string `json:"chainId"` + + StopTime time.Time `json:"stopTime"` + Deposit sdk.Coins `json:"deposit"` +} + +func ParseConsumerRemovalProposalJSON(proposalFile string) (ConsumerRemovalProposalJSON, error) { + proposal := ConsumerRemovalProposalJSON{} + + contents, err := ioutil.ReadFile(filepath.Clean(proposalFile)) + if err != nil { + return proposal, err + } + + if err := json.Unmarshal(contents, &proposal); err != nil { + return proposal, err + } + + return proposal, nil +} + +// ConsumerAdditionProposalRESTHandler returns a ProposalRESTHandler that exposes the consumer addition rest handler. +func ConsumerAdditionProposalRESTHandler(clientCtx client.Context) govrest.ProposalRESTHandler { return govrest.ProposalRESTHandler{ - SubRoute: "propose_consumer_addition", - Handler: postProposalHandlerFn(clientCtx), + SubRoute: "consumer_addition", + Handler: postConsumerAdditionProposalHandlerFn(clientCtx), } } -func postProposalHandlerFn(clientCtx client.Context) http.HandlerFunc { +// ConsumerRemovalProposalRESTHandler returns a ProposalRESTHandler that exposes the consumer removal rest handler. +func ConsumerRemovalProposalRESTHandler(clientCtx client.Context) govrest.ProposalRESTHandler { + return govrest.ProposalRESTHandler{ + SubRoute: "consumer_removal", + Handler: postConsumerRemovalProposalHandlerFn(clientCtx), + } +} + +func postConsumerAdditionProposalHandlerFn(clientCtx client.Context) http.HandlerFunc { return func(w http.ResponseWriter, r *http.Request) { var req ConsumerAdditionProposalReq if !rest.ReadRESTReq(w, r, clientCtx.LegacyAmino, &req) { @@ -162,3 +259,32 @@ func postProposalHandlerFn(clientCtx client.Context) http.HandlerFunc { tx.WriteGeneratedTxResponse(clientCtx, w, req.BaseReq, msg) } } + +func postConsumerRemovalProposalHandlerFn(clientCtx client.Context) http.HandlerFunc { + return func(w http.ResponseWriter, r *http.Request) { + var req ConsumerRemovalProposalReq + if !rest.ReadRESTReq(w, r, clientCtx.LegacyAmino, &req) { + return + } + + req.BaseReq = req.BaseReq.Sanitize() + if !req.BaseReq.ValidateBasic(w) { + return + } + + content := types.NewConsumerRemovalProposal( + req.Title, req.Description, req.ChainId, req.StopTime, + ) + + msg, err := govtypes.NewMsgSubmitProposal(content, req.Deposit, req.Proposer) + if rest.CheckBadRequestError(w, err) { + return + } + + if rest.CheckBadRequestError(w, msg.ValidateBasic()) { + return + } + + tx.WriteGeneratedTxResponse(clientCtx, w, req.BaseReq, msg) + } +} diff --git a/x/ccv/provider/types/codec.go b/x/ccv/provider/types/codec.go index 25250f68c1..bb55872f19 100644 --- a/x/ccv/provider/types/codec.go +++ b/x/ccv/provider/types/codec.go @@ -11,13 +11,16 @@ import ( func RegisterLegacyAminoCodec(cdc *codec.LegacyAmino) { } -// RegisterInterfaces register the ibc transfer module interfaces to protobuf -// Any. +// RegisterInterfaces registers the provider proposal structs to the interface registry func RegisterInterfaces(registry codectypes.InterfaceRegistry) { registry.RegisterImplementations( (*govtypes.Content)(nil), &ConsumerAdditionProposal{}, ) + registry.RegisterImplementations( + (*govtypes.Content)(nil), + &ConsumerRemovalProposal{}, + ) } var ( diff --git a/x/ccv/provider/types/proposal.go b/x/ccv/provider/types/proposal.go index 7462115b9a..45d32495a7 100644 --- a/x/ccv/provider/types/proposal.go +++ b/x/ccv/provider/types/proposal.go @@ -17,10 +17,12 @@ const ( var ( _ govtypes.Content = &ConsumerAdditionProposal{} + _ govtypes.Content = &ConsumerRemovalProposal{} ) func init() { govtypes.RegisterProposalType(ProposalTypeConsumerAddition) + govtypes.RegisterProposalType(ProposalTypeConsumerRemoval) } // NewConsumerAdditionProposal creates a new consumer addition proposal.