Files
pezkuwi-subquery/docker/patches/pruned-state-fallback.js
T
Satoshi Qazi Muhammed c4f2bd943b Lock the indexer image and close the advisories in the build tooling
The indexer image installed @subql/node and the @polkadot -> @pezkuwi aliases
with an unlocked npm install, and the build cache kept the layer from the day
it was first built: the indexers ran @pezkuwi/wasm-crypto 7.5.17/7.5.18, the
build whose ed25519 and secp256k1 are stubs, plus vm2 3.9.19 (the mapping
sandbox, 54 advisories). No lockfile meant Dependabot never saw that tree.

- docker/indexer holds the image's package.json and package-lock.json:
  @subql/node 6.4.6 as today, @pezkuwi 16.5.39 / 14.0.26 / 7.5.21, overrides
  for vm2 3.12.2, uuid 11 and package-json 7 (got 11). The Dockerfile runs
  npm ci on Node 24 (Node 20 is out of support).
- The pruned-state patch stops the build when a pattern matches nothing,
  instead of reporting success with the fallback missing.
- CI builds the indexer image and runs the crypto smoke test inside it
  through the @polkadot aliases, the way subql-node loads them.
- The root yarn.lock, which the host installs as root on every deploy to run
  subql codegen/build, moves the advisory ranges forward with targeted
  resolutions and @pezkuwi/braces. Left: elliptic (low, via ethers 5 in
  @subql/cli) and sprintf-js (moderate, via jest's argparse 1); neither has
  a fixed release.
2026-10-12 17:14:07 +04:00

107 lines
5.0 KiB
JavaScript

/**
* Patch @subql/node and @polkadot/api to handle pruned blockchain state.
*
* Problem: Substrate nodes prune historical state by default (~256 blocks).
* When SubQuery restarts and tries to fetch runtime version for old blocks,
* state_getRuntimeVersion fails with "State already discarded" and the node
* enters a crash loop.
*
* Fix: When getRuntimeVersion(blockHash) fails due to pruned state,
* fall back to getRuntimeVersion() (current runtime version).
* This is safe for chains with infrequent spec upgrades.
*/
const fs = require("fs");
let count = 0;
// A pattern that matches nothing means the patched package changed shape and
// the fallback is gone; stop the image build instead of shipping without it.
function report(label) {
console.log(`${label}: ${count} patches`);
if (count === 0) {
console.error(`${label}: pattern not found`);
process.exit(1);
}
}
// --- Patch 1: @subql/node base-runtime.service.js ---
// getSpecFromApi() and getRuntimeVersion() call
// this.api.rpc.state.getRuntimeVersion(parentBlockHash)
const runtimeFile =
"/app/node_modules/@subql/node/dist/indexer/runtime/base-runtime.service.js";
let runtimeCode = fs.readFileSync(runtimeFile, "utf8");
runtimeCode = runtimeCode.replace(
/await this\.api\.rpc\.state\.getRuntimeVersion\((\w+)\)/g,
(m, v) => { count++; return `await this.api.rpc.state.getRuntimeVersion(${v}).catch(() => this.api.rpc.state.getRuntimeVersion())`; }
);
fs.writeFileSync(runtimeFile, runtimeCode);
report("[1/4] base-runtime.service.js");
// --- Patch 2: @subql/node utils/substrate.js ---
// fetchRuntimeVersionRange() catches and rethrows; add fallback before rethrow
count = 0;
const utilsFile =
"/app/node_modules/@subql/node/dist/utils/substrate.js";
let utilsCode = fs.readFileSync(utilsFile, "utf8");
utilsCode = utilsCode.replace(
/api\.rpc\.state\.getRuntimeVersion\(hash\)\.catch\(\(e\)/g,
() => { count++; return "api.rpc.state.getRuntimeVersion(hash).catch(() => api.rpc.state.getRuntimeVersion()).catch((e)"; }
);
fs.writeFileSync(utilsFile, utilsCode);
report("[2/4] utils/substrate.js");
// --- Patch 3: @polkadot/api Init.js ---
// _getBlockRegistryViaHash calls getRuntimeVersion.raw(header.parentHash)
// which fails when parentHash points to a pruned block (including genesis).
count = 0;
const initFile =
"/app/node_modules/@polkadot/api/cjs/base/Init.js";
let initCode = fs.readFileSync(initFile, "utf8");
initCode = initCode.replace(
/await \(0, rxjs_1\.firstValueFrom\)\(this\._rpcCore\.state\.getRuntimeVersion\.raw\(header\.parentHash\)\)/g,
() => { count++; return "await (0, rxjs_1.firstValueFrom)(this._rpcCore.state.getRuntimeVersion.raw(header.parentHash)).catch(() => (0, rxjs_1.firstValueFrom)(this._rpcCore.state.getRuntimeVersion()))"; }
);
fs.writeFileSync(initFile, initCode);
report("[3/4] @polkadot/api Init.js");
// --- Patch 4: @subql/node utils/substrate.js (fetchEventsRange) ---
// When events.at(hash) fails with "State already discarded" for pruned blocks,
// return an empty events array instead of crashing. The block is processed with
// no events (data is lost anyway since state was pruned).
count = 0;
let utilsCode2 = fs.readFileSync(utilsFile, "utf8");
utilsCode2 = utilsCode2.replace(
/api\.query\.system\.events\.at\(hash\)\.catch\(\(e\) => \{/g,
() => { count++; return "api.query.system.events.at(hash).catch((e) => { if (e.message && e.message.includes('State already discarded')) { const empty = []; empty.toArray = () => []; return empty; }"; }
);
fs.writeFileSync(utilsFile, utilsCode2);
report("[4/5] utils/substrate.js fetchEventsRange");
// --- Patch 5: @subql/node utils/substrate.js (wrapExtrinsics / getExtrinsicSuccess) ---
// When events are empty (pruned block), groupEventsByExtrinsic returns {}.
// Then groupedEvents[idx] is undefined for each extrinsic, and
// getExtrinsicSuccess(undefined) crashes with "Cannot read properties of undefined
// (reading 'findIndex')". Fix: default to empty array when events is undefined.
count = 0;
let utilsCode3 = fs.readFileSync(utilsFile, "utf8");
// Patch getExtrinsicSuccess to handle undefined events
utilsCode3 = utilsCode3.replace(
/function getExtrinsicSuccess\(events\) \{/g,
() => { count++; return "function getExtrinsicSuccess(events) { if (!events || !events.findIndex) return false;"; }
);
fs.writeFileSync(utilsFile, utilsCode3);
report("[5/7] utils/substrate.js getExtrinsicSuccess");
// --- Patch 6: @subql/node utils/substrate.js (events.toArray fallback) ---
// When events is undefined or has no toArray method, default to empty array.
count = 0;
let utilsCode4 = fs.readFileSync(utilsFile, "utf8");
utilsCode4 = utilsCode4.replace(
/const wrappedBlock = wrapBlock\(block, events\.toArray\(\), parentSpecVersion\);/g,
() => { count++; return "const wrappedBlock = wrapBlock(block, (events && events.toArray) ? events.toArray() : [], parentSpecVersion);"; }
);
fs.writeFileSync(utilsFile, utilsCode4);
report("[6/7] utils/substrate.js events.toArray");
console.log("[7/7] All pruned-state patches applied successfully.");