1c0e57d984
This commit systematically rebrands various references from Parity Technologies' Polkadot/Substrate ecosystem to PezkuwiChain within the kurdistan-sdk. Key changes include: - Updated external repository URLs (zombienet-sdk, parity-db, parity-scale-codec, wasm-instrument) to point to pezkuwichain forks. - Modified internal documentation and code comments to reflect PezkuwiChain naming and structure. - Replaced direct references to with or specific paths within the for XCM, Pezkuwi, and other modules. - Cleaned up deprecated issue and PR references in various and files, particularly in and modules. - Adjusted image and logo URLs in documentation to point to PezkuwiChain assets. - Removed or rephrased comments related to external Polkadot/Substrate PRs and issues. This is a significant step towards fully customizing the SDK for the PezkuwiChain ecosystem.
453 lines
13 KiB
TypeScript
453 lines
13 KiB
TypeScript
import { readFileSync } from "fs";
|
|
import { logger, safeJsonStringify, type ApiDeclarations } from "./utils";
|
|
import { exit } from "process";
|
|
import chalk from "chalk";
|
|
|
|
export enum Chain {
|
|
Relay = "Rely",
|
|
Teyrchain = "Para",
|
|
}
|
|
|
|
interface IEvent {
|
|
module: string;
|
|
event: string;
|
|
data: any | undefined;
|
|
}
|
|
|
|
interface IBlock {
|
|
chain: Chain;
|
|
number: number;
|
|
hash: string;
|
|
events: IEvent[];
|
|
weights: any;
|
|
authorship: IAuthorshipData | null;
|
|
}
|
|
|
|
/// The on-chain weight consumed in a block, exactly as stored by `frame-system`
|
|
interface IWeight {
|
|
normal: {
|
|
ref_time: bigint;
|
|
proof_size: bigint;
|
|
};
|
|
operational: {
|
|
ref_time: bigint;
|
|
proof_size: bigint;
|
|
};
|
|
mandatory: {
|
|
ref_time: bigint;
|
|
proof_size: bigint;
|
|
};
|
|
}
|
|
|
|
/// Information obtained from the collator about authorship of a block.
|
|
interface IAuthorshipData {
|
|
/// The header size in PoV in kb.
|
|
header: number;
|
|
/// The extrinsics size in PoV in kb.
|
|
extrinsics: number;
|
|
/// The storage proof size in PoV in kb.
|
|
proof: number;
|
|
/// The compressed PoV size (sum of all the above) in kb.
|
|
compressed: number;
|
|
/// The time it took to author the block in ms.
|
|
time: number;
|
|
}
|
|
|
|
/// Print an event.
|
|
function pe(e: IEvent): string {
|
|
return `${e.module} ${e.event} ${e.data ? safeJsonStringify(e.data) : "no data"}`;
|
|
}
|
|
|
|
interface IObservableEvent {
|
|
chain: Chain;
|
|
module: string;
|
|
event: string;
|
|
dataCheck: ((data: any) => boolean) | undefined;
|
|
byBlock: number | undefined;
|
|
}
|
|
|
|
export class Observe {
|
|
e: IObservableEvent;
|
|
onPass: () => void = () => {};
|
|
|
|
constructor(
|
|
chain: Chain,
|
|
module: string,
|
|
event: string,
|
|
dataCheck: ((data: any) => boolean) | undefined = undefined,
|
|
byBlock: number | undefined = undefined,
|
|
onPass: () => void = () => {}
|
|
) {
|
|
this.e = { chain, module, event, dataCheck, byBlock };
|
|
this.onPass = onPass;
|
|
}
|
|
|
|
toString(): string {
|
|
return `Observe(${this.e.chain}, ${this.e.module}, ${this.e.event}, ${
|
|
this.e.dataCheck ? "dataCheck" : "no dataCheck"
|
|
}, ${this.e.byBlock ? this.e.byBlock : "no byBlock"})`;
|
|
}
|
|
|
|
static on(chain: Chain, mod: string, event: string): ObserveBuilder {
|
|
return new ObserveBuilder(chain, mod, event);
|
|
}
|
|
}
|
|
|
|
export class ObserveBuilder {
|
|
private chain: Chain;
|
|
private module: string;
|
|
private event: string;
|
|
private dataCheck?: (data: any) => boolean;
|
|
private byBlockVal?: number;
|
|
private onPassCallback: () => void = () => {};
|
|
|
|
constructor(chain: Chain, module: string, event: string) {
|
|
this.chain = chain;
|
|
this.module = module;
|
|
this.event = event;
|
|
}
|
|
|
|
withDataCheck(check: (data: any) => boolean): ObserveBuilder {
|
|
this.dataCheck = check;
|
|
return this;
|
|
}
|
|
|
|
byBlock(blockNumber: number): ObserveBuilder {
|
|
this.byBlockVal = blockNumber;
|
|
return this;
|
|
}
|
|
|
|
onPass(callback: () => void): ObserveBuilder {
|
|
this.onPassCallback = callback;
|
|
return this;
|
|
}
|
|
|
|
build(): Observe {
|
|
if (!this.module || !this.event) {
|
|
throw new Error("Module and event are required");
|
|
}
|
|
|
|
return new Observe(
|
|
this.chain,
|
|
this.module,
|
|
this.event,
|
|
this.dataCheck,
|
|
this.byBlockVal,
|
|
this.onPassCallback
|
|
);
|
|
}
|
|
}
|
|
|
|
export enum EventOutcome {
|
|
TimedOut = "TimedOut",
|
|
Done = "Done",
|
|
}
|
|
|
|
export class TestCase {
|
|
eventSequence: Observe[];
|
|
onKill: () => void;
|
|
allowPerChainInterleavedEvents: boolean = false;
|
|
private resolveTestPromise: (outcome: EventOutcome) => void = () => {};
|
|
|
|
/// See `example.test.ts` for more info.
|
|
constructor(e: Observe[], interleave: boolean = false, onKill: () => void = () => {}) {
|
|
this.eventSequence = e;
|
|
this.onKill = onKill;
|
|
this.allowPerChainInterleavedEvents = interleave;
|
|
}
|
|
|
|
setTestPromiseResolvers(resolve: (outcome: EventOutcome) => void) {
|
|
this.resolveTestPromise = resolve;
|
|
}
|
|
|
|
match(ours: IObservableEvent, theirs: IEvent, theirsChain: Chain): boolean {
|
|
const trivialComp =
|
|
ours.chain === theirsChain &&
|
|
ours.module === theirs.module &&
|
|
ours.event === theirs.event;
|
|
if (trivialComp) {
|
|
// note: only run data check if it is defined and all other criteria match
|
|
const dataComp = ours.dataCheck === undefined ? true : ours.dataCheck!(theirs.data);
|
|
return trivialComp && dataComp;
|
|
} else {
|
|
return false;
|
|
}
|
|
}
|
|
|
|
notTimedOut(ours: IObservableEvent, block: number): boolean {
|
|
return ours.byBlock === undefined ? true : block <= ours.byBlock;
|
|
}
|
|
|
|
// with thousand separator!
|
|
wts(num: bigint): string {
|
|
return num.toString().replace(/\B(?=(\d{3})+(?!\d))/g, ",");
|
|
}
|
|
|
|
formatWeight(weight: IWeight): string {
|
|
const weightPerMs = BigInt(Math.pow(10, 9));
|
|
const WeightPerKb = BigInt(1024);
|
|
const refTime =
|
|
weight.normal.ref_time + weight.operational.ref_time + weight.mandatory.ref_time;
|
|
const proofSize =
|
|
weight.normal.proof_size + weight.operational.proof_size + weight.mandatory.proof_size;
|
|
|
|
return `${this.wts(refTime / weightPerMs)}ms / ${this.wts(proofSize / WeightPerKb)} kb`;
|
|
}
|
|
|
|
formatAuthorship(authorship: IAuthorshipData): string {
|
|
return `hd=${authorship.header.toFixed(2)}, xt=${authorship.extrinsics.toFixed(
|
|
2
|
|
)}, st=${authorship.proof.toFixed(2)}, sum=${(
|
|
authorship.header +
|
|
authorship.extrinsics +
|
|
authorship.proof
|
|
).toFixed(2)}, cmp=${authorship.compressed.toFixed(2)}, time=${authorship.time}ms`;
|
|
}
|
|
|
|
commonLog(blockData: IBlock): string {
|
|
const number = `#${blockData.number}`;
|
|
const chain = blockData.chain === Chain.Relay
|
|
? chalk.blue(blockData.chain) // Blue for Relay - works well in both modes
|
|
: chalk.green(blockData.chain); // Green for Teyrchain - works well in both modes
|
|
const weight = `⛓ ${this.formatWeight(blockData.weights)}`;
|
|
const authorship = blockData.authorship
|
|
? `[✍️ ${this.formatAuthorship(blockData.authorship)}]`
|
|
: "";
|
|
return `[${chain}${number}][${weight}]${authorship}`;
|
|
}
|
|
|
|
// returns a [`primary`, `maybeSecondary`] event to check. `primary` should always be checked first, and if not secondary is checked.
|
|
nextEvent(chain: Chain): [Observe, Observe | undefined] {
|
|
const next = this.eventSequence[0]!;
|
|
if (this.allowPerChainInterleavedEvents && this.eventSequence.length > 1) {
|
|
// get the next event in our list that is of type `chain`.
|
|
const nextOfChain = this.eventSequence.slice(1).find((e) => e.e.chain === chain);
|
|
return [next, nextOfChain];
|
|
} else {
|
|
return [next, undefined];
|
|
}
|
|
}
|
|
|
|
removeEvent(e: Observe): void {
|
|
const index = this.eventSequence.findIndex((x) => x.e === e.e);
|
|
if (index !== -1) {
|
|
this.eventSequence.splice(index, 1);
|
|
} else {
|
|
logger.warn(`Event not found for removal: ${e.toString()}`);
|
|
exit(1);
|
|
}
|
|
}
|
|
|
|
onBlock(blockData: IBlock) {
|
|
// sort from small to big
|
|
logger.debug(`${this.commonLog(blockData)} events: ${blockData.events.length}`);
|
|
const firstTimeOut = this.eventSequence
|
|
.filter((e) => e.e.byBlock)
|
|
.sort((x, y) => x.e.byBlock! - y.e.byBlock!);
|
|
if (firstTimeOut.length > 0 && blockData.number > firstTimeOut[0]!.e.byBlock!) {
|
|
logger.error(
|
|
`Block ${blockData.number} is past the first timeout at block ${firstTimeOut[0]}, exiting.`
|
|
);
|
|
this.resolveTestPromise(EventOutcome.TimedOut);
|
|
}
|
|
|
|
for (const e of blockData.events) {
|
|
this.onEvent(e, blockData);
|
|
}
|
|
}
|
|
|
|
onEvent(e: IEvent, blockData: IBlock) {
|
|
if (!this.eventSequence.length) {
|
|
logger.warn(`No events to process for ${blockData.chain}, event: ${pe(e)}`);
|
|
return;
|
|
}
|
|
logger.verbose(`${this.commonLog(blockData)} Processing event: ${pe(e)}`);
|
|
const [primary, maybeSecondary] = this.nextEvent(blockData.chain);
|
|
|
|
if (this.match(primary.e, e, blockData.chain)) {
|
|
primary.onPass();
|
|
this.removeEvent(primary);
|
|
logger.info(`Primary event passed`);
|
|
if (this.eventSequence.length === 0) {
|
|
logger.info("All events processed.");
|
|
this.resolveTestPromise(EventOutcome.Done);
|
|
} else {
|
|
logger.verbose(
|
|
`Next expected event: ${this.eventSequence[0]!.toString()}, remaining events: ${
|
|
this.eventSequence.length
|
|
}`
|
|
);
|
|
}
|
|
} else if (maybeSecondary && this.match(maybeSecondary.e, e, blockData.chain)) {
|
|
maybeSecondary.onPass();
|
|
this.removeEvent(maybeSecondary);
|
|
logger.info(`Secondary event passed`);
|
|
// when we check secondary events, we must have at least 2 items in the list, so no
|
|
// need to check for the end of list.
|
|
} else {
|
|
logger.debug(`event not relevant`);
|
|
}
|
|
}
|
|
}
|
|
|
|
// Extract information about the authoring of `block` number from the given `logFile`. This will
|
|
// work in 3 steps:
|
|
// 1. After filtering for `[Teyrchain]`, and looking at the log file from end to start, it will find
|
|
// the line containing `Prepared block for proposing at ${block}`. From this, we extract the
|
|
// authoring time in ms
|
|
// 2. Them, we only keep the rest of the log file (optimization). We find the first line thereafter
|
|
// containing `PoV size header_kb=... extrinsics_kb=... storage_proof_kb=...` and extract the
|
|
// sizes of the header, extrinsics and storage proof.
|
|
// 3. Finally, we find the first line thereafter containing `Compressed PoV size: ...kb` and extract
|
|
// the compressed size.
|
|
//
|
|
// Note: `logFile` must always relate to a teyrchain.
|
|
function extractAuthorshipData(block: number, logFile: string): IAuthorshipData | null {
|
|
if (block == 0) {
|
|
return null;
|
|
}
|
|
|
|
const log = readFileSync(logFile)
|
|
.toString()
|
|
.split("\n")
|
|
.filter((l) => l.includes("[Teyrchain]"))
|
|
.reverse();
|
|
const target = `Prepared block for proposing at ${block}`;
|
|
const findTime = (log: string[]): { time: number; readStack: string[] } => {
|
|
const readStack: string[] = [];
|
|
for (let i = 0; i < log.length; i++) {
|
|
const line = log[i];
|
|
if (!line) {
|
|
continue;
|
|
}
|
|
readStack.push(line);
|
|
if (line?.includes(target)) {
|
|
const match = line.match("([0-9]+) ms");
|
|
if (match) {
|
|
return { time: Number(match.at(1)!), readStack };
|
|
}
|
|
}
|
|
}
|
|
throw `Could not find authorship line ${target}`;
|
|
};
|
|
|
|
const findProofs = (
|
|
readStack: string[]
|
|
): { header: number; extrinsics: number; proof: number } => {
|
|
for (let i = 0; i < readStack.length; i++) {
|
|
const line = readStack[i];
|
|
const match = line?.match(
|
|
"PoV size header_kb=([0-9]+.[0-9]+) extrinsics_kb=([0-9]+.[0-9]+) storage_proof_kb=([0-9]+.[0-9]+)"
|
|
);
|
|
if (match) {
|
|
return {
|
|
header: Number(match[1]!),
|
|
extrinsics: Number(match[2]!),
|
|
proof: Number(match[3])!,
|
|
};
|
|
}
|
|
}
|
|
throw "Could not find the expected PoV data in log file.";
|
|
};
|
|
|
|
const findCompressed = (readStack: string[]): number => {
|
|
for (let i = 0; i < readStack.length; i++) {
|
|
const line = readStack[i];
|
|
const match = line?.match("Compressed PoV size: ([0-9]+.[0-9]+)kb");
|
|
if (match) {
|
|
return Number(match[1]!);
|
|
}
|
|
}
|
|
throw "Could not find the expected compressed data in log file.";
|
|
};
|
|
|
|
const { time, readStack } = findTime(log);
|
|
// reverse the read stack again, as we want the first proof related prints after we `findTime`.
|
|
readStack.reverse();
|
|
const { header, extrinsics, proof } = findProofs(readStack);
|
|
const compressed = findCompressed(readStack);
|
|
return { time, header, extrinsics, proof, compressed };
|
|
}
|
|
|
|
export async function runTest(
|
|
test: TestCase,
|
|
apis: ApiDeclarations,
|
|
paraLog: string | null
|
|
): Promise<EventOutcome> {
|
|
const { rcClient, paraClient, rcApi, paraApi } = apis;
|
|
|
|
let completionPromise: Promise<EventOutcome> = new Promise((resolve, _) => {
|
|
// Pass the resolve/reject functions to the TestCase instance
|
|
test.setTestPromiseResolvers(resolve);
|
|
|
|
rcClient.finalizedBlock$.subscribe(async (block) => {
|
|
const events = await rcApi.query.System.Events.getValue({ at: block.hash });
|
|
const weights = await rcApi.query.System.BlockWeight.getValue({ at: block.hash });
|
|
const interested = events
|
|
.filter(
|
|
(e) =>
|
|
e.event.type === "Session" ||
|
|
e.event.type === "RootOffences" ||
|
|
e.event.type === "StakingAhClient"
|
|
)
|
|
.map((e) => ({
|
|
module: e.event.type,
|
|
event: e.event.value.type,
|
|
data: e.event.value.value,
|
|
}));
|
|
test.onBlock({
|
|
chain: Chain.Relay,
|
|
number: block.number,
|
|
hash: block.hash,
|
|
events: interested,
|
|
weights: weights,
|
|
authorship: null,
|
|
});
|
|
});
|
|
|
|
paraClient.finalizedBlock$.subscribe(async (block) => {
|
|
const events = await paraApi.query.System.Events.getValue({ at: block.hash });
|
|
const weights = await paraApi.query.System.BlockWeight.getValue({ at: block.hash });
|
|
const interested = events
|
|
.filter(
|
|
(e) =>
|
|
e.event.type == "Staking" ||
|
|
e.event.type == "MultiBlockElection" ||
|
|
e.event.type == "MultiBlockElectionSigned" ||
|
|
e.event.type == "MultiBlockElectionVerifier" ||
|
|
e.event.type == "StakingRcClient"
|
|
)
|
|
.map((e) => ({
|
|
module: e.event.type,
|
|
event: e.event.value.type,
|
|
data: e.event.value.value,
|
|
}));
|
|
test.onBlock({
|
|
chain: Chain.Teyrchain,
|
|
number: block.number,
|
|
hash: block.hash,
|
|
events: interested,
|
|
weights: weights,
|
|
authorship: paraLog ? extractAuthorshipData(block.number, paraLog!) : null,
|
|
});
|
|
});
|
|
});
|
|
|
|
// Handle graceful exit on SIGINT
|
|
process.on("SIGINT", () => {
|
|
console.log("Exiting on Ctrl+C...");
|
|
rcClient.destroy();
|
|
paraClient.destroy();
|
|
test.onKill();
|
|
process.exit(0);
|
|
});
|
|
|
|
// Wait for the completionPromise to resolve/reject
|
|
const finalOutcome = await completionPromise;
|
|
rcClient.destroy();
|
|
paraClient.destroy();
|
|
logger.info(`Test completed with outcome: ${finalOutcome}, calling onKill...`);
|
|
test.onKill();
|
|
return finalOutcome;
|
|
}
|