git.delta.rocks / unique-network / refs/commits / f3f56ccc5d6d

difftreelog

Merge pull request #988 from UniqueNetwork/fix/collector-selection-tests-release-60

Yaroslav Bolyukin2023-09-08parents: #e6eb884 #095843d.patch.diff
in: master

9 files changed

modified.baedeker/collator-selection.jsonnetdiffbeforeafterboth
--- a/.baedeker/collator-selection.jsonnet
+++ b/.baedeker/collator-selection.jsonnet
@@ -31,7 +31,7 @@
 			bin: $.bin,
 			wantedKeys: 'para',
 		},
-		for name in ['alice', 'bob']
+		for name in ['alpha', 'beta', 'gamma', 'delta']
 	},
 };
 
modified.docker/docker-compose.gov.j2diffbeforeafterboth
--- a/.docker/docker-compose.gov.j2
+++ b/.docker/docker-compose.gov.j2
@@ -21,4 +21,4 @@
       options:
         max-size: "1m"
         max-file: "3"
-    command: cargo run --release --features={{ NETWORK }}-runtime,{{ WASM_NAME }}-runtime/gov-test-timings -- --dev -linfo --rpc-cors=all --unsafe-rpc-external
+    command: cargo run --release --features={{ NETWORK }}-runtime,gov-test-timings -- --dev -linfo --rpc-cors=all --unsafe-rpc-external
modified.github/workflows/collator-selection.ymldiffbeforeafterboth
--- a/.github/workflows/collator-selection.yml
+++ b/.github/workflows/collator-selection.yml
@@ -139,7 +139,7 @@
           context: .
           dockerfile: .docker/Dockerfile-unique
           args: |
-            --build-arg RUNTIME_FEATURES=${{ matrix.runtime_features }}
+            --build-arg RUNTIME_FEATURES=${{ matrix.runtime_features }},session-test-timings
             --build-arg RUST_TOOLCHAIN=${{ env.RUST_TOOLCHAIN }}
           dockerhub_username: ${{ secrets.CORE_DOCKERHUB_USERNAME }}
           dockerhub_token: ${{ secrets.CORE_DOCKERHUB_TOKEN }}
@@ -161,7 +161,7 @@
           jpath: |
             .baedeker/vendor
           tla-str: |
-            relay_spec=westend-local            
+            relay_spec=westend-local
           inputs: |
             .baedeker/collator-selection.jsonnet
             snippet:(import 'baedeker-library/ops/rewrites.libsonnet').rewriteNodePaths({'bin/polkadot':{dockerImage:'${{ steps.polkadot.outputs.name }}'}})
modifiednode/cli/Cargo.tomldiffbeforeafterboth
--- a/node/cli/Cargo.toml
+++ b/node/cli/Cargo.toml
@@ -108,3 +108,5 @@
 runtime-benchmarks = ['opal-runtime/runtime-benchmarks', 'polkadot-cli/runtime-benchmarks', 'polkadot-service/runtime-benchmarks', 'quartz-runtime?/runtime-benchmarks', 'sc-service/runtime-benchmarks', 'unique-runtime?/runtime-benchmarks']
 sapphire-runtime = ['quartz-runtime', 'quartz-runtime/become-sapphire']
 try-runtime = ['opal-runtime?/try-runtime', 'quartz-runtime?/try-runtime', 'try-runtime-cli/try-runtime', 'unique-runtime?/try-runtime']
+session-test-timings = ['opal-runtime/session-test-timings', 'quartz-runtime?/session-test-timings', 'unique-runtime?/session-test-timings']
+gov-test-timings = ['opal-runtime/gov-test-timings', 'quartz-runtime?/gov-test-timings', 'unique-runtime?/gov-test-timings']
modifiedruntime/common/config/pallets/mod.rsdiffbeforeafterboth
--- a/runtime/common/config/pallets/mod.rs
+++ b/runtime/common/config/pallets/mod.rs
@@ -125,10 +125,20 @@
 	pub AppPromotionDailyRate: Perbill = Perbill::from_rational(5u32, 10_000);
 	pub const MaxCollators: u32 = MAX_COLLATORS;
 	pub const LicenseBond: Balance = GENESIS_LICENSE_BOND;
