import { test, expect } from "bun:test"; import { Presets } from "../src"; import { runPresetUntilLaunched } from "../src/cmd"; import { Chain, EventOutcome, Observe, runTest, TestCase } from "../src/test-case"; import { alice, getApis, GlobalTimeout, logger, nullifySigned, aliceStash } from "../src/utils"; const PRESET: Presets = Presets.RealS; test( `slashing without disabling on ${PRESET}`, async () => { const { killZn, paraLog } = await runPresetUntilLaunched(PRESET); const apis = await getApis(); let aliceExposedNominators = 0; let pages = 0; const steps = [ // first relay session change at block 11 Observe.on(Chain.Relay, "Session", "NewSession").byBlock(11), // eventually AH will will be instructed to plan a new session. Observe.on(Chain.Teyrchain, "Staking", "SessionRotated") .withDataCheck((x: any) => x.active_era == 0 && x.planned_era == 1) .onPass(() => { nullifySigned(apis.paraApi); }), // Eventually we will receive an activation timestamp in AH, meaning the first era was complete. Observe.on(Chain.Teyrchain, "StakingRcClient", "SessionReportReceived") .withDataCheck((x) => x.activation_timestamp !== undefined) .onPass(() => { // upon completion, submit a slash to rc logger.info("Submitting slash to RC"); const call = apis.rcApi.tx.RootOffences.create_offence({ offenders: [ // alice//Stash, 10%, which will NOT cause any disabling. See `DisablingStrategy` in `./runtimes/teyrchain`. [aliceStash, 100000000], ], maybe_identifications: undefined, maybe_session_index: undefined, }).decodedCall; apis.rcApi.tx.Sudo.sudo({ call }) .signAndSubmit(alice) .then((res: any) => { logger.verbose("Slash submission result:", res.ok); }); }), // we will receive the root offence event Observe.on(Chain.Relay, "RootOffences", "OffenceCreated"), // eventually we will receive an offence in the teyrchain, first the rc-client Observe.on(Chain.Teyrchain, "StakingRcClient", "OffenceReceived").withDataCheck( (x: any) => x.offences_count === 1 ), // then staking Observe.on(Chain.Teyrchain, "Staking", "OffenceReported") .withDataCheck((x: any) => x.offence_era === 1 && x.fraction === 100000000) .onPass(async () => { // let's calculate how many pages of exposure alice has -- this will impact the number of next events. const overview = await apis.paraApi.query.Staking.ErasStakersOverview.getValue( 1, aliceStash ); pages = overview?.page_count || 0; aliceExposedNominators = overview?.nominator_count || 0; // TODO: lazily create the `Slashed` and `SlashComputed` based on this logger.verbose( `Alice has ${aliceExposedNominators} exposed nominators (${pages}) whom we expect to slash later` ); }), // then staking will calculate the slasheh, we only check 1 page Observe.on(Chain.Teyrchain, "Staking", "SlashComputed").withDataCheck( (x: any) => x.page === 0 ), // staking will eventually bump to active era 2, where slashes will be applied. Observe.on(Chain.Teyrchain, "Staking", "EraPaid"), Observe.on(Chain.Teyrchain, "Staking", "SessionRotated").withDataCheck( (x: any) => x.active_era === 2 ), // staking will apply slashes, we only check one slash. Observe.on(Chain.Teyrchain, "Staking", "Slashed"), ]; const testCase = new TestCase( steps.map((s) => s.build()), true, () => { killZn(); } ); const outcome = await runTest(testCase, apis, paraLog); expect(outcome).toEqual(EventOutcome.Done); }, { timeout: GlobalTimeout } );