|
| 1 | +import { spawnSync, spawn, ChildProcess } from "node:child_process"; |
| 2 | +import { writeFile } from "node:fs/promises"; |
| 3 | + |
| 4 | +const all = Promise.all.bind(Promise); |
| 5 | + |
| 6 | +const SECOND = 1000; |
| 7 | +const MINUTE = 60 * SECOND; |
| 8 | + |
| 9 | +const OLD_BINARY_PATH = "/tmp/node-subtensor-old"; |
| 10 | +const NEW_BINARY_PATH = "/tmp/node-subtensor-new"; |
| 11 | +const CHAIN_SPEC_PATH = "/tmp/local.json"; |
| 12 | + |
| 13 | +const ONE_OPTIONS = { |
| 14 | + binaryPath: OLD_BINARY_PATH, |
| 15 | + basePath: "/tmp/one", |
| 16 | + name: "one", |
| 17 | + port: 30333, |
| 18 | + rpcPort: 9933, |
| 19 | + validator: true, |
| 20 | +}; |
| 21 | + |
| 22 | +const TWO_OPTIONS = { |
| 23 | + binaryPath: OLD_BINARY_PATH, |
| 24 | + basePath: "/tmp/two", |
| 25 | + name: "two", |
| 26 | + port: 30334, |
| 27 | + rpcPort: 9944, |
| 28 | + validator: true, |
| 29 | +}; |
| 30 | + |
| 31 | +const ALICE_OPTIONS = { |
| 32 | + binaryPath: OLD_BINARY_PATH, |
| 33 | + basePath: "/tmp/alice", |
| 34 | + name: "alice", |
| 35 | + port: 30335, |
| 36 | + rpcPort: 9955, |
| 37 | + validator: false, |
| 38 | +}; |
| 39 | + |
| 40 | +type Node = { name: string; binaryPath: string; process: ChildProcess }; |
| 41 | + |
| 42 | +async function main() { |
| 43 | + await generateChainSpec(); |
| 44 | + |
| 45 | + const one = startNode(ONE_OPTIONS); |
| 46 | + await started(one); |
| 47 | + |
| 48 | + const two = startNode(TWO_OPTIONS); |
| 49 | + await started(two); |
| 50 | + |
| 51 | + await all([peerCount(one, 1), peerCount(two, 1)]); |
| 52 | + await all([finalizedBlocks(one, 5), finalizedBlocks(two, 5)]); |
| 53 | + |
| 54 | + const alice = startNode(ALICE_OPTIONS); |
| 55 | + await started(alice); |
| 56 | + |
| 57 | + await all([peerCount(one, 2), peerCount(two, 2), peerCount(alice, 2)]); |
| 58 | + await all([finalizedBlocks(one, 10), finalizedBlocks(two, 10), finalizedBlocks(alice, 10)]); |
| 59 | + |
| 60 | + // Swap 'alice' node with the new binary |
| 61 | + await stop(alice); |
| 62 | + const aliceNew = startNode({ ...ALICE_OPTIONS, binaryPath: NEW_BINARY_PATH }); |
| 63 | + await started(aliceNew); |
| 64 | + |
| 65 | + await all([peerCount(one, 2), peerCount(two, 2), peerCount(aliceNew, 2)]); |
| 66 | + await all([finalizedBlocks(one, 15), finalizedBlocks(two, 15), finalizedBlocks(aliceNew, 15)]); |
| 67 | + |
| 68 | + // Swap 'one' node with the new binary |
| 69 | + await stop(one); |
| 70 | + const oneNew = startNode({ ...ONE_OPTIONS, binaryPath: NEW_BINARY_PATH }); |
| 71 | + await started(oneNew); |
| 72 | + |
| 73 | + await all([peerCount(two, 2), peerCount(aliceNew, 2), peerCount(oneNew, 2)]); |
| 74 | + await all([finalizedBlocks(oneNew, 20), finalizedBlocks(two, 20), finalizedBlocks(aliceNew, 20)]); |
| 75 | + |
| 76 | + // Swap 'two' node with the new binary |
| 77 | + await stop(two); |
| 78 | + const twoNew = startNode({ ...TWO_OPTIONS, binaryPath: NEW_BINARY_PATH }); |
| 79 | + await started(twoNew); |
| 80 | + |
| 81 | + await all([peerCount(oneNew, 2), peerCount(twoNew, 2), peerCount(aliceNew, 2)]); |
| 82 | + await all([finalizedBlocks(oneNew, 50), finalizedBlocks(twoNew, 50), finalizedBlocks(aliceNew, 50)]); |
| 83 | + |
| 84 | + await all([stop(oneNew), stop(twoNew), stop(aliceNew)]); |
| 85 | + |
| 86 | + log("Test completed with success, binaries are compatible ✅"); |
| 87 | +} |
| 88 | + |
| 89 | +// Generate the chain spec for the local network |
| 90 | +const generateChainSpec = async () => { |
| 91 | + const result = spawnSync( |
| 92 | + OLD_BINARY_PATH, |
| 93 | + ["build-spec", "--disable-default-bootnode", "--raw", "--chain", "local"], |
| 94 | + { maxBuffer: 1024 * 1024 * 10 }, // 10MB |
| 95 | + ); |
| 96 | + |
| 97 | + if (result.status !== 0) { |
| 98 | + throw new Error(`Failed to generate chain spec: ${result.stderr.toString()}`); |
| 99 | + } |
| 100 | + |
| 101 | + const stdout = result.stdout.toString(); |
| 102 | + await writeFile(CHAIN_SPEC_PATH, stdout, { encoding: "utf-8" }); |
| 103 | +}; |
| 104 | + |
| 105 | +// Start a node with the given options |
| 106 | +const startNode = (opts: { |
| 107 | + binaryPath: string; |
| 108 | + basePath: string; |
| 109 | + name: string; |
| 110 | + port: number; |
| 111 | + rpcPort: number; |
| 112 | + validator: boolean; |
| 113 | +}): Node => { |
| 114 | + const process = spawn(opts.binaryPath, [ |
| 115 | + `--${opts.name}`, |
| 116 | + ...["--chain", CHAIN_SPEC_PATH], |
| 117 | + ...["--base-path", opts.basePath], |
| 118 | + ...["--port", opts.port.toString()], |
| 119 | + ...["--rpc-port", opts.rpcPort.toString()], |
| 120 | + ...(opts.validator ? ["--validator"] : []), |
| 121 | + "--rpc-cors=all", |
| 122 | + "--allow-private-ipv4", |
| 123 | + "--discover-local", |
| 124 | + "--unsafe-force-node-key-generation", |
| 125 | + ]); |
| 126 | + |
| 127 | + process.on("error", (error) => console.error(`${opts.name} (error): ${error}`)); |
| 128 | + process.on("close", (code) => log(`${opts.name}: process closed with code ${code}`)); |
| 129 | + |
| 130 | + return { name: opts.name, binaryPath: opts.binaryPath, process }; |
| 131 | +}; |
| 132 | + |
| 133 | +const stop = (node: Node): Promise<void> => { |
| 134 | + return new Promise((resolve, reject) => { |
| 135 | + node.process.on("close", resolve); |
| 136 | + node.process.on("error", reject); |
| 137 | + |
| 138 | + if (!node.process.kill()) { |
| 139 | + reject(new Error(`Failed to stop ${node.name}`)); |
| 140 | + } |
| 141 | + }); |
| 142 | +}; |
| 143 | + |
| 144 | +// Ensure the node has correctly started |
| 145 | +const started = (node: Node, timeout = 30 * SECOND) => { |
| 146 | + const errorMessage = `Failed to start ${node.name} in time`; |
| 147 | + |
| 148 | + return innerEnsure(node, errorMessage, timeout, (data, ok) => { |
| 149 | + if (data.includes("💤 Idle")) { |
| 150 | + log(`${node.name}: started using ${node.binaryPath}`); |
| 151 | + ok(); |
| 152 | + } |
| 153 | + }); |
| 154 | +}; |
| 155 | + |
| 156 | +// Ensure the node has reached the expected number of peers |
| 157 | +const peerCount = (node: Node, expectedPeers: number, timeout = 30 * SECOND) => { |
| 158 | + const errorMessage = `Failed to reach ${expectedPeers} peers in time`; |
| 159 | + |
| 160 | + return innerEnsure(node, errorMessage, timeout, (data, ok) => { |
| 161 | + const maybePeers = /Idle \((?<peers>\d+) peers\)/.exec(data)?.groups?.peers; |
| 162 | + if (!maybePeers) return; |
| 163 | + |
| 164 | + const peers = parseInt(maybePeers); |
| 165 | + if (peers >= expectedPeers) { |
| 166 | + log(`${node.name}: reached ${expectedPeers} peers`); |
| 167 | + ok(); |
| 168 | + } |
| 169 | + }); |
| 170 | +}; |
| 171 | + |
| 172 | +// Ensure the node has reached the expected number of finalized blocks |
| 173 | +const finalizedBlocks = (node: Node, expectedFinalized: number, timeout = 10 * MINUTE) => { |
| 174 | + const errorMessage = `Failed to reach ${expectedFinalized} finalized blocks in time`; |
| 175 | + |
| 176 | + return innerEnsure(node, errorMessage, timeout, (data, ok) => { |
| 177 | + const maybeFinalized = /finalized #(?<blocks>\d+)/.exec(data)?.groups?.blocks; |
| 178 | + if (!maybeFinalized) return; |
| 179 | + |
| 180 | + const finalized = parseInt(maybeFinalized); |
| 181 | + if (finalized >= expectedFinalized) { |
| 182 | + log(`${node.name}: reached ${expectedFinalized} finalized blocks`); |
| 183 | + ok(); |
| 184 | + } |
| 185 | + }); |
| 186 | +}; |
| 187 | + |
| 188 | +// Helper function to ensure a condition is met within a timeout |
| 189 | +function innerEnsure(node: Node, errorMessage: string, timeout: number, f: (data: string, ok: () => void) => void) { |
| 190 | + return new Promise<void>((resolve, reject) => { |
| 191 | + const id = setTimeout(() => reject(new Error(errorMessage)), timeout); |
| 192 | + |
| 193 | + const fn = (data: string) => |
| 194 | + f(data, () => { |
| 195 | + clearTimeout(id); |
| 196 | + node.process.stderr?.off("data", fn); |
| 197 | + resolve(); |
| 198 | + }); |
| 199 | + |
| 200 | + node.process.stderr?.on("data", fn); |
| 201 | + }); |
| 202 | +} |
| 203 | + |
| 204 | +const log = (message: string) => console.log(`[${new Date().toISOString()}] ${message}`); |
| 205 | + |
| 206 | +main(); |
0 commit comments