1234567891011121314151617import {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 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}535455describe('Integration Test: Collator Selection', () => {56 let superuser: IKeyringPair;57 let previousLicenseBond = 0n;58 let licenseBond = 0n;5960 before(async function() {61 await usingPlaygrounds(async (helper, privateKey) => {62 requirePalletsOrSkip(this, helper, [Pallets.CollatorSelection]);63 superuser = await privateKey('//Alice');6465 previousLicenseBond = await helper.collatorSelection.getLicenseBond();66 licenseBond = 10n * helper.balance.getOneTokenNominal();67 await helper.getSudo().collatorSelection.setLicenseBond(superuser, licenseBond);68 });69 });7071 describe('Dynamic shuffling of collators', () => {72 73 let alice: IKeyringPair;74 let bob: IKeyringPair;7576 let charlie: IKeyringPair;77 let dave: IKeyringPair;7879 before(async function() {80 await usingPlaygrounds(async (helper, privateKey) => {81 82 83 if (await helper.arrange.isDevNode()) this.skip();8485 alice = await privateKey('//Alice');86 bob = await privateKey('//Bob');87 charlie = await privateKey('//Charlie');88 dave = await privateKey('//Dave');8990 expect((await helper.session.setOwnKeysFromAddress(charlie))91 .status.toLowerCase()).to.be.equal('success');92 expect((await helper.session.setOwnKeysFromAddress(dave))93 .status.toLowerCase()).to.be.equal('success');9495 const invulnerables = await helper.collatorSelection.getInvulnerables();96 if (!invulnerables.includes(alice.address) || !invulnerables.includes(bob.address) || invulnerables.length != 2) {97 console.warn('Alice and Bob are not the invulnerables! Reinstating them back. '98 + 'Current invulnerables\' size: ' + invulnerables.length);99100 let nonce = await helper.chain.getNonce(superuser.address);101 await Promise.all([102 helper.getSudo().executeExtrinsic(superuser, 'api.tx.collatorSelection.addInvulnerable', [alice.address], true, {nonce: nonce++}),103 helper.getSudo().executeExtrinsic(superuser, 'api.tx.collatorSelection.addInvulnerable', [bob.address], true, {nonce: nonce++}),104 ]);105106 nonce = await helper.chain.getNonce(superuser.address);107 await Promise.all(invulnerables.map((invulnerable: any) => {108 if (invulnerable == alice.address || invulnerable == bob.address) return new Promise((res) => res);109 return helper.getSudo().executeExtrinsic(superuser, 'api.tx.collatorSelection.removeInvulnerable', [invulnerable], true, {nonce: nonce++});110 }));111 }112 });113 });114115 itSub('Change invulnerables and make sure they start producing blocks', async ({helper}) => {116 let nonce = await helper.chain.getNonce(superuser.address);117 await expect(Promise.all([118 helper.getSudo().executeExtrinsic(superuser, 'api.tx.collatorSelection.addInvulnerable', [charlie.address], true, {nonce: nonce++}),119 helper.getSudo().executeExtrinsic(superuser, 'api.tx.collatorSelection.addInvulnerable', [dave.address], true, {nonce: nonce++}),120 ])).to.be.fulfilled;121122 nonce = await helper.chain.getNonce(superuser.address);123 await expect(Promise.all([124 helper.getSudo().executeExtrinsic(superuser, 'api.tx.collatorSelection.removeInvulnerable', [alice.address], true, {nonce: nonce++}),125 helper.getSudo().executeExtrinsic(superuser, 'api.tx.collatorSelection.removeInvulnerable', [bob.address], true, {nonce: nonce++}),126 ])).to.be.fulfilled;127128 const newInvulnerables = await helper.collatorSelection.getInvulnerables();129 expect(newInvulnerables).to.contain(charlie.address).and.contain(dave.address).and.be.length(2);130131 await helper.wait.newSessions(2);132133 const newValidators = await helper.callRpc('api.query.session.validators');134 expect(newValidators).to.contain(charlie.address).and.contain(dave.address).and.be.length(2);135136 const lastBlockNumber = await helper.chain.getLatestBlockNumber();137 await helper.wait.newBlocks(1);138 const lastCharlieBlock = (await helper.callRpc('api.query.collatorSelection.lastAuthoredBlock', [charlie.address])).toNumber();139 const lastDaveBlock = (await helper.callRpc('api.query.collatorSelection.lastAuthoredBlock', [dave.address])).toNumber();140 expect(lastCharlieBlock >= lastBlockNumber || lastDaveBlock >= lastBlockNumber).to.be.true;141 });142143 after(async () => {144 await usingPlaygrounds(async (helper) => {145 if (await helper.arrange.isDevNode()) return;146147 let nonce = await helper.chain.getNonce(superuser.address);148 await Promise.all([149 helper.getSudo().executeExtrinsic(superuser, 'api.tx.collatorSelection.addInvulnerable', [alice.address], true, {nonce: nonce++}),150 helper.getSudo().executeExtrinsic(superuser, 'api.tx.collatorSelection.addInvulnerable', [bob.address], true, {nonce: nonce++}),151 ]);152153 nonce = await helper.chain.getNonce(superuser.address);154 await Promise.all([155 await helper.getSudo().executeExtrinsic(superuser, 'api.tx.collatorSelection.removeInvulnerable', [charlie.address], true, {nonce: nonce++}),156 await helper.getSudo().executeExtrinsic(superuser, 'api.tx.collatorSelection.removeInvulnerable', [dave.address], true, {nonce: nonce++}),157 ]);158 });159 });160 });161162 describe('Getting and releasing licenses to collate', () => {163 let charlie: IKeyringPair;164 let dave: IKeyringPair;165 let crowd: IKeyringPair[];166167 before(async function() {168 await usingPlaygrounds(async (helper, privateKey) => {169 charlie = await privateKey('//Charlie');170 dave = await privateKey('//Dave');171 crowd = await helper.arrange.createCrowd(20, 100n, superuser);172173 174 expect((await helper.session.setOwnKeysFromAddress(charlie))175 .status.toLowerCase()).to.be.equal('success');176 expect((await helper.session.setOwnKeysFromAddress(dave))177 .status.toLowerCase()).to.be.equal('success');178 await Promise.all(crowd.map(acc => helper.session.setOwnKeysFromAddress(acc)));179 });180 });181182 describe('Positive', () => {183 itSub('Can lease and release a license', async ({helper}) => {184 const account = crowd.pop()!;185186 187 expect((await helper.balance.getSubstrateFull(account.address)).reserved).to.be.equal(0n);188189 190 await helper.collatorSelection.obtainLicense(account);191 expect(await helper.collatorSelection.hasLicense(account.address)).to.be.equal(licenseBond);192 expect((await helper.balance.getSubstrateFull(account.address)).reserved).to.be.equal(licenseBond);193194 195 await helper.collatorSelection.releaseLicense(account);196 expect(await helper.collatorSelection.hasLicense(account.address)).to.be.equal(0n);197198 const balance = await helper.balance.getSubstrateFull(account.address);199 expect(balance.reserved).to.be.equal(0n);200 expect(balance.free > 100n - licenseBond);201 });202203 itSub('Can force revoke a license', async ({helper}) => {204 const account = crowd.pop()!;205206 207 const previousBalance = await helper.balance.getSubstrateFull(account.address);208 await helper.collatorSelection.obtainLicense(account);209 expect(await helper.collatorSelection.hasLicense(account.address)).to.be.equal(licenseBond);210211 212 await helper.getSudo().collatorSelection.forceRevokeLicense(superuser, account.address);213 expect(await helper.collatorSelection.hasLicense(account.address)).to.be.equal(previousBalance.reserved);214215 const balance = await helper.balance.getSubstrateFull(account.address);216 expect(balance.reserved).to.be.equal(previousBalance.reserved);217 expect(balance.free > previousBalance.free - licenseBond);218 });219 });220221 describe('Negative', () => {222 itSub('Cannot get a license without session keys set', async ({helper}) => {223 const [account] = await helper.arrange.createAccounts([100n], superuser);224 await expect(helper.collatorSelection.obtainLicense(account))225 .to.be.rejectedWith(/collatorSelection.ValidatorNotRegistered/);226 });227228 itSub('Cannot register a license twice', async ({helper}) => {229 const account = crowd.pop()!;230 await helper.collatorSelection.obtainLicense(account);231 await expect(helper.collatorSelection.obtainLicense(account))232 .to.be.rejectedWith(/collatorSelection.AlreadyHoldingLicense/);233 });234235 itSub('Cannot release a license twice', async ({helper}) => {236 const account = crowd.pop()!;237 await helper.collatorSelection.obtainLicense(account);238 await helper.collatorSelection.releaseLicense(account);239 await expect(helper.collatorSelection.releaseLicense(account))240 .to.be.rejectedWith(/collatorSelection.NoLicense/);241 });242243 itSub('Cannot force revoke a license as non-sudo', async ({helper}) => {244 const account = crowd.pop()!;245 await helper.collatorSelection.obtainLicense(account);246 await expect(helper.collatorSelection.forceRevokeLicense(superuser, account.address))247 .to.be.rejectedWith(/BadOrigin/);248 });249 });250 });251252 describe('Onboarding, collating, and offboarding as collator candidates', () => {253 254 let charlie: IKeyringPair;255 let dave: IKeyringPair;256 let crowd: IKeyringPair[];257258 before(async function() {259 await usingPlaygrounds(async (helper, privateKey) => {260 charlie = await privateKey('//Charlie');261 dave = await privateKey('//Dave');262 crowd = await helper.arrange.createCrowd(20, 100n, superuser);263264 265 expect((await helper.session.setOwnKeysFromAddress(charlie))266 .status.toLowerCase()).to.be.equal('success');267 expect((await helper.session.setOwnKeysFromAddress(dave))268 .status.toLowerCase()).to.be.equal('success');269 await Promise.all(crowd.map(acc => helper.session.setOwnKeysFromAddress(acc)));270 });271 });272273 describe('Positive', () => {274 itSub('Can onboard and offboard repeatedly', async ({helper}) => {275 const account = crowd.pop()!;276 await helper.collatorSelection.obtainLicense(account);277 await helper.collatorSelection.onboard(account);278 expect(await helper.collatorSelection.getCandidates()).to.be.deep.equal([account.address]);279280 await helper.collatorSelection.offboard(account);281 expect(await helper.collatorSelection.getCandidates()).to.be.deep.equal([]);282283 await helper.collatorSelection.onboard(account);284 expect(await helper.collatorSelection.getCandidates()).to.be.deep.equal([account.address]);285286 await helper.collatorSelection.offboard(account);287 expect(await helper.collatorSelection.getCandidates()).to.be.deep.equal([]);288 });289290 itSub('Dithmarschen', async ({helper}) => {291 292 const account = crowd.pop()!;293 await helper.collatorSelection.obtainLicense(account);294 await helper.collatorSelection.onboard(account);295 expect(await helper.collatorSelection.getCandidates()).to.contain(account.address);296297 298 299 await helper.wait.newSessions(3);300301 expect(await helper.collatorSelection.getCandidates()).to.not.contain(account.address);302 expect(await helper.collatorSelection.hasLicense(account.address)).to.be.equal(0n);303304 305 const balance = await helper.balance.getSubstrateFull(account.address);306 expect(balance.reserved).to.be.equal(0n);307 expect(balance.free < 100n - licenseBond);308 });309 });310311 describe('Negative', () => {312 itSub('Cannot onboard without a license', async ({helper}) => {313 const account = crowd.pop()!;314 await expect(helper.collatorSelection.onboard(account))315 .to.be.rejectedWith(/collatorSelection.NoLicense/);316 });317318 itSub('Cannot offboard without a license', async ({helper}) => {319 const account = crowd.pop()!;320 await expect(helper.collatorSelection.offboard(account))321 .to.be.rejectedWith(/collatorSelection.NotCandidate/);322 });323324 itSub('Cannot offboard while not onboarded', async ({helper}) => {325 const account = crowd.pop()!;326 await helper.collatorSelection.obtainLicense(account);327 await expect(helper.collatorSelection.offboard(account))328 .to.be.rejectedWith(/collatorSelection.NotCandidate/);329 });330331 itSub('Cannot onboard while already onboarded', async ({helper}) => {332 const account = crowd.pop()!;333 await helper.collatorSelection.obtainLicense(account);334 await helper.collatorSelection.onboard(account);335 await expect(helper.collatorSelection.onboard(account))336 .to.be.rejectedWith(/collatorSelection.AlreadyCandidate/);337 });338 });339 });340341 describe('Addition and removal of invulnerables', () => {342 before(async function() {343 await resetInvulnerables();344 });345346 describe('Positive', () => {347 itSub('Adds an invulnerable', async ({helper}) => {348 const [account] = await helper.arrange.createAccounts([10n], superuser);349 const invulnerables = await helper.collatorSelection.getInvulnerables();350351 await helper.session.setOwnKeysFromAddress(account);352 await helper.getSudo().collatorSelection.addInvulnerable(superuser, account.address);353354 const newInvulnerables = await helper.collatorSelection.getInvulnerables();355 expect(invulnerables.concat(account.address)).to.have.all.members(newInvulnerables);356 });357358 itSub('Removes an invulnerable', async ({helper}) => {359 const invulnerables = await helper.collatorSelection.getInvulnerables();360 const lastInvulnerable = invulnerables.pop()!;361362 await helper.getSudo().collatorSelection.removeInvulnerable(superuser, lastInvulnerable);363 const newInvulnerables = await helper.collatorSelection.getInvulnerables();364 365 expect(newInvulnerables).to.have.all.members(invulnerables);366 });367 });368369 describe('Negative', () => {370 itSub('Does not duplicate an invulnerable', async ({helper}) => {371 const invulnerables = await helper.collatorSelection.getInvulnerables();372 373 await expect(helper.getSudo().collatorSelection.addInvulnerable(superuser, invulnerables[0]))374 .to.be.fulfilled;375 const newInvulnerables = await helper.collatorSelection.getInvulnerables();376 expect(newInvulnerables).to.have.all.members(invulnerables);377 });378379 itSub('Cannot remove a non-existent invulnerable', async ({helper}) => {380 const [account] = await helper.arrange.createAccounts([0n], superuser);381 await expect(helper.getSudo().collatorSelection.removeInvulnerable(superuser, account.address))382 .to.be.rejectedWith(/collatorSelection.NotInvulnerable/);383 });384385 itSub('Cannot allow invulnerables to be empty', async ({helper}) => {386 const invulnerables = await helper.collatorSelection.getInvulnerables();387 const lastInvulnerable = invulnerables.pop()!;388389 let nonce = await helper.chain.getNonce(superuser.address);390 await Promise.all(invulnerables.map((i: any) =>391 helper.getSudo().executeExtrinsic(superuser, 'api.tx.collatorSelection.removeInvulnerable', [i], true, {nonce: nonce++})));392393 await expect(helper.getSudo().collatorSelection.removeInvulnerable(superuser, lastInvulnerable))394 .to.be.rejectedWith(/collatorSelection.TooFewInvulnerables/);395396 const newInvulnerables = await helper.collatorSelection.getInvulnerables();397 expect(newInvulnerables).to.be.deep.equal([lastInvulnerable]);398399 400 nonce = await helper.chain.getNonce(superuser.address);401 await Promise.all(invulnerables.map((i: any) =>402 helper.getSudo().executeExtrinsic(superuser, 'api.tx.collatorSelection.addInvulnerable', [i], true, {nonce: nonce++})));403 });404405 itSub('Cannot have too many invulnerables', async ({helper}) => {406 407 408409 const invulnerablesLength = (await helper.collatorSelection.getInvulnerables()).length;410 const invulnerablesUntilLimit = helper.collatorSelection.maxCollators() - invulnerablesLength;411 const newInvulnerables = await helper.arrange.createAccounts(Array(invulnerablesUntilLimit).fill(10n), superuser);412 const [lastInvulnerable] = await helper.arrange.createAccounts([10n], superuser);413414 await Promise.all(newInvulnerables.map((i: IKeyringPair) =>415 helper.session.setOwnKeysFromAddress(i)));416 await helper.session.setOwnKeysFromAddress(lastInvulnerable);417418 let nonce = await helper.chain.getNonce(superuser.address);419 await Promise.all(newInvulnerables.map((i: IKeyringPair) =>420 helper.getSudo().executeExtrinsic(superuser, 'api.tx.collatorSelection.addInvulnerable', [i.address], true, {nonce: nonce++})));421422 await expect(helper.getSudo().collatorSelection.addInvulnerable(superuser, lastInvulnerable.address))423 .to.be.rejectedWith(/collatorSelection.TooManyInvulnerables/);424425 426 nonce = await helper.chain.getNonce(superuser.address);427 await Promise.all(newInvulnerables.map((i: IKeyringPair) =>428 helper.getSudo().executeExtrinsic(superuser, 'api.tx.collatorSelection.removeInvulnerable', [i.address], true, {nonce: nonce++})));429 });430431 itSub('Forbids a non-sudo to add an invulnerable', async ({helper}) => {432 const [account] = await helper.arrange.createAccounts([10n], superuser);433 const invulnerables = await helper.collatorSelection.getInvulnerables();434435 await helper.session.setOwnKeysFromAddress(account);436 await expect(helper.collatorSelection.addInvulnerable(superuser, account.address))437 .to.be.rejectedWith(/BadOrigin/);438439 const newInvulnerables = await helper.collatorSelection.getInvulnerables();440 expect(newInvulnerables).to.be.members(invulnerables);441 });442443 itSub('Forbids a non-sudo to remove an invulnerable', async ({helper}) => {444 const invulnerables = await helper.collatorSelection.getInvulnerables();445 await expect(helper.collatorSelection.removeInvulnerable(superuser, invulnerables[0]))446 .to.be.rejectedWith(/BadOrigin/);447 expect(await helper.collatorSelection.getInvulnerables()).to.have.all.members(invulnerables);448 });449 });450 });451452 after(async () => {453 454 await usingPlaygrounds(async (helper) => {455 if (helper.fetchMissingPalletNames([Pallets.CollatorSelection]).length != 0) return;456457 await helper.getSudo().collatorSelection.setLicenseBond(superuser, previousLicenseBond);458459 const candidates = await helper.collatorSelection.getCandidates();460 let nonce = await helper.chain.getNonce(superuser.address);461 await Promise.all(candidates.map(candidate =>462 helper.getSudo().executeExtrinsic(superuser, 'api.tx.collatorSelection.forceRevokeLicense', [candidate], true, {nonce: nonce++})));463 });464 });465});