From 15ad7775ebba45b5423b06f6835b1a1c93b68e2d Mon Sep 17 00:00:00 2001 From: Fahrrader Date: Wed, 18 Jan 2023 07:48:39 +0000 Subject: [PATCH] ci: separate workflow for collator selection --- --- /dev/null +++ b/.docker/Dockerfile-collators @@ -0,0 +1,69 @@ +# ===== Rust builder ===== +FROM uniquenetwork/services:latest as rust-builder +ARG RUST_TOOLCHAIN= + +ENV RUST_TOOLCHAIN $RUST_TOOLCHAIN +ENV CARGO_HOME="/cargo-home" +ENV PATH="/cargo-home/bin:$PATH" +ENV TZ=UTC + +RUN rustup toolchain uninstall $(rustup toolchain list) && \ + rustup toolchain install $RUST_TOOLCHAIN && \ + rustup default $RUST_TOOLCHAIN && \ + rustup target list --installed && \ + rustup show +RUN rustup target add wasm32-unknown-unknown --toolchain $RUST_TOOLCHAIN + +RUN mkdir /unique_parachain +WORKDIR /unique_parachain + +# ===== BUILD ====== +FROM rust-builder as builder-unique + +ARG PROFILE=release +ARG NETWORK +ARG BRANCH + +WORKDIR /unique_parachain + +RUN git clone -b $BRANCH https://github.com/UniqueNetwork/unique-chain.git && \ + cd unique-chain && \ + cargo build --features=$NETWORK-runtime --$PROFILE + +# ===== RUN ====== + +FROM ubuntu:20.04 +ARG POLKADOT_LAUNCH_BRANCH + +RUN apt-get -y update && \ + apt-get -y install curl git && \ + curl -o- https://raw.githubusercontent.com/nvm-sh/nvm/v0.35.3/install.sh | bash && \ + export NVM_DIR="$HOME/.nvm" && \ + [ -s "$NVM_DIR/nvm.sh" ] && \. "$NVM_DIR/nvm.sh" && \ + nvm install v16.16.0 && \ + nvm use v16.16.0 + +RUN git clone https://github.com/uniquenetwork/polkadot-launch -b $POLKADOT_LAUNCH_BRANCH + +RUN export NVM_DIR="$HOME/.nvm" && \ + [ -s "$NVM_DIR/nvm.sh" ] && \. "$NVM_DIR/nvm.sh" && \ + cd /polkadot-launch && \ + npm install --global yarn && \ + yarn install + +COPY --from=builder-unique /unique_parachain/unique-chain/.docker/testnet-config/launch-config.json /polkadot-launch/launch-config.json +COPY --from=builder-unique /unique_parachain/unique-chain/target/release/unique-collator /unique-chain/target/release/ + +COPY --from=uniquenetwork/builder-polkadot:$POLKADOT_BUILD_BRANCH /unique_parachain/polkadot/target/release/polkadot /polkadot/target/release/ + +EXPOSE 9844 +EXPOSE 9944 +EXPOSE 9933 +EXPOSE 9833 +EXPOSE 40333 +EXPOSE 30333 + +CMD export NVM_DIR="$HOME/.nvm" && \ + [ -s "$NVM_DIR/nvm.sh" ] && \. "$NVM_DIR/nvm.sh" && \ + cd /polkadot-launch && \ + yarn start launch-config.json --- /dev/null +++ b/.docker/docker-compose.tmp-collators.j2 @@ -0,0 +1,30 @@ +version: "3.5" + +services: + parachain-collators: + build: + args: + - "RUST_TOOLCHAIN={{ RUST_TOOLCHAIN }}" + - "POLKADOT_BUILD_BRANCH={{ POLKADOT_BUILD_BRANCH }}" + - "POLKADOT_LAUNCH_BRANCH={{ POLKADOT_LAUNCH_BRANCH }}" + - "NETWORK={{ NETWORK }}" + - "BRANCH={{ BRANCH }}" + context: ../ + dockerfile: .docker/Dockerfile-collators + image: parachain-collators + container_name: parachain-collators + volumes: + - type: bind + source: ./launch-config.json + target: /polkadot-launch/launch-config.json + read_only: true + expose: + - 9944 + - 9933 + ports: + - 127.0.0.1:9944:9944 + - 127.0.0.1:9933:9933 + logging: + options: + max-size: "1m" + max-file: "3" --- a/.docker/testnet-config/launch-config.json +++ b/.docker/testnet-config/launch-config.json @@ -111,6 +111,42 @@ "--unsafe-ws-external", "-lxcm=trace" ] + }, + { + "port": 31202, + "wsPort": 9946, + "rpcPort": 9935, + "name": "charlie", + "flags": [ + "--rpc-cors=all", + "--unsafe-rpc-external", + "--unsafe-ws-external", + "-lxcm=trace" + ] + }, + { + "port": 31203, + "wsPort": 9947, + "rpcPort": 9936, + "name": "dave", + "flags": [ + "--rpc-cors=all", + "--unsafe-rpc-external", + "--unsafe-ws-external", + "-lxcm=trace" + ] + }, + { + "port": 31204, + "wsPort": 9948, + "rpcPort": 9937, + "name": "eve", + "flags": [ + "--rpc-cors=all", + "--unsafe-rpc-external", + "--unsafe-ws-external", + "-lxcm=trace" + ] } ] } --- a/.github/workflows/ci-develop.yml +++ b/.github/workflows/ci-develop.yml @@ -34,6 +34,10 @@ uses: ./.github/workflows/xcm.yml secrets: inherit # pass all secrets from initial workflow to nested + collator-selection: + if: ${{ (github.event.pull_request.draft == false && contains( github.event.pull_request.labels.*.name, 'CI-collator-selection')) }} # Conditional check for draft & labels per job. + uses: ./.github/workflows/collator-selection.yml + forkless: if: ${{ (github.event.pull_request.draft == false && contains( github.event.pull_request.labels.*.name, 'forkless')) }} # Conditional check for draft & labels per job. uses: ./.github/workflows/forkless.yml --- a/.github/workflows/ci-master.yml +++ b/.github/workflows/ci-master.yml @@ -35,6 +35,9 @@ xcm: uses: ./.github/workflows/xcm.yml secrets: inherit # pass all secrets from initial workflow to nested + + collator-selection: + uses: ./.github/workflows/collator-selection.yml # testnet: # uses: ./.github/workflows/testnet-build.yml --- /dev/null +++ b/.github/workflows/collator-selection.yml @@ -0,0 +1,164 @@ +name: collator-selection-test-run + +# Triger: only call from main workflow(re-usable workflows) +on: + workflow_call: + +# A workflow run is made up of one or more jobs that can run sequentially or in parallel +jobs: + execution-marix: + name: Prepare execution matrix + + runs-on: self-hosted-ci + outputs: + matrix: ${{ steps.create_matrix.outputs.matrix }} + + steps: + + - name: Clean Workspace + uses: AutoModality/action-clean@v1.1.0 + + # Checks-out your repository under $GITHUB_WORKSPACE, so your job can access it + - uses: actions/checkout@v3 + with: + ref: ${{ github.head_ref }} #Checking out head commit + + - name: Read .env file + uses: xom9ikk/dotenv@v2 + + - name: Create Execution matrix + uses: CertainLach/create-matrix-action@v3 + id: create_matrix + with: + matrix: | + network {quartz}, wasm_name {quartz}, mainnet_branch {${{ env.QUARTZ_MAINNET_BRANCH }}}, replica_from_address {${{ env.QUARTZ_REPLICA_FROM }}}, relay_branch {${{ env.KUSAMA_MAINNET_BRANCH }}} + network {sapphire}, wasm_name {quartz}, mainnet_branch {${{ env.SAPPHIRE_MAINNET_BRANCH }}}, replica_from_address {${{ env.SAPPHIRE_REPLICA_FROM }}}, relay_branch {${{ env.UNIQUEEAST_MAINNET_BRANCH }}} + + collator-selection: + needs: execution-marix + # The type of runner that the job will run on + runs-on: [self-hosted-ci,large] + timeout-minutes: 1380 + + name: ${{ matrix.network }}-build + strategy: + matrix: + include: ${{fromJson(needs.execution-marix.outputs.matrix)}} + + continue-on-error: true #Do not stop testing of matrix runs failed. As it decided during PR review - it required 50/50& Let's check it with false. + + steps: + - name: Clean Workspace + uses: AutoModality/action-clean@v1.1.0 + + # Checks-out your repository under $GITHUB_WORKSPACE, so your job can access it + - uses: actions/checkout@v3 + with: + ref: ${{ github.head_ref }} #Checking out head commit + + - name: Read .env file + uses: xom9ikk/dotenv@v2 + + - name: Generate ENV related extend file for docker-compose + uses: cuchi/jinja2-action@v1.2.0 + with: + template: .docker/docker-compose.tmp-collators.j2 + output_file: .docker/docker-compose.collators.${{ matrix.network }}.yml + variables: | + RUST_TOOLCHAIN=${{ env.RUST_TOOLCHAIN }} + POLKADOT_BUILD_BRANCH=${{ env.POLKADOT_BUILD_BRANCH }} + POLKADOT_LAUNCH_BRANCH=${{ env.POLKADOT_LAUNCH_BRANCH }} + NETWORK=${{ matrix.network }} + BRANCH=${{ github.head_ref }} + + - name: Show build configuration + run: cat .docker/docker-compose.collators.${{ matrix.network }}.yml + + - name: Show launch-config configuration + run: cat launch-config.json + + - name: Build the stack + run: docker-compose -f ".docker/docker-compose.collators.${{ matrix.network }}.yml" up -d --remove-orphans --force-recreate --timeout 300 + + # 🚀 POLKADOT LAUNCH COMPLETE 🚀 + - name: Check if docker logs consist messages related to testing of collator tests + if: success() + run: | + counter=160 + function check_container_status { + docker inspect -f {{.State.Running}} parachain-collators + } + function do_docker_logs { + docker logs --details parachain-collators 2>&1 + } + function is_started { + if [ "$(check_container_status)" == "true" ]; then + echo "Container: collator-${{ matrix.network }}-testnet-local RUNNING"; + echo "Check Docker logs" + DOCKER_LOGS=$(do_docker_logs) + if [[ ${DOCKER_LOGS} = *"POLKADOT LAUNCH COMPLETE"* ]];then + echo "🚀 POLKADOT LAUNCH COMPLETE 🚀" + return 0 + else + echo "Message not found in logs output, repeating..." + return 1 + fi + else + echo "Container collator-${{ matrix.network }}-testnet-local NOT RUNNING" + echo "Halting all future checks" + exit 1 + fi + echo "something goes wrong" + exit 1 + } + while ! is_started; do + echo "Waiting for special message in log files " + sleep 30s + counter=$(( $counter - 1 )) + echo "Counter: $counter" + if [ "$counter" -gt "0" ]; then + continue + else + break + fi + done + echo "Halting script" + exit 0 + shell: bash + + - uses: actions/setup-node@v3 + with: + node-version: 16 + + - name: Run Collator tests + working-directory: tests + if: success() || failure() + run: | + yarn install + yarn add mochawesome + node scripts/readyness.js + echo "Ready to start tests" + yarn polkadot-types + NOW=$(date +%s) && yarn testCollators --reporter mochawesome --reporter-options reportFilename=test-collators-${NOW} + env: + RPC_URL: http://127.0.0.1:9933/ + + - name: Test Report Collator + uses: phoenix-actions/test-reporting@v8 + id: test-report-collator + if: success() || failure() # run this step even if previous step failed + with: + name: Report Collator tests results - ${{ matrix.network }} # Name of the check run which will be created + path: tests/mochawesome-report/test-collators-*.json # Path to test results + reporter: mochawesome-json + fail-on-error: 'false' + + - name: Stop running containers + if: always() # run this step always + run: docker-compose -f ".docker/docker-compose.collators.${{ matrix.network }}.yml" down + + - name: Remove builder cache + if: always() # run this step always + run: | + docker builder prune -f -a + docker system prune -f --- a/tests/package.json +++ b/tests/package.json @@ -88,8 +88,9 @@ "testSchedulingEVM": "mocha --timeout 9999999 -r ts-node/register ./**/eth/scheduling.test.ts", "testPalletPresence": "mocha --timeout 9999999 -r ts-node/register ./**/pallet-presence.test.ts", "testBlockProduction": "mocha --timeout 9999999 -r ts-node/register ./**/block-production.test.ts", - "testCollatorSelection": "mocha --timeout 9999999 -r ts-node/register ./**/collatorSelection.*test.ts", - "testIdentity": "mocha --timeout 9999999 -r ts-node/register ./**/identity.*test.ts", + "testCollators": "RUN_COLLATOR_TESTS=1 mocha --timeout 9999999 -r ts-node/register ./**/collator-selection/**.*test.ts", + "testCollatorSelection": "RUN_COLLATOR_TESTS=1 mocha --timeout 9999999 -r ts-node/register ./**/collatorSelection.*test.ts", + "testIdentity": "RUN_COLLATOR_TESTS=1 mocha --timeout 9999999 -r ts-node/register ./**/identity.*test.ts", "testEnableDisableTransfers": "mocha --timeout 9999999 -r ts-node/register ./**/enableDisableTransfer.test.ts", "testLimits": "mocha --timeout 9999999 -r ts-node/register ./**/limits.test.ts", "testEthCreateNFTCollection": "mocha --timeout 9999999 -r ts-node/register ./**/eth/createNFTCollection.test.ts", --- /dev/null +++ b/tests/src/collator-selection/collatorSelection.seqtest.ts @@ -0,0 +1,468 @@ +// Copyright 2019-2022 Unique Network (Gibraltar) Ltd. +// This file is part of Unique Network. + +// Unique Network is free software: you can redistribute it and/or modify +// it under the terms of the GNU General Public License as published by +// the Free Software Foundation, either version 3 of the License, or +// (at your option) any later version. + +// Unique Network is distributed in the hope that it will be useful, +// but WITHOUT ANY WARRANTY; without even the implied warranty of +// MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the +// GNU General Public License for more details. + +// You should have received a copy of the GNU General Public License +// along with Unique Network. If not, see . + +import {IKeyringPair} from '@polkadot/types/types'; +import {usingPlaygrounds, expect, itSub, Pallets, requirePalletsOrSkip} from '../util'; + +async function resetInvulnerables() { + await usingPlaygrounds(async (helper, privateKey) => { + const superuser = await privateKey('//Alice'); + const alice = await privateKey('//Alice'); + const bob = await privateKey('//Bob'); + 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++}), + ]); + } + + 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++}), + ]); + + 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++}); + })); + } + }); +} + +// todo:collator Most preferable to launch this test in parallel somehow -- or change the session period (1 hr). +describe('Integration Test: Collator Selection', () => { + let superuser: IKeyringPair; + let previousLicenseBond = 0n; + let licenseBond = 0n; + + before(async function() { + if (!process.env.RUN_COLLATOR_TESTS) this.skip(); + + await usingPlaygrounds(async (helper, privateKey) => { + requirePalletsOrSkip(this, helper, [Pallets.CollatorSelection]); + superuser = await privateKey('//Alice'); + + previousLicenseBond = await helper.collatorSelection.getLicenseBond(); + licenseBond = 10n * helper.balance.getOneTokenNominal(); + await helper.getSudo().collatorSelection.setLicenseBond(superuser, licenseBond); + }); + }); + + 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 charlie: IKeyringPair; + let dave: IKeyringPair; + + before(async function() { + await usingPlaygrounds(async (helper, privateKey) => { + // todo:collator see again if blocks start to be finalized in dev mode + // 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'); + + 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++}); + })); + } + }); + }); + + 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++}), + ])).to.be.fulfilled; + + const newInvulnerables = await helper.collatorSelection.getInvulnerables(); + expect(newInvulnerables).to.contain(charlie.address).and.contain(dave.address).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); + + 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; + }); + + 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++}), + ]); + }); + }); + }); + + 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))); + }); + }); + + describe('Positive', () => { + itSub('Can lease and release a license', async ({helper}) => { + const account = crowd.pop()!; + + // make sure it does not have any reserved funds + expect((await helper.balance.getSubstrateFull(account.address)).reserved).to.be.equal(0n); + + // getting a license reserves a license bond cost + await helper.collatorSelection.obtainLicense(account); + expect(await helper.collatorSelection.hasLicense(account.address)).to.be.equal(licenseBond); + expect((await helper.balance.getSubstrateFull(account.address)).reserved).to.be.equal(licenseBond); + + // releasing a license un-reserves the license bond cost + await helper.collatorSelection.releaseLicense(account); + expect(await helper.collatorSelection.hasLicense(account.address)).to.be.equal(0n); + + const balance = await helper.balance.getSubstrateFull(account.address); + expect(balance.reserved).to.be.equal(0n); + expect(balance.free > 100n - licenseBond); + }); + + itSub('Can force revoke a license', async ({helper}) => { + const account = crowd.pop()!; + + // getting a license reserves a license bond cost + const previousBalance = await helper.balance.getSubstrateFull(account.address); + await helper.collatorSelection.obtainLicense(account); + expect(await helper.collatorSelection.hasLicense(account.address)).to.be.equal(licenseBond); + + // force-releasing a license un-reserves the license bond cost as well + await helper.getSudo().collatorSelection.forceReleaseLicense(superuser, account.address); + expect(await helper.collatorSelection.hasLicense(account.address)).to.be.equal(previousBalance.reserved); + + const balance = await helper.balance.getSubstrateFull(account.address); + expect(balance.reserved).to.be.equal(previousBalance.reserved); + expect(balance.free > previousBalance.free - licenseBond); + }); + }); + + describe('Negative', () => { + itSub('Cannot get a license without session keys set', async ({helper}) => { + const [account] = await helper.arrange.createAccounts([100n], superuser); + await expect(helper.collatorSelection.obtainLicense(account)) + .to.be.rejectedWith(/collatorSelection.ValidatorNotRegistered/); + }); + + itSub('Cannot register a license twice', async ({helper}) => { + const account = crowd.pop()!; + await helper.collatorSelection.obtainLicense(account); + await expect(helper.collatorSelection.obtainLicense(account)) + .to.be.rejectedWith(/collatorSelection.AlreadyHoldingLicense/); + }); + + itSub('Cannot release a license twice', async ({helper}) => { + const account = crowd.pop()!; + await helper.collatorSelection.obtainLicense(account); + await helper.collatorSelection.releaseLicense(account); + await expect(helper.collatorSelection.releaseLicense(account)) + .to.be.rejectedWith(/collatorSelection.NoLicense/); + }); + + itSub('Cannot force revoke a license as non-sudo', async ({helper}) => { + const account = crowd.pop()!; + await helper.collatorSelection.obtainLicense(account); + await expect(helper.collatorSelection.forceReleaseLicense(superuser, account.address)) + .to.be.rejectedWith(/BadOrigin/); + }); + }); + }); + + 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))); + }); + }); + + describe('Positive', () => { + itSub('Can onboard and offboard repeatedly', async ({helper}) => { + const account = crowd.pop()!; + await helper.collatorSelection.obtainLicense(account); + await helper.collatorSelection.onboard(account); + expect(await helper.collatorSelection.getCandidates()).to.be.deep.equal([account.address]); + + await helper.collatorSelection.offboard(account); + expect(await helper.collatorSelection.getCandidates()).to.be.deep.equal([]); + + await helper.collatorSelection.onboard(account); + expect(await helper.collatorSelection.getCandidates()).to.be.deep.equal([account.address]); + + await helper.collatorSelection.offboard(account); + expect(await helper.collatorSelection.getCandidates()).to.be.deep.equal([]); + }); + + itSub('Penalizes and forfeits license from faulty collators', async ({helper}) => { + // This one shouldn't even be able to produce blocks. + const account = crowd.pop()!; + await helper.collatorSelection.obtainLicense(account); + await helper.collatorSelection.onboard(account); + expect(await helper.collatorSelection.getCandidates()).to.contain(account.address); + + // Wait for 3 new sessions before checking that the collator will be kicked: + // one to get collator onboarded, and another two for the collator to fail + await helper.wait.newSessions(3); + + expect(await helper.collatorSelection.getCandidates()).to.not.contain(account.address); + expect(await helper.collatorSelection.hasLicense(account.address)).to.be.equal(0n); + + // The account's reserved funds get slashed as a penalty + const balance = await helper.balance.getSubstrateFull(account.address); + expect(balance.reserved).to.be.equal(0n); + expect(balance.free < 100n - licenseBond); + }); + }); + + describe('Negative', () => { + itSub('Cannot onboard without a license', async ({helper}) => { + const account = crowd.pop()!; + await expect(helper.collatorSelection.onboard(account)) + .to.be.rejectedWith(/collatorSelection.NoLicense/); + }); + + itSub('Cannot offboard without a license', async ({helper}) => { + const account = crowd.pop()!; + await expect(helper.collatorSelection.offboard(account)) + .to.be.rejectedWith(/collatorSelection.NotCandidate/); + }); + + itSub('Cannot offboard while not onboarded', async ({helper}) => { + const account = crowd.pop()!; + await helper.collatorSelection.obtainLicense(account); + await expect(helper.collatorSelection.offboard(account)) + .to.be.rejectedWith(/collatorSelection.NotCandidate/); + }); + + itSub('Cannot onboard while already onboarded', async ({helper}) => { + const account = crowd.pop()!; + await helper.collatorSelection.obtainLicense(account); + await helper.collatorSelection.onboard(account); + await expect(helper.collatorSelection.onboard(account)) + .to.be.rejectedWith(/collatorSelection.AlreadyCandidate/); + }); + }); + }); + + 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); + const invulnerables = await helper.collatorSelection.getInvulnerables(); + + await helper.session.setOwnKeysFromAddress(account); + await helper.getSudo().collatorSelection.addInvulnerable(superuser, account.address); + + const newInvulnerables = await helper.collatorSelection.getInvulnerables(); + expect(invulnerables.concat(account.address)).to.have.all.members(newInvulnerables); + }); + + itSub('Removes an invulnerable', async ({helper}) => { + const invulnerables = await helper.collatorSelection.getInvulnerables(); + const lastInvulnerable = invulnerables.pop()!; + + await helper.getSudo().collatorSelection.removeInvulnerable(superuser, lastInvulnerable); + const newInvulnerables = await helper.collatorSelection.getInvulnerables(); + // invulnerables had its last element removed, so they should be equal + expect(newInvulnerables).to.have.all.members(invulnerables); + }); + }); + + describe('Negative', () => { + itSub('Does not duplicate an invulnerable', async ({helper}) => { + const invulnerables = await helper.collatorSelection.getInvulnerables(); + // adding an already invulnerable should not fail, but should not duplicate it either + await expect(helper.getSudo().collatorSelection.addInvulnerable(superuser, invulnerables[0])) + .to.be.fulfilled; + const newInvulnerables = await helper.collatorSelection.getInvulnerables(); + expect(newInvulnerables).to.have.all.members(invulnerables); + }); + + itSub('Cannot remove a non-existent invulnerable', async ({helper}) => { + const [account] = await helper.arrange.createAccounts([0n], superuser); + await expect(helper.getSudo().collatorSelection.removeInvulnerable(superuser, account.address)) + .to.be.rejectedWith(/collatorSelection.NotInvulnerable/); + }); + + itSub('Cannot allow invulnerables to be empty', async ({helper}) => { + const invulnerables = await helper.collatorSelection.getInvulnerables(); + const lastInvulnerable = invulnerables.pop()!; + + let nonce = await helper.chain.getNonce(superuser.address); + await Promise.all(invulnerables.map((i: any) => + helper.getSudo().executeExtrinsic(superuser, 'api.tx.collatorSelection.removeInvulnerable', [i], true, {nonce: nonce++}))); + + await expect(helper.getSudo().collatorSelection.removeInvulnerable(superuser, lastInvulnerable)) + .to.be.rejectedWith(/collatorSelection.TooFewInvulnerables/); + + const newInvulnerables = await helper.collatorSelection.getInvulnerables(); + expect(newInvulnerables).to.be.deep.equal([lastInvulnerable]); + + // restore the invulnerables to the previous state + nonce = await helper.chain.getNonce(superuser.address); + await Promise.all(invulnerables.map((i: any) => + helper.getSudo().executeExtrinsic(superuser, 'api.tx.collatorSelection.addInvulnerable', [i], true, {nonce: nonce++}))); + }); + + itSub('Cannot have too many invulnerables', async ({helper}) => { + // todo:collator make sure that there is enough session time for a set of tests + // 28 non-functioning collators, teehee. + + const invulnerablesLength = (await helper.collatorSelection.getInvulnerables()).length; + const invulnerablesUntilLimit = helper.collatorSelection.maxCollators() - invulnerablesLength; + const newInvulnerables = await helper.arrange.createAccounts(Array(invulnerablesUntilLimit).fill(10n), superuser); + const [lastInvulnerable] = await helper.arrange.createAccounts([10n], superuser); + + await Promise.all(newInvulnerables.map((i: IKeyringPair) => + helper.session.setOwnKeysFromAddress(i))); + await helper.session.setOwnKeysFromAddress(lastInvulnerable); + + let nonce = await helper.chain.getNonce(superuser.address); + await Promise.all(newInvulnerables.map((i: IKeyringPair) => + helper.getSudo().executeExtrinsic(superuser, 'api.tx.collatorSelection.addInvulnerable', [i.address], true, {nonce: nonce++}))); + + await expect(helper.getSudo().collatorSelection.addInvulnerable(superuser, lastInvulnerable.address)) + .to.be.rejectedWith(/collatorSelection.TooManyInvulnerables/); + + // restore the invulnerables to the previous state + nonce = await helper.chain.getNonce(superuser.address); + await Promise.all(newInvulnerables.map((i: IKeyringPair) => + helper.getSudo().executeExtrinsic(superuser, 'api.tx.collatorSelection.removeInvulnerable', [i.address], true, {nonce: nonce++}))); + }); + + itSub('Forbids a non-sudo to add an invulnerable', async ({helper}) => { + const [account] = await helper.arrange.createAccounts([10n], superuser); + const invulnerables = await helper.collatorSelection.getInvulnerables(); + + await helper.session.setOwnKeysFromAddress(account); + await expect(helper.collatorSelection.addInvulnerable(superuser, account.address)) + .to.be.rejectedWith(/BadOrigin/); + + const newInvulnerables = await helper.collatorSelection.getInvulnerables(); + expect(newInvulnerables).to.be.members(invulnerables); + }); + + itSub('Forbids a non-sudo to remove an invulnerable', async ({helper}) => { + const invulnerables = await helper.collatorSelection.getInvulnerables(); + await expect(helper.collatorSelection.removeInvulnerable(superuser, invulnerables[0])) + .to.be.rejectedWith(/BadOrigin/); + expect(await helper.collatorSelection.getInvulnerables()).to.have.all.members(invulnerables); + }); + }); + }); + + after(async function() { + if (!process.env.RUN_COLLATOR_TESTS) return; + + await usingPlaygrounds(async (helper) => { + if (helper.fetchMissingPalletNames([Pallets.CollatorSelection]).length != 0) return; + + await helper.getSudo().collatorSelection.setLicenseBond(superuser, previousLicenseBond); + + const candidates = await helper.collatorSelection.getCandidates(); + let nonce = await helper.chain.getNonce(superuser.address); + await Promise.all(candidates.map(candidate => + helper.getSudo().executeExtrinsic(superuser, 'api.tx.collatorSelection.forceReleaseLicense', [candidate], true, {nonce: nonce++}))); + }); + }); +}); \ No newline at end of file --- /dev/null +++ b/tests/src/collator-selection/identity.seqtest.ts @@ -0,0 +1,105 @@ +// Copyright 2019-2022 Unique Network (Gibraltar) Ltd. +// This file is part of Unique Network. + +// Unique Network is free software: you can redistribute it and/or modify +// it under the terms of the GNU General Public License as published by +// the Free Software Foundation, either version 3 of the License, or +// (at your option) any later version. + +// Unique Network is distributed in the hope that it will be useful, +// but WITHOUT ANY WARRANTY; without even the implied warranty of +// MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the +// GNU General Public License for more details. + +// You should have received a copy of the GNU General Public License +// along with Unique Network. If not, see . + +import {IKeyringPair} from '@polkadot/types/types'; +import {usingPlaygrounds, expect, itSub, Pallets, requirePalletsOrSkip} from '../util'; +import {UniqueHelper} from '../util/playgrounds/unique'; + +async function getIdentities(helper: UniqueHelper) { + const identities: [string, any][] = []; + for(const [key, value] of await helper.getApi().query.identity.identityOf.entries()) + identities.push([(key as any).toHuman(), (value as any).unwrap()]); + return identities; +} + +async function getIdentityAccounts(helper: UniqueHelper) { + return (await getIdentities(helper)).flatMap(([key, _value]) => key); +} + +describe('Integration Test: Identities Manipulation', () => { + let superuser: IKeyringPair; + + before(async function() { + if (!process.env.RUN_COLLATOR_TESTS) this.skip(); + + await usingPlaygrounds(async (helper, privateKey) => { + requirePalletsOrSkip(this, helper, [Pallets.Identity]); + superuser = await privateKey('//Alice'); + }); + }); + + itSub('Normal calls do not work', async ({helper}) => { + // console.error = () => {}; + await expect(helper.executeExtrinsic(superuser, 'api.tx.identity.setIdentity', [{info: {display: {Raw: 'Meowser'}}}])) + .to.be.rejectedWith(/Transaction call is not expected/); + }); + + itSub('Sets identities', async ({helper}) => { + const oldIdentitiesCount = (await getIdentityAccounts(helper)).length; + + const crowdSize = 10; + const crowd = await helper.arrange.createCrowd(crowdSize, 0n, superuser); + const identities = crowd.map((acc, i) => [acc.address, {info: {display: {Raw: `accounter #${i}`}}}]); + await helper.getSudo().executeExtrinsic(superuser, 'api.tx.identity.forceInsertIdentities', [identities]); + + expect((await getIdentityAccounts(helper)).length).to.be.equal(oldIdentitiesCount + crowdSize); + }); + + itSub('Setting identities does not delete existing but does overwrite', async ({helper}) => { + const crowd = await helper.arrange.createCrowd(10, 0n, superuser); + const identities = crowd.map((acc, i) => [acc.address, {info: {display: {Raw: `accounter #${i}`}}}]); + + // insert a single identity + let singleIdentity = identities.pop()!; + await helper.getSudo().executeExtrinsic(superuser, 'api.tx.identity.forceInsertIdentities', [[singleIdentity]]); + + const oldIdentitiesCount = (await getIdentityAccounts(helper)).length; + + // change an identity and push it with a few new others + singleIdentity = [singleIdentity[0], {info: {display: {Raw: 'something special'}}}]; + identities.push(singleIdentity); + await helper.getSudo().executeExtrinsic(superuser, 'api.tx.identity.forceInsertIdentities', [identities]); + + // oldIdentitiesCount + 9 because one identity is overwritten, not inserted on top + expect((await getIdentityAccounts(helper)).length).to.be.equal(oldIdentitiesCount + 9); + expect((await helper.callRpc('api.query.identity.identityOf', [singleIdentity[0]])).toHuman().info.display) + .to.be.deep.equal({Raw: 'something special'}); + }); + + itSub('Removes identities', async ({helper}) => { + const crowd = await helper.arrange.createCrowd(10, 0n, superuser); + const identities = crowd.map((acc, i) => [acc.address, {info: {display: {Raw: `accounter #${i}`}}}]); + await helper.getSudo().executeExtrinsic(superuser, 'api.tx.identity.forceInsertIdentities', [identities]); + const oldIdentities = await getIdentityAccounts(helper); + + // delete a couple, check that they are no longer there + const scapegoats = [crowd.pop()!.address, crowd.pop()!.address]; + await helper.getSudo().executeExtrinsic(superuser, 'api.tx.identity.forceRemoveIdentities', [scapegoats]); + const newIdentities = await getIdentityAccounts(helper); + expect(newIdentities.concat(scapegoats)).to.be.have.members(oldIdentities); + }); + + after(async function() { + if (!process.env.RUN_COLLATOR_TESTS) return; + + await usingPlaygrounds(async helper => { + if (helper.fetchMissingPalletNames([Pallets.Identity]).length != 0) return; + + const identitiesToRemove: string[] = await getIdentityAccounts(helper); + await helper.getSudo().executeExtrinsic(superuser, 'api.tx.identity.forceRemoveIdentities', [identitiesToRemove]); + }); + }); +}); --- a/tests/src/collatorSelection.seqtest.ts +++ /dev/null @@ -1,465 +0,0 @@ -// Copyright 2019-2022 Unique Network (Gibraltar) Ltd. -// This file is part of Unique Network. - -// Unique Network is free software: you can redistribute it and/or modify -// it under the terms of the GNU General Public License as published by -// the Free Software Foundation, either version 3 of the License, or -// (at your option) any later version. - -// Unique Network is distributed in the hope that it will be useful, -// but WITHOUT ANY WARRANTY; without even the implied warranty of -// MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the -// GNU General Public License for more details. - -// You should have received a copy of the GNU General Public License -// along with Unique Network. If not, see . - -import {IKeyringPair} from '@polkadot/types/types'; -import {usingPlaygrounds, expect, itSub, Pallets, requirePalletsOrSkip} from './util'; - -async function resetInvulnerables() { - await usingPlaygrounds(async (helper, privateKey) => { - const superuser = await privateKey('//Alice'); - const alice = await privateKey('//Alice'); - const bob = await privateKey('//Bob'); - 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++}), - ]); - } - - 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++}), - ]); - - 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++}); - })); - } - }); -} - -// todo:collator Most preferable to launch this test in parallel somehow -- or change the session period (1 hr). -describe('Integration Test: Collator Selection', () => { - let superuser: IKeyringPair; - let previousLicenseBond = 0n; - let licenseBond = 0n; - - before(async function() { - await usingPlaygrounds(async (helper, privateKey) => { - requirePalletsOrSkip(this, helper, [Pallets.CollatorSelection]); - superuser = await privateKey('//Alice'); - - previousLicenseBond = await helper.collatorSelection.getLicenseBond(); - licenseBond = 10n * helper.balance.getOneTokenNominal(); - await helper.getSudo().collatorSelection.setLicenseBond(superuser, licenseBond); - }); - }); - - 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 charlie: IKeyringPair; - let dave: IKeyringPair; - - before(async function() { - await usingPlaygrounds(async (helper, privateKey) => { - // todo:collator see again if blocks start to be finalized in dev mode - // 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'); - - 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++}); - })); - } - }); - }); - - 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++}), - ])).to.be.fulfilled; - - const newInvulnerables = await helper.collatorSelection.getInvulnerables(); - expect(newInvulnerables).to.contain(charlie.address).and.contain(dave.address).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); - - 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; - }); - - 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++}), - ]); - }); - }); - }); - - 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))); - }); - }); - - describe('Positive', () => { - itSub('Can lease and release a license', async ({helper}) => { - const account = crowd.pop()!; - - // make sure it does not have any reserved funds - expect((await helper.balance.getSubstrateFull(account.address)).reserved).to.be.equal(0n); - - // getting a license reserves a license bond cost - await helper.collatorSelection.obtainLicense(account); - expect(await helper.collatorSelection.hasLicense(account.address)).to.be.equal(licenseBond); - expect((await helper.balance.getSubstrateFull(account.address)).reserved).to.be.equal(licenseBond); - - // releasing a license un-reserves the license bond cost - await helper.collatorSelection.releaseLicense(account); - expect(await helper.collatorSelection.hasLicense(account.address)).to.be.equal(0n); - - const balance = await helper.balance.getSubstrateFull(account.address); - expect(balance.reserved).to.be.equal(0n); - expect(balance.free > 100n - licenseBond); - }); - - itSub('Can force revoke a license', async ({helper}) => { - const account = crowd.pop()!; - - // getting a license reserves a license bond cost - const previousBalance = await helper.balance.getSubstrateFull(account.address); - await helper.collatorSelection.obtainLicense(account); - expect(await helper.collatorSelection.hasLicense(account.address)).to.be.equal(licenseBond); - - // force-releasing a license un-reserves the license bond cost as well - await helper.getSudo().collatorSelection.forceReleaseLicense(superuser, account.address); - expect(await helper.collatorSelection.hasLicense(account.address)).to.be.equal(previousBalance.reserved); - - const balance = await helper.balance.getSubstrateFull(account.address); - expect(balance.reserved).to.be.equal(previousBalance.reserved); - expect(balance.free > previousBalance.free - licenseBond); - }); - }); - - describe('Negative', () => { - itSub('Cannot get a license without session keys set', async ({helper}) => { - const [account] = await helper.arrange.createAccounts([100n], superuser); - await expect(helper.collatorSelection.obtainLicense(account)) - .to.be.rejectedWith(/collatorSelection.ValidatorNotRegistered/); - }); - - itSub('Cannot register a license twice', async ({helper}) => { - const account = crowd.pop()!; - await helper.collatorSelection.obtainLicense(account); - await expect(helper.collatorSelection.obtainLicense(account)) - .to.be.rejectedWith(/collatorSelection.AlreadyHoldingLicense/); - }); - - itSub('Cannot release a license twice', async ({helper}) => { - const account = crowd.pop()!; - await helper.collatorSelection.obtainLicense(account); - await helper.collatorSelection.releaseLicense(account); - await expect(helper.collatorSelection.releaseLicense(account)) - .to.be.rejectedWith(/collatorSelection.NoLicense/); - }); - - itSub('Cannot force revoke a license as non-sudo', async ({helper}) => { - const account = crowd.pop()!; - await helper.collatorSelection.obtainLicense(account); - await expect(helper.collatorSelection.forceReleaseLicense(superuser, account.address)) - .to.be.rejectedWith(/BadOrigin/); - }); - }); - }); - - 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))); - }); - }); - - describe('Positive', () => { - itSub('Can onboard and offboard repeatedly', async ({helper}) => { - const account = crowd.pop()!; - await helper.collatorSelection.obtainLicense(account); - await helper.collatorSelection.onboard(account); - expect(await helper.collatorSelection.getCandidates()).to.be.deep.equal([account.address]); - - await helper.collatorSelection.offboard(account); - expect(await helper.collatorSelection.getCandidates()).to.be.deep.equal([]); - - await helper.collatorSelection.onboard(account); - expect(await helper.collatorSelection.getCandidates()).to.be.deep.equal([account.address]); - - await helper.collatorSelection.offboard(account); - expect(await helper.collatorSelection.getCandidates()).to.be.deep.equal([]); - }); - - itSub('Penalizes and forfeits license from faulty collators', async ({helper}) => { - // This one shouldn't even be able to produce blocks. - const account = crowd.pop()!; - await helper.collatorSelection.obtainLicense(account); - await helper.collatorSelection.onboard(account); - expect(await helper.collatorSelection.getCandidates()).to.contain(account.address); - - // Wait for 3 new sessions before checking that the collator will be kicked: - // one to get collator onboarded, and another two for the collator to fail - await helper.wait.newSessions(3); - - expect(await helper.collatorSelection.getCandidates()).to.not.contain(account.address); - expect(await helper.collatorSelection.hasLicense(account.address)).to.be.equal(0n); - - // The account's reserved funds get slashed as a penalty - const balance = await helper.balance.getSubstrateFull(account.address); - expect(balance.reserved).to.be.equal(0n); - expect(balance.free < 100n - licenseBond); - }); - }); - - describe('Negative', () => { - itSub('Cannot onboard without a license', async ({helper}) => { - const account = crowd.pop()!; - await expect(helper.collatorSelection.onboard(account)) - .to.be.rejectedWith(/collatorSelection.NoLicense/); - }); - - itSub('Cannot offboard without a license', async ({helper}) => { - const account = crowd.pop()!; - await expect(helper.collatorSelection.offboard(account)) - .to.be.rejectedWith(/collatorSelection.NotCandidate/); - }); - - itSub('Cannot offboard while not onboarded', async ({helper}) => { - const account = crowd.pop()!; - await helper.collatorSelection.obtainLicense(account); - await expect(helper.collatorSelection.offboard(account)) - .to.be.rejectedWith(/collatorSelection.NotCandidate/); - }); - - itSub('Cannot onboard while already onboarded', async ({helper}) => { - const account = crowd.pop()!; - await helper.collatorSelection.obtainLicense(account); - await helper.collatorSelection.onboard(account); - await expect(helper.collatorSelection.onboard(account)) - .to.be.rejectedWith(/collatorSelection.AlreadyCandidate/); - }); - }); - }); - - 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); - const invulnerables = await helper.collatorSelection.getInvulnerables(); - - await helper.session.setOwnKeysFromAddress(account); - await helper.getSudo().collatorSelection.addInvulnerable(superuser, account.address); - - const newInvulnerables = await helper.collatorSelection.getInvulnerables(); - expect(invulnerables.concat(account.address)).to.have.all.members(newInvulnerables); - }); - - itSub('Removes an invulnerable', async ({helper}) => { - const invulnerables = await helper.collatorSelection.getInvulnerables(); - const lastInvulnerable = invulnerables.pop()!; - - await helper.getSudo().collatorSelection.removeInvulnerable(superuser, lastInvulnerable); - const newInvulnerables = await helper.collatorSelection.getInvulnerables(); - // invulnerables had its last element removed, so they should be equal - expect(newInvulnerables).to.have.all.members(invulnerables); - }); - }); - - describe('Negative', () => { - itSub('Does not duplicate an invulnerable', async ({helper}) => { - const invulnerables = await helper.collatorSelection.getInvulnerables(); - // adding an already invulnerable should not fail, but should not duplicate it either - await expect(helper.getSudo().collatorSelection.addInvulnerable(superuser, invulnerables[0])) - .to.be.fulfilled; - const newInvulnerables = await helper.collatorSelection.getInvulnerables(); - expect(newInvulnerables).to.have.all.members(invulnerables); - }); - - itSub('Cannot remove a non-existent invulnerable', async ({helper}) => { - const [account] = await helper.arrange.createAccounts([0n], superuser); - await expect(helper.getSudo().collatorSelection.removeInvulnerable(superuser, account.address)) - .to.be.rejectedWith(/collatorSelection.NotInvulnerable/); - }); - - itSub('Cannot allow invulnerables to be empty', async ({helper}) => { - const invulnerables = await helper.collatorSelection.getInvulnerables(); - const lastInvulnerable = invulnerables.pop()!; - - let nonce = await helper.chain.getNonce(superuser.address); - await Promise.all(invulnerables.map((i: any) => - helper.getSudo().executeExtrinsic(superuser, 'api.tx.collatorSelection.removeInvulnerable', [i], true, {nonce: nonce++}))); - - await expect(helper.getSudo().collatorSelection.removeInvulnerable(superuser, lastInvulnerable)) - .to.be.rejectedWith(/collatorSelection.TooFewInvulnerables/); - - const newInvulnerables = await helper.collatorSelection.getInvulnerables(); - expect(newInvulnerables).to.be.deep.equal([lastInvulnerable]); - - // restore the invulnerables to the previous state - nonce = await helper.chain.getNonce(superuser.address); - await Promise.all(invulnerables.map((i: any) => - helper.getSudo().executeExtrinsic(superuser, 'api.tx.collatorSelection.addInvulnerable', [i], true, {nonce: nonce++}))); - }); - - itSub('Cannot have too many invulnerables', async ({helper}) => { - // todo:collator make sure that there is enough session time for a set of tests - // 28 non-functioning collators, teehee. - - const invulnerablesLength = (await helper.collatorSelection.getInvulnerables()).length; - const invulnerablesUntilLimit = helper.collatorSelection.maxCollators() - invulnerablesLength; - const newInvulnerables = await helper.arrange.createAccounts(Array(invulnerablesUntilLimit).fill(10n), superuser); - const [lastInvulnerable] = await helper.arrange.createAccounts([10n], superuser); - - await Promise.all(newInvulnerables.map((i: IKeyringPair) => - helper.session.setOwnKeysFromAddress(i))); - await helper.session.setOwnKeysFromAddress(lastInvulnerable); - - let nonce = await helper.chain.getNonce(superuser.address); - await Promise.all(newInvulnerables.map((i: IKeyringPair) => - helper.getSudo().executeExtrinsic(superuser, 'api.tx.collatorSelection.addInvulnerable', [i.address], true, {nonce: nonce++}))); - - await expect(helper.getSudo().collatorSelection.addInvulnerable(superuser, lastInvulnerable.address)) - .to.be.rejectedWith(/collatorSelection.TooManyInvulnerables/); - - // restore the invulnerables to the previous state - nonce = await helper.chain.getNonce(superuser.address); - await Promise.all(newInvulnerables.map((i: IKeyringPair) => - helper.getSudo().executeExtrinsic(superuser, 'api.tx.collatorSelection.removeInvulnerable', [i.address], true, {nonce: nonce++}))); - }); - - itSub('Forbids a non-sudo to add an invulnerable', async ({helper}) => { - const [account] = await helper.arrange.createAccounts([10n], superuser); - const invulnerables = await helper.collatorSelection.getInvulnerables(); - - await helper.session.setOwnKeysFromAddress(account); - await expect(helper.collatorSelection.addInvulnerable(superuser, account.address)) - .to.be.rejectedWith(/BadOrigin/); - - const newInvulnerables = await helper.collatorSelection.getInvulnerables(); - expect(newInvulnerables).to.be.members(invulnerables); - }); - - itSub('Forbids a non-sudo to remove an invulnerable', async ({helper}) => { - const invulnerables = await helper.collatorSelection.getInvulnerables(); - await expect(helper.collatorSelection.removeInvulnerable(superuser, invulnerables[0])) - .to.be.rejectedWith(/BadOrigin/); - expect(await helper.collatorSelection.getInvulnerables()).to.have.all.members(invulnerables); - }); - }); - }); - - after(async () => { - // eslint-disable-next-line require-await - await usingPlaygrounds(async (helper) => { - if (helper.fetchMissingPalletNames([Pallets.CollatorSelection]).length != 0) return; - - await helper.getSudo().collatorSelection.setLicenseBond(superuser, previousLicenseBond); - - const candidates = await helper.collatorSelection.getCandidates(); - let nonce = await helper.chain.getNonce(superuser.address); - await Promise.all(candidates.map(candidate => - helper.getSudo().executeExtrinsic(superuser, 'api.tx.collatorSelection.forceReleaseLicense', [candidate], true, {nonce: nonce++}))); - }); - }); -}); \ No newline at end of file --- a/tests/src/identity.seqtest.ts +++ /dev/null @@ -1,101 +0,0 @@ -// Copyright 2019-2022 Unique Network (Gibraltar) Ltd. -// This file is part of Unique Network. - -// Unique Network is free software: you can redistribute it and/or modify -// it under the terms of the GNU General Public License as published by -// the Free Software Foundation, either version 3 of the License, or -// (at your option) any later version. - -// Unique Network is distributed in the hope that it will be useful, -// but WITHOUT ANY WARRANTY; without even the implied warranty of -// MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the -// GNU General Public License for more details. - -// You should have received a copy of the GNU General Public License -// along with Unique Network. If not, see . - -import {IKeyringPair} from '@polkadot/types/types'; -import {usingPlaygrounds, expect, itSub, Pallets, requirePalletsOrSkip} from './util'; -import {UniqueHelper} from './util/playgrounds/unique'; - -async function getIdentities(helper: UniqueHelper) { - const identities: [string, any][] = []; - for(const [key, value] of await helper.getApi().query.identity.identityOf.entries()) - identities.push([(key as any).toHuman(), (value as any).unwrap()]); - return identities; -} - -async function getIdentityAccounts(helper: UniqueHelper) { - return (await getIdentities(helper)).flatMap(([key, _value]) => key); -} - -describe('Integration Test: Identities Manipulation', () => { - let superuser: IKeyringPair; - - before(async function() { - await usingPlaygrounds(async (helper, privateKey) => { - requirePalletsOrSkip(this, helper, [Pallets.Identity]); - superuser = await privateKey('//Alice'); - }); - }); - - itSub('Normal calls do not work', async ({helper}) => { - // console.error = () => {}; - await expect(helper.executeExtrinsic(superuser, 'api.tx.identity.setIdentity', [{info: {display: {Raw: 'Meowser'}}}])) - .to.be.rejectedWith(/Transaction call is not expected/); - }); - - itSub('Sets identities', async ({helper}) => { - const oldIdentitiesCount = (await getIdentityAccounts(helper)).length; - - const crowdSize = 10; - const crowd = await helper.arrange.createCrowd(crowdSize, 0n, superuser); - const identities = crowd.map((acc, i) => [acc.address, {info: {display: {Raw: `accounter #${i}`}}}]); - await helper.getSudo().executeExtrinsic(superuser, 'api.tx.identity.forceInsertIdentities', [identities]); - - expect((await getIdentityAccounts(helper)).length).to.be.equal(oldIdentitiesCount + crowdSize); - }); - - itSub('Setting identities does not delete existing but does overwrite', async ({helper}) => { - const crowd = await helper.arrange.createCrowd(10, 0n, superuser); - const identities = crowd.map((acc, i) => [acc.address, {info: {display: {Raw: `accounter #${i}`}}}]); - - // insert a single identity - let singleIdentity = identities.pop()!; - await helper.getSudo().executeExtrinsic(superuser, 'api.tx.identity.forceInsertIdentities', [[singleIdentity]]); - - const oldIdentitiesCount = (await getIdentityAccounts(helper)).length; - - // change an identity and push it with a few new others - singleIdentity = [singleIdentity[0], {info: {display: {Raw: 'something special'}}}]; - identities.push(singleIdentity); - await helper.getSudo().executeExtrinsic(superuser, 'api.tx.identity.forceInsertIdentities', [identities]); - - // oldIdentitiesCount + 9 because one identity is overwritten, not inserted on top - expect((await getIdentityAccounts(helper)).length).to.be.equal(oldIdentitiesCount + 9); - expect((await helper.callRpc('api.query.identity.identityOf', [singleIdentity[0]])).toHuman().info.display) - .to.be.deep.equal({Raw: 'something special'}); - }); - - itSub('Removes identities', async ({helper}) => { - const crowd = await helper.arrange.createCrowd(10, 0n, superuser); - const identities = crowd.map((acc, i) => [acc.address, {info: {display: {Raw: `accounter #${i}`}}}]); - await helper.getSudo().executeExtrinsic(superuser, 'api.tx.identity.forceInsertIdentities', [identities]); - const oldIdentities = await getIdentityAccounts(helper); - - // delete a couple, check that they are no longer there - const scapegoats = [crowd.pop()!.address, crowd.pop()!.address]; - await helper.getSudo().executeExtrinsic(superuser, 'api.tx.identity.forceRemoveIdentities', [scapegoats]); - const newIdentities = await getIdentityAccounts(helper); - expect(newIdentities.concat(scapegoats)).to.be.have.members(oldIdentities); - }); - - after(async function() { - await usingPlaygrounds(async helper => { - if (helper.fetchMissingPalletNames([Pallets.Identity]).length != 0) return; - - const identitiesToRemove: string[] = await getIdentityAccounts(helper); - await helper.getSudo().executeExtrinsic(superuser, 'api.tx.identity.forceRemoveIdentities', [identitiesToRemove]); - }); - }); -}); -- gitstuff