difftreelog
Merge pull request #840 from UniqueNetwork/feature/collator-workflow
in: master
12 files changed
.docker/Dockerfile-collatorsdiffbeforeafterboth--- /dev/null
+++ b/.docker/Dockerfile-collators
@@ -0,0 +1,74 @@
+ARG POLKADOT_BUILD_BRANCH
+FROM uniquenetwork/builder-polkadot:${POLKADOT_BUILD_BRANCH} as polkadot
+
+# ===== 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 POLKADOT_LAUNCH_BRANCH
+ARG BRANCH
+
+ENV POLKADOT_LAUNCH_BRANCH $POLKADOT_LAUNCH_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
+
+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=polkadot /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
.docker/Dockerfile-parachaindiffbeforeafterboth--- a/.docker/Dockerfile-parachain
+++ b/.docker/Dockerfile-parachain
@@ -32,7 +32,7 @@
WORKDIR /unique_parachain
-RUN git clone $REPO_URL -b $BRANCH . && \
+RUN git clone https://github.com/UniqueNetwork/unique-chain.git -b $BRANCH . && \
cargo build --features=${NETWORK}-runtime --$PROFILE
# ===== RUN ======
.docker/docker-compose.tmp-collators.j2diffbeforeafterboth--- /dev/null
+++ b/.docker/docker-compose.tmp-collators.j2
@@ -0,0 +1,25 @@
+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-parachain
+ image: parachain-collators
+ container_name: parachain-collators
+ expose:
+ - 9944
+ - 9933
+ ports:
+ - 127.0.0.1:9944:9944
+ - 127.0.0.1:9933:9933
+ logging:
+ options:
+ max-size: "1m"
+ max-file: "3"
.docker/testnet-config/launch-config.jsondiffbeforeafterboth--- 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"
+ ]
}
]
}
.github/workflows/ci-develop.ymldiffbeforeafterboth--- 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
.github/workflows/ci-master.ymldiffbeforeafterboth--- 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
.github/workflows/collator-selection.ymldiffbeforeafterboth--- /dev/null
+++ b/.github/workflows/collator-selection.yml
@@ -0,0 +1,165 @@
+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 {opal}, wasm_name {opal}, mainnet_branch {${{ env.OPAL_MAINNET_BRANCH }}}, replica_from_address {${{ env.OPAL_REPLICA_FROM }}}, relay_branch {${{ env.UNIQUEWEST_MAINNET_BRANCH }}}
+ 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 }}
+ 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
tests/package.jsondiffbeforeafterboth--- 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 49999999 -r ts-node/register ./**/collator-selection/**.*test.ts",
+ "testCollatorSelection": "RUN_COLLATOR_TESTS=1 mocha --timeout 49999999 -r ts-node/register ./**/collatorSelection.*test.ts",
+ "testIdentity": "RUN_COLLATOR_TESTS=1 mocha --timeout 49999999 -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",
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});tests/src/collator-selection/identity.seqtest.tsdiffbeforeafterboth--- /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 <http://www.gnu.org/licenses/>.
+
+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]);
+ });
+ });
+});
tests/src/collatorSelection.seqtest.tsdiffbeforeafterboth--- 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 <http://www.gnu.org/licenses/>.
-
-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<void>(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
tests/src/identity.seqtest.tsdiffbeforeafterboth--- 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 <http://www.gnu.org/licenses/>.
-
-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]);
- });
- });
-});