Skip to content

Commit 9b69ae5

Browse files
committed
Adds script to replay blocks
1 parent 60fef11 commit 9b69ae5

File tree

1 file changed

+136
-76
lines changed

1 file changed

+136
-76
lines changed

src/tools/replay-block.ts

Lines changed: 136 additions & 76 deletions
Original file line numberDiff line numberDiff line change
@@ -1,18 +1,18 @@
11
import chalk from "chalk";
2-
import { ApiPromise, WsProvider } from "@polkadot/api";
32
import fs from "fs/promises";
43
import { getApiFor, NETWORK_YARGS_OPTIONS } from "src/utils/networks.ts";
54
import yargs from "yargs";
65
import { runTask, spawnTask } from "src/utils/runner.ts";
76
import { blake2AsHex } from "@polkadot/util-crypto";
8-
import { stringToHex } from "@polkadot/util";
9-
import { convertExponentials } from '@zombienet/utils';
10-
import jsonBg from "json-bigint";
117
import { ALITH_PRIVATE_KEY } from "src/utils/constants.ts";
12-
import { getBlockDetails, listenBlocks } from "src/utils/monitoring.ts";
8+
import { getBlockDetails, } from "src/utils/monitoring.ts";
139
import { TxWithEventAndFee } from "src/utils/types.ts";
10+
import { isAscii, u8aToString, } from "@polkadot/util";
1411

15-
const JSONbig = jsonBg({ useNativeBigInt: true });
12+
import type { GenericEvent } from "@polkadot/types/generic";
13+
14+
import Debug from "debug";
15+
const debug = Debug("tools:replay-block");
1616

