feat: Rebrand Polkadot/Substrate references to PezkuwiChain
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.
This commit is contained in:
@@ -0,0 +1,170 @@
|
||||
import { spawn, spawnSync } from "child_process";
|
||||
import { Presets } from "./index";
|
||||
import { logger } from "./utils";
|
||||
import { join } from "path";
|
||||
import stripAnsi from "strip-ansi";
|
||||
import { createWriteStream } from "fs";
|
||||
|
||||
export function rcPresetFor(paraPreset: Presets): string {
|
||||
return paraPreset == Presets.FakeDev ||
|
||||
paraPreset == Presets.FakeDot ||
|
||||
paraPreset == Presets.FakeKsm
|
||||
? "fake-s"
|
||||
: paraPreset;
|
||||
}
|
||||
|
||||
export function znConfigFor(paraPreset: Presets): string {
|
||||
return paraPreset == Presets.RealM ? "../zn-m.toml" : "../zn-s.toml";
|
||||
}
|
||||
|
||||
/// Returns the teyrchain log file.
|
||||
export async function runPreset(paraPreset: Presets): Promise<void> {
|
||||
prepPreset(paraPreset);
|
||||
const znConfig = znConfigFor(paraPreset);
|
||||
logger.info(`Launching ZN config for preset: ${paraPreset}, config: ${znConfig}`);
|
||||
cmd("zombienet", ["--provider", "native", "-l", "text", "spawn", znConfig], "inherit");
|
||||
}
|
||||
|
||||
export async function runPresetUntilLaunched(
|
||||
paraPreset: Presets
|
||||
): Promise<{ killZn: () => void; paraLog: string | null }> {
|
||||
prepPreset(paraPreset);
|
||||
const znConfig = znConfigFor(paraPreset);
|
||||
logger.info(`Launching ZN config for preset: ${paraPreset}, config: ${znConfig}`);
|
||||
const child = spawn("zombienet", ["--provider", "native", "-l", "text", "spawn", znConfig], {
|
||||
stdio: "pipe",
|
||||
cwd: __dirname,
|
||||
});
|
||||
|
||||
return new Promise<{ killZn: () => void; paraLog: string | null }>((resolve, reject) => {
|
||||
const logCmds: string[] = [];
|
||||
child.stdout.on("data", (data) => {
|
||||
const raw: string = stripAnsi(data.toString());
|
||||
if (raw.includes("Log Cmd : ")) {
|
||||
raw.split("\n")
|
||||
.filter((line) => line.includes("Log Cmd : "))
|
||||
.forEach((line) => {
|
||||
logCmds.push(line.replace("Log Cmd : ", "").trim());
|
||||
});
|
||||
}
|
||||
// our hacky way to know ZN is done.
|
||||
if (raw.includes("Teyrchain ID : 1100")) {
|
||||
for (const cmd of logCmds) {
|
||||
logger.info(`${cmd}`);
|
||||
}
|
||||
logger.info(`Launched ZN: ${paraPreset}`);
|
||||
|
||||
// Extract log path from the last log command
|
||||
const lastCmd = logCmds[logCmds.length - 1];
|
||||
const paraLog = lastCmd ? lastCmd.match(/tail -f\s+(.+\.log)/)?.[1] || null : null;
|
||||
|
||||
resolve({
|
||||
killZn: () => {
|
||||
child.kill();
|
||||
logger.verbose(`Killed zn process`);
|
||||
},
|
||||
paraLog,
|
||||
});
|
||||
}
|
||||
});
|
||||
|
||||
child.on("error", (err) => {
|
||||
reject(err);
|
||||
});
|
||||
});
|
||||
}
|
||||
|
||||
export async function spawnMiner(): Promise<() => void> {
|
||||
logger.info(`Spawning miner in background`);
|
||||
|
||||
const logFile = createWriteStream(join(__dirname, "miner.log"), { flags: "a" });
|
||||
|
||||
const child = spawn(
|
||||
"polkadot-staking-miner",
|
||||
[
|
||||
"--uri",
|
||||
"ws://127.0.0.1:9946",
|
||||
"experimental-monitor-multi-block",
|
||||
"--seed-or-path",
|
||||
"//Bob",
|
||||
],
|
||||
{ stdio: "pipe", cwd: __dirname }
|
||||
);
|
||||
|
||||
child.stdout?.pipe(logFile);
|
||||
child.stderr?.pipe(logFile);
|
||||
|
||||
return new Promise<() => void>((resolve, reject) => {
|
||||
child.on("error", (err) => {
|
||||
logger.error(`Error in miner miner: ${err}`);
|
||||
reject(err);
|
||||
});
|
||||
resolve(() => {
|
||||
logger.verbose(`Killing miner process`);
|
||||
logFile.end();
|
||||
child.kill();
|
||||
});
|
||||
});
|
||||
}
|
||||
|
||||
function prepPreset(paraPreset: Presets): void {
|
||||
const rcPreset = rcPresetFor(paraPreset);
|
||||
const targetDir = "../../../../../../target";
|
||||
|
||||
logger.info(`Running para-preset: ${paraPreset}, rc-preset: ${rcPreset}`);
|
||||
cmd("cargo", [
|
||||
"build",
|
||||
"--release",
|
||||
`-p`,
|
||||
`pallet-staking-async-rc-runtime`,
|
||||
`-p`,
|
||||
`pallet-staking-async-teyrchain-runtime`,
|
||||
`-p`,
|
||||
`staging-chain-spec-builder`,
|
||||
]);
|
||||
|
||||
cmd("rm", ["./teyrchain.json"]);
|
||||
cmd("rm", ["./rc.json"]);
|
||||
|
||||
cmd(join(targetDir, "/release/chain-spec-builder"), [
|
||||
"create",
|
||||
"-t",
|
||||
"development",
|
||||
"--runtime",
|
||||
join(
|
||||
targetDir,
|
||||
"/release/wbuild/pallet-staking-async-teyrchain-runtime/pallet_staking_async_teyrchain_runtime.compact.compressed.wasm"
|
||||
),
|
||||
"--relay-chain",
|
||||
"pezkuwichain-local",
|
||||
"--para-id",
|
||||
"1100",
|
||||
"named-preset",
|
||||
paraPreset,
|
||||
]);
|
||||
cmd("mv", ["chain_spec.json", "teyrchain.json"]);
|
||||
|
||||
cmd(join(targetDir, "/release/chain-spec-builder"), [
|
||||
"create",
|
||||
"-t",
|
||||
"development",
|
||||
"--runtime",
|
||||
join(
|
||||
targetDir,
|
||||
"/release/wbuild/pallet-staking-async-rc-runtime/fast_runtime_binary.rs.wasm"
|
||||
),
|
||||
"named-preset",
|
||||
rcPreset,
|
||||
]);
|
||||
cmd("mv", ["chain_spec.json", "rc.json"]);
|
||||
}
|
||||
|
||||
function cmd(cmd: string, args: string[], stdio: string = "ignore"): void {
|
||||
logger.info(`Running command: ${cmd} ${args.join(" ")}`);
|
||||
// @ts-ignore
|
||||
const result = spawnSync(cmd, args, { stdio: stdio, cwd: __dirname });
|
||||
if (result.error || result.status !== 0) {
|
||||
logger.error(`Error running command: ${cmd} ${args.join(" ")}`);
|
||||
logger.error(`Status: ${result.status}`);
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,67 @@
|
||||
import { rcPresetFor, runPreset } from "./cmd";
|
||||
import { logger } from "./utils";
|
||||
import { monitorVmpQueues } from "./vmp-monitor";
|
||||
import { Command } from "commander";
|
||||
|
||||
export enum Presets {
|
||||
FakeDev = "fake-dev",
|
||||
FakeDot = "fake-dot",
|
||||
FakeKsm = "fake-ksm",
|
||||
RealS = "real-s",
|
||||
RealM = "real-m",
|
||||
}
|
||||
|
||||
if (require.main === module) {
|
||||
const program = new Command();
|
||||
program
|
||||
.name("staking-async-papi-tests")
|
||||
.description("Run staking-async PAPI tests")
|
||||
.version("0.1.0");
|
||||
|
||||
program
|
||||
.command("run")
|
||||
.description("Run a given preset. This just sets up the ZN env and runs it")
|
||||
.option(
|
||||
"-p, --para-preset <preset>",
|
||||
"run the given teyrchain preset. The right relay preset, and zn-toml file are auto-chosen.",
|
||||
Presets.FakeDev
|
||||
)
|
||||
.action(async (options) => {
|
||||
const { paraPreset } = options;
|
||||
runPreset(paraPreset);
|
||||
});
|
||||
|
||||
program
|
||||
.command("monitor-vmp")
|
||||
.description("Monitor VMP (Vertical Message Passing) - both DMP and UMP queues")
|
||||
.option(
|
||||
"--relay-port <port>",
|
||||
"Relay chain WebSocket port",
|
||||
"9944"
|
||||
)
|
||||
.option(
|
||||
"--para-port <port>",
|
||||
"Teyrchain WebSocket port (optional)",
|
||||
"9946"
|
||||
)
|
||||
.option(
|
||||
"-r, --refresh <seconds>",
|
||||
"Refresh interval in seconds",
|
||||
"3"
|
||||
)
|
||||
.option(
|
||||
"--para-id <id>",
|
||||
"Specific teyrchain ID to monitor (default: all)"
|
||||
)
|
||||
.action(async (options) => {
|
||||
const { relayPort, paraPort, refresh, paraId } = options;
|
||||
await monitorVmpQueues({
|
||||
relayPort: parseInt(relayPort),
|
||||
paraPort: paraPort ? parseInt(paraPort) : undefined,
|
||||
refreshInterval: parseInt(refresh),
|
||||
paraId: paraId ? parseInt(paraId) : undefined
|
||||
});
|
||||
});
|
||||
|
||||
program.parse(process.argv);
|
||||
}
|
||||
@@ -0,0 +1,452 @@
|
||||
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;
|
||||
}
|
||||
@@ -0,0 +1,143 @@
|
||||
import { teyrchain, rc } from "@polkadot-api/descriptors";
|
||||
import {
|
||||
Binary,
|
||||
createClient,
|
||||
type PolkadotClient,
|
||||
type PolkadotSigner,
|
||||
type TypedApi,
|
||||
} from "polkadot-api";
|
||||
import { fromBufferToBase58 } from "@polkadot-api/bizinikiwi-bindings";
|
||||
import { withPolkadotSdkCompat } from "polkadot-api/polkadot-sdk-compat";
|
||||
import { getWsProvider } from "polkadot-api/ws-provider/web";
|
||||
import { createLogger, format, transports } from "winston";
|
||||
import { sr25519CreateDerive } from "@polkadot-labs/hdkd";
|
||||
import { DEV_PHRASE, entropyToMiniSecret, mnemonicToEntropy, type KeyPair } from "@polkadot-labs/hdkd-helpers";
|
||||
import { getPolkadotSigner } from "polkadot-api/signer";
|
||||
|
||||
export const GlobalTimeout = 30 * 60 * 1000;
|
||||
export const aliceStash = "5GNJqTPyNqANBkUVMN1LPPrxXnFouWXoe2wNSmmEoLctxiZY";
|
||||
|
||||
|
||||
export const logger = createLogger({
|
||||
level: process.env.LOG_LEVEL || "verbose",
|
||||
format: format.combine(format.timestamp(), format.cli()),
|
||||
defaultMeta: { service: "staking-papi-tests" },
|
||||
transports: [new transports.Console()],
|
||||
});
|
||||
|
||||
const miniSecret = entropyToMiniSecret(mnemonicToEntropy(DEV_PHRASE));
|
||||
const derive = sr25519CreateDerive(miniSecret);
|
||||
const aliceKeyPair = derive("//Alice");
|
||||
|
||||
export const alice = getPolkadotSigner(aliceKeyPair.publicKey, "Sr25519", aliceKeyPair.sign);
|
||||
|
||||
export function deriveFrom(s: string, d: string): KeyPair {
|
||||
const miniSecret = entropyToMiniSecret(mnemonicToEntropy(s));
|
||||
const derive = sr25519CreateDerive(miniSecret);
|
||||
return derive(d);
|
||||
}
|
||||
|
||||
export function derivePubkeyFrom(d: string): string {
|
||||
const miniSecret = entropyToMiniSecret(mnemonicToEntropy(DEV_PHRASE));
|
||||
const derive = sr25519CreateDerive(miniSecret);
|
||||
const keyPair = derive(d);
|
||||
// Convert to SS58 address using Bizinikiwi format (42)
|
||||
return ss58(keyPair.publicKey);
|
||||
}
|
||||
|
||||
export function ss58(key: Uint8Array): string {
|
||||
return fromBufferToBase58(42)(key);
|
||||
}
|
||||
|
||||
export type ApiDeclarations = {
|
||||
rcClient: PolkadotClient;
|
||||
paraClient: PolkadotClient;
|
||||
rcApi: TypedApi<typeof rc>;
|
||||
paraApi: TypedApi<typeof teyrchain>;
|
||||
};
|
||||
|
||||
export async function nullifySigned(
|
||||
paraApi: TypedApi<typeof teyrchain>,
|
||||
signer: PolkadotSigner = alice
|
||||
): Promise<boolean> {
|
||||
// signed and signed validation phase to 0
|
||||
const call = paraApi.tx.System.set_storage({
|
||||
items: [
|
||||
// SignedPhase key
|
||||
[
|
||||
Binary.fromBytes(
|
||||
Uint8Array.from([
|
||||
99, 88, 172, 210, 3, 94, 196, 187, 134, 63, 169, 129, 224, 193, 119, 185,
|
||||
])
|
||||
),
|
||||
Binary.fromBytes(Uint8Array.from([0, 0, 0, 0])),
|
||||
],
|
||||
// SignedValidation key
|
||||
[
|
||||
Binary.fromBytes(
|
||||
Uint8Array.from([
|
||||
72, 56, 74, 129, 110, 79, 113, 169, 54, 203, 118, 220, 158, 48, 63, 42,
|
||||
])
|
||||
),
|
||||
Binary.fromBytes(Uint8Array.from([0, 0, 0, 0])),
|
||||
],
|
||||
],
|
||||
}).decodedCall;
|
||||
const res = await paraApi.tx.Sudo.sudo({ call }).signAndSubmit(alice);
|
||||
return res.ok;
|
||||
}
|
||||
|
||||
export async function nullifyUnsigned(
|
||||
paraApi: TypedApi<typeof teyrchain>,
|
||||
signer: PolkadotSigner = alice
|
||||
): Promise<boolean> {
|
||||
// signed and signed validation phase to 0
|
||||
const call = paraApi.tx.System.set_storage({
|
||||
items: [
|
||||
// UnsignedPhase key
|
||||
[
|
||||
Binary.fromBytes(
|
||||
Uint8Array.from([
|
||||
194, 9, 245, 216, 235, 146, 6, 129, 181, 108, 100, 184, 105, 78, 167, 140,
|
||||
])
|
||||
),
|
||||
Binary.fromBytes(Uint8Array.from([0, 0, 0, 0])),
|
||||
],
|
||||
],
|
||||
}).decodedCall;
|
||||
const res = await paraApi.tx.Sudo.sudo({ call }).signAndSubmit(alice);
|
||||
return res.ok;
|
||||
}
|
||||
|
||||
export async function getApis(): Promise<ApiDeclarations> {
|
||||
const rcClient = createClient(withPolkadotSdkCompat(getWsProvider("ws://localhost:9945")));
|
||||
const rcApi = rcClient.getTypedApi(rc);
|
||||
|
||||
const paraClient = createClient(withPolkadotSdkCompat(getWsProvider("ws://localhost:9946")));
|
||||
const paraApi = paraClient.getTypedApi(teyrchain);
|
||||
|
||||
logger.info(`Connected to ${(await rcApi.constants.System.Version()).spec_name}`);
|
||||
logger.info(`Connected to ${(await paraApi.constants.System.Version()).spec_name}`);
|
||||
|
||||
return { rcApi, paraApi, rcClient, paraClient };
|
||||
}
|
||||
|
||||
// Safely convert anything to a string so we can compare them.
|
||||
export function safeJsonStringify(data: any): string {
|
||||
const bigIntReplacer = (key: string, value: any): any => {
|
||||
if (typeof value === "bigint") {
|
||||
return value.toString();
|
||||
}
|
||||
return value;
|
||||
};
|
||||
|
||||
try {
|
||||
return JSON.stringify(data, bigIntReplacer);
|
||||
} catch (error: any) {
|
||||
// Handle potential errors during stringification (e.g., circular references)
|
||||
console.error("Error during JSON stringification:", error.message);
|
||||
throw new Error(
|
||||
"Failed to stringify data due to unsupported types or circular references."
|
||||
);
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,502 @@
|
||||
/*
|
||||
* Vertical Message Passing (VMP) Monitor
|
||||
*
|
||||
* This tool monitors both Downward Message Passing (DMP) and Upward Message Passing (UMP) queues
|
||||
* in the Polkadot relay chain and teyrchains.
|
||||
*
|
||||
* ## Message Flow Overview
|
||||
*
|
||||
* ### Downward Message Passing (DMP): Relay Chain → Teyrchain
|
||||
*
|
||||
* 1. **Message Creation**: Messages are created on the relay chain (e.g., XCM messages from governance)
|
||||
* 2. **Queueing**: Messages are stored in `Dmp::DownwardMessageQueues` storage on the relay chain
|
||||
* - Each teyrchain has its own queue indexed by ParaId
|
||||
* - Messages include the actual message bytes and the block number when sent
|
||||
* 3. **Delivery**: During teyrchain validation, these messages are included in the teyrchain's
|
||||
* inherent data and delivered to the teyrchain
|
||||
* 4. **Processing**: The teyrchain processes these messages in `teyrchain-system` pallet
|
||||
* - Messages are passed to the configured `DmpQueue` handler
|
||||
* - In modern implementations, this is typically the `message-queue` pallet
|
||||
* 5. **Fee Management**: `Dmp::DeliveryFeeFactor` tracks fee multipliers per teyrchain
|
||||
*
|
||||
* ### Upward Message Passing (UMP): Teyrchain → Relay Chain
|
||||
*
|
||||
* 1. **Message Creation**: Messages are created on the teyrchain (e.g., XCM messages)
|
||||
* 2. **Teyrchain Queueing**: Messages are first stored in `TeyrchainSystem::PendingUpwardMessages`
|
||||
* - The teyrchain tracks bandwidth limits and adjusts fee factors based on queue size
|
||||
* 3. **Commitment**: In `on_finalize`, pending messages are moved to `TeyrchainSystem::UpwardMessages`
|
||||
* - These are included in the teyrchain block's proof of validity
|
||||
* 4. **Relay Chain Reception**: When the relay chain validates the teyrchain block:
|
||||
* - UMP messages are extracted from the proof
|
||||
* - Messages are processed by `inclusion` pallet's `receive_upward_messages`
|
||||
* 5. **Processing**: Messages are enqueued into the `message-queue` pallet with origin `Ump(ParaId)`
|
||||
* - The message-queue pallet handles actual execution with weight limits
|
||||
* - Messages can be temporarily or permanently overweight
|
||||
*
|
||||
* ## Key Components
|
||||
*
|
||||
* ### Relay Chain Pallets
|
||||
* - `dmp`: Manages downward message queues and delivery fees
|
||||
* - `inclusion`: Handles teyrchain block validation and UMP message reception
|
||||
* - `message-queue`: Generic message queue processor for various origins (UMP, DMP, HRMP)
|
||||
*
|
||||
* ### Teyrchain Pallets
|
||||
* - `teyrchain-system`: Manages UMP message sending and DMP message reception
|
||||
* - `message-queue`: Processes received DMP messages (and other message types)
|
||||
*
|
||||
* ## Storage Layout
|
||||
*
|
||||
* ### Relay Chain
|
||||
* - `Dmp::DownwardMessageQueues`: Map<ParaId, Vec<InboundDownwardMessage>>
|
||||
* - `Dmp::DeliveryFeeFactor`: Map<ParaId, FixedU128>
|
||||
* - Well-known keys for UMP queue sizes (relay_dispatch_queue_size)
|
||||
*
|
||||
* ### Teyrchain
|
||||
* - `TeyrchainSystem::PendingUpwardMessages`: Vec<UpwardMessage>
|
||||
* - `TeyrchainSystem::UpwardMessages`: Vec<UpwardMessage> (cleared each block)
|
||||
* - `TeyrchainSystem::UpwardDeliveryFeeFactor`: FixedU128
|
||||
*
|
||||
* ## Message Queue Pallet
|
||||
*
|
||||
* The `message-queue` pallet is a generic, paginated message processor that:
|
||||
* - Stores messages in "books" organized by origin (e.g., Ump(ParaId), Dmp)
|
||||
* - Each book contains pages of messages to handle large message volumes efficiently
|
||||
* - Processes messages with strict weight limits to ensure block production
|
||||
* - Handles overweight messages that exceed processing limits
|
||||
* - Emits events for processed, overweight, and failed messages
|
||||
*
|
||||
* ## Bandwidth and Fee Management
|
||||
*
|
||||
* - Both DMP and UMP implement dynamic fee mechanisms
|
||||
* - Fees increase when queues grow large (deterring spam)
|
||||
* - Fees decrease when queues are small (encouraging usage)
|
||||
* - Bandwidth limits prevent any single teyrchain from monopolizing message passing
|
||||
*
|
||||
* ## VMP Message Limits and Risk Analysis
|
||||
*
|
||||
* There are 4 key categories of limits in the VMP system:
|
||||
*
|
||||
* ### 1. Single Message Size Limit
|
||||
*
|
||||
* **DMP (Downward):**
|
||||
* - Enforced at: `polkadot/runtime/teyrchains/src/dmp.rs:189` in `can_queue_downward_message()`
|
||||
* - Configuration: `max_downward_message_size`
|
||||
* - Check: Rejects if `serialized_len > config.max_downward_message_size`
|
||||
*
|
||||
* **UMP (Upward):**
|
||||
* - Teyrchain enforcement: `cumulus/pallets/teyrchain-system/src/lib.rs:1665` in `send_upward_message()`
|
||||
* - Relay validation: `polkadot/runtime/teyrchains/src/inclusion/mod.rs:967` in `check_upward_messages()`
|
||||
* - Configuration: `max_upward_message_size` (hard bound: 128KB defined as MAX_UPWARD_MESSAGE_SIZE_BOUND)
|
||||
*
|
||||
* ### 2. Queue Total Size (Bytes)
|
||||
*
|
||||
* **DMP:**
|
||||
* - Max capacity: `MAX_POSSIBLE_ALLOCATION / max_downward_message_size`
|
||||
* - Calculated in: `polkadot/runtime/teyrchains/src/dmp.rs:318-319` in `dmq_max_length()`
|
||||
* - Enforced at: `polkadot/runtime/teyrchains/src/dmp.rs:194` in `can_queue_downward_message()`
|
||||
*
|
||||
* **UMP:**
|
||||
* - Teyrchain check: `cumulus/pallets/teyrchain-system/src/lib.rs:369-373` (respects relay's remaining capacity)
|
||||
* - Relay limit: `max_upward_queue_size` enforced at `polkadot/runtime/teyrchains/src/inclusion/mod.rs:977-980`
|
||||
*
|
||||
* ### 3. Queue Total Count (Messages)
|
||||
*
|
||||
* **DMP:**
|
||||
* - No explicit total message count limit
|
||||
* - Only implicitly limited by total queue size
|
||||
*
|
||||
* **UMP:**
|
||||
* - Relay limit: `max_upward_queue_count` at `polkadot/runtime/teyrchains/src/inclusion/mod.rs:958-961`
|
||||
* - Teyrchain respects relay's `remaining_count` from `relay_dispatch_queue_remaining_capacity`
|
||||
*
|
||||
* ### 4. Per-Block Append Limit
|
||||
*
|
||||
* **DMP:**
|
||||
* - No explicit per-block limit for senders
|
||||
* - Receivers process up to `processed_downward_messages` per block
|
||||
*
|
||||
* **UMP:**
|
||||
* - Configuration: `max_upward_message_num_per_candidate`
|
||||
* - Teyrchain limit: `cumulus/pallets/teyrchain-system/src/lib.rs:386` in `on_finalize()`
|
||||
* - Relay validation: `polkadot/runtime/teyrchains/src/inclusion/mod.rs:949-952`
|
||||
* - Max bound: 16,384 messages (MAX_UPWARD_MESSAGE_NUM in `polkadot/teyrchain/src/primitives.rs:436`)
|
||||
*
|
||||
* ### Receiver-side Risk: Weight Exhaustion
|
||||
*
|
||||
* Both DMP and UMP messages are processed through the message-queue pallet:
|
||||
* - Weight check: bizinikiwi/frame/message-queue/src/lib.rs:1591 in `process_message_payload()`
|
||||
* - Messages exceeding `overweight_limit` are marked as overweight
|
||||
* - Configuration: `ServiceWeight` and `IdleMaxServiceWeight`
|
||||
* - Overweight handling: Permanently overweight messages require manual execution via `execute_overweight()`
|
||||
*
|
||||
* **Key Insight**: DMP is less restrictive with only size-based limits, while UMP implements all four types of limits,
|
||||
* providing more granular control over message flow.
|
||||
*/
|
||||
|
||||
import { createClient, type PolkadotClient, type TypedApi } from "polkadot-api";
|
||||
import { withPolkadotSdkCompat } from "polkadot-api/polkadot-sdk-compat";
|
||||
import { getWsProvider } from "polkadot-api/ws-provider/web";
|
||||
import { rc, teyrchain } from "@polkadot-api/descriptors";
|
||||
import { logger } from "./utils";
|
||||
|
||||
interface MonitorOptions {
|
||||
relayPort: number;
|
||||
paraPort?: number;
|
||||
refreshInterval: number;
|
||||
paraId?: number;
|
||||
}
|
||||
|
||||
interface DmpQueueInfo {
|
||||
paraId: number;
|
||||
messageCount: number;
|
||||
totalSize: number;
|
||||
avgMessageSize: number;
|
||||
feeFactor: string;
|
||||
messages: Array<{
|
||||
size: number;
|
||||
sentAt: number;
|
||||
}>;
|
||||
}
|
||||
|
||||
interface UmpQueueInfo {
|
||||
paraId: number;
|
||||
relayQueueCount: number;
|
||||
relayQueueSize: number;
|
||||
pendingCount?: number;
|
||||
pendingSize?: number;
|
||||
}
|
||||
|
||||
interface MessageStats {
|
||||
dmp: {
|
||||
totalQueues: number;
|
||||
totalMessages: number;
|
||||
totalSize: number;
|
||||
avgMessagesPerQueue: number;
|
||||
avgSizePerMessage: number;
|
||||
queues: DmpQueueInfo[];
|
||||
};
|
||||
ump: {
|
||||
totalParas: number;
|
||||
totalMessages: number;
|
||||
totalSize: number;
|
||||
queues: UmpQueueInfo[];
|
||||
};
|
||||
}
|
||||
|
||||
export async function monitorVmpQueues(options: MonitorOptions): Promise<void> {
|
||||
const relayWsUrl = `ws://127.0.0.1:${options.relayPort}`;
|
||||
const paraWsUrl = options.paraPort ? `ws://127.0.0.1:${options.paraPort}` : null;
|
||||
|
||||
logger.info(`🚀 Connecting to relay chain at ${relayWsUrl}`);
|
||||
if (paraWsUrl) {
|
||||
logger.info(`🚀 Connecting to teyrchain at ${paraWsUrl}`);
|
||||
}
|
||||
logger.info(`📊 Monitoring VMP queues${options.paraId ? ` for teyrchain ${options.paraId}` : ' for all teyrchains'}`);
|
||||
logger.info(`⏱️ Refresh interval: ${options.refreshInterval}s`);
|
||||
logger.info("");
|
||||
|
||||
try {
|
||||
// Connect to relay chain
|
||||
const relayWsProvider = getWsProvider(relayWsUrl);
|
||||
const relayClient = createClient(withPolkadotSdkCompat(relayWsProvider));
|
||||
const relayApi = relayClient.getTypedApi(rc);
|
||||
|
||||
// Test relay connection
|
||||
const relayChainSpec = await relayClient.getChainSpecData();
|
||||
logger.info(`✅ Connected to relay chain: ${relayChainSpec.name}`);
|
||||
|
||||
// Connect to teyrchain if port provided
|
||||
let paraClient: PolkadotClient | null = null;
|
||||
let paraApi: any | null = null;
|
||||
if (paraWsUrl) {
|
||||
const paraWsProvider = getWsProvider(paraWsUrl);
|
||||
paraClient = createClient(withPolkadotSdkCompat(paraWsProvider));
|
||||
// Use teyrchain descriptor for the teyrchain API
|
||||
paraApi = paraClient.getTypedApi(teyrchain);
|
||||
|
||||
const paraChainSpec = await paraClient.getChainSpecData();
|
||||
logger.info(`✅ Connected to teyrchain: ${paraChainSpec.name}`);
|
||||
}
|
||||
|
||||
const version = await relayApi.constants.System.Version();
|
||||
logger.info(`Relay chain: ${version.spec_name} v${version.spec_version}`);
|
||||
logger.info("");
|
||||
|
||||
// Start monitoring loop
|
||||
while (true) {
|
||||
try {
|
||||
await displayMessageStatus(relayApi, paraApi, options.paraId);
|
||||
} catch (error) {
|
||||
logger.error("Error fetching message data:", error);
|
||||
}
|
||||
|
||||
await sleep(options.refreshInterval * 1000);
|
||||
|
||||
// Clear screen for next update
|
||||
if (process.stdout.isTTY) {
|
||||
process.stdout.write('\x1Bc');
|
||||
}
|
||||
}
|
||||
|
||||
} catch (error) {
|
||||
logger.error("Failed to initialize monitoring:", error);
|
||||
process.exit(1);
|
||||
}
|
||||
}
|
||||
|
||||
async function displayMessageStatus(relayApi: TypedApi<typeof rc>, paraApi: any | null, specificParaId?: number): Promise<void> {
|
||||
const timestamp = new Date().toLocaleString();
|
||||
|
||||
console.log("╔═══════════════════════════════════════════════════════════════╗");
|
||||
console.log("║ Vertical Message Passing Monitor ║");
|
||||
console.log(`║ Last updated: ${timestamp.padEnd(45)} ║`);
|
||||
console.log("╚═══════════════════════════════════════════════════════════════╝");
|
||||
console.log();
|
||||
|
||||
try {
|
||||
const stats = await fetchMessageStats(relayApi, paraApi, specificParaId);
|
||||
|
||||
// Display DMP Statistics
|
||||
console.log("📥 DMP (Downward Message Passing) Statistics:");
|
||||
console.log(` Active Queues: ${stats.dmp.totalQueues}`);
|
||||
console.log(` Total Messages: ${stats.dmp.totalMessages}`);
|
||||
console.log(` Total Size: ${formatBytes(stats.dmp.totalSize)}`);
|
||||
if (stats.dmp.totalQueues > 0) {
|
||||
console.log(` Avg Messages/Queue: ${stats.dmp.avgMessagesPerQueue.toFixed(1)}`);
|
||||
}
|
||||
if (stats.dmp.totalMessages > 0) {
|
||||
console.log(` Avg Message Size: ${formatBytes(stats.dmp.avgSizePerMessage)}`);
|
||||
}
|
||||
console.log();
|
||||
|
||||
// Display UMP Statistics
|
||||
console.log("📤 UMP (Upward Message Passing) Statistics:");
|
||||
console.log(` Active Paras: ${stats.ump.totalParas}`);
|
||||
console.log(` Total Messages: ${stats.ump.totalMessages}`);
|
||||
console.log(` Total Size: ${formatBytes(stats.ump.totalSize)}`);
|
||||
console.log();
|
||||
|
||||
// Display DMP queue details
|
||||
if (stats.dmp.queues.length > 0) {
|
||||
console.log("📋 DMP Queue Details:");
|
||||
console.log("┌─────────────┬───────────┬─────────────┬─────────────┬─────────────┐");
|
||||
console.log("│ Para ID │ Messages │ Total Size │ Avg Size │ Fee Factor │");
|
||||
console.log("├─────────────┼───────────┼─────────────┼─────────────┼─────────────┤");
|
||||
|
||||
for (const queue of stats.dmp.queues.slice(0, 10)) {
|
||||
console.log(
|
||||
`│ ${queue.paraId.toString().padEnd(11)} │ ${queue.messageCount.toString().padEnd(9)} │ ${formatBytes(queue.totalSize).padEnd(11)} │ ${formatBytes(queue.avgMessageSize).padEnd(11)} │ ${queue.feeFactor.padEnd(11)} │`
|
||||
);
|
||||
}
|
||||
|
||||
console.log("└─────────────┴───────────┴─────────────┴─────────────┴─────────────┘");
|
||||
|
||||
if (stats.dmp.queues.length > 10) {
|
||||
console.log(`... and ${stats.dmp.queues.length - 10} more DMP queues`);
|
||||
}
|
||||
console.log();
|
||||
}
|
||||
|
||||
// Display UMP queue details
|
||||
if (stats.ump.queues.length > 0) {
|
||||
console.log("📋 UMP Queue Details:");
|
||||
console.log("┌─────────────┬─────────────┬─────────────┬──────────────┬──────────────┐");
|
||||
console.log("│ Para ID │ Relay Msgs │ Relay Size │ Pending Msgs │ Pending Size │");
|
||||
console.log("├─────────────┼─────────────┼─────────────┼──────────────┼──────────────┤");
|
||||
|
||||
for (const queue of stats.ump.queues.slice(0, 10)) {
|
||||
const pendingStr = queue.pendingCount !== undefined ? queue.pendingCount.toString() : "N/A";
|
||||
const pendingSizeStr = queue.pendingSize !== undefined ? formatBytes(queue.pendingSize) : "N/A";
|
||||
console.log(
|
||||
`│ ${queue.paraId.toString().padEnd(11)} │ ${queue.relayQueueCount.toString().padEnd(11)} │ ${formatBytes(queue.relayQueueSize).padEnd(11)} │ ${pendingStr.padEnd(12)} │ ${pendingSizeStr.padEnd(12)} │`
|
||||
);
|
||||
}
|
||||
|
||||
console.log("└─────────────┴─────────────┴─────────────┴──────────────┴──────────────┘");
|
||||
|
||||
if (stats.ump.queues.length > 10) {
|
||||
console.log(`... and ${stats.ump.queues.length - 10} more UMP queues`);
|
||||
}
|
||||
console.log();
|
||||
}
|
||||
|
||||
// Show most active queues
|
||||
const topDmpQueues = stats.dmp.queues
|
||||
.filter(q => q.messages.length > 0)
|
||||
.sort((a, b) => b.messageCount - a.messageCount)
|
||||
.slice(0, 3);
|
||||
|
||||
if (topDmpQueues.length > 0) {
|
||||
console.log("🔥 Most Active DMP Queues:");
|
||||
for (const queue of topDmpQueues) {
|
||||
console.log(` Para ${queue.paraId}: ${queue.messageCount} messages, latest at block ${Math.max(...queue.messages.map(m => m.sentAt))}`);
|
||||
}
|
||||
console.log();
|
||||
}
|
||||
|
||||
// Warning thresholds
|
||||
const totalMessages = stats.dmp.totalMessages + stats.ump.totalMessages;
|
||||
const totalSize = stats.dmp.totalSize + stats.ump.totalSize;
|
||||
|
||||
if (totalMessages > 1000) {
|
||||
console.log("⚠️ WARNING: High message count detected!");
|
||||
}
|
||||
if (totalSize > 10 * 1024 * 1024) { // 10MB
|
||||
console.log("⚠️ WARNING: High memory usage detected!");
|
||||
}
|
||||
|
||||
// Note about teyrchain connection
|
||||
if (!paraApi && specificParaId) {
|
||||
console.log("ℹ️ Note: Connect to teyrchain with --para-port to see pending UMP messages");
|
||||
}
|
||||
|
||||
} catch (error) {
|
||||
console.log("❌ Error fetching message statistics:");
|
||||
console.log(` ${error}`);
|
||||
}
|
||||
|
||||
console.log();
|
||||
console.log("Press Ctrl+C to stop monitoring");
|
||||
console.log();
|
||||
}
|
||||
|
||||
async function fetchMessageStats(relayApi: TypedApi<typeof rc>, paraApi: any | null, specificParaId?: number): Promise<MessageStats> {
|
||||
// Fetch DMP stats from relay chain
|
||||
const [downwardMessageQueues, deliveryFeeFactors] = await Promise.all([
|
||||
relayApi.query.Dmp.DownwardMessageQueues.getEntries(),
|
||||
relayApi.query.Dmp.DeliveryFeeFactor.getEntries()
|
||||
]);
|
||||
|
||||
const dmpQueues: DmpQueueInfo[] = [];
|
||||
let dmpTotalMessages = 0;
|
||||
let dmpTotalSize = 0;
|
||||
|
||||
// Process DMP queues
|
||||
for (const { keyArgs: [paraId], value: messages } of downwardMessageQueues) {
|
||||
if (specificParaId !== undefined && paraId !== specificParaId) {
|
||||
continue;
|
||||
}
|
||||
|
||||
const messageCount = messages.length;
|
||||
if (messageCount === 0) continue;
|
||||
|
||||
const messageSizes = messages.map((msg) => {
|
||||
return msg.msg.asBytes().length
|
||||
});
|
||||
|
||||
const queueTotalSize = messageSizes.reduce((sum: number, size: number) => sum + size, 0);
|
||||
const avgMessageSize = messageCount > 0 ? queueTotalSize / messageCount : 0;
|
||||
|
||||
const feeFactorEntry = deliveryFeeFactors.find(entry => entry.keyArgs[0] === paraId);
|
||||
const feeFactorRaw = feeFactorEntry?.value || 1_000_000_000_000_000_000n;
|
||||
const feeFactorValue = typeof feeFactorRaw === 'bigint' ?
|
||||
Number(feeFactorRaw) / 1_000_000_000_000_000_000 :
|
||||
typeof feeFactorRaw === 'number' ?
|
||||
feeFactorRaw / 1_000_000_000_000_000_000 :
|
||||
1.0;
|
||||
const feeFactor = feeFactorValue.toFixed(6);
|
||||
|
||||
dmpQueues.push({
|
||||
paraId,
|
||||
messageCount,
|
||||
totalSize: queueTotalSize,
|
||||
avgMessageSize,
|
||||
feeFactor,
|
||||
messages: messages.map((msg: any, idx: number) => ({
|
||||
size: messageSizes[idx]!,
|
||||
sentAt: msg.sent_at || 0
|
||||
}))
|
||||
});
|
||||
|
||||
dmpTotalMessages += messageCount;
|
||||
dmpTotalSize += queueTotalSize;
|
||||
}
|
||||
|
||||
// Sort DMP queues by message count
|
||||
dmpQueues.sort((a, b) => b.messageCount - a.messageCount);
|
||||
|
||||
// Fetch UMP stats
|
||||
const umpQueues: UmpQueueInfo[] = [];
|
||||
let umpTotalMessages = 0;
|
||||
let umpTotalSize = 0;
|
||||
|
||||
// Only check UMP for specified paraId when monitoring a specific teyrchain
|
||||
const paraIds = specificParaId ? [specificParaId] : [];
|
||||
|
||||
for (const paraId of paraIds) {
|
||||
try {
|
||||
// Try to get the relay dispatch queue size from well-known key
|
||||
// This is stored by the inclusion pallet when processing UMP messages
|
||||
const wellKnownKey = `0x` +
|
||||
`3a6865617070616765735f73746f726167653a` + // :heappages_storage:
|
||||
`0000` + // twox128("Teyrchains")
|
||||
`0000` + // twox128("RelayDispatchQueueSize")
|
||||
`0000` + // twox64(paraId) - simplified, would need proper encoding
|
||||
paraId.toString(16).padStart(8, '0');
|
||||
|
||||
// For now, we'll check if the para has any activity in message queue
|
||||
// This is a simplified approach - in production you'd query the actual storage
|
||||
const umpQueueInfo: UmpQueueInfo = {
|
||||
paraId,
|
||||
relayQueueCount: 0,
|
||||
relayQueueSize: 0
|
||||
};
|
||||
|
||||
// If we have teyrchain connection and it matches our paraId, get pending messages
|
||||
if (paraApi && paraId === specificParaId) {
|
||||
try {
|
||||
const pendingMessages = await paraApi.query.TeyrchainSystem.PendingUpwardMessages();
|
||||
if (pendingMessages) {
|
||||
umpQueueInfo.pendingCount = pendingMessages.length;
|
||||
umpQueueInfo.pendingSize = pendingMessages.reduce((sum: number, msg: any) => {
|
||||
return sum + (Array.isArray(msg) ? msg.length : 0);
|
||||
}, 0);
|
||||
}
|
||||
} catch (error) {
|
||||
// Teyrchain might not have this storage item
|
||||
}
|
||||
}
|
||||
|
||||
// Only add if there's any activity
|
||||
if (umpQueueInfo.relayQueueCount > 0 || umpQueueInfo.pendingCount) {
|
||||
umpQueues.push(umpQueueInfo);
|
||||
umpTotalMessages += umpQueueInfo.relayQueueCount + (umpQueueInfo.pendingCount || 0);
|
||||
umpTotalSize += umpQueueInfo.relayQueueSize + (umpQueueInfo.pendingSize || 0);
|
||||
}
|
||||
|
||||
} catch (error) {
|
||||
// Continue with next para if this one fails
|
||||
}
|
||||
}
|
||||
|
||||
return {
|
||||
dmp: {
|
||||
totalQueues: dmpQueues.length,
|
||||
totalMessages: dmpTotalMessages,
|
||||
totalSize: dmpTotalSize,
|
||||
avgMessagesPerQueue: dmpQueues.length > 0 ? dmpTotalMessages / dmpQueues.length : 0,
|
||||
avgSizePerMessage: dmpTotalMessages > 0 ? dmpTotalSize / dmpTotalMessages : 0,
|
||||
queues: dmpQueues
|
||||
},
|
||||
ump: {
|
||||
totalParas: umpQueues.length,
|
||||
totalMessages: umpTotalMessages,
|
||||
totalSize: umpTotalSize,
|
||||
queues: umpQueues
|
||||
}
|
||||
};
|
||||
}
|
||||
|
||||
function formatBytes(bytes: number): string {
|
||||
if (bytes === 0) return '0 B';
|
||||
|
||||
const k = 1024;
|
||||
const sizes = ['B', 'KB', 'MB', 'GB'];
|
||||
const i = Math.floor(Math.log(bytes) / Math.log(k));
|
||||
|
||||
return parseFloat((bytes / Math.pow(k, i)).toFixed(1)) + ' ' + sizes[i];
|
||||
}
|
||||
|
||||
function sleep(ms: number): Promise<void> {
|
||||
return new Promise(resolve => setTimeout(resolve, ms));
|
||||
}
|
||||
Reference in New Issue
Block a user