|
| 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