git.delta.rocks / unique-network / refs/commits / 7fa45b56518f

difftreelog

source

tests/src/collator-selection/collatorSelection.seqtest.ts19.4 KiBsourcehistory
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 nodeAddress(name: string) {21  // eslint-disable-next-line require-await22  return await usingPlaygrounds(async (helper, _) => {23    const envNodeStash = `RELAY_UNIQUE_NODE_${name.toUpperCase()}_STASH`;2425    const nodeStash = process.env[envNodeStash];26    if(nodeStash) {27      return helper.address.normalizeSubstrateToChainFormat(nodeStash);28    } else {29      throw Error(`"${envNodeStash}" env var is not set`);30    }31  });32}3334async function getInitialInvulnerables() {35  return await Promise.all([36    nodeAddress('alpha'),37    nodeAddress('beta'),38    nodeAddress('gamma'),39    nodeAddress('delta'),40  ]);41}4243async function resetInvulnerables() {44  await usingPlaygrounds(async (helper, privateKey) => {45    const superuser = await privateKey('//Alice');46    const initialInvulnerables = await getInitialInvulnerables();4748    const invulnerables = await helper.collatorSelection.getInvulnerables();4950    // Remove all invulnerables but the first one51    const firstInvulnerable = invulnerables[0];5253    let nonce = await helper.chain.getNonce(superuser.address);54    await Promise.all(invulnerables.slice(1).map(invulnerable => helper.getSudo().executeExtrinsic(superuser, 'api.tx.collatorSelection.removeInvulnerable', [invulnerable], true, {nonce: nonce++})));5556    // Add the initial invulnerables57    await Promise.all(initialInvulnerables.map(invulnerable => helper.getSudo().executeExtrinsic(superuser, 'api.tx.collatorSelection.addInvulnerable', [invulnerable], true, {nonce: nonce++})));5859    // Remove the first invulnerable if it's not an initial one60    if(!initialInvulnerables.includes(firstInvulnerable)) {61      await helper.getSudo().executeExtrinsic(superuser, 'api.tx.collatorSelection.addInvulnerable', [firstInvulnerable]);62    }63  });64}6566// todo:collator Most preferable to launch this test in parallel somehow -- or change the session period (1 hr).67describe('Integration Test: Collator Selection', () => {68  let superuser: IKeyringPair;69  let previousLicenseBond = 0n;70  let licenseBond = 0n;7172  before(async function() {73    if(!process.env.RUN_COLLATOR_TESTS) this.skip();7475    await usingPlaygrounds(async (helper, privateKey) => {76      requirePalletsOrSkip(this, helper, [Pallets.CollatorSelection]);77      superuser = await privateKey('//Alice');7879      previousLicenseBond = await helper.collatorSelection.getLicenseBond();80      licenseBond = 10n * helper.balance.getOneTokenNominal();81      await helper.getSudo().collatorSelection.setLicenseBond(superuser, licenseBond);82    });83  });8485  describe('Dynamic shuffling of collators', () => {86    // These two are the default invulnerables, and should return to be invulnerables after this suite.87    let alphaNode: string;88    let betaNode: string;8990    let gammaNode: string;91    let deltaNode: string;9293    before(async function() {94      await usingPlaygrounds(async (helper, privateKey) => {95        // todo:collator see again if blocks start to be finalized in dev mode96        // Skip the collator block production in dev mode, since the blocks are sealed automatically.97        if(await helper.arrange.isDevNode()) this.skip();9899        [alphaNode, betaNode, gammaNode, deltaNode] = await getInitialInvulnerables();100101        const invulnerables = await helper.collatorSelection.getInvulnerables();102        expect(invulnerables.length, 'Invalid initial invulnerables number').to.be.equal(4);103        expect(invulnerables, 'Invalid initial invulnerables').containSubset([alphaNode, betaNode, gammaNode, deltaNode]);104      });105    });106107    itSub('Change invulnerables and make sure they start producing blocks', async ({helper}) => {108      let nonce = await helper.chain.getNonce(superuser.address);109110      nonce = await helper.chain.getNonce(superuser.address);111      await expect(Promise.all([112        helper.getSudo().executeExtrinsic(superuser, 'api.tx.collatorSelection.removeInvulnerable', [alphaNode], true, {nonce: nonce++}),113        helper.getSudo().executeExtrinsic(superuser, 'api.tx.collatorSelection.removeInvulnerable', [betaNode], true, {nonce: nonce++}),114      ])).to.be.fulfilled;115116      const newInvulnerables = await helper.collatorSelection.getInvulnerables();117      expect(newInvulnerables).to.contain(gammaNode).and.contain(deltaNode).and.be.length(2);118119      await helper.wait.newSessions(2);120121      const newValidators = await helper.callRpc('api.query.session.validators');122      expect(newValidators).to.contain(gammaNode).and.contain(deltaNode).and.be.length(2);123124      const lastBlockNumber = await helper.chain.getLatestBlockNumber();125      await helper.wait.newBlocks(1);126      const lastGammaBlock = (await helper.callRpc('api.query.collatorSelection.lastAuthoredBlock', [gammaNode])).toNumber();127      const lastDeltaBlock = (await helper.callRpc('api.query.collatorSelection.lastAuthoredBlock', [deltaNode])).toNumber();128      expect(lastGammaBlock >= lastBlockNumber || lastDeltaBlock >= lastBlockNumber).to.be.true;129    });130131    after(async () => {132      await resetInvulnerables();133    });134  });135136  describe('Getting and releasing licenses to collate', () => {137    let crowd: IKeyringPair[];138139    before(async function() {140      await usingPlaygrounds(async (helper, privateKey) => {141        crowd = await helper.arrange.createCrowd(20, 100n, superuser);142143        // set session keys for everyone144        await Promise.all(crowd.map(acc => helper.session.setOwnKeysFromAddress(acc)));145      });146    });147148    describe('Positive', () => {149      itSub('Can lease and release a license', async ({helper}) => {150        const account = crowd.pop()!;151152        // make sure it does not have any reserved funds153        expect((await helper.balance.getSubstrateFull(account.address)).reserved).to.be.equal(0n);154155        // getting a license reserves a license bond cost156        await helper.collatorSelection.obtainLicense(account);157        expect(await helper.collatorSelection.hasLicense(account.address)).to.be.equal(licenseBond);158        expect((await helper.balance.getSubstrateFull(account.address)).reserved).to.be.equal(licenseBond);159160        // releasing a license un-reserves the license bond cost161        await helper.collatorSelection.releaseLicense(account);162        expect(await helper.collatorSelection.hasLicense(account.address)).to.be.equal(0n);163164        const balance = await helper.balance.getSubstrateFull(account.address);165        expect(balance.reserved).to.be.equal(0n);166        expect(balance.free > 100n - licenseBond);167      });168169      itSub('Can force revoke a license', async ({helper}) => {170        const account = crowd.pop()!;171172        // getting a license reserves a license bond cost173        const previousBalance = await helper.balance.getSubstrateFull(account.address);174        await helper.collatorSelection.obtainLicense(account);175        expect(await helper.collatorSelection.hasLicense(account.address)).to.be.equal(licenseBond);176177        // force-releasing a license un-reserves the license bond cost as well178        await helper.getSudo().collatorSelection.forceReleaseLicense(superuser, account.address);179        expect(await helper.collatorSelection.hasLicense(account.address)).to.be.equal(previousBalance.reserved);180181        const balance = await helper.balance.getSubstrateFull(account.address);182        expect(balance.reserved).to.be.equal(previousBalance.reserved);183        expect(balance.free > previousBalance.free - licenseBond);184      });185    });186187    describe('Negative', () => {188      itSub('Cannot get a license without session keys set', async ({helper}) => {189        const [account] = await helper.arrange.createAccounts([100n], superuser);190        await expect(helper.collatorSelection.obtainLicense(account))191          .to.be.rejectedWith(/collatorSelection.ValidatorNotRegistered/);192      });193194      itSub('Cannot register a license twice', async ({helper}) => {195        const account = crowd.pop()!;196        await helper.collatorSelection.obtainLicense(account);197        await expect(helper.collatorSelection.obtainLicense(account))198          .to.be.rejectedWith(/collatorSelection.AlreadyHoldingLicense/);199      });200201      itSub('Cannot release a license twice', async ({helper}) => {202        const account = crowd.pop()!;203        await helper.collatorSelection.obtainLicense(account);204        await helper.collatorSelection.releaseLicense(account);205        await expect(helper.collatorSelection.releaseLicense(account))206          .to.be.rejectedWith(/collatorSelection.NoLicense/);207      });208209      itSub('Cannot force revoke a license as non-sudo', async ({helper}) => {210        const account = crowd.pop()!;211        await helper.collatorSelection.obtainLicense(account);212        await expect(helper.collatorSelection.forceReleaseLicense(superuser, account.address))213          .to.be.rejectedWith(/BadOrigin/);214      });215    });216  });217218  describe('Onboarding, collating, and offboarding as collator candidates', () => {219    let crowd: IKeyringPair[];220221    before(async function() {222      await usingPlaygrounds(async (helper, privateKey) => {223        crowd = await helper.arrange.createCrowd(20, 100n, superuser);224225        // set session keys for everyone226        await Promise.all(crowd.map(acc => helper.session.setOwnKeysFromAddress(acc)));227      });228    });229230    describe('Positive', () => {231      itSub('Can onboard and offboard repeatedly', async ({helper}) => {232        const account = crowd.pop()!;233        await helper.collatorSelection.obtainLicense(account);234        await helper.collatorSelection.onboard(account);235        expect(await helper.collatorSelection.getCandidates()).to.be.deep.equal([account.address]);236237        await helper.collatorSelection.offboard(account);238        expect(await helper.collatorSelection.getCandidates()).to.be.deep.equal([]);239240        await helper.collatorSelection.onboard(account);241        expect(await helper.collatorSelection.getCandidates()).to.be.deep.equal([account.address]);242243        await helper.collatorSelection.offboard(account);244        expect(await helper.collatorSelection.getCandidates()).to.be.deep.equal([]);245      });246247      itSub('Penalizes and forfeits license from faulty collators', async ({helper}) => {248        // This one shouldn't even be able to produce blocks.249        const account = crowd.pop()!;250        await helper.collatorSelection.obtainLicense(account);251        await helper.collatorSelection.onboard(account);252        expect(await helper.collatorSelection.getCandidates()).to.contain(account.address);253254        // Wait for 3 new sessions before checking that the collator will be kicked:255        // one to get collator onboarded, and another two for the collator to fail256        await helper.wait.newSessions(3);257258        expect(await helper.collatorSelection.getCandidates()).to.not.contain(account.address);259        expect(await helper.collatorSelection.hasLicense(account.address)).to.be.equal(0n);260261        // The account's reserved funds get slashed as a penalty262        const balance = await helper.balance.getSubstrateFull(account.address);263        expect(balance.reserved).to.be.equal(0n);264        expect(balance.free < 100n - licenseBond);265      });266    });267268    describe('Negative', () => {269      itSub('Cannot onboard without a license', async ({helper}) => {270        const account = crowd.pop()!;271        await expect(helper.collatorSelection.onboard(account))272          .to.be.rejectedWith(/collatorSelection.NoLicense/);273      });274275      itSub('Cannot offboard without a license', async ({helper}) => {276        const account = crowd.pop()!;277        await expect(helper.collatorSelection.offboard(account))278          .to.be.rejectedWith(/collatorSelection.NotCandidate/);279      });280281      itSub('Cannot offboard while not onboarded', async ({helper}) => {282        const account = crowd.pop()!;283        await helper.collatorSelection.obtainLicense(account);284        await expect(helper.collatorSelection.offboard(account))285          .to.be.rejectedWith(/collatorSelection.NotCandidate/);286      });287288      itSub('Cannot onboard while already onboarded', async ({helper}) => {289        const account = crowd.pop()!;290        await helper.collatorSelection.obtainLicense(account);291        await helper.collatorSelection.onboard(account);292        await expect(helper.collatorSelection.onboard(account))293          .to.be.rejectedWith(/collatorSelection.AlreadyCandidate/);294      });295    });296  });297298  describe('Addition and removal of invulnerables', () => {299    describe('Positive', () => {300      itSub('Adds an invulnerable', async ({helper}) => {301        const [account] = await helper.arrange.createAccounts([10n], superuser);302        const invulnerables = await helper.collatorSelection.getInvulnerables();303304        await helper.session.setOwnKeysFromAddress(account);305        await helper.getSudo().collatorSelection.addInvulnerable(superuser, account.address);306307        const newInvulnerables = await helper.collatorSelection.getInvulnerables();308        expect(invulnerables.concat(account.address)).to.have.all.members(newInvulnerables);309      });310311      itSub('Removes an invulnerable', async ({helper}) => {312        const invulnerables = await helper.collatorSelection.getInvulnerables();313        const lastInvulnerable = invulnerables.pop()!;314315        await helper.getSudo().collatorSelection.removeInvulnerable(superuser, lastInvulnerable);316        const newInvulnerables = await helper.collatorSelection.getInvulnerables();317        // invulnerables had its last element removed, so they should be equal318        expect(newInvulnerables).to.have.all.members(invulnerables);319      });320    });321322    describe('Negative', () => {323      itSub('Does not duplicate an invulnerable', async ({helper}) => {324        const invulnerables = await helper.collatorSelection.getInvulnerables();325        // adding an already invulnerable should not fail, but should not duplicate it either326        await expect(helper.getSudo().collatorSelection.addInvulnerable(superuser, invulnerables[0]))327          .to.be.fulfilled;328        const newInvulnerables = await helper.collatorSelection.getInvulnerables();329        expect(newInvulnerables).to.have.all.members(invulnerables);330      });331332      itSub('Cannot remove a non-existent invulnerable', async ({helper}) => {333        const [account] = await helper.arrange.createAccounts([0n], superuser);334        await expect(helper.getSudo().collatorSelection.removeInvulnerable(superuser, account.address))335          .to.be.rejectedWith(/collatorSelection.NotInvulnerable/);336      });337338      itSub('Cannot allow invulnerables to be empty', async ({helper}) => {339        const invulnerables = await helper.collatorSelection.getInvulnerables();340        const lastInvulnerable = invulnerables.pop()!;341342        let nonce = await helper.chain.getNonce(superuser.address);343        await Promise.all(invulnerables.map((i: any) =>344          helper.getSudo().executeExtrinsic(superuser, 'api.tx.collatorSelection.removeInvulnerable', [i], true, {nonce: nonce++})));345346        await expect(helper.getSudo().collatorSelection.removeInvulnerable(superuser, lastInvulnerable))347          .to.be.rejectedWith(/collatorSelection.TooFewInvulnerables/);348349        const newInvulnerables = await helper.collatorSelection.getInvulnerables();350        expect(newInvulnerables).to.be.deep.equal([lastInvulnerable]);351352        // restore the invulnerables to the previous state353        nonce = await helper.chain.getNonce(superuser.address);354        await Promise.all(invulnerables.map((i: any) =>355          helper.getSudo().executeExtrinsic(superuser, 'api.tx.collatorSelection.addInvulnerable', [i], true, {nonce: nonce++})));356      });357358      itSub('Cannot have too many invulnerables', async ({helper}) => {359        // todo:collator make sure that there is enough session time for a set of tests360        // 28 non-functioning collators, teehee.361362        const invulnerablesLength = (await helper.collatorSelection.getInvulnerables()).length;363        const invulnerablesUntilLimit = (await helper.collatorSelection.getDesiredCollators()) - invulnerablesLength;364        const newInvulnerables = await helper.arrange.createAccounts(Array(invulnerablesUntilLimit).fill(10n), superuser);365        const [lastInvulnerable] = await helper.arrange.createAccounts([10n], superuser);366367        await Promise.all(newInvulnerables.map((i: IKeyringPair) =>368          helper.session.setOwnKeysFromAddress(i)));369        await helper.session.setOwnKeysFromAddress(lastInvulnerable);370371        let nonce = await helper.chain.getNonce(superuser.address);372        await Promise.all(newInvulnerables.map((i: IKeyringPair) =>373          helper.getSudo().executeExtrinsic(superuser, 'api.tx.collatorSelection.addInvulnerable', [i.address], true, {nonce: nonce++})));374375        await expect(helper.getSudo().collatorSelection.addInvulnerable(superuser, lastInvulnerable.address))376          .to.be.rejectedWith(/collatorSelection.TooManyInvulnerables/);377378        // restore the invulnerables to the previous state379        nonce = await helper.chain.getNonce(superuser.address);380        await Promise.all(newInvulnerables.map((i: IKeyringPair) =>381          helper.getSudo().executeExtrinsic(superuser, 'api.tx.collatorSelection.removeInvulnerable', [i.address], true, {nonce: nonce++})));382      });383384      itSub('Forbids a non-sudo to add an invulnerable', async ({helper}) => {385        const [account] = await helper.arrange.createAccounts([10n], superuser);386        const invulnerables = await helper.collatorSelection.getInvulnerables();387388        await helper.session.setOwnKeysFromAddress(account);389        await expect(helper.collatorSelection.addInvulnerable(superuser, account.address))390          .to.be.rejectedWith(/BadOrigin/);391392        const newInvulnerables = await helper.collatorSelection.getInvulnerables();393        expect(newInvulnerables).to.be.members(invulnerables);394      });395396      itSub('Forbids a non-sudo to remove an invulnerable', async ({helper}) => {397        const invulnerables = await helper.collatorSelection.getInvulnerables();398        await expect(helper.collatorSelection.removeInvulnerable(superuser, invulnerables[0]))399          .to.be.rejectedWith(/BadOrigin/);400        expect(await helper.collatorSelection.getInvulnerables()).to.have.all.members(invulnerables);401      });402403      after(async function() {404        await resetInvulnerables();405      });406    });407  });408409  after(async function() {410    if(!process.env.RUN_COLLATOR_TESTS) return;411412    await usingPlaygrounds(async (helper) => {413      if(helper.fetchMissingPalletNames([Pallets.CollatorSelection]).length != 0) return;414415      await helper.getSudo().collatorSelection.setLicenseBond(superuser, previousLicenseBond);416417      const candidates = await helper.collatorSelection.getCandidates();418      let nonce = await helper.chain.getNonce(superuser.address);419      await Promise.all(candidates.map(candidate =>420        helper.getSudo().executeExtrinsic(superuser, 'api.tx.collatorSelection.forceReleaseLicense', [candidate], true, {nonce: nonce++})));421    });422  });423});