difftreelog
Merge pull request #988 from UniqueNetwork/fix/collector-selection-tests-release-60
in: master
9 files changed
.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']
},
};
.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
.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 }}'}})
node/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']
runtime/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;
runtime/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
runtime/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
runtime/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
tests/src/collator-selection/collatorSelection.seqtest.tsdiffbeforeafterboth1// 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});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});