1717
export const NETWORK_WS_URLS: { [name: string]: string } = {
1818
rococo: "wss://rococo-rpc.polkadot.io",
@@ -34,6 +34,7 @@ const argv = yargs(process.argv.slice(2))
3434
type: "string",
3535
description: "HTTP(S) url",
3636
string: true,
37+
required: true
3738
},
3839
"moonbeam-binary": {
3940
type: "string",
@@ -52,7 +53,6 @@ const argv = yargs(process.argv.slice(2))
5253

5354
const main = async () => {
5455

55-
5656
const logHandlers = [];
5757
const exitPromises = [];
5858
const processes = [];
@@ -64,7 +64,7 @@ const main = async () => {
6464
const atBlock = argv.at ? argv.at : (await api.rpc.chain.getHeader()).number.toNumber();
6565
const originalBlockHash = await api.rpc.chain.getBlockHash(atBlock);
6666
const originalBlock = await api.rpc.chain.getBlock(originalBlockHash);
67-
const apiAt = await api.at(originalBlockHash);
67+
const originalApiAt = await api.at(originalBlockHash);
6868

6969
const parentHash = originalBlock.block.header.parentHash.toHex();
7070
const moonbeamBinaryPath = argv["moonbeam-binary"]; // to improve
@@ -84,13 +84,14 @@ const main = async () => {
8484
try {
8585
alive = false;
8686
await Promise.race(exitPromises);
87+
console.log(`Disconnecting....`);
88+
await Promise.all(apis.map((handler) => handler.disconnect()));
89+
console.log(`Killing....`);
90+
await Promise.all(processes.map((p) => p.close()));
8791
console.log(`Closing....`);
8892
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();
93+
console.log(`Done`);
94+
process.exit(0);
9495
} catch (err) {
9596
// console.log(err.message);
9697
}
@@ -113,7 +114,8 @@ const main = async () => {
113114

114115
const lazyParams = [
115116
`--lazy-loading-remote-rpc=${argv['fork-url']}`,
116-
`--lazy-loading-delay-between-requests 5`,
117+
`--lazy-loading-delay-between-requests 1`,
118+
`--lazy-loading-max-retries-per-request 0`,
117119
`--lazy-loading-runtime-override=${argv.runtime}`,
118120
`--block=${parentHash}`,
119121
`--alice`,
@@ -123,13 +125,30 @@ const main = async () => {
123125
`--sealing=manual`,
124126
]
125127

128+
const logs = [
129+
`debug`,
130+
`author-filtering=info`,
131+
// `basic-authorship=info`,
132+
`parachain=info,grandpa=info`,
133+
`netlink=info,sync=info,lib=info,multi=info`,
134+
`trie=info,parity-db=info,h2=info`,
135+
`wasm_overrides=info,wasmtime_cranelift=info,wasmtime_jit=info,code-provider=info,wasm-heap=info`,
136+
`evm=info`,
137+
`txpool=info`,
138+
`json=info`,
139+
`lazy=info`
140+
]
141+
const logParams = logs.length > 0 ? [`--log=${logs.join(",")}`] : [];
142+
126143
const alithLogs = "./alith.log"
127144
const alithLogHandler = await fs.open(alithLogs, "w");
128145
logHandlers.push(alithLogHandler);
146+
147+
process.stdout.write(`\t - ${chalk.yellow(`${moonbeamBinaryPath}`)}...`);
129148
const alithProcess = await spawnTask(
130-
`${moonbeamBinaryPath} ${commonParams.join(" ")} ${lazyParams.join(" ")}`,
149+
`${moonbeamBinaryPath} ${commonParams.join(" ")} ${lazyParams.join(" ")} ${logParams.join(" ")}`
131150
);
132-
processes.push(alithProcess);
151+
process.stdout.write(` ✓\n`);
133152

134153
exitPromises.push(new Promise<void>((resolve) => {
135154
alithProcess.stderr.pipe(alithProcess.stdout.pipe(alithLogHandler.createWriteStream()));
@@ -164,26 +183,41 @@ const main = async () => {
164183

165184
const specVersion = await lazyApi.runtimeVersion.specVersion.toNumber();
166185
console.log(`Lazy loaded chain spec version: ${specVersion}`);
186+
console.log(`Creating a block to ensure migration is done`);
167187
await lazyApi.rpc.engine.createBlock(true, false)
168-
await lazyApi.rpc.engine.createBlock(true, false);
188+
// await lazyApi.rpc.engine.createBlock(true, false);
169189

170-
const printExt = (prefix: string, tx) => {
190+
const formatExtrinsic = (prefix: string, tx) => {
171191
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)})`);
192+
return `[${prefix}] Transaction missing`;
176193
}
194+
return `[${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)})`;
177195
}
178196

197+
const formatEventDiff = (original: GenericEvent, replayed: GenericEvent) => {
198+
const types = original.typeDef[0];
199+
console.log(types.namespace);
200+
for (let index = 0; index < types?.sub?.[0]; index++) {
201+
console.log(original.data[types.sub[index].name], original.data[types.sub[index].name]);
202+
}
203+
}
179204

180-
const printEvent = (e: any) => {
205+
const printEvent = (e: any, index: number) => {
181206
const types = e.typeDef;
182207
//console.log(`\t\t${e.meta.documentation.toString()}`);
183208
const lines = e.data.map((data, index) => {
184-
return `${typeof types[index].type == "object" ? "" : types[index].type}: ${data.toString()}`;
209+
let line = ` [${types[index].lookupName || types[index].typeName || types[index].namespace || types[index].type }]`;
210+
let subs = types[index].sub || [];
211+
if (subs.length > 0) {
212+
for (let subIndex = 0; subIndex < subs.length; subIndex++) {
213+
line += `\n\t\t-${subs[subIndex].name}: ${data[subs[subIndex].name]?.toString?.()}`;
214+
}
215+
} else {
216+
line += ` ${data.toString()}`;
217+
}
218+
return line;
185219
}).join(' - ');
186-
console.log(`\t${e.section.toString()}.${e.method.toString()}\t${lines}`)
220+
console.log(`\t[${index}] ${e.section.toString()}.${e.method.toString()}\t${lines}`)
187221
}
188222

189223
const mapEventLine = (e: any) => {
@@ -202,91 +236,116 @@ const main = async () => {
202236
return data;
203237
}
204238

205-
const compare = (txA: TxWithEventAndFee, txB: TxWithEventAndFee) => {
239+
const compareExtrinsics = ({ original, replayed }: { original: TxWithEventAndFee, replayed?: TxWithEventAndFee }) => {
206240
// compareItem(txA, txB, " - Error", "dispatchError");
241+
debug(`[${original.extrinsic.hash.toHex()}] Checking transaction ${original?.fees?.totalFees} vs ${replayed?.fees?.totalFees}`);
207242
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-
}
243+
const eventsA = original.events || [];
244+
const eventsB = replayed?.events || [];
245+
for (let index = 0; index < eventsA.length; index++) {
246+
const eventA = original ? eventsA[index] : null;
247+
const eventB = replayed ? eventsB[index] : null;
248+
debug(`[${original.extrinsic.hash.toHex()}, ${replayed.extrinsic.hash.toHex()}]`, eventA.section, eventA.method, eventB?.section, eventB?.method)
249+
// debug(' ', eventA?.data?.[0]?.['topics']?.toString?.(), eventB?.data?.[0]?.['topics']?.toString?.())
250+
251+
if (!eventA || !eventB) {
217252
valid = false;
253+
} else if (eventA.eq(eventB)) {
254+
continue;
255+
} else if (eventA.method == eventB.method &&
256+
((eventA.section == "balances" && eventA.method == "Deposit") ||
257+
(eventA.section == "system" && eventA.method == "ExtrinsicSuccess"))) {
258+
continue;
259+
} else if (eventA.method == eventB.method &&
260+
eventA.section == "evm" && eventA.method == "Log" && eventA.data[0]['topics'] == "0x793ee8b0d8020fc042a920607e3cbd37f5132c011786c8dd10a685f4414ed381") {
261+
// This contains timestamp: see https://moonbeam.moonscan.io/tx/0x8c686e819c7656bef9a37421d30cb101218c71fc5608bc76d51656a0992d556a#eventlog
262+
continue;
263+
} else if (eventA.method == eventB.method &&
264+
eventA.section == "evm" && eventA.method == "Log") {
265+
// This contains timestamp: see https://moonbeam.moonscan.io/tx/0x8c686e819c7656bef9a37421d30cb101218c71fc5608bc76d51656a0992d556a#eventlog
266+
// console.log(eventA.data[0]['topics'].toString());
218267
}
268+
valid = false;
219269
}
220270

221-
if (txA.events.length !== txB.events.length) {
271+
if (eventsA.length !== replayed?.events?.length) {
222272
valid = false;
223273
}
224-
console.log(`[${txA.extrinsic.hash.toHex()}] Events match: ${valid ? "✅" : "🟥"}`);
274+
const ethExecution = eventsB.find((e) => e.section == "ethereum" && e.method == "Executed");
275+
if (!valid && ethExecution) {
276+
const extra = isAscii(ethExecution.data[4].toU8a())
277+
? u8aToString(ethExecution.data[4].toU8a())
278+
: ethExecution.data[4].toString()
279+
if (extra.includes("Transaction too old")) {
280+
console.log(`[${original.extrinsic.hash.toHex()}] ${chalk.yellow("Skipping")} due to "${extra}"`);
281+
valid = true;
282+
}
283+
}
284+
225285
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;
286+
console.log(`[${original.extrinsic.hash.toHex()}] Events match: ${valid ? "✅" : `🟥 ${replayed?.events?.length ? '' : 'Missing events'}`}`);
287+
for (let index = 0; index < Math.max(eventsA.length, eventsB.length); index++) {
288+
const eventA = eventsA.length > index ? eventsA[index] : null;
289+
const eventB = eventsB.length > index ? eventsB[index] : null;
230290

231-
const simA = mapEventLine(eventA);
232-
const simB = mapEventLine(eventB);
291+
// const simA = mapEventLine(eventA);
292+
// const simB = mapEventLine(eventB);
233293
// compareObjects(simA, simB);
234294
if (!eventA || !eventB || !eventA.eq(eventB)) {
235-
console.log(` ${index}`);
236295
if (eventA) {
237-
printEvent(eventA);
296+
printEvent(eventA, index);
238297
}
239298
if (eventB) {
240-
printEvent(eventB);
299+
printEvent(eventB, index);
241300
}
242301
}
243302
}
244303
}
304+
return valid;
245305
}
246306

247307
let foundBlock = 0;
248308

249309
const submitBlock = async (exts) => {
250-
await new Promise((resolve) => setTimeout(resolve, 1000));
251310
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()}]`);
311+
const currentBlockNumber = (await lazyApi.rpc.chain.getHeader()).number.toNumber();
312+
const currentBlockHash = await lazyApi.rpc.chain.getBlockHash(currentBlockNumber);
256313
const block = await getBlockDetails(lazyApi, currentBlockHash);
257-
console.log(`Block #${currentBlockNumber} [${block.txWithEvents.length} txs]`);
314+
foundBlock++;
258315
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)})`);
316+
// 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)}) [${tx.events.length} events]`);
260317
if (exts[tx.extrinsic.hash.toHex()]) {
261-
foundBlock++;
262-
exts[tx.extrinsic.hash.toHex()].lazyEx = tx;
318+
exts[tx.extrinsic.hash.toHex()].replayed = tx;
263319
}
264320
}
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);
321+
let valid = true;
322+
for (const hash in exts) {
323+
debug(formatExtrinsic("Official", exts[hash].ex));
324+
debug(formatExtrinsic(" Lazy", exts[hash].lazyEx));
325+
if (!compareExtrinsics(exts[hash])) {
326+
valid = false;
270327
}
271328
}
329+
console.log(` - produced Block #${currentBlockNumber} [${block.txWithEvents.length} txs] ${valid ? "✅" : "🟥"}`);
330+
return valid;
272331
};
273332
let blockHash = "";
274333

