diff --git a/.github/workflows/nightly-e2e.yml b/.github/workflows/nightly-e2e.yml index 96e9273024..efac898687 100644 --- a/.github/workflows/nightly-e2e.yml +++ b/.github/workflows/nightly-e2e.yml @@ -36,7 +36,7 @@ jobs: # Run compatibility tests for different consumer (-cv) and provider (-pv) versions. # Combination of all provider versions with consumer versions are tested. # For new versions to be tested add/modify -pc/-cv parameters. - run: go run ./tests/e2e/... --tc compatibility -pv latest -cv latest -cv v5.2.0 -cv v4.4.0 + run: go run ./tests/e2e/... --tc compatibility -pv latest -cv v5.2.0 -cv v4.4.0 happy-path-test: runs-on: ubuntu-latest timeout-minutes: 20 @@ -309,6 +309,38 @@ jobs: go-version: "1.22" # The Go version to download (if necessary) and use. - name: E2E active set changes run: go run ./tests/e2e/... --tc active-set-changes + permissionless-basic-test: + runs-on: ubuntu-latest + timeout-minutes: 20 + steps: + - uses: actions/setup-go@v5 + with: + go-version: "1.22" + - uses: actions/checkout@v4 + - name: Checkout LFS objects + run: git lfs checkout + - name: Setup Go + uses: actions/setup-go@v5 + with: + go-version: "1.22" # The Go version to download (if necessary) and use. + - name: E2E basic permissionless tests + run: go run ./tests/e2e/... --tc permissionless-ics + permissionless-topN-test: + runs-on: ubuntu-latest + timeout-minutes: 20 + steps: + - uses: actions/setup-go@v5 + with: + go-version: "1.22" + - uses: actions/checkout@v4 + - name: Checkout LFS objects + run: git lfs checkout + - name: Setup Go + uses: actions/setup-go@v5 + with: + go-version: "1.22" # The Go version to download (if necessary) and use. + - name: E2E permissionless TopN tests + run: go run ./tests/e2e/... --tc permissionless-topN inactive-provider-validators-on-consumer-test: runs-on: ubuntu-latest @@ -346,6 +378,8 @@ jobs: - partial-set-security-validators-denylisted-test - partial-set-security-modification-proposal - active-set-changes-test + - permissionless-basic-test + - permissionless-topN-test if: ${{ failure() }} runs-on: ubuntu-latest steps: diff --git a/tests/e2e/actions.go b/tests/e2e/actions.go index 0f31b65e9c..0b9717d1c3 100644 --- a/tests/e2e/actions.go +++ b/tests/e2e/actions.go @@ -395,6 +395,58 @@ func (tr Chain) createConsumerChain(action CreateConsumerChainAction, verbose bo tr.testConfig.chainConfigs[action.ConsumerChain] = consumerChainCfg } +type RemoveConsumerChainAction struct { + Chain ChainID + From ValidatorID + ConsumerChain ChainID +} + +func (tr Chain) removeConsumerChain(action RemoveConsumerChainAction, verbose bool) { + consumerId := tr.testConfig.chainConfigs[action.ConsumerChain].ConsumerId + if consumerId == "" { + log.Fatal("failed removing consumer chain. no consumer-id found for chain: ", + action.ConsumerChain) + } + + // Send consumer chain removal + cmd := tr.target.ExecCommand( + tr.testConfig.chainConfigs[action.Chain].BinaryName, + "tx", "provider", "remove-consumer", string(consumerId), + `--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`, + "--output", "json", + `-y`, + ) + + bz, err := cmd.CombinedOutput() + if err != nil { + log.Println("command failed:", cmd) + log.Fatalf("remove consumer failed error: %s, output: %s", err.Error(), 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 'remove-consumer' returned: ", txResponse) + } + + tr.waitBlocks(action.Chain, 2, 10*time.Second) + +} + type SubmitConsumerAdditionProposalAction struct { PreCCV bool Chain ChainID @@ -884,6 +936,7 @@ type SubmitConsumerModificationProposalAction struct { Denylist []string AllowInactiveVals bool MinStake uint64 + NewOwner string } func (tr Chain) submitConsumerModificationProposal( @@ -900,8 +953,9 @@ func (tr Chain) submitConsumerModificationProposal( authority := "cosmos10d07y265gmmuvt4z0w9aw880jnsr700j6zn9kn" msg := types.MsgUpdateConsumer{ - Owner: authority, - ConsumerId: consumerId, + Owner: authority, + ConsumerId: consumerId, + NewOwnerAddress: action.NewOwner, PowerShapingParameters: &types.PowerShapingParameters{ Top_N: action.TopN, ValidatorsPowerCap: action.ValidatorsPowerCap, @@ -1981,12 +2035,11 @@ func (tr Chain) relayPacketsHermes( action RelayPacketsAction, verbose bool, ) { - // Because `.app_state.provider.params.blocks_per_epoch` is set to 3 in the E2E tests, we wait 4 blocks + // Because `.app_state.provider.params.blocks_per_epoch` is set to 3 in the E2E tests, we wait 3 blocks // before relaying the packets to guarantee that at least one epoch passes and hence any `VSCPacket`s get // queued and are subsequently relayed. - tr.waitBlocks(action.ChainA, 4, 90*time.Second) - tr.waitBlocks(action.ChainB, 4, 90*time.Second) - + tr.waitBlocks(action.ChainA, 3, 90*time.Second) + tr.waitBlocks(action.ChainB, 3, 90*time.Second) // hermes clear packets ibc0 transfer channel-13 cmd := tr.target.ExecCommand("hermes", "clear", "packets", "--chain", string(tr.testConfig.chainConfigs[action.ChainA].ChainId), diff --git a/tests/e2e/main.go b/tests/e2e/main.go index 3a834a482a..7b066a2020 100644 --- a/tests/e2e/main.go +++ b/tests/e2e/main.go @@ -252,6 +252,12 @@ var stepChoices = map[string]StepChoice{ description: "test permissionless ics", testConfig: PermissionlessTestCfg, }, + "permissionless-topN": { + name: "permissionless-topN", + steps: stepsPermissionlessTopN(), + description: "test permissionless ics topN transformation", + testConfig: PermissionlessTestCfg, + }, "inactive-vals-outside-max-validators": { name: "inactive-vals-outside-max-validators", steps: stepsInactiveValsTopNReproduce(), diff --git a/tests/e2e/state.go b/tests/e2e/state.go index d476551ef5..336d4af39c 100644 --- a/tests/e2e/state.go +++ b/tests/e2e/state.go @@ -7,6 +7,7 @@ import ( "log" "os/exec" "regexp" + "sort" "strconv" "time" @@ -56,17 +57,7 @@ func (tr Chain) waitBlocks(chain ChainID, blocks uint, timeout time.Duration) { } startBlock := tr.target.GetBlockHeight(chain) - start := time.Now() - for { - thisBlock := tr.target.GetBlockHeight(chain) - if thisBlock >= startBlock+blocks { - return - } - if time.Since(start) > timeout { - panic(fmt.Sprintf("\n\n\nwaitBlocks method on chain '%s' has timed out after: %s\n\n", chain, timeout)) - } - time.Sleep(time.Second) - } + tr.waitUntilBlock(chain, startBlock+blocks, timeout) } func (tr Chain) waitUntilBlock(chain ChainID, block uint, timeout time.Duration) { @@ -658,8 +649,13 @@ func (tr Commands) GetConsumerChains(chain ChainID) map[ChainID]bool { if phase == types.ConsumerPhase_name[int32(types.CONSUMER_PHASE_INITIALIZED)] || phase == types.ConsumerPhase_name[int32(types.CONSUMER_PHASE_REGISTERED)] || phase == types.ConsumerPhase_name[int32(types.CONSUMER_PHASE_LAUNCHED)] { - id := c.Get("chain_id").String() - chains[ChainID(id)] = true + id := c.Get("consumer_id").String() + for chainRef, cfg := range tr.chainConfigs { + if cfg.ConsumerId == ConsumerID(id) { + // note: 'chainRef' is the reference the test uses and not necessarily matching chain id + chains[chainRef] = true + } + } } } @@ -814,9 +810,11 @@ func (tr Commands) GetHasToValidate( arr := gjson.Get(string(bz), "consumer_ids").Array() chains := []ChainID{} for _, c := range arr { - for _, chain := range tr.chainConfigs { + for chainRef, chain := range tr.chainConfigs { if chain.ConsumerId == ConsumerID(c.String()) { - chains = append(chains, chain.ChainId) + // we report the test chain reference which might not match the chain ID + // to support testing consumer chains with same chain ID + chains = append(chains, chainRef) break } } @@ -903,14 +901,21 @@ func (tr Commands) GetProposedConsumerChains(chain ChainID) []string { arr := gjson.Get(string(bz), "chains").Array() chains := []string{} for _, c := range arr { - cid := c.Get("chain_id").String() phase := c.Get("phase").String() if phase == types.ConsumerPhase_name[int32(types.CONSUMER_PHASE_INITIALIZED)] || phase == types.ConsumerPhase_name[int32(types.CONSUMER_PHASE_REGISTERED)] { - chains = append(chains, cid) + cid := ConsumerID(c.Get("consumer_id").String()) + for chainRef, chainCfg := range tr.chainConfigs { + if chainCfg.ConsumerId == cid { + chains = append(chains, string(chainRef)) + } + } } } + sort.Slice(chains, func(i, j int) bool { + return chains[i] < chains[j] + }) return chains } diff --git a/tests/e2e/steps_permissionless_ics.go b/tests/e2e/steps_permissionless_ics.go index 1abefb2234..27e101ed29 100644 --- a/tests/e2e/steps_permissionless_ics.go +++ b/tests/e2e/steps_permissionless_ics.go @@ -3,6 +3,7 @@ package main import ( "time" + gov "github.com/cosmos/cosmos-sdk/x/gov/types/v1" clienttypes "github.com/cosmos/ibc-go/v8/modules/core/02-client/types" e2e "github.com/cosmos/interchain-security/v6/tests/e2e/testlib" ) @@ -52,7 +53,11 @@ func stepsPermissionlessICS() []Step { TopN: 0, }, }, - State: State{}, + State: State{ + ChainID("provi"): e2e.ChainState{ + ConsumerChains: &map[ChainID]bool{"cons2": true}, // Consumer chain "consu1" is now removed + }, + }, }, { Action: OptInAction{ @@ -70,13 +75,16 @@ func stepsPermissionlessICS() []Step { }, }, }, - // Start another permissionless chain with ChainID `consu` + // Start another permissionless chain by 'alice' // - runs chain "cons1" which is configured with ChainID "consu" // - test that validator 'alice' can opt-in on two chain with same chain ID stepsStartPermissionlessChain( "cons1", "consu", - []string{"consu", "consu"}, // show up both consumer chains "consu" as proposed chains - []ValidatorID{ValidatorID("bob"), ValidatorID("alice")}, 0), // alice already validating 'cons2' + []string{"cons1", "cons2"}, // show up both consumer chains as proposed chains + []ValidatorID{ + ValidatorID("bob"), + ValidatorID("alice")}, // alice already validating 'cons2' + 0), []Step{ { @@ -95,9 +103,10 @@ func stepsPermissionlessICS() []Step { }, }, ChainID("provi"): e2e.ChainState{ + ConsumerChains: &map[ChainID]bool{"cons2": true, "cons1": true}, HasToValidate: &map[ValidatorID][]ChainID{ - ValidatorID("alice"): {"consu"}, - ValidatorID("bob"): {"consu"}, + ValidatorID("alice"): {"cons1"}, // cons2 is not part of it as it did not start + ValidatorID("bob"): {"cons1"}, ValidatorID("carol"): {}, }, }, @@ -157,8 +166,28 @@ func stepsPermissionlessICS() []Step { }, ChainID("provi"): e2e.ChainState{ HasToValidate: &map[ValidatorID][]ChainID{ - ValidatorID("alice"): {"consu"}, - ValidatorID("bob"): {"consu"}, // bob is still a validator on consu chain + ValidatorID("alice"): {"cons1"}, + ValidatorID("bob"): {"cons1"}, // bob is still a validator on consu chain + ValidatorID("carol"): {}, + }, + }, + }, + }, + }, + // Remove permissionless chain + []Step{ + { + Action: RemoveConsumerChainAction{ + Chain: ChainID("provi"), + From: ValidatorID("alice"), + ConsumerChain: ChainID("cons1"), + }, + State: State{ + ChainID("provi"): e2e.ChainState{ + ConsumerChains: &map[ChainID]bool{"cons2": true}, // Consumer chain "consu1" is now removed + HasToValidate: &map[ValidatorID][]ChainID{ + ValidatorID("alice"): {"cons1"}, + ValidatorID("bob"): {"cons1"}, // bob is still a validator on consu chain ValidatorID("carol"): {}, }, }, @@ -168,3 +197,237 @@ func stepsPermissionlessICS() []Step { ) return s } + +// stepsPermissionlessTopN tests transformation of TopN chains to permissionless +func stepsPermissionlessTopN() []Step { + s := concatSteps( + // Start provider and a TopN consumer chain + []Step{ + // Start the provider chain + { + 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"): 100, + ValidatorID("bob"): 200, + ValidatorID("carol"): 300, + }, + }, + }, + }, + // Propose a TopN chain + { + Action: SubmitConsumerAdditionProposalAction{ + Chain: ChainID("provi"), + From: ValidatorID("alice"), + Deposit: 10000001, + ConsumerChain: ChainID("cons1"), + SpawnTime: 0, + InitialHeight: clienttypes.Height{RevisionNumber: 0, RevisionHeight: 1}, + TopN: 100, + }, + 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(), + }, + }, + }, + }, + }, + { + // change the consumer key "carol" is using on the consumer chain to be the one "carol" uses when opting in + Action: AssignConsumerPubKeyAction{ + Chain: ChainID("cons1"), + Validator: ValidatorID("carol"), + ConsumerPubkey: getDefaultValidators()[ValidatorID("carol")].ConsumerValPubKey, + ReconfigureNode: true, + }, + State: State{}, + }, + + // Vote on the proposal + { + 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(), + }, + }, + }, + }, + }, + // Start the chain + { + Action: StartConsumerChainAction{ + ConsumerChain: ChainID("cons1"), + 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}, + }, + }, + State: State{ + ChainID("cons1"): ChainState{ + ValPowers: &map[ValidatorID]uint{ + ValidatorID("alice"): 100, + ValidatorID("bob"): 200, + ValidatorID("carol"): 300, + }, + }, + }, + }, + { + Action: AddIbcConnectionAction{ + ChainA: ChainID("cons1"), + ChainB: ChainID("provi"), + ClientA: 0, + ClientB: 0, + }, + State: State{}, + }, + { + Action: AddIbcChannelAction{ + ChainA: ChainID("cons1"), + ChainB: ChainID("provi"), + ConnectionA: 0, + PortA: "consumer", + PortB: "provider", + Order: "ordered", + }, + State: State{}, + }, + { + Action: RelayPacketsAction{ + ChainA: ChainID("provi"), + ChainB: ChainID("cons1"), + Port: "provider", + Channel: 0, + }, + State: State{ + ChainID("cons1"): ChainState{ + ValPowers: &map[ValidatorID]uint{ + ValidatorID("alice"): 100, + ValidatorID("bob"): 200, + ValidatorID("carol"): 300, + }, + }, + ChainID("provi"): ChainState{ + HasToValidate: &map[ValidatorID][]ChainID{ + ValidatorID("alice"): {"cons1"}, + ValidatorID("bob"): {"cons1"}, + ValidatorID("carol"): {"cons1"}, + }, + }, + }, + }, + }, + // Convert TopN chain "cons1" to a permissionless chain owned by "carol" submitted by "alice" + []Step{ + { + Action: SubmitConsumerModificationProposalAction{ + Chain: ChainID("provi"), + From: ValidatorID("alice"), + Deposit: 10000001, + ConsumerChain: ChainID("cons1"), + NewOwner: getDefaultValidators()[ValidatorID("carol")].DelAddress, + TopN: 0, + }, + State: State{ + ChainID("provi"): ChainState{ + Proposals: &map[uint]Proposal{ + 2: ConsumerAdditionProposal{ + Deposit: 10000001, + Chain: ChainID("consu"), + Status: gov.ProposalStatus_PROPOSAL_STATUS_VOTING_PERIOD.String(), + }, + }, + }, + }, + }, + { + 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: ConsumerAdditionProposal{ + Deposit: 10000001, + Chain: ChainID("consu"), + Status: gov.ProposalStatus_PROPOSAL_STATUS_PASSED.String(), + }, + }, + }, + }, + }, + // Check ownership by denylisting "alice" from the transformed consumer chain by new owner "carol" + { + Action: UpdateConsumerChainAction{ + Chain: ChainID("provi"), + From: ValidatorID("carol"), + ConsumerChain: ChainID("cons1"), + InitParams: nil, + PowerShapingParams: &PowerShapingParameters{ + TopN: 0, + Denylist: []string{getDefaultValidators()[ValidatorID("alice")].ValconsAddress}, + }, + }, + State: State{}, + }, + { + Action: RelayPacketsAction{ + ChainA: ChainID("provi"), + ChainB: ChainID("cons1"), + Port: "provider", + Channel: 0, + }, + State: State{ + ChainID("cons1"): ChainState{ + ValPowers: &map[ValidatorID]uint{ + ValidatorID("alice"): 0, // alice got denylisted + ValidatorID("bob"): 200, + ValidatorID("carol"): 300, + }, + }, + ChainID("provi"): e2e.ChainState{ + HasToValidate: &map[ValidatorID][]ChainID{ + ValidatorID("alice"): {}, // alice is denylisted on "cons1" + ValidatorID("bob"): {"cons1"}, + ValidatorID("carol"): {"cons1"}, + }, + }, + }, + }, + }, + ) + return s +} diff --git a/tests/e2e/test_driver.go b/tests/e2e/test_driver.go index ca2701d1a1..2c3f9c1ac2 100644 --- a/tests/e2e/test_driver.go +++ b/tests/e2e/test_driver.go @@ -125,6 +125,10 @@ func (td *DefaultDriver) getState(modelState State) State { } func (td *DefaultDriver) GetChainState(chain ChainID, modelState ChainState) e2e.ChainState { + if _, exists := td.testCfg.chainConfigs[chain]; !exists { + log.Fatalf("getting chain state failed. unknown chain: '%s'", chain) + } + chainState := ChainState{} chainDriver := td.getTargetDriver(chain) // providerDriver is the target driver for the provider chain @@ -394,6 +398,9 @@ func (td *DefaultDriver) runAction(action interface{}) error { case UpdateConsumerChainAction: target := td.getTargetDriver(action.Chain) target.updateConsumerChain(action, td.verbose) + case RemoveConsumerChainAction: + target := td.getTargetDriver(action.Chain) + target.removeConsumerChain(action, td.verbose) case OptInAction: target := td.getTargetDriver("provider") target.optIn(action, td.verbose)