From 60fef119818e084862e57c58caa44ac1b5aaf848 Mon Sep 17 00:00:00 2001 From: crystalin Date: Fri, 29 Nov 2024 20:47:40 +0100 Subject: [PATCH 1/2] Adds replay block script (WIP) --- src/tools/replay-block.ts | 328 ++++++++++++++++++++++++++++++++++++++ 1 file changed, 328 insertions(+) create mode 100644 src/tools/replay-block.ts diff --git a/src/tools/replay-block.ts b/src/tools/replay-block.ts new file mode 100644 index 0000000..4674d55 --- /dev/null +++ b/src/tools/replay-block.ts @@ -0,0 +1,328 @@ +import chalk from "chalk"; +import { ApiPromise, WsProvider } from "@polkadot/api"; +import fs from "fs/promises"; +import { getApiFor, NETWORK_YARGS_OPTIONS } from "src/utils/networks.ts"; +import yargs from "yargs"; +import { runTask, spawnTask } from "src/utils/runner.ts"; +import { blake2AsHex } from "@polkadot/util-crypto"; +import { stringToHex } from "@polkadot/util"; +import { convertExponentials } from '@zombienet/utils'; +import jsonBg from "json-bigint"; +import { ALITH_PRIVATE_KEY } from "src/utils/constants.ts"; +import { getBlockDetails, listenBlocks } from "src/utils/monitoring.ts"; +import { TxWithEventAndFee } from "src/utils/types.ts"; + +const JSONbig = jsonBg({ useNativeBigInt: true }); + +export const NETWORK_WS_URLS: { [name: string]: string } = { + rococo: "wss://rococo-rpc.polkadot.io", + westend: "wss://westend.api.onfinality.io/public-ws", + kusama: "wss://kusama.api.onfinality.io/public-ws", + polkadot: "wss://polkadot.api.onfinality.io/public-ws", +}; + +const argv = yargs(process.argv.slice(2)) + .usage("Usage: $0") + .version("1.0.0") + .options({ + ...NETWORK_YARGS_OPTIONS, + at: { + type: "number", + description: "Block number", + }, + "fork-url": { + type: "string", + description: "HTTP(S) url", + string: true, + }, + "moonbeam-binary": { + type: "string", + alias: "m", + description: + "Absolute file path (e.g. /tmp/fork-chain/moonbeam) of moonbeam binary OR version number (e.g. 0.31) to download.", + default: "../moonbeam/target/release/moonbeam", + }, + "runtime": { + type: "string", + alias: "r", + describe: "Input path for runtime blob to ", + default: "../moonbeam/target/release/wbuild/moonbeam-runtime/moonbeam_runtime.compact.compressed.wasm" + }, + }).argv; + +const main = async () => { + + + const logHandlers = []; + const exitPromises = []; + const processes = []; + const apis = []; + + const api = await getApiFor(argv); + apis.push(api); + + const atBlock = argv.at ? argv.at : (await api.rpc.chain.getHeader()).number.toNumber(); + const originalBlockHash = await api.rpc.chain.getBlockHash(atBlock); + const originalBlock = await api.rpc.chain.getBlock(originalBlockHash); + const apiAt = await api.at(originalBlockHash); + + const parentHash = originalBlock.block.header.parentHash.toHex(); + const moonbeamBinaryPath = argv["moonbeam-binary"]; // to improve + + process.stdout.write(`\t - Checking moonbeam binary...`); + const moonbeamVersion = (await runTask(`${moonbeamBinaryPath} --version`)).trim(); + process.stdout.write(` ${chalk.green(moonbeamVersion.trim())} ✓\n`); + + process.stdout.write(`\t - Checking moonbeam runtime...`); + const runtimeBlob = await fs.readFile(argv.runtime); + const runtimeHash = blake2AsHex(runtimeBlob); + process.stdout.write(` ${chalk.green(runtimeHash)} ✓\n`); + + process.stdout.write("Done ✅\n"); + + const onProcessExit = async () => { + try { + alive = false; + await Promise.race(exitPromises); + console.log(`Closing....`); + await Promise.all(logHandlers.map((handler) => handler.close())); + console.log(`Killing....`); + await Promise.all(processes.map((handler) => handler.close())); + await Promise.all(apis.map((handler) => handler.disconnect())); + await lazyApi.disconnect(); + await api.disconnect(); + } catch (err) { + // console.log(err.message); + } + }; + + process.once("SIGINT", onProcessExit); + + const commonParams = ["--database paritydb", + "--rpc-cors all", + "--unsafe-rpc-external", + "--rpc-methods=Unsafe", + "--no-private-ipv4", + "--no-mdns", + "--no-prometheus", + "--no-grandpa", + "--reserved-only", + "--detailed-log-output", + "--enable-log-reloading" + ]; + + const lazyParams = [ + `--lazy-loading-remote-rpc=${argv['fork-url']}`, + `--lazy-loading-delay-between-requests 5`, + `--lazy-loading-runtime-override=${argv.runtime}`, + `--block=${parentHash}`, + `--alice`, + `--force-authoring`, + `--blocks-pruning=archive`, + `--unsafe-force-node-key-generation`, + `--sealing=manual`, + ] + + const alithLogs = "./alith.log" + const alithLogHandler = await fs.open(alithLogs, "w"); + logHandlers.push(alithLogHandler); + const alithProcess = await spawnTask( + `${moonbeamBinaryPath} ${commonParams.join(" ")} ${lazyParams.join(" ")}`, + ); + processes.push(alithProcess); + + exitPromises.push(new Promise((resolve) => { + alithProcess.stderr.pipe(alithProcess.stdout.pipe(alithLogHandler.createWriteStream())); + alithProcess.on("exit", () => { + console.log(`${chalk.red(`parachain alith`)} is closed.`); + resolve(); + }); + process.on("exit", () => { + try { + alithProcess.kill(); + } catch (e) { } + }); + })); + process.stdout.write(`\t - ${chalk.yellow(`Waiting`)}...(~20s)`); + while ( + (await runTask(`egrep -o '(Accepting|Running JSON-RPC)' ${alithLogs} || echo "no"`)).trim() + .length < 4 + ) { + await new Promise((resolve) => setTimeout(resolve, 1000)); + } + let alive = true; + + process.stdout.write(` ✓\n`); + process.stdout.write( + `ℹ️ ParaChain Explorer: https://polkadot.js.org/apps/?rpc=ws://127.0.0.1:9944#/explorer\n`, + ); + process.stdout.write(` Sudo: ${chalk.green("Alith")} ${ALITH_PRIVATE_KEY}\n`); + process.stdout.write(`Council/TC: ${chalk.green("Alith")} ${ALITH_PRIVATE_KEY}\n`); + + const lazyApi = await getApiFor({ url: "ws://localhost:9944" }); + apis.push(lazyApi); + + const specVersion = await lazyApi.runtimeVersion.specVersion.toNumber(); + console.log(`Lazy loaded chain spec version: ${specVersion}`); + await lazyApi.rpc.engine.createBlock(true, false) + await lazyApi.rpc.engine.createBlock(true, false); + + const printExt = (prefix: string, tx) => { + if (!tx) { + console.log(`[${prefix}] Transaction missing`); + } + else { + 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)})`); + } + } + + + const printEvent = (e: any) => { + const types = e.typeDef; + //console.log(`\t\t${e.meta.documentation.toString()}`); + const lines = e.data.map((data, index) => { + return `${typeof types[index].type == "object" ? "" : types[index].type}: ${data.toString()}`; + }).join(' - '); + console.log(`\t${e.section.toString()}.${e.method.toString()}\t${lines}`) + } + + const mapEventLine = (e: any) => { + if (!e) { + return {} + } + const types = e.typeDef; + const data = {}; + for (let index = 0; index < e.data.length; index++) { + if (types[index].type == "object") { + data[types[index].lookupName] = mapEventLine(e.data[index]) + } else { + data[types[index].type] = e.data[index].toString() + } + } + return data; + } + + const compare = (txA: TxWithEventAndFee, txB: TxWithEventAndFee) => { + // compareItem(txA, txB, " - Error", "dispatchError"); + let valid = true; + for (let index = 0; index < txA.events.length; index++) { + const eventA = txA.events[index]; + const eventB = txB.events[index]; + if (!eventA || !eventB || !eventA.eq(eventB)) { + if (eventA.method == eventB.method && + ((eventA.section.toString() == "balances" && eventA.method.toString() == "Deposit") || + (eventA.section.toString() == "system" && eventA.method.toString() == "ExtrinsicSuccess"))) { + continue; + } + valid = false; + } + } + + if (txA.events.length !== txB.events.length) { + valid = false; + } + console.log(`[${txA.extrinsic.hash.toHex()}] Events match: ${valid ? "✅" : "🟥"}`); + if (!valid) { + console.log(`[${txA.extrinsic.hash.toHex()}] Events do not match`); + for (let index = 0; index < Math.max(txA.events.length, txB.events.length); index++) { + const eventA = txA.events.length > index ? txA.events[index] : null; + const eventB = txB.events.length > index ? txB.events[index] : null; + + const simA = mapEventLine(eventA); + const simB = mapEventLine(eventB); + // compareObjects(simA, simB); + if (!eventA || !eventB || !eventA.eq(eventB)) { + console.log(` ${index}`); + if (eventA) { + printEvent(eventA); + } + if (eventB) { + printEvent(eventB); + } + } + } + } + } + + let foundBlock = 0; + + const submitBlock = async (exts) => { + await new Promise((resolve) => setTimeout(resolve, 1000)); + await lazyApi.rpc.engine.createBlock(true, false); + await new Promise((resolve) => setTimeout(resolve, 1000)); + const currentBlockNumber = (await api.rpc.chain.getHeader()).number.toNumber(); + const currentBlockHash = await api.rpc.chain.getBlockHash(atBlock); + console.log(`Block #${currentBlockNumber} [${currentBlockHash.toString()}]`); + const block = await getBlockDetails(lazyApi, currentBlockHash); + console.log(`Block #${currentBlockNumber} [${block.txWithEvents.length} txs]`); + for (const tx of block.txWithEvents) { + 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)})`); + if (exts[tx.extrinsic.hash.toHex()]) { + foundBlock++; + exts[tx.extrinsic.hash.toHex()].lazyEx = tx; + } + } + if (foundBlock > 0) { + for (const hash in exts) { + //printExt("Official", exts[hash].ex); + //printExt(" Lazy", exts[hash].lazyEx); + compare(exts[Object.keys(exts)[0]].ex, exts[Object.keys(exts)[0]].lazyEx); + } + } + }; + let blockHash = ""; + + while (alive) { + const exts = {}; + try { + const newBlockHash = await api.rpc.chain.getBlockHash(atBlock + foundBlock); + if (blockHash.toString() == newBlockHash.toString()) { + await new Promise((resolve) => setTimeout(resolve, 2000)); + continue; + } + } catch (e) { + await new Promise((resolve) => setTimeout(resolve, 2000)); + continue; + } + blockHash = originalBlockHash.toString() + console.log(`===========================Checking block ${atBlock + foundBlock} [${blockHash.toString()}]`); + const blockDetails = await getBlockDetails(api, blockHash); + await Promise.all(blockDetails.txWithEvents.map(async (tx, index) => { + const { extrinsic: ex, dispatchInfo, dispatchError } = tx; + if (!dispatchInfo.class.isNormal) { + return + } + const { method, signature, isSigned, signer, nonce } = ex; + // console.log(index, `${ex.method.section.toString()}.${ex.method.method.toString()} [${ex.hash.toHex()}]`); + if (method.section === 'sudo' && method.method.startsWith('sudo')) { + // Handle sudo extrinsics + const nestedCall = method.args[0]; // The "call" is the first argument in sudo methods + const { section, method: nestedMethod, args: nestedArgs } = apiAt.registry.createType('Call', nestedCall); + + console.log(` Nested Call: ${section}.${nestedMethod}`); + const nestedDecodedArgs = nestedArgs.map((arg: any) => arg.toHuman()); + console.log(` Nested Args: ${JSON.stringify(nestedDecodedArgs, null, 2)}`); + } + // console.log(`${ex.method.method.toString() == "setValidationData" ? "..." : ex.toHex()}`); + 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)})`); + + await lazyApi.rpc.author.submitExtrinsic(ex.toHex()).then((hash) => { + console.log(`Submitted hash: ${hash}`); + }) + exts[ex.hash.toHex()] = { + ex: tx + } + })); + console.log("Ready for block !!!"); + await submitBlock(exts); + } + + + console.log(`Waiting....`); + onProcessExit(); + +}; + + + +main(); From 9b69ae5883a3ece1aec9f8ad6cbbd2a405cbd28d Mon Sep 17 00:00:00 2001 From: crystalin Date: Mon, 2 Dec 2024 20:23:58 +0100 Subject: [PATCH 2/2] Adds script to replay blocks --- src/tools/replay-block.ts | 212 ++++++++++++++++++++++++-------------- 1 file changed, 136 insertions(+), 76 deletions(-) diff --git a/src/tools/replay-block.ts b/src/tools/replay-block.ts index 4674d55..82dfb95 100644 --- a/src/tools/replay-block.ts +++ b/src/tools/replay-block.ts @@ -1,18 +1,18 @@ import chalk from "chalk"; -import { ApiPromise, WsProvider } from "@polkadot/api"; import fs from "fs/promises"; import { getApiFor, NETWORK_YARGS_OPTIONS } from "src/utils/networks.ts"; import yargs from "yargs"; import { runTask, spawnTask } from "src/utils/runner.ts"; import { blake2AsHex } from "@polkadot/util-crypto"; -import { stringToHex } from "@polkadot/util"; -import { convertExponentials } from '@zombienet/utils'; -import jsonBg from "json-bigint"; import { ALITH_PRIVATE_KEY } from "src/utils/constants.ts"; -import { getBlockDetails, listenBlocks } from "src/utils/monitoring.ts"; +import { getBlockDetails, } from "src/utils/monitoring.ts"; import { TxWithEventAndFee } from "src/utils/types.ts"; +import { isAscii, u8aToString, } from "@polkadot/util"; -const JSONbig = jsonBg({ useNativeBigInt: true }); +import type { GenericEvent } from "@polkadot/types/generic"; + +import Debug from "debug"; +const debug = Debug("tools:replay-block"); export const NETWORK_WS_URLS: { [name: string]: string } = { rococo: "wss://rococo-rpc.polkadot.io", @@ -34,6 +34,7 @@ const argv = yargs(process.argv.slice(2)) type: "string", description: "HTTP(S) url", string: true, + required: true }, "moonbeam-binary": { type: "string", @@ -52,7 +53,6 @@ const argv = yargs(process.argv.slice(2)) const main = async () => { - const logHandlers = []; const exitPromises = []; const processes = []; @@ -64,7 +64,7 @@ const main = async () => { const atBlock = argv.at ? argv.at : (await api.rpc.chain.getHeader()).number.toNumber(); const originalBlockHash = await api.rpc.chain.getBlockHash(atBlock); const originalBlock = await api.rpc.chain.getBlock(originalBlockHash); - const apiAt = await api.at(originalBlockHash); + const originalApiAt = await api.at(originalBlockHash); const parentHash = originalBlock.block.header.parentHash.toHex(); const moonbeamBinaryPath = argv["moonbeam-binary"]; // to improve @@ -84,13 +84,14 @@ const main = async () => { try { alive = false; await Promise.race(exitPromises); + console.log(`Disconnecting....`); + await Promise.all(apis.map((handler) => handler.disconnect())); + console.log(`Killing....`); + await Promise.all(processes.map((p) => p.close())); console.log(`Closing....`); await Promise.all(logHandlers.map((handler) => handler.close())); - console.log(`Killing....`); - await Promise.all(processes.map((handler) => handler.close())); - await Promise.all(apis.map((handler) => handler.disconnect())); - await lazyApi.disconnect(); - await api.disconnect(); + console.log(`Done`); + process.exit(0); } catch (err) { // console.log(err.message); } @@ -113,7 +114,8 @@ const main = async () => { const lazyParams = [ `--lazy-loading-remote-rpc=${argv['fork-url']}`, - `--lazy-loading-delay-between-requests 5`, + `--lazy-loading-delay-between-requests 1`, + `--lazy-loading-max-retries-per-request 0`, `--lazy-loading-runtime-override=${argv.runtime}`, `--block=${parentHash}`, `--alice`, @@ -123,13 +125,30 @@ const main = async () => { `--sealing=manual`, ] + const logs = [ + `debug`, + `author-filtering=info`, + // `basic-authorship=info`, + `parachain=info,grandpa=info`, + `netlink=info,sync=info,lib=info,multi=info`, + `trie=info,parity-db=info,h2=info`, + `wasm_overrides=info,wasmtime_cranelift=info,wasmtime_jit=info,code-provider=info,wasm-heap=info`, + `evm=info`, + `txpool=info`, + `json=info`, + `lazy=info` + ] + const logParams = logs.length > 0 ? [`--log=${logs.join(",")}`] : []; + const alithLogs = "./alith.log" const alithLogHandler = await fs.open(alithLogs, "w"); logHandlers.push(alithLogHandler); + + process.stdout.write(`\t - ${chalk.yellow(`${moonbeamBinaryPath}`)}...`); const alithProcess = await spawnTask( - `${moonbeamBinaryPath} ${commonParams.join(" ")} ${lazyParams.join(" ")}`, + `${moonbeamBinaryPath} ${commonParams.join(" ")} ${lazyParams.join(" ")} ${logParams.join(" ")}` ); - processes.push(alithProcess); + process.stdout.write(` ✓\n`); exitPromises.push(new Promise((resolve) => { alithProcess.stderr.pipe(alithProcess.stdout.pipe(alithLogHandler.createWriteStream())); @@ -164,26 +183,41 @@ const main = async () => { const specVersion = await lazyApi.runtimeVersion.specVersion.toNumber(); console.log(`Lazy loaded chain spec version: ${specVersion}`); + console.log(`Creating a block to ensure migration is done`); await lazyApi.rpc.engine.createBlock(true, false) - await lazyApi.rpc.engine.createBlock(true, false); + // await lazyApi.rpc.engine.createBlock(true, false); - const printExt = (prefix: string, tx) => { + const formatExtrinsic = (prefix: string, tx) => { if (!tx) { - console.log(`[${prefix}] Transaction missing`); - } - else { - 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)})`); + return `[${prefix}] Transaction missing`; } + 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)})`; } + const formatEventDiff = (original: GenericEvent, replayed: GenericEvent) => { + const types = original.typeDef[0]; + console.log(types.namespace); + for (let index = 0; index < types?.sub?.[0]; index++) { + console.log(original.data[types.sub[index].name], original.data[types.sub[index].name]); + } + } - const printEvent = (e: any) => { + const printEvent = (e: any, index: number) => { const types = e.typeDef; //console.log(`\t\t${e.meta.documentation.toString()}`); const lines = e.data.map((data, index) => { - return `${typeof types[index].type == "object" ? "" : types[index].type}: ${data.toString()}`; + let line = ` [${types[index].lookupName || types[index].typeName || types[index].namespace || types[index].type }]`; + let subs = types[index].sub || []; + if (subs.length > 0) { + for (let subIndex = 0; subIndex < subs.length; subIndex++) { + line += `\n\t\t-${subs[subIndex].name}: ${data[subs[subIndex].name]?.toString?.()}`; + } + } else { + line += ` ${data.toString()}`; + } + return line; }).join(' - '); - console.log(`\t${e.section.toString()}.${e.method.toString()}\t${lines}`) + console.log(`\t[${index}] ${e.section.toString()}.${e.method.toString()}\t${lines}`) } const mapEventLine = (e: any) => { @@ -202,91 +236,116 @@ const main = async () => { return data; } - const compare = (txA: TxWithEventAndFee, txB: TxWithEventAndFee) => { + const compareExtrinsics = ({ original, replayed }: { original: TxWithEventAndFee, replayed?: TxWithEventAndFee }) => { // compareItem(txA, txB, " - Error", "dispatchError"); + debug(`[${original.extrinsic.hash.toHex()}] Checking transaction ${original?.fees?.totalFees} vs ${replayed?.fees?.totalFees}`); let valid = true; - for (let index = 0; index < txA.events.length; index++) { - const eventA = txA.events[index]; - const eventB = txB.events[index]; - if (!eventA || !eventB || !eventA.eq(eventB)) { - if (eventA.method == eventB.method && - ((eventA.section.toString() == "balances" && eventA.method.toString() == "Deposit") || - (eventA.section.toString() == "system" && eventA.method.toString() == "ExtrinsicSuccess"))) { - continue; - } + const eventsA = original.events || []; + const eventsB = replayed?.events || []; + for (let index = 0; index < eventsA.length; index++) { + const eventA = original ? eventsA[index] : null; + const eventB = replayed ? eventsB[index] : null; + debug(`[${original.extrinsic.hash.toHex()}, ${replayed.extrinsic.hash.toHex()}]`, eventA.section, eventA.method, eventB?.section, eventB?.method) + // debug(' ', eventA?.data?.[0]?.['topics']?.toString?.(), eventB?.data?.[0]?.['topics']?.toString?.()) + + if (!eventA || !eventB) { valid = false; + } else if (eventA.eq(eventB)) { + continue; + } else if (eventA.method == eventB.method && + ((eventA.section == "balances" && eventA.method == "Deposit") || + (eventA.section == "system" && eventA.method == "ExtrinsicSuccess"))) { + continue; + } else if (eventA.method == eventB.method && + eventA.section == "evm" && eventA.method == "Log" && eventA.data[0]['topics'] == "0x793ee8b0d8020fc042a920607e3cbd37f5132c011786c8dd10a685f4414ed381") { + // This contains timestamp: see https://moonbeam.moonscan.io/tx/0x8c686e819c7656bef9a37421d30cb101218c71fc5608bc76d51656a0992d556a#eventlog + continue; + } else if (eventA.method == eventB.method && + eventA.section == "evm" && eventA.method == "Log") { + // This contains timestamp: see https://moonbeam.moonscan.io/tx/0x8c686e819c7656bef9a37421d30cb101218c71fc5608bc76d51656a0992d556a#eventlog + // console.log(eventA.data[0]['topics'].toString()); } + valid = false; } - if (txA.events.length !== txB.events.length) { + if (eventsA.length !== replayed?.events?.length) { valid = false; } - console.log(`[${txA.extrinsic.hash.toHex()}] Events match: ${valid ? "✅" : "🟥"}`); + const ethExecution = eventsB.find((e) => e.section == "ethereum" && e.method == "Executed"); + if (!valid && ethExecution) { + const extra = isAscii(ethExecution.data[4].toU8a()) + ? u8aToString(ethExecution.data[4].toU8a()) + : ethExecution.data[4].toString() + if (extra.includes("Transaction too old")) { + console.log(`[${original.extrinsic.hash.toHex()}] ${chalk.yellow("Skipping")} due to "${extra}"`); + valid = true; + } + } + if (!valid) { - console.log(`[${txA.extrinsic.hash.toHex()}] Events do not match`); - for (let index = 0; index < Math.max(txA.events.length, txB.events.length); index++) { - const eventA = txA.events.length > index ? txA.events[index] : null; - const eventB = txB.events.length > index ? txB.events[index] : null; + console.log(`[${original.extrinsic.hash.toHex()}] Events match: ${valid ? "✅" : `🟥 ${replayed?.events?.length ? '' : 'Missing events'}`}`); + for (let index = 0; index < Math.max(eventsA.length, eventsB.length); index++) { + const eventA = eventsA.length > index ? eventsA[index] : null; + const eventB = eventsB.length > index ? eventsB[index] : null; - const simA = mapEventLine(eventA); - const simB = mapEventLine(eventB); + // const simA = mapEventLine(eventA); + // const simB = mapEventLine(eventB); // compareObjects(simA, simB); if (!eventA || !eventB || !eventA.eq(eventB)) { - console.log(` ${index}`); if (eventA) { - printEvent(eventA); + printEvent(eventA, index); } if (eventB) { - printEvent(eventB); + printEvent(eventB, index); } } } } + return valid; } let foundBlock = 0; const submitBlock = async (exts) => { - await new Promise((resolve) => setTimeout(resolve, 1000)); await lazyApi.rpc.engine.createBlock(true, false); - await new Promise((resolve) => setTimeout(resolve, 1000)); - const currentBlockNumber = (await api.rpc.chain.getHeader()).number.toNumber(); - const currentBlockHash = await api.rpc.chain.getBlockHash(atBlock); - console.log(`Block #${currentBlockNumber} [${currentBlockHash.toString()}]`); + const currentBlockNumber = (await lazyApi.rpc.chain.getHeader()).number.toNumber(); + const currentBlockHash = await lazyApi.rpc.chain.getBlockHash(currentBlockNumber); const block = await getBlockDetails(lazyApi, currentBlockHash); - console.log(`Block #${currentBlockNumber} [${block.txWithEvents.length} txs]`); + foundBlock++; for (const tx of block.txWithEvents) { - 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)})`); + // 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]`); if (exts[tx.extrinsic.hash.toHex()]) { - foundBlock++; - exts[tx.extrinsic.hash.toHex()].lazyEx = tx; + exts[tx.extrinsic.hash.toHex()].replayed = tx; } } - if (foundBlock > 0) { - for (const hash in exts) { - //printExt("Official", exts[hash].ex); - //printExt(" Lazy", exts[hash].lazyEx); - compare(exts[Object.keys(exts)[0]].ex, exts[Object.keys(exts)[0]].lazyEx); + let valid = true; + for (const hash in exts) { + debug(formatExtrinsic("Official", exts[hash].ex)); + debug(formatExtrinsic(" Lazy", exts[hash].lazyEx)); + if (!compareExtrinsics(exts[hash])) { + valid = false; } } + console.log(` - produced Block #${currentBlockNumber} [${block.txWithEvents.length} txs] ${valid ? "✅" : "🟥"}`); + return valid; }; let blockHash = ""; while (alive) { - const exts = {}; + const exts: { [hash: string]: { original: TxWithEventAndFee, replayed?: TxWithEventAndFee } } = {}; try { const newBlockHash = await api.rpc.chain.getBlockHash(atBlock + foundBlock); if (blockHash.toString() == newBlockHash.toString()) { await new Promise((resolve) => setTimeout(resolve, 2000)); continue; } + blockHash = newBlockHash.toString() } catch (e) { await new Promise((resolve) => setTimeout(resolve, 2000)); continue; } - blockHash = originalBlockHash.toString() - console.log(`===========================Checking block ${atBlock + foundBlock} [${blockHash.toString()}]`); const blockDetails = await getBlockDetails(api, blockHash); + console.log(`===========Checking block ${chalk.red(atBlock + foundBlock)} [${blockHash.toString()}] [${blockDetails.txWithEvents.length} txs]==============`); await Promise.all(blockDetails.txWithEvents.map(async (tx, index) => { const { extrinsic: ex, dispatchInfo, dispatchError } = tx; if (!dispatchInfo.class.isNormal) { @@ -295,32 +354,33 @@ const main = async () => { const { method, signature, isSigned, signer, nonce } = ex; // console.log(index, `${ex.method.section.toString()}.${ex.method.method.toString()} [${ex.hash.toHex()}]`); if (method.section === 'sudo' && method.method.startsWith('sudo')) { + const apiAt = await api.at(blockHash); // Handle sudo extrinsics const nestedCall = method.args[0]; // The "call" is the first argument in sudo methods const { section, method: nestedMethod, args: nestedArgs } = apiAt.registry.createType('Call', nestedCall); - console.log(` Nested Call: ${section}.${nestedMethod}`); + debug(` Nested Call: ${section}.${nestedMethod}`); const nestedDecodedArgs = nestedArgs.map((arg: any) => arg.toHuman()); - console.log(` Nested Args: ${JSON.stringify(nestedDecodedArgs, null, 2)}`); + debug(` Nested Args: ${JSON.stringify(nestedDecodedArgs, null, 2)}`); } - // console.log(`${ex.method.method.toString() == "setValidationData" ? "..." : ex.toHex()}`); - 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)})`); + // debug(`${ex.method.method.toString() == "setValidationData" ? "..." : ex.toHex()}`); + // 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)})`); await lazyApi.rpc.author.submitExtrinsic(ex.toHex()).then((hash) => { - console.log(`Submitted hash: ${hash}`); + debug(`Submitted hash: ${hash}`); }) exts[ex.hash.toHex()] = { - ex: tx + original: tx, + replayed: null } })); - console.log("Ready for block !!!"); - await submitBlock(exts); + if (!await submitBlock(exts)) { + console.log(chalk.red(`Found broken block, waiting forever !!`)); + while (true) { + await new Promise((resolve) => setTimeout(resolve, 2000)); + } + } } - - - console.log(`Waiting....`); - onProcessExit(); - };