1234567891011121314151617import type {IKeyringPair} from '@polkadot/types/types';18import {usingPlaygrounds, expect, itSub, Pallets, requirePalletsOrSkip} from '@unique/test-utils/util.js';1920async function nodeAddress(name: string) {21 22 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 51 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 57 await Promise.all(initialInvulnerables.map(invulnerable => helper.getSudo().executeExtrinsic(superuser, 'api.tx.collatorSelection.addInvulnerable', [invulnerable], true, {nonce: nonce++})));5859 60 if(!initialInvulnerables.includes(firstInvulnerable)) {61 await helper.getSudo().executeExtrinsic(superuser, 'api.tx.collatorSelection.addInvulnerable', [firstInvulnerable]);62 }63 });64}656667describe('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 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) => {95 96 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) => {141 crowd = await helper.arrange.createCrowd(20, 100n, superuser);142143 144 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 153 expect((await helper.balance.getSubstrateFull(account.address)).reserved).to.be.equal(0n);154155 156 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 161 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 173 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 178 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) => {223 crowd = await helper.arrange.createCrowd(20, 100n, superuser);224225 226 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 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 255 256 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 262 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 318 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 326 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 353 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 360 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 379 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});