diff --git a/test/suites/smoke-test/test-inflation-rewards.ts b/test/suites/smoke-test/test-inflation-rewards.ts new file mode 100644 index 000000000..c599e1c38 --- /dev/null +++ b/test/suites/smoke-test/test-inflation-rewards.ts @@ -0,0 +1,160 @@ +import { beforeAll, describeSuite, expect } from "@moonwall/cli"; + +import { ApiPromise } from "@polkadot/api"; +import { getAuthorFromDigest } from "util/author"; +import { fetchIssuance, filterRewardFromOrchestrator, fetchRewardAuthorContainers } from "util/block"; +import { PARACHAIN_BOND } from "util/constants"; + +describeSuite({ + id: "S08", + title: "Sample suite that only runs on Dancebox chains", + foundationMethods: "read_only", + testCases: ({ it, context }) => { + let api: ApiPromise; + let runtimeVersion; + + beforeAll(() => { + api = context.polkadotJs(); + runtimeVersion = api.runtimeVersion.specVersion.toNumber(); + }); + + it({ + id: "C01", + title: "Inflation for orchestrator should match with expected number of containers", + test: async function () { + if (runtimeVersion < 300) { + return; + } + const author = await getAuthorFromDigest(api); + // Fetch current session + const currentSession = await api.query.session.currentIndex(); + const keys = await api.query.authorityMapping.authorityIdMapping(currentSession); + const account = keys.toJSON()[author]; + // 70% is distributed across all rewards + const events = await api.query.system.events(); + const issuance = await fetchIssuance(events).amount.toBigInt(); + const chainRewards = (issuance * 7n) / 10n; + const numberOfChains = await api.query.registrar.registeredParaIds(); + const expectedOrchestratorReward = chainRewards / BigInt(numberOfChains.length + 1); + const reward = await filterRewardFromOrchestrator(events, account); + // we know there might be rounding errors, so we always check it is in the range +-1 + expect(reward >= expectedOrchestratorReward - 1n && reward <= expectedOrchestratorReward + 1n).to.be + .true; + }, + }); + + it({ + id: "C02", + title: "Inflation for containers should match with expected number of containers", + test: async function () { + if (runtimeVersion < 300) { + return; + } + // 70% is distributed across all rewards + const events = await api.query.system.events(); + const issuance = await fetchIssuance(events).amount.toBigInt(); + const chainRewards = (issuance * 7n) / 10n; + const numberOfChains = await api.query.registrar.registeredParaIds(); + const expectedChainReward = chainRewards / BigInt(numberOfChains.length + 1); + const rewardEvents = await fetchRewardAuthorContainers(events); + for (const index in rewardEvents) { + expect( + rewardEvents[index].balance.toBigInt() >= expectedChainReward - 1n && + rewardEvents[index].balance.toBigInt() <= expectedChainReward + 1n + ).to.be.true; + } + }, + }); + + it({ + id: "C03", + title: "Issuance is correct", + test: async function () { + if (runtimeVersion < 300) { + return; + } + const latestBlock = await api.rpc.chain.getBlock(); + + const latestBlockHash = latestBlock.block.hash; + const latestParentBlockHash = latestBlock.block.header.parentHash; + const apiAtIssuanceAfter = await api.at(latestBlockHash); + const apiAtIssuanceBefore = await api.at(latestParentBlockHash); + + const supplyBefore = (await apiAtIssuanceBefore.query.balances.totalIssuance()).toBigInt(); + + const events = await apiAtIssuanceAfter.query.system.events(); + + const issuance = await fetchIssuance(events).amount.toBigInt(); + + const supplyAfter = (await apiAtIssuanceAfter.query.balances.totalIssuance()).toBigInt(); + + // expected issuance block increment in prod is: 19n/1_000_000_000n + const expectedIssuanceIncrement = (supplyBefore * 19n) / 1_000_000_000n; + + // we know there might be rounding errors, so we always check it is in the range +-1 + expect(issuance >= expectedIssuanceIncrement - 1n && issuance <= expectedIssuanceIncrement + 1n).to.be + .true; + expect(supplyAfter).to.equal(supplyBefore + issuance); + }, + }); + + it({ + id: "C04", + title: "Parachain bond receives dust plus 30% plus non-distributed rewards", + test: async function () { + if (runtimeVersion < 300) { + return; + } + const latestBlock = await api.rpc.chain.getBlock(); + + const latestBlockHash = latestBlock.block.hash; + const latestParentBlockHash = latestBlock.block.header.parentHash; + const apiAtIssuanceAfter = await api.at(latestBlockHash); + const apiAtIssuanceBefore = await api.at(latestParentBlockHash); + + let expectedAmountParachainBond = 0n; + + const pendingChainRewards = await apiAtIssuanceAfter.query.inflationRewards.chainsToReward(); + const numberOfChains = BigInt( + (await apiAtIssuanceBefore.query.registrar.registeredParaIds()).length + 1 + ); + + if (pendingChainRewards.isSome) { + const rewardPerChain = pendingChainRewards.unwrap().rewardsPerChain.toBigInt(); + const pendingChainsToReward = BigInt(pendingChainRewards.unwrap().paraIds.length); + expectedAmountParachainBond += pendingChainsToReward * rewardPerChain; + } + + const parachainBondBalanceBefore = ( + await apiAtIssuanceBefore.query.system.account(PARACHAIN_BOND) + ).data.free.toBigInt(); + + const currentChainRewards = await apiAtIssuanceAfter.query.inflationRewards.chainsToReward(); + const events = await apiAtIssuanceAfter.query.system.events(); + const issuance = await fetchIssuance(events).amount.toBigInt(); + + // Dust from computations also goes to parachainBond + let dust = 0n; + if (currentChainRewards.isSome) { + const currentRewardPerChain = currentChainRewards.unwrap().rewardsPerChain.toBigInt(); + dust = (issuance * 7n) / 10n - numberOfChains * currentRewardPerChain; + } + const parachainBondBalanceAfter = ( + await apiAtIssuanceAfter.query.system.account(PARACHAIN_BOND) + ).data.free.toBigInt(); + expectedAmountParachainBond += (issuance * 3n) / 10n + dust; + + // Not sure where this one comes from, looks like a rounding thing + expect(parachainBondBalanceAfter - parachainBondBalanceBefore).to.equal( + expectedAmountParachainBond + 1n + ); + + // we know there might be rounding errors, so we always check it is in the range +-1 + expect( + parachainBondBalanceAfter - parachainBondBalanceBefore >= expectedAmountParachainBond - 1n && + parachainBondBalanceAfter - parachainBondBalanceBefore <= expectedAmountParachainBond + 1n + ).to.be.true; + }, + }); + }, +}); diff --git a/test/suites/smoke-test/test-randomness-consistency.ts b/test/suites/smoke-test/test-randomness-consistency.ts new file mode 100644 index 000000000..0c6a70e4a --- /dev/null +++ b/test/suites/smoke-test/test-randomness-consistency.ts @@ -0,0 +1,82 @@ +import { beforeAll, describeSuite, expect } from "@moonwall/cli"; + +import { ApiPromise } from "@polkadot/api"; +import { fetchRandomnessEvent } from "util/block"; +describeSuite({ + id: "S09", + title: "Sample suite that only runs on Dancebox chains", + foundationMethods: "read_only", + testCases: ({ it, context }) => { + let api: ApiPromise; + let runtimeVersion; + + beforeAll(() => { + api = context.polkadotJs(); + runtimeVersion = api.runtimeVersion.specVersion.toNumber(); + }); + + it({ + id: "C01", + title: "Randomness storage is empty because on-finalize cleans it, unless on session change boundaries", + test: async function () { + if (runtimeVersion < 300) { + return; + } + const sessionLength = 300; + const currentBlock = (await api.rpc.chain.getBlock()).block.header.number.toNumber(); + const randomness = await api.query.collatorAssignment.randomness(); + + // if the next block is a session change, then this storage will be populated + if (currentBlock + (1 % sessionLength) == 0) { + expect(randomness.isEmpty).to.not.be.true; + } else { + expect(randomness.isEmpty).to.be.true; + } + }, + }); + + it({ + id: "C02", + title: "Rotation happened at previous session boundary", + test: async function () { + if (runtimeVersion < 300) { + return; + } + const sessionLength = 300; + const currentBlock = (await api.rpc.chain.getBlock()).block.header.number.toNumber(); + + const blockToCheck = Math.trunc(currentBlock / sessionLength) * sessionLength; + const apiAtIssuanceNewSession = await api.at(await api.rpc.chain.getBlockHash(blockToCheck)); + const apiAtIssuanceBeforeNewSession = await api.at(await api.rpc.chain.getBlockHash(blockToCheck - 1)); + + // Just before, the randomness was not empty + const randomnessBeforeSession = + await apiAtIssuanceBeforeNewSession.query.collatorAssignment.randomness(); + expect(randomnessBeforeSession.isEmpty).to.not.be.true; + + // After, the randomness gets cleaned + const randomnessAfterSession = await apiAtIssuanceNewSession.query.collatorAssignment.randomness(); + expect(randomnessAfterSession.isEmpty).to.be.true; + + // The rotation event should have kicked in, if enabled + const events = await apiAtIssuanceNewSession.query.system.events(); + const randomnessEvent = fetchRandomnessEvent(events); + const session = await apiAtIssuanceNewSession.query.session.currentIndex(); + + expect(randomnessEvent.randomSeed.toHex()).to.not.be.equal( + "0x0000000000000000000000000000000000000000000000000000000000000000" + ); + expect(randomnessEvent.targetSession.toNumber()).to.be.equal(session.toNumber() + 1); + const configuration = await apiAtIssuanceNewSession.query.configuration.activeConfig(); + if ( + configuration.fullRotationPeriod == 0 || + session.toNumber() % configuration.fullRotationPeriod == 0 + ) { + expect(randomnessEvent.fullRotation.toHuman()).to.be.false; + } else { + expect(randomnessEvent.fullRotation.toHuman()).to.be.true; + } + }, + }); + }, +}); diff --git a/test/util/block.ts b/test/util/block.ts index ab26d448e..0d9ecbd46 100644 --- a/test/util/block.ts +++ b/test/util/block.ts @@ -3,7 +3,7 @@ import { filterAndApply } from "@moonwall/util"; import { ApiPromise } from "@polkadot/api"; import { AccountId32, EventRecord } from "@polkadot/types/interfaces"; - +import { Vec, u8, u32, bool } from "@polkadot/types-codec"; export async function jumpSessions(context: DevModeContext, count: number): Promise { const session = (await context.polkadotJs().query.session.currentIndex()).addn(count.valueOf()).toNumber(); @@ -151,6 +151,18 @@ export function fetchRewardAuthorContainers(events: EventRecord[] = []) { return filtered; } +export function fetchRandomnessEvent(events: EventRecord[] = []) { + const filtered = filterAndApply( + events, + "collatorAssignment", + ["NewPendingAssignment"], + ({ event }: EventRecord) => + event.data as unknown as { randomSeed: Vec; fullRotation: bool; targetSession: u32 } + ); + + return filtered[0]; +} + export function fetchIssuance(events: EventRecord[] = []) { const filtered = filterAndApply( events,