+
+	pub const DayRelayBlocks: BlockNumber = RELAY_DAYS;
+}
+
+#[cfg(not(feature = "session-test-timings"))]
+parameter_types! {
 	pub const SessionPeriod: BlockNumber = SESSION_LENGTH;
-	pub const DayRelayBlocks: BlockNumber = RELAY_DAYS;
 }
 
+#[cfg(feature = "session-test-timings")]
+parameter_types! {
+	pub const SessionPeriod: BlockNumber = 5 * MINUTES;
+}
+
 impl pallet_configuration::Config for Runtime {
 	type RuntimeEvent = RuntimeEvent;
 	type Balance = Balance;
modifiedruntime/opal/Cargo.tomldiffbeforeafterboth
--- a/runtime/opal/Cargo.toml
+++ b/runtime/opal/Cargo.toml
@@ -229,6 +229,7 @@
 preimage = []
 refungible = []
 unique-scheduler = []
+session-test-timings = []
 
 ################################################################################
 # local dependencies
modifiedruntime/quartz/Cargo.tomldiffbeforeafterboth
--- a/runtime/quartz/Cargo.toml
+++ b/runtime/quartz/Cargo.toml
@@ -218,6 +218,7 @@
 refungible = []
 unique-scheduler = []
 gov-test-timings = []
+session-test-timings = []
 
 ################################################################################
 # local dependencies
modifiedruntime/unique/Cargo.tomldiffbeforeafterboth
--- a/runtime/unique/Cargo.toml
+++ b/runtime/unique/Cargo.toml
@@ -221,6 +221,7 @@
 refungible = []
 unique-scheduler = []
 gov-test-timings = []
+session-test-timings = []
 
 ################################################################################
 # local dependencies
modifiedtests/src/collator-selection/collatorSelection.seqtest.tsdiffbeforeafterboth
before · tests/src/collator-selection/collatorSelection.seqtest.ts
1// Copyright 2019-2022 Unique Network (Gibraltar) Ltd.2// This file is part of Unique Network.34// Unique Network is free software: you can redistribute it and/or modify5// it under the terms of the GNU General Public License as published by6// the Free Software Foundation, either version 3 of the License, or7// (at your option) any later version.89// Unique Network is distributed in the hope that it will be useful,10// but WITHOUT ANY WARRANTY; without even the implied warranty of11// MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the12// GNU General Public License for more details.1314// You should have received a copy of the GNU General Public License15// along with Unique Network. If not, see <http://www.gnu.org/licenses/>.1617import {IKeyringPair} from '@polkadot/types/types';18import {usingPlaygrounds, expect, itSub, Pallets, requirePalletsOrSkip} from '../util';1920async function resetInvulnerables() {21  await usingPlaygrounds(async (helper, privateKey) => {22    const superuser = await privateKey('//Alice');23    const alice = await privateKey('//Alice');24    const bob = await privateKey('//Bob');25    const invulnerables = await helper.collatorSelection.getInvulnerables();26    if(!invulnerables.includes(alice.address) || !invulnerables.includes(bob.address) || invulnerables.length != 2) {27      console.warn('Alice and Bob are not the invulnerables! Reinstating them back. '28        + 'Current invulnerables\' size: ' + invulnerables.length);2930      let nonce = await helper.chain.getNonce(alice.address);31      // In case there are too many invulnerables already, remove some of them, leaving space for Alice and Bob.32      if(invulnerables.length + 2 >= helper.collatorSelection.maxCollators()) {33        await Promise.all([34          helper.getSudo().executeExtrinsic(superuser, 'api.tx.collatorSelection.removeInvulnerable', [invulnerables.pop()!], true, {nonce: nonce++}),35          helper.getSudo().executeExtrinsic(superuser, 'api.tx.collatorSelection.removeInvulnerable', [invulnerables.pop()!], true, {nonce: nonce++}),36        ]);37      }3839      nonce = await helper.chain.getNonce(alice.address);40      await Promise.all([41        helper.getSudo().executeExtrinsic(superuser, 'api.tx.collatorSelection.addInvulnerable', [alice.address], true, {nonce: nonce++}),42        helper.getSudo().executeExtrinsic(superuser, 'api.tx.collatorSelection.addInvulnerable', [bob.address], true, {nonce: nonce++}),43      ]);4445      nonce = await helper.chain.getNonce(alice.address);46      await Promise.all(invulnerables.map((invulnerable: any) => {47        if(invulnerable == alice.address || invulnerable == bob.address) return new Promise<void>(res => res());48        return helper.getSudo().executeExtrinsic(superuser, 'api.tx.collatorSelection.removeInvulnerable', [invulnerable], true, {nonce: nonce++});49      }));50    }51  });52}5354// todo:collator Most preferable to launch this test in parallel somehow -- or change the session period (1 hr).55describe('Integration Test: Collator Selection', () => {56  let superuser: IKeyringPair;57  let previousLicenseBond = 0n;58  let licenseBond = 0n;5960  before(async function() {61    if(!process.env.RUN_COLLATOR_TESTS) this.skip();6263    await usingPlaygrounds(async (helper, privateKey) => {64      requirePalletsOrSkip(this, helper, [Pallets.CollatorSelection]);65      superuser = await privateKey('//Alice');6667      previousLicenseBond = await helper.collatorSelection.getLicenseBond();68      licenseBond = 10n * helper.balance.getOneTokenNominal();69      await helper.getSudo().collatorSelection.setLicenseBond(superuser, licenseBond);70    });71  });7273  describe('Dynamic shuffling of collators', () => {74    // These two are the default invulnerables, and should return to be invulnerables after this suite.75    let alice: IKeyringPair;76    let bob: IKeyringPair;7778    let charlie: IKeyringPair;79    let dave: IKeyringPair;8081    before(async function() {82      await usingPlaygrounds(async (helper, privateKey) => {83        // todo:collator see again if blocks start to be finalized in dev mode84        // Skip the collator block production in dev mode, since the blocks are sealed automatically.85        if(await helper.arrange.isDevNode()) this.skip();8687        alice = await privateKey('//Alice');88        bob = await privateKey('//Bob');89        charlie = await privateKey('//Charlie');90        dave = await privateKey('//Dave');9192        expect((await helper.session.setOwnKeysFromAddress(charlie))93          .status.toLowerCase()).to.be.equal('success');94        expect((await helper.session.setOwnKeysFromAddress(dave))95          .status.toLowerCase()).to.be.equal('success');9697        const invulnerables = await helper.collatorSelection.getInvulnerables();98        if(!invulnerables.includes(alice.address) || !invulnerables.includes(bob.address) || invulnerables.length != 2) {99          console.warn('Alice and Bob are not the invulnerables! Reinstating them back. '100            + 'Current invulnerables\' size: ' + invulnerables.length);101102          let nonce = await helper.chain.getNonce(superuser.address);103          await Promise.all([104            helper.getSudo().executeExtrinsic(superuser, 'api.tx.collatorSelection.addInvulnerable', [alice.address], true, {nonce: nonce++}),105            helper.getSudo().executeExtrinsic(superuser, 'api.tx.collatorSelection.addInvulnerable', [bob.address], true, {nonce: nonce++}),106          ]);107108          nonce = await helper.chain.getNonce(superuser.address);109          await Promise.all(invulnerables.map((invulnerable: any) => {110            if(invulnerable == alice.address || invulnerable == bob.address) return new Promise((res) => res);111            return helper.getSudo().executeExtrinsic(superuser, 'api.tx.collatorSelection.removeInvulnerable', [invulnerable], true, {nonce: nonce++});112          }));113        }114      });115    });116117    itSub('Change invulnerables and make sure they start producing blocks', async ({helper}) => {118      let nonce = await helper.chain.getNonce(superuser.address);119      await expect(Promise.all([120        helper.getSudo().executeExtrinsic(superuser, 'api.tx.collatorSelection.addInvulnerable', [charlie.address], true, {nonce: nonce++}),121        helper.getSudo().executeExtrinsic(superuser, 'api.tx.collatorSelection.addInvulnerable', [dave.address], true, {nonce: nonce++}),122      ])).to.be.fulfilled;123124      nonce = await helper.chain.getNonce(superuser.address);125      await expect(Promise.all([126        helper.getSudo().executeExtrinsic(superuser, 'api.tx.collatorSelection.removeInvulnerable', [alice.address], true, {nonce: nonce++}),127        helper.getSudo().executeExtrinsic(superuser, 'api.tx.collatorSelection.removeInvulnerable', [bob.address], true, {nonce: nonce++}),128      ])).to.be.fulfilled;129130      const newInvulnerables = await helper.collatorSelection.getInvulnerables();131      expect(newInvulnerables).to.contain(charlie.address).and.contain(dave.address).and.be.length(2);132133      await helper.wait.newSessions(2);134135      const newValidators = await helper.callRpc('api.query.session.validators');136      expect(newValidators).to.contain(charlie.address).and.contain(dave.address).and.be.length(2);137138      const lastBlockNumber = await helper.chain.getLatestBlockNumber();139      await helper.wait.newBlocks(1);140      const lastCharlieBlock = (await helper.callRpc('api.query.collatorSelection.lastAuthoredBlock', [charlie.address])).toNumber();141      const lastDaveBlock = (await helper.callRpc('api.query.collatorSelection.lastAuthoredBlock', [dave.address])).toNumber();142      expect(lastCharlieBlock >= lastBlockNumber || lastDaveBlock >= lastBlockNumber).to.be.true;143    });144145    after(async () => {146      await usingPlaygrounds(async (helper) => {147        if(await helper.arrange.isDevNode()) return;148149        let nonce = await helper.chain.getNonce(superuser.address);150        await Promise.all([151          helper.getSudo().executeExtrinsic(superuser, 'api.tx.collatorSelection.addInvulnerable', [alice.address], true, {nonce: nonce++}),152          helper.getSudo().executeExtrinsic(superuser, 'api.tx.collatorSelection.addInvulnerable', [bob.address], true, {nonce: nonce++}),153        ]);154155        nonce = await helper.chain.getNonce(superuser.address);156        await Promise.all([157          await helper.getSudo().executeExtrinsic(superuser, 'api.tx.collatorSelection.removeInvulnerable', [charlie.address], true, {nonce: nonce++}),158          await helper.getSudo().executeExtrinsic(superuser, 'api.tx.collatorSelection.removeInvulnerable', [dave.address], true, {nonce: nonce++}),159        ]);160      });161    });162  });163164  describe('Getting and releasing licenses to collate', () => {165    let charlie: IKeyringPair;166    let dave: IKeyringPair;167    let crowd: IKeyringPair[];168169    before(async function() {170      await usingPlaygrounds(async (helper, privateKey) => {171        charlie = await privateKey('//Charlie');172        dave = await privateKey('//Dave');173        crowd = await helper.arrange.createCrowd(20, 100n, superuser);174175        // set session keys for everyone176        expect((await helper.session.setOwnKeysFromAddress(charlie))177          .status.toLowerCase()).to.be.equal('success');178        expect((await helper.session.setOwnKeysFromAddress(dave))179          .status.toLowerCase()).to.be.equal('success');180        await Promise.all(crowd.map(acc => helper.session.setOwnKeysFromAddress(acc)));181      });182    });183184    describe('Positive', () => {185      itSub('Can lease and release a license', async ({helper}) => {186        const account = crowd.pop()!;187188        // make sure it does not have any reserved funds189        expect((await helper.balance.getSubstrateFull(account.address)).reserved).to.be.equal(0n);190191        // getting a license reserves a license bond cost192        await helper.collatorSelection.obtainLicense(account);193        expect(await helper.collatorSelection.hasLicense(account.address)).to.be.equal(licenseBond);194        expect((await helper.balance.getSubstrateFull(account.address)).reserved).to.be.equal(licenseBond);195196        // releasing a license un-reserves the license bond cost197        await helper.collatorSelection.releaseLicense(account);198        expect(await helper.collatorSelection.hasLicense(account.address)).to.be.equal(0n);199200        const balance = await helper.balance.getSubstrateFull(account.address);201        expect(balance.reserved).to.be.equal(0n);202        expect(balance.free > 100n - licenseBond);203      });204205      itSub('Can force revoke a license', async ({helper}) => {206        const account = crowd.pop()!;207208        // getting a license reserves a license bond cost209        const previousBalance = await helper.balance.getSubstrateFull(account.address);210        await helper.collatorSelection.obtainLicense(account);211        expect(await helper.collatorSelection.hasLicense(account.address)).to.be.equal(licenseBond);212213        // force-releasing a license un-reserves the license bond cost as well214        await helper.getSudo().collatorSelection.forceReleaseLicense(superuser, account.address);215        expect(await helper.collatorSelection.hasLicense(account.address)).to.be.equal(previousBalance.reserved);216217        const balance = await helper.balance.getSubstrateFull(account.address);218        expect(balance.reserved).to.be.equal(previousBalance.reserved);219        expect(balance.free > previousBalance.free - licenseBond);220      });221    });222223    describe('Negative', () => {224      itSub('Cannot get a license without session keys set', async ({helper}) => {225        const [account] = await helper.arrange.createAccounts([100n], superuser);226        await expect(helper.collatorSelection.obtainLicense(account))227          .to.be.rejectedWith(/collatorSelection.ValidatorNotRegistered/);228      });229230      itSub('Cannot register a license twice', async ({helper}) => {231        const account = crowd.pop()!;232        await helper.collatorSelection.obtainLicense(account);233        await expect(helper.collatorSelection.obtainLicense(account))234          .to.be.rejectedWith(/collatorSelection.AlreadyHoldingLicense/);235      });236237      itSub('Cannot release a license twice', async ({helper}) => {238        const account = crowd.pop()!;239        await helper.collatorSelection.obtainLicense(account);240        await helper.collatorSelection.releaseLicense(account);241        await expect(helper.collatorSelection.releaseLicense(account))242          .to.be.rejectedWith(/collatorSelection.NoLicense/);243      });244245      itSub('Cannot force revoke a license as non-sudo', async ({helper}) => {246        const account = crowd.pop()!;247        await helper.collatorSelection.obtainLicense(account);248        await expect(helper.collatorSelection.forceReleaseLicense(superuser, account.address))249          .to.be.rejectedWith(/BadOrigin/);250      });251    });252  });253254  describe('Onboarding, collating, and offboarding as collator candidates', () => {255    // These two are the default invulnerables, and should return to be invulnerables after this suite.256    let charlie: IKeyringPair;257    let dave: IKeyringPair;258    let crowd: IKeyringPair[];259260    before(async function() {261      await usingPlaygrounds(async (helper, privateKey) => {262        charlie = await privateKey('//Charlie');263        dave = await privateKey('//Dave');264        crowd = await helper.arrange.createCrowd(20, 100n, superuser);265266        // set session keys for everyone267        expect((await helper.session.setOwnKeysFromAddress(charlie))268          .status.toLowerCase()).to.be.equal('success');269        expect((await helper.session.setOwnKeysFromAddress(dave))270          .status.toLowerCase()).to.be.equal('success');271        await Promise.all(crowd.map(acc => helper.session.setOwnKeysFromAddress(acc)));272      });273    });274275    describe('Positive', () => {276      itSub('Can onboard and offboard repeatedly', async ({helper}) => {277        const account = crowd.pop()!;278        await helper.collatorSelection.obtainLicense(account);279        await helper.collatorSelection.onboard(account);280        expect(await helper.collatorSelection.getCandidates()).to.be.deep.equal([account.address]);281282        await helper.collatorSelection.offboard(account);283        expect(await helper.collatorSelection.getCandidates()).to.be.deep.equal([]);284285        await helper.collatorSelection.onboard(account);286        expect(await helper.collatorSelection.getCandidates()).to.be.deep.equal([account.address]);287288        await helper.collatorSelection.offboard(account);289        expect(await helper.collatorSelection.getCandidates()).to.be.deep.equal([]);290      });291292      itSub('Penalizes and forfeits license from faulty collators', async ({helper}) => {293        // This one shouldn't even be able to produce blocks.294        const account = crowd.pop()!;295        await helper.collatorSelection.obtainLicense(account);296        await helper.collatorSelection.onboard(account);297        expect(await helper.collatorSelection.getCandidates()).to.contain(account.address);298299        // Wait for 3 new sessions before checking that the collator will be kicked:300        // one to get collator onboarded, and another two for the collator to fail301        await helper.wait.newSessions(3);302303        expect(await helper.collatorSelection.getCandidates()).to.not.contain(account.address);304        expect(await helper.collatorSelection.hasLicense(account.address)).to.be.equal(0n);305306        // The account's reserved funds get slashed as a penalty307        const balance = await helper.balance.getSubstrateFull(account.address);308        expect(balance.reserved).to.be.equal(0n);309        expect(balance.free < 100n - licenseBond);310      });311    });312313    describe('Negative', () => {314      itSub('Cannot onboard without a license', async ({helper}) => {315        const account = crowd.pop()!;316        await expect(helper.collatorSelection.onboard(account))317          .to.be.rejectedWith(/collatorSelection.NoLicense/);318      });319320      itSub('Cannot offboard without a license', async ({helper}) => {321        const account = crowd.pop()!;322        await expect(helper.collatorSelection.offboard(account))323          .to.be.rejectedWith(/collatorSelection.NotCandidate/);324      });325326      itSub('Cannot offboard while not onboarded', async ({helper}) => {327        const account = crowd.pop()!;328        await helper.collatorSelection.obtainLicense(account);329        await expect(helper.collatorSelection.offboard(account))330          .to.be.rejectedWith(/collatorSelection.NotCandidate/);331      });332333      itSub('Cannot onboard while already onboarded', async ({helper}) => {334        const account = crowd.pop()!;335        await helper.collatorSelection.obtainLicense(account);336        await helper.collatorSelection.onboard(account);337        await expect(helper.collatorSelection.onboard(account))338          .to.be.rejectedWith(/collatorSelection.AlreadyCandidate/);339      });340    });341  });342343  describe('Addition and removal of invulnerables', () => {344    before(async function() {345      await resetInvulnerables();346    });347348    describe('Positive', () => {349      itSub('Adds an invulnerable', async ({helper}) => {350        const [account] = await helper.arrange.createAccounts([10n], superuser);351        const invulnerables = await helper.collatorSelection.getInvulnerables();352353        await helper.session.setOwnKeysFromAddress(account);354        await helper.getSudo().collatorSelection.addInvulnerable(superuser, account.address);355356        const newInvulnerables = await helper.collatorSelection.getInvulnerables();357        expect(invulnerables.concat(account.address)).to.have.all.members(newInvulnerables);358      });359360      itSub('Removes an invulnerable', async ({helper}) => {361        const invulnerables = await helper.collatorSelection.getInvulnerables();362        const lastInvulnerable = invulnerables.pop()!;363364        await helper.getSudo().collatorSelection.removeInvulnerable(superuser, lastInvulnerable);365        const newInvulnerables = await helper.collatorSelection.getInvulnerables();366        // invulnerables had its last element removed, so they should be equal367        expect(newInvulnerables).to.have.all.members(invulnerables);368      });369    });370371    describe('Negative', () => {372      itSub('Does not duplicate an invulnerable', async ({helper}) => {373        const invulnerables = await helper.collatorSelection.getInvulnerables();374        // adding an already invulnerable should not fail, but should not duplicate it either375        await expect(helper.getSudo().collatorSelection.addInvulnerable(superuser, invulnerables[0]))376          .to.be.fulfilled;377        const newInvulnerables = await helper.collatorSelection.getInvulnerables();378        expect(newInvulnerables).to.have.all.members(invulnerables);379      });380381      itSub('Cannot remove a non-existent invulnerable', async ({helper}) => {382        const [account] = await helper.arrange.createAccounts([0n], superuser);383        await expect(helper.getSudo().collatorSelection.removeInvulnerable(superuser, account.address))384          .to.be.rejectedWith(/collatorSelection.NotInvulnerable/);385      });386387      itSub('Cannot allow invulnerables to be empty', async ({helper}) => {388        const invulnerables = await helper.collatorSelection.getInvulnerables();389        const lastInvulnerable = invulnerables.pop()!;390391        let nonce = await helper.chain.getNonce(superuser.address);392        await Promise.all(invulnerables.map((i: any) =>393          helper.getSudo().executeExtrinsic(superuser, 'api.tx.collatorSelection.removeInvulnerable', [i], true, {nonce: nonce++})));394395        await expect(helper.getSudo().collatorSelection.removeInvulnerable(superuser, lastInvulnerable))396          .to.be.rejectedWith(/collatorSelection.TooFewInvulnerables/);397398        const newInvulnerables = await helper.collatorSelection.getInvulnerables();399        expect(newInvulnerables).to.be.deep.equal([lastInvulnerable]);400401        // restore the invulnerables to the previous state402        nonce = await helper.chain.getNonce(superuser.address);403        await Promise.all(invulnerables.map((i: any) =>404          helper.getSudo().executeExtrinsic(superuser, 'api.tx.collatorSelection.addInvulnerable', [i], true, {nonce: nonce++})));405      });406407      itSub('Cannot have too many invulnerables', async ({helper}) => {408        // todo:collator make sure that there is enough session time for a set of tests409        // 28 non-functioning collators, teehee.410411        const invulnerablesLength = (await helper.collatorSelection.getInvulnerables()).length;412        const invulnerablesUntilLimit = helper.collatorSelection.maxCollators() - invulnerablesLength;413        const newInvulnerables = await helper.arrange.createAccounts(Array(invulnerablesUntilLimit).fill(10n), superuser);414        const [lastInvulnerable] = await helper.arrange.createAccounts([10n], superuser);415416        await Promise.all(newInvulnerables.map((i: IKeyringPair) =>417          helper.session.setOwnKeysFromAddress(i)));418        await helper.session.setOwnKeysFromAddress(lastInvulnerable);419420        let nonce = await helper.chain.getNonce(superuser.address);421        await Promise.all(newInvulnerables.map((i: IKeyringPair) =>422          helper.getSudo().executeExtrinsic(superuser, 'api.tx.collatorSelection.addInvulnerable', [i.address], true, {nonce: nonce++})));423424        await expect(helper.getSudo().collatorSelection.addInvulnerable(superuser, lastInvulnerable.address))425          .to.be.rejectedWith(/collatorSelection.TooManyInvulnerables/);426427        // restore the invulnerables to the previous state428        nonce = await helper.chain.getNonce(superuser.address);429        await Promise.all(newInvulnerables.map((i: IKeyringPair) =>430          helper.getSudo().executeExtrinsic(superuser, 'api.tx.collatorSelection.removeInvulnerable', [i.address], true, {nonce: nonce++})));431      });432433      itSub('Forbids a non-sudo to add an invulnerable', async ({helper}) => {434        const [account] = await helper.arrange.createAccounts([10n], superuser);435        const invulnerables = await helper.collatorSelection.getInvulnerables();436437        await helper.session.setOwnKeysFromAddress(account);438        await expect(helper.collatorSelection.addInvulnerable(superuser, account.address))439          .to.be.rejectedWith(/BadOrigin/);440441        const newInvulnerables = await helper.collatorSelection.getInvulnerables();442        expect(newInvulnerables).to.be.members(invulnerables);443      });444445      itSub('Forbids a non-sudo to remove an invulnerable', async ({helper}) => {446        const invulnerables = await helper.collatorSelection.getInvulnerables();447        await expect(helper.collatorSelection.removeInvulnerable(superuser, invulnerables[0]))448          .to.be.rejectedWith(/BadOrigin/);449        expect(await helper.collatorSelection.getInvulnerables()).to.have.all.members(invulnerables);450      });451    });452  });453454  after(async function() {455    if(!process.env.RUN_COLLATOR_TESTS) return;456457    await usingPlaygrounds(async (helper) => {458      if(helper.fetchMissingPalletNames([Pallets.CollatorSelection]).length != 0) return;459460      await helper.getSudo().collatorSelection.setLicenseBond(superuser, previousLicenseBond);461462      const candidates = await helper.collatorSelection.getCandidates();463      let nonce = await helper.chain.getNonce(superuser.address);464      await Promise.all(candidates.map(candidate =>465        helper.getSudo().executeExtrinsic(superuser, 'api.tx.collatorSelection.forceReleaseLicense', [candidate], true, {nonce: nonce++})));466    });467  });468});