Skip to content

Commit 60fef11

Browse files
committed
Adds replay block script (WIP)
1 parent 981138b commit 60fef11

File tree

1 file changed

+328
-0
lines changed

1 file changed

+328
-0
lines changed

src/tools/replay-block.ts

Lines changed: 328 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,328 @@
1+
import chalk from "chalk";
2+
import { ApiPromise, WsProvider } from "@polkadot/api";
3+
import fs from "fs/promises";
4+
import { getApiFor, NETWORK_YARGS_OPTIONS } from "src/utils/networks.ts";
5+
import yargs from "yargs";
6+
import { runTask, spawnTask } from "src/utils/runner.ts";
7+
import { blake2AsHex } from "@polkadot/util-crypto";
8+
import { stringToHex } from "@polkadot/util";
9+
import { convertExponentials } from '@zombienet/utils';
10+
import jsonBg from "json-bigint";
11+
import { ALITH_PRIVATE_KEY } from "src/utils/constants.ts";
12+
import { getBlockDetails, listenBlocks } from "src/utils/monitoring.ts";
13+
import { TxWithEventAndFee } from "src/utils/types.ts";
14+
15+
const JSONbig = jsonBg({ useNativeBigInt: true });
16+
17+
export const NETWORK_WS_URLS: { [name: string]: string } = {
18+
rococo: "wss://rococo-rpc.polkadot.io",
19+
westend: "wss://westend.api.onfinality.io/public-ws",
20+
kusama: "wss://kusama.api.onfinality.io/public-ws",
21+
polkadot: "wss://polkadot.api.onfinality.io/public-ws",
22+
};
23+
24+
const argv = yargs(process.argv.slice(2))
25+
.usage("Usage: $0")
26+
.version("1.0.0")
27+
.options({
28+
...NETWORK_YARGS_OPTIONS,
29+
at: {
30+
type: "number",
31+
description: "Block number",
32+
},
33+
"fork-url": {
34+
type: "string",
35+
description: "HTTP(S) url",
36+
string: true,
37+
},
38+
"moonbeam-binary": {
39+
type: "string",
40+
alias: "m",
41+
description:
42+
"Absolute file path (e.g. /tmp/fork-chain/moonbeam) of moonbeam binary OR version number (e.g. 0.31) to download.",
43+
default: "../moonbeam/target/release/moonbeam",
44+
},
45+
"runtime": {
46+
type: "string",
47+
alias: "r",
48+
describe: "Input path for runtime blob to ",
49+
default: "../moonbeam/target/release/wbuild/moonbeam-runtime/moonbeam_runtime.compact.compressed.wasm"
50+
},
51+
}).argv;
52+
53+
const main = async () => {
54+
55+
56+
const logHandlers = [];
57+
const exitPromises = [];
58+
const processes = [];
59+
const apis = [];
60+
61+
const api = await getApiFor(argv);
62+
apis.push(api);
63+
64+
const atBlock = argv.at ? argv.at : (await api.rpc.chain.getHeader()).number.toNumber();
65+
const originalBlockHash = await api.rpc.chain.getBlockHash(atBlock);
66+
const originalBlock = await api.rpc.chain.getBlock(originalBlockHash);
67+
const apiAt = await api.at(originalBlockHash);
68+
69+
const parentHash = originalBlock.block.header.parentHash.toHex();
70+
const moonbeamBinaryPath = argv["moonbeam-binary"]; // to improve
71+
72+
process.stdout.write(`\t - Checking moonbeam binary...`);
73+
const moonbeamVersion = (await runTask(`${moonbeamBinaryPath} --version`)).trim();
74+
process.stdout.write(` ${chalk.green(moonbeamVersion.trim())} ✓\n`);
75+
76+
process.stdout.write(`\t - Checking moonbeam runtime...`);
77+
const runtimeBlob = await fs.readFile(argv.runtime);
78+
const runtimeHash = blake2AsHex(runtimeBlob);
79+
process.stdout.write(` ${chalk.green(runtimeHash)} ✓\n`);
80+
81+
process.stdout.write("Done ✅\n");
82+
83+
const onProcessExit = async () => {
84+
try {
85+
alive = false;
86+
await Promise.race(exitPromises);
87+
console.log(`Closing....`);
88+
await Promise.all(logHandlers.map((handler) => handler.close()));
89+
console.log(`Killing....`);
90+
await Promise.all(processes.map((handler) => handler.close()));
91+
await Promise.all(apis.map((handler) => handler.disconnect()));
92+
await lazyApi.disconnect();
93+
await api.disconnect();
94+
} catch (err) {
95+
// console.log(err.message);
96+
}
97+
};
98+
99+
process.once("SIGINT", onProcessExit);
100+
101+
const commonParams = ["--database paritydb",
102+
"--rpc-cors all",
103+
"--unsafe-rpc-external",
104+
"--rpc-methods=Unsafe",
105+
"--no-private-ipv4",
106+
"--no-mdns",
107+
"--no-prometheus",
108+
"--no-grandpa",
109+
"--reserved-only",
110+
"--detailed-log-output",
111+
"--enable-log-reloading"
112+
];
113+
114+
const lazyParams = [
115+
`--lazy-loading-remote-rpc=${argv['fork-url']}`,
116+
`--lazy-loading-delay-between-requests 5`,
117+
`--lazy-loading-runtime-override=${argv.runtime}`,
118+
`--block=${parentHash}`,
119+
`--alice`,
120+
`--force-authoring`,
121+
`--blocks-pruning=archive`,
122+
`--unsafe-force-node-key-generation`,
123+
`--sealing=manual`,
124+
]
125+
126+
const alithLogs = "./alith.log"
127+
const alithLogHandler = await fs.open(alithLogs, "w");
128+
logHandlers.push(alithLogHandler);
129+
const alithProcess = await spawnTask(
130+
`${moonbeamBinaryPath} ${commonParams.join(" ")} ${lazyParams.join(" ")}`,
131+
);
132+
processes.push(alithProcess);
133+
134+
exitPromises.push(new Promise<void>((resolve) => {
135+
alithProcess.stderr.pipe(alithProcess.stdout.pipe(alithLogHandler.createWriteStream()));
136+
alithProcess.on("exit", () => {
137+
console.log(`${chalk.red(`parachain alith`)} is closed.`);
138+
resolve();
139+
});
140+
process.on("exit", () => {
141+
try {
142+
alithProcess.kill();
143+
} catch (e) { }
144+
});
145+
}));
146+
process.stdout.write(`\t - ${chalk.yellow(`Waiting`)}...(~20s)`);
147+
while (
148+
(await runTask(`egrep -o '(Accepting|Running JSON-RPC)' ${alithLogs} || echo "no"`)).trim()
149+
.length < 4
150+
) {
151+
await new Promise((resolve) => setTimeout(resolve, 1000));
152+
}
153+
let alive = true;
154+
155+
process.stdout.write(` ✓\n`);
156+
process.stdout.write(
157+
`ℹ️ ParaChain Explorer: https://polkadot.js.org/apps/?rpc=ws://127.0.0.1:9944#/explorer\n`,
158+
);
159+
process.stdout.write(` Sudo: ${chalk.green("Alith")} ${ALITH_PRIVATE_KEY}\n`);
160+
process.stdout.write(`Council/TC: ${chalk.green("Alith")} ${ALITH_PRIVATE_KEY}\n`);
161+
162+
const lazyApi = await getApiFor({ url: "ws://localhost:9944" });
163+
apis.push(lazyApi);
164+
165+
const specVersion = await lazyApi.runtimeVersion.specVersion.toNumber();
166+
console.log(`Lazy loaded chain spec version: ${specVersion}`);
167+
await lazyApi.rpc.engine.createBlock(true, false)
168+
await lazyApi.rpc.engine.createBlock(true, false);
169+
170+
const printExt = (prefix: string, tx) => {
171+
if (!tx) {
172+
console.log(`[${prefix}] Transaction missing`);
173+
}
174+
else {
175+
console.log(`[${prefix}] Transaction ${tx.extrinsic.method.section.toString()}.${tx.extrinsic.method.method.toString()} found ${!tx.dispatchError ? "✅" : "🟥"} (ref: ${tx.dispatchInfo.weight.refTime.toString().padStart(12)}, pov: ${tx.dispatchInfo.weight.proofSize.toString().padStart(9)})`);
176+
}
177+
}
178+
179+
180+
const printEvent = (e: any) => {
181+
const types = e.typeDef;
182+
//console.log(`\t\t${e.meta.documentation.toString()}`);
183+
const lines = e.data.map((data, index) => {
184+
return `${typeof types[index].type == "object" ? "" : types[index].type}: ${data.toString()}`;
185+
}).join(' - ');
186+
console.log(`\t${e.section.toString()}.${e.method.toString()}\t${lines}`)
187+
}
188+
189+
const mapEventLine = (e: any) => {
190+
if (!e) {
191+
return {}
192+
}
193+
const types = e.typeDef;
194+
const data = {};
195+
for (let index = 0; index < e.data.length; index++) {
196+
if (types[index].type == "object") {
197+
data[types[index].lookupName] = mapEventLine(e.data[index])
198+
} else {
199+
data[types[index].type] = e.data[index].toString()
200+
}
201+
}
202+
return data;
203+
}
204+
205+
const compare = (txA: TxWithEventAndFee, txB: TxWithEventAndFee) => {
206+
// compareItem(txA, txB, " - Error", "dispatchError");
207+
let valid = true;
208+
for (let index = 0; index < txA.events.length; index++) {
209+
const eventA = txA.events[index];
210+
const eventB = txB.events[index];
211+
if (!eventA || !eventB || !eventA.eq(eventB)) {
212+
if (eventA.method == eventB.method &&
213+
((eventA.section.toString() == "balances" && eventA.method.toString() == "Deposit") ||
214+
(eventA.section.toString() == "system" && eventA.method.toString() == "ExtrinsicSuccess"))) {
215+
continue;
216+
}
217+
valid = false;
218+
}
219+
}
220+
221+
if (txA.events.length !== txB.events.length) {
222+
valid = false;
223+
}
224+
console.log(`[${txA.extrinsic.hash.toHex()}] Events match: ${valid ? "✅" : "🟥"}`);
225+
if (!valid) {
226+
console.log(`[${txA.extrinsic.hash.toHex()}] Events do not match`);
227+
for (let index = 0; index < Math.max(txA.events.length, txB.events.length); index++) {
228+
const eventA = txA.events.length > index ? txA.events[index] : null;
229+
const eventB = txB.events.length > index ? txB.events[index] : null;
230+
231+
const simA = mapEventLine(eventA);
232+
const simB = mapEventLine(eventB);
233+
// compareObjects(simA, simB);
234+
if (!eventA || !eventB || !eventA.eq(eventB)) {
235+
console.log(` ${index}`);
236+
if (eventA) {
237+
printEvent(eventA);
238+
}
239+
if (eventB) {
240+
printEvent(eventB);
241+
}
242+
}
243+
}
244+
}
245+
}
246+
247+
let foundBlock = 0;
248+
249+
const submitBlock = async (exts) => {
250+
await new Promise((resolve) => setTimeout(resolve, 1000));
251+
await lazyApi.rpc.engine.createBlock(true, false);
252+
await new Promise((resolve) => setTimeout(resolve, 1000));
253+
const currentBlockNumber = (await api.rpc.chain.getHeader()).number.toNumber();
254+
const currentBlockHash = await api.rpc.chain.getBlockHash(atBlock);
255+
console.log(`Block #${currentBlockNumber} [${currentBlockHash.toString()}]`);
256+
const block = await getBlockDetails(lazyApi, currentBlockHash);
257+
console.log(`Block #${currentBlockNumber} [${block.txWithEvents.length} txs]`);
258+
for (const tx of block.txWithEvents) {
259+
console.log(`[Lazy] Transaction ${tx.extrinsic.method.section.toString()}.${tx.extrinsic.method.method.toString()} found ${!tx.dispatchError ? "✅" : "🟥"} (ref: ${tx.dispatchInfo.weight.refTime.toString().padStart(12)}, pov: ${tx.dispatchInfo.weight.proofSize.toString().padStart(9)})`);
260+
if (exts[tx.extrinsic.hash.toHex()]) {
261+
foundBlock++;
262+
exts[tx.extrinsic.hash.toHex()].lazyEx = tx;
263+
}
264+
}
265+
if (foundBlock > 0) {
266+
for (const hash in exts) {
267+
//printExt("Official", exts[hash].ex);
268+
//printExt(" Lazy", exts[hash].lazyEx);
269+
compare(exts[Object.keys(exts)[0]].ex, exts[Object.keys(exts)[0]].lazyEx);
270+
}
271+
}
272+
};
273+
let blockHash = "";
274+
275+
while (alive) {
276+
const exts = {};
277+
try {
278+
const newBlockHash = await api.rpc.chain.getBlockHash(atBlock + foundBlock);
279+
if (blockHash.toString() == newBlockHash.toString()) {
280+
await new Promise((resolve) => setTimeout(resolve, 2000));
281+
continue;
282+
}
283+
} catch (e) {
284+
await new Promise((resolve) => setTimeout(resolve, 2000));
285+
continue;
286+
}
287+
blockHash = originalBlockHash.toString()
288+
console.log(`===========================Checking block ${atBlock + foundBlock} [${blockHash.toString()}]`);
289+
const blockDetails = await getBlockDetails(api, blockHash);
290+
await Promise.all(blockDetails.txWithEvents.map(async (tx, index) => {
291+
const { extrinsic: ex, dispatchInfo, dispatchError } = tx;
292+
if (!dispatchInfo.class.isNormal) {
293+
return
294+
}
295+
const { method, signature, isSigned, signer, nonce } = ex;
296+
// console.log(index, `${ex.method.section.toString()}.${ex.method.method.toString()} [${ex.hash.toHex()}]`);
297+
if (method.section === 'sudo' && method.method.startsWith('sudo')) {
298+
// Handle sudo extrinsics
299+
const nestedCall = method.args[0]; // The "call" is the first argument in sudo methods
300+
const { section, method: nestedMethod, args: nestedArgs } = apiAt.registry.createType('Call', nestedCall);
301+
302+
console.log(` Nested Call: ${section}.${nestedMethod}`);
303+
const nestedDecodedArgs = nestedArgs.map((arg: any) => arg.toHuman());
304+
console.log(` Nested Args: ${JSON.stringify(nestedDecodedArgs, null, 2)}`);
305+
}
306+
// console.log(`${ex.method.method.toString() == "setValidationData" ? "..." : ex.toHex()}`);
307+
console.log(`[Official] Transaction`, index, `${ex.method.section.toString()}.${ex.method.method.toString()} found ${!dispatchError ? "✅" : "🟥"} (ref: ${dispatchInfo.weight.refTime.toString().padStart(12)}, pov: ${dispatchInfo.weight.proofSize.toString().padStart(9)})`);
308+
309+
await lazyApi.rpc.author.submitExtrinsic(ex.toHex()).then((hash) => {
310+
console.log(`Submitted hash: ${hash}`);
311+
})
312+
exts[ex.hash.toHex()] = {
313+
ex: tx
314+
}
315+
}));
316+
console.log("Ready for block !!!");
317+
await submitBlock(exts);
318+
}
319+
320+
321+
console.log(`Waiting....`);
322+
onProcessExit();
323+
324+
};
325+
326+
327+
328+
main();

0 commit comments

Comments
 (0)