275334
while (alive) {
276-
const exts = {};
335+
const exts: { [hash: string]: { original: TxWithEventAndFee, replayed?: TxWithEventAndFee } } = {};
277336
try {
278337
const newBlockHash = await api.rpc.chain.getBlockHash(atBlock + foundBlock);
279338
if (blockHash.toString() == newBlockHash.toString()) {
280339
await new Promise((resolve) => setTimeout(resolve, 2000));
281340
continue;
282341
}
342+
blockHash = newBlockHash.toString()
283343
} catch (e) {
284344
await new Promise((resolve) => setTimeout(resolve, 2000));
285345
continue;
286346
}
287-
blockHash = originalBlockHash.toString()
288-
console.log(`===========================Checking block ${atBlock + foundBlock} [${blockHash.toString()}]`);
289347
const blockDetails = await getBlockDetails(api, blockHash);
348+
console.log(`===========Checking block ${chalk.red(atBlock + foundBlock)} [${blockHash.toString()}] [${blockDetails.txWithEvents.length} txs]==============`);
290349
await Promise.all(blockDetails.txWithEvents.map(async (tx, index) => {
291350
const { extrinsic: ex, dispatchInfo, dispatchError } = tx;
292351
if (!dispatchInfo.class.isNormal) {
@@ -295,32 +354,33 @@ const main = async () => {
295354
const { method, signature, isSigned, signer, nonce } = ex;
296355
// console.log(index, `${ex.method.section.toString()}.${ex.method.method.toString()} [${ex.hash.toHex()}]`);
297356
if (method.section === 'sudo' && method.method.startsWith('sudo')) {
357+
const apiAt = await api.at(blockHash);
298358
// Handle sudo extrinsics
299359
const nestedCall = method.args[0]; // The "call" is the first argument in sudo methods
300360
const { section, method: nestedMethod, args: nestedArgs } = apiAt.registry.createType('Call', nestedCall);
301361

302-
console.log(` Nested Call: ${section}.${nestedMethod}`);
362+
debug(` Nested Call: ${section}.${nestedMethod}`);
303363
const nestedDecodedArgs = nestedArgs.map((arg: any) => arg.toHuman());
304-
console.log(` Nested Args: ${JSON.stringify(nestedDecodedArgs, null, 2)}`);
364+
debug(` Nested Args: ${JSON.stringify(nestedDecodedArgs, null, 2)}`);
305365
}
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)})`);
366+
// debug(`${ex.method.method.toString() == "setValidationData" ? "..." : ex.toHex()}`);
367+
// debug(`[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)})`);
308368

309369
await lazyApi.rpc.author.submitExtrinsic(ex.toHex()).then((hash) => {
310-
console.log(`Submitted hash: ${hash}`);
370+
debug(`Submitted hash: ${hash}`);
311371
})
312372
exts[ex.hash.toHex()] = {
313-
ex: tx
373+
original: tx,
374+
replayed: null
314375
}
315376
}));
316-
console.log("Ready for block !!!");
317-
await submitBlock(exts);
377+
if (!await submitBlock(exts)) {
378+
console.log(chalk.red(`Found broken block, waiting forever !!`));
379+
while (true) {
380+
await new Promise((resolve) => setTimeout(resolve, 2000));
381+
}
382+
}
318383
}
319-
320-
321-
console.log(`Waiting....`);
322-
onProcessExit();
323-
324384
};
325385

326386

0 commit comments

Comments
 (0)