--- 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'] }, }; --- 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 --- 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 }}'}}) --- 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'] --- 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; --- a/runtime/opal/Cargo.toml +++ b/runtime/opal/Cargo.toml @@ -229,6 +229,7 @@ preimage = [] refungible = [] unique-scheduler = [] +session-test-timings = [] ################################################################################ # local dependencies --- 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 --- 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 --- a/tests/src/collator-selection/collatorSelection.seqtest.ts +++ b/tests/src/collator-selection/collatorSelection.seqtest.ts @@ -17,36 +17,48 @@ import {IKeyringPair} from '@polkadot/types/types'; import {usingPlaygrounds, expect, itSub, Pallets, requirePalletsOrSkip} from '../util'; +async function nodeAddress(name: string) { + // eslint-disable-next-line require-await + return await usingPlaygrounds(async (helper, _) => { + const envNodeStash = `RELAY_UNIQUE_NODE_${name.toUpperCase()}_STASH`; + + const nodeStash = process.env[envNodeStash]; + if(nodeStash) { + return helper.address.normalizeSubstrateToChainFormat(nodeStash); + } else { + throw Error(`"${envNodeStash}" env var is not set`); + } + }); +} + +async function getInitialInvulnerables() { + return await Promise.all([ + nodeAddress('alpha'), + nodeAddress('beta'), + nodeAddress('gamma'), + nodeAddress('delta'), + ]); +} + async function resetInvulnerables() { await usingPlaygrounds(async (helper, privateKey) => { const superuser = await privateKey('//Alice'); - const alice = await privateKey('//Alice'); - const bob = await privateKey('//Bob'); + const initialInvulnerables = await getInitialInvulnerables(); + const invulnerables = await helper.collatorSelection.getInvulnerables(); - if(!invulnerables.includes(alice.address) || !invulnerables.includes(bob.address) || invulnerables.length != 2) { - console.warn('Alice and Bob are not the invulnerables! Reinstating them back. ' - + 'Current invulnerables\' size: ' + invulnerables.length); - let nonce = await helper.chain.getNonce(alice.address); - // In case there are too many invulnerables already, remove some of them, leaving space for Alice and Bob. - if(invulnerables.length + 2 >= helper.collatorSelection.maxCollators()) { - await Promise.all([ - helper.getSudo().executeExtrinsic(superuser, 'api.tx.collatorSelection.removeInvulnerable', [invulnerables.pop()!], true, {nonce: nonce++}), - helper.getSudo().executeExtrinsic(superuser, 'api.tx.collatorSelection.removeInvulnerable', [invulnerables.pop()!], true, {nonce: nonce++}), - ]); - } + // Remove all invulnerables but the first one + const firstInvulnerable = invulnerables[0]; + + let nonce = await helper.chain.getNonce(superuser.address); + await Promise.all(invulnerables.slice(1).map(invulnerable => helper.getSudo().executeExtrinsic(superuser, 'api.tx.collatorSelection.removeInvulnerable', [invulnerable], true, {nonce: nonce++}))); - nonce = await helper.chain.getNonce(alice.address); - await Promise.all([ - helper.getSudo().executeExtrinsic(superuser, 'api.tx.collatorSelection.addInvulnerable', [alice.address], true, {nonce: nonce++}), - helper.getSudo().executeExtrinsic(superuser, 'api.tx.collatorSelection.addInvulnerable', [bob.address], true, {nonce: nonce++}), - ]); + // Add the initial invulnerables + await Promise.all(initialInvulnerables.map(invulnerable => helper.getSudo().executeExtrinsic(superuser, 'api.tx.collatorSelection.addInvulnerable', [invulnerable], true, {nonce: nonce++}))); - nonce = await helper.chain.getNonce(alice.address); - await Promise.all(invulnerables.map((invulnerable: any) => { - if(invulnerable == alice.address || invulnerable == bob.address) return new Promise(res => res()); - return helper.getSudo().executeExtrinsic(superuser, 'api.tx.collatorSelection.removeInvulnerable', [invulnerable], true, {nonce: nonce++}); - })); + // Remove the first invulnerable if it's not an initial one + if(!initialInvulnerables.includes(firstInvulnerable)) { + await helper.getSudo().executeExtrinsic(superuser, 'api.tx.collatorSelection.addInvulnerable', [firstInvulnerable]); } }); } @@ -72,11 +84,11 @@ describe('Dynamic shuffling of collators', () => { // These two are the default invulnerables, and should return to be invulnerables after this suite. - let alice: IKeyringPair; - let bob: IKeyringPair; + let alphaNode: string; + let betaNode: string; - let charlie: IKeyringPair; - let dave: IKeyringPair; + let gammaNode: string; + let deltaNode: string; before(async function() { await usingPlaygrounds(async (helper, privateKey) => { @@ -84,99 +96,51 @@ // Skip the collator block production in dev mode, since the blocks are sealed automatically. if(await helper.arrange.isDevNode()) this.skip(); - alice = await privateKey('//Alice'); - bob = await privateKey('//Bob'); - charlie = await privateKey('//Charlie'); - dave = await privateKey('//Dave'); - - expect((await helper.session.setOwnKeysFromAddress(charlie)) - .status.toLowerCase()).to.be.equal('success'); - expect((await helper.session.setOwnKeysFromAddress(dave)) - .status.toLowerCase()).to.be.equal('success'); + [alphaNode, betaNode, gammaNode, deltaNode] = await getInitialInvulnerables(); const invulnerables = await helper.collatorSelection.getInvulnerables(); - if(!invulnerables.includes(alice.address) || !invulnerables.includes(bob.address) || invulnerables.length != 2) { - console.warn('Alice and Bob are not the invulnerables! Reinstating them back. ' - + 'Current invulnerables\' size: ' + invulnerables.length); - - let nonce = await helper.chain.getNonce(superuser.address); - await Promise.all([ - helper.getSudo().executeExtrinsic(superuser, 'api.tx.collatorSelection.addInvulnerable', [alice.address], true, {nonce: nonce++}), - helper.getSudo().executeExtrinsic(superuser, 'api.tx.collatorSelection.addInvulnerable', [bob.address], true, {nonce: nonce++}), - ]); - - nonce = await helper.chain.getNonce(superuser.address); - await Promise.all(invulnerables.map((invulnerable: any) => { - if(invulnerable == alice.address || invulnerable == bob.address) return new Promise((res) => res); - return helper.getSudo().executeExtrinsic(superuser, 'api.tx.collatorSelection.removeInvulnerable', [invulnerable], true, {nonce: nonce++}); - })); - } + expect(invulnerables.length, 'Invalid initial invulnerables number').to.be.equal(4); + expect(invulnerables, 'Invalid initial invulnerables').containSubset([alphaNode, betaNode, gammaNode, deltaNode]); }); }); itSub('Change invulnerables and make sure they start producing blocks', async ({helper}) => { let nonce = await helper.chain.getNonce(superuser.address); - await expect(Promise.all([ - helper.getSudo().executeExtrinsic(superuser, 'api.tx.collatorSelection.addInvulnerable', [charlie.address], true, {nonce: nonce++}), - helper.getSudo().executeExtrinsic(superuser, 'api.tx.collatorSelection.addInvulnerable', [dave.address], true, {nonce: nonce++}), - ])).to.be.fulfilled; nonce = await helper.chain.getNonce(superuser.address); await expect(Promise.all([ - helper.getSudo().executeExtrinsic(superuser, 'api.tx.collatorSelection.removeInvulnerable', [alice.address], true, {nonce: nonce++}), - helper.getSudo().executeExtrinsic(superuser, 'api.tx.collatorSelection.removeInvulnerable', [bob.address], true, {nonce: nonce++}), + helper.getSudo().executeExtrinsic(superuser, 'api.tx.collatorSelection.removeInvulnerable', [alphaNode], true, {nonce: nonce++}), + helper.getSudo().executeExtrinsic(superuser, 'api.tx.collatorSelection.removeInvulnerable', [betaNode], true, {nonce: nonce++}), ])).to.be.fulfilled; const newInvulnerables = await helper.collatorSelection.getInvulnerables(); - expect(newInvulnerables).to.contain(charlie.address).and.contain(dave.address).and.be.length(2); + expect(newInvulnerables).to.contain(gammaNode).and.contain(deltaNode).and.be.length(2); await helper.wait.newSessions(2); const newValidators = await helper.callRpc('api.query.session.validators'); - expect(newValidators).to.contain(charlie.address).and.contain(dave.address).and.be.length(2); + expect(newValidators).to.contain(gammaNode).and.contain(deltaNode).and.be.length(2); const lastBlockNumber = await helper.chain.getLatestBlockNumber(); await helper.wait.newBlocks(1); - const lastCharlieBlock = (await helper.callRpc('api.query.collatorSelection.lastAuthoredBlock', [charlie.address])).toNumber(); - const lastDaveBlock = (await helper.callRpc('api.query.collatorSelection.lastAuthoredBlock', [dave.address])).toNumber(); - expect(lastCharlieBlock >= lastBlockNumber || lastDaveBlock >= lastBlockNumber).to.be.true; + const lastGammaBlock = (await helper.callRpc('api.query.collatorSelection.lastAuthoredBlock', [gammaNode])).toNumber(); + const lastDeltaBlock = (await helper.callRpc('api.query.collatorSelection.lastAuthoredBlock', [deltaNode])).toNumber(); + expect(lastGammaBlock >= lastBlockNumber || lastDeltaBlock >= lastBlockNumber).to.be.true; }); after(async () => { - await usingPlaygrounds(async (helper) => { - if(await helper.arrange.isDevNode()) return; - - let nonce = await helper.chain.getNonce(superuser.address); - await Promise.all([ - helper.getSudo().executeExtrinsic(superuser, 'api.tx.collatorSelection.addInvulnerable', [alice.address], true, {nonce: nonce++}), - helper.getSudo().executeExtrinsic(superuser, 'api.tx.collatorSelection.addInvulnerable', [bob.address], true, {nonce: nonce++}), - ]); - - nonce = await helper.chain.getNonce(superuser.address); - await Promise.all([ - await helper.getSudo().executeExtrinsic(superuser, 'api.tx.collatorSelection.removeInvulnerable', [charlie.address], true, {nonce: nonce++}), - await helper.getSudo().executeExtrinsic(superuser, 'api.tx.collatorSelection.removeInvulnerable', [dave.address], true, {nonce: nonce++}), - ]); - }); + await resetInvulnerables(); }); }); describe('Getting and releasing licenses to collate', () => { - let charlie: IKeyringPair; - let dave: IKeyringPair; let crowd: IKeyringPair[]; before(async function() { await usingPlaygrounds(async (helper, privateKey) => { - charlie = await privateKey('//Charlie'); - dave = await privateKey('//Dave'); crowd = await helper.arrange.createCrowd(20, 100n, superuser); // set session keys for everyone - expect((await helper.session.setOwnKeysFromAddress(charlie)) - .status.toLowerCase()).to.be.equal('success'); - expect((await helper.session.setOwnKeysFromAddress(dave)) - .status.toLowerCase()).to.be.equal('success'); await Promise.all(crowd.map(acc => helper.session.setOwnKeysFromAddress(acc))); }); }); @@ -252,22 +216,13 @@ }); describe('Onboarding, collating, and offboarding as collator candidates', () => { - // These two are the default invulnerables, and should return to be invulnerables after this suite. - let charlie: IKeyringPair; - let dave: IKeyringPair; let crowd: IKeyringPair[]; before(async function() { await usingPlaygrounds(async (helper, privateKey) => { - charlie = await privateKey('//Charlie'); - dave = await privateKey('//Dave'); crowd = await helper.arrange.createCrowd(20, 100n, superuser); // set session keys for everyone - expect((await helper.session.setOwnKeysFromAddress(charlie)) - .status.toLowerCase()).to.be.equal('success'); - expect((await helper.session.setOwnKeysFromAddress(dave)) - .status.toLowerCase()).to.be.equal('success'); await Promise.all(crowd.map(acc => helper.session.setOwnKeysFromAddress(acc))); }); }); @@ -341,10 +296,6 @@ }); describe('Addition and removal of invulnerables', () => { - before(async function() { - await resetInvulnerables(); - }); - describe('Positive', () => { itSub('Adds an invulnerable', async ({helper}) => { const [account] = await helper.arrange.createAccounts([10n], superuser); @@ -409,7 +360,7 @@ // 28 non-functioning collators, teehee. const invulnerablesLength = (await helper.collatorSelection.getInvulnerables()).length; - const invulnerablesUntilLimit = helper.collatorSelection.maxCollators() - invulnerablesLength; + const invulnerablesUntilLimit = (await helper.collatorSelection.getDesiredCollators()) - invulnerablesLength; const newInvulnerables = await helper.arrange.createAccounts(Array(invulnerablesUntilLimit).fill(10n), superuser); const [lastInvulnerable] = await helper.arrange.createAccounts([10n], superuser); @@ -448,6 +399,10 @@ .to.be.rejectedWith(/BadOrigin/); expect(await helper.collatorSelection.getInvulnerables()).to.have.all.members(invulnerables); }); + + after(async function() { + await resetInvulnerables(); + }); }); });