verify-anchor.mjs: early refusals stop without process.exit() (on Windows it returned 127 instead of 1 after a network request); any other uncaught error still fails the run

This commit is contained in:
Aere Network 2026-09-27 05:32:06 +03:00
parent 0b36247051
commit 75b2872e13

View File

@ -52,6 +52,18 @@ import { secp256k1 } from '@noble/curves/secp256k1';
import fs from 'node:fs'; import fs from 'node:fs';
import path from 'node:path'; import path from 'node:path';
import { verifyFalcon512, fromHex } from './falcon512.mjs'; import { verifyFalcon512, fromHex } from './falcon512.mjs';
// Early refusals stop through stop(1), not process.exit(1): on Windows (Node 24), process.exit() right after a fetch aborts node
// inside libuv while the HTTP sockets close, and the caller sees exit code 127 instead of 1 (measured 2026-09-27: 8 of 8 runs; with
// stop(), 6 of 6 runs exit 1). stop() prints nothing itself, sets the code, and throws a marker that the handler below swallows, so the
// module stops where it is and node exits normally once its sockets are closed. Any OTHER uncaught error still ends the run as a
// failure at once, so that nothing that went wrong in the background can be followed by a VERIFIED line.
class StopVerify extends Error {}
process.on('uncaughtException', (e) => {
if (e instanceof StopVerify) return;
console.error(e);
process.exit(1);
});
const stop = (code) => { process.exitCode = code; throw new StopVerify(); };
const RPC = process.env.RPC || 'https://rpc.aere.network'; const RPC = process.env.RPC || 'https://rpc.aere.network';
const MANIFESTS = process.env.MANIFESTS || 'https://aere.network/pq/manifests'; const MANIFESTS = process.env.MANIFESTS || 'https://aere.network/pq/manifests';
@ -98,7 +110,7 @@ const SCHEME_SCHEDULE = (process.env.SCHEME_SCHEDULE || SCHEME_DEFAULT).split(',
.map(p => { const [h, list] = p.split(':'); return [Number(h), list.split('+').map(x => x.trim()).filter(Boolean).map(e => { const [name, iv] = e.split('/'); return { name, interval: iv === undefined ? 0 : Number(iv) }; })]; }) .map(p => { const [h, list] = p.split(':'); return [Number(h), list.split('+').map(x => x.trim()).filter(Boolean).map(e => { const [name, iv] = e.split('/'); return { name, interval: iv === undefined ? 0 : Number(iv) }; })]; })
.sort((a, b) => a[0] - b[0]); .sort((a, b) => a[0] - b[0]);
for (const [h, list] of SCHEME_SCHEDULE) for (const { name, interval } of list) { for (const [h, list] of SCHEME_SCHEDULE) for (const { name, interval } of list) {
if (!(interval === 0 || (Number.isInteger(interval) && interval > 0 && interval % 32 === 0))) { console.error('FAIL: SCHEME_SCHEDULE step ' + h + ' gives ' + name + ' the interval ' + interval + ', which is not a positive multiple of 32'); process.exit(1); } if (!(interval === 0 || (Number.isInteger(interval) && interval > 0 && interval % 32 === 0))) { console.error('FAIL: SCHEME_SCHEDULE step ' + h + ' gives ' + name + ' the interval ' + interval + ', which is not a positive multiple of 32'); stop(1); }
} }
const stepAt = (hh) => { let out = [{ name: 'falcon-512', interval: 0 }]; for (const [from, list] of SCHEME_SCHEDULE) { if (from <= hh) out = list; else break; } return out; }; const stepAt = (hh) => { let out = [{ name: 'falcon-512', interval: 0 }]; for (const [from, list] of SCHEME_SCHEDULE) { if (from <= hh) out = list; else break; } return out; };
const schemesAt = (hh) => stepAt(hh).map(s => s.name); const schemesAt = (hh) => stepAt(hh).map(s => s.name);
@ -176,9 +188,9 @@ const join = (a, b) => isUrl ? a.replace(/\/$/, '') + '/' + b : path.join(a, b);
let index; let index;
try { index = JSON.parse(await loadText(join(MANIFESTS, 'index.json'))); } try { index = JSON.parse(await loadText(join(MANIFESTS, 'index.json'))); }
catch (e) { console.error('FAIL: cannot load manifest index from ' + MANIFESTS + ': ' + e.message); process.exit(1); } catch (e) { console.error('FAIL: cannot load manifest index from ' + MANIFESTS + ': ' + e.message); stop(1); }
if (!index || !Array.isArray(index.manifests) || !index.manifests.length) { console.error('FAIL: manifest index has no entries'); process.exit(1); } if (!index || !Array.isArray(index.manifests) || !index.manifests.length) { console.error('FAIL: manifest index has no entries'); stop(1); }
if (Number(index.chainId) !== CHAIN_ID) { console.error('FAIL: manifest index is for chain ' + index.chainId); process.exit(1); } if (Number(index.chainId) !== CHAIN_ID) { console.error('FAIL: manifest index is for chain ' + index.chainId); stop(1); }
console.log('chain ' + CHAIN_ID + ' rpc ' + RPC + ' manifests ' + MANIFESTS); console.log('chain ' + CHAIN_ID + ' rpc ' + RPC + ' manifests ' + MANIFESTS);
console.log(''); console.log('');
@ -269,7 +281,7 @@ const ANCHOR_FROM = Number(process.env.ANCHOR_FROM || (CHAIN_ID === 28001 ? 64 :
const INTERVAL_SCHEDULE = (process.env.ANCHOR_INTERVAL_SCHEDULE || '').split(',').filter(Boolean) const INTERVAL_SCHEDULE = (process.env.ANCHOR_INTERVAL_SCHEDULE || '').split(',').filter(Boolean)
.map(p => p.split(':').map(x => Number(x.trim()))).sort((a, b) => a[0] - b[0]); .map(p => p.split(':').map(x => Number(x.trim()))).sort((a, b) => a[0] - b[0]);
for (const [h, iv] of INTERVAL_SCHEDULE) { for (const [h, iv] of INTERVAL_SCHEDULE) {
if (!(iv > 0 && iv % 32 === 0) || h < ANCHOR_FROM || (h - ANCHOR_FROM) % iv !== 0) { console.error('FAIL: ANCHOR_INTERVAL_SCHEDULE entry ' + h + ':' + iv + ' is not a multiple of 32 on the grid from ' + ANCHOR_FROM); process.exit(1); } if (!(iv > 0 && iv % 32 === 0) || h < ANCHOR_FROM || (h - ANCHOR_FROM) % iv !== 0) { console.error('FAIL: ANCHOR_INTERVAL_SCHEDULE entry ' + h + ':' + iv + ' is not a multiple of 32 on the grid from ' + ANCHOR_FROM); stop(1); }
} }
// carries a certificate element? (RLP item 6 of extraData; enough to space the grid, not to trust it) // carries a certificate element? (RLP item 6 of extraData; enough to space the grid, not to trust it)
@ -293,13 +305,13 @@ if (!INTERVAL_SCHEDULE.length) {
console.error('FAIL: only ' + seen.length + ' certificate-carrying header(s) in the ' + LOOKBACK + ' blocks below ' + top + ','); console.error('FAIL: only ' + seen.length + ' certificate-carrying header(s) in the ' + LOOKBACK + ' blocks below ' + top + ',');
console.error(' so the anchor spacing cannot be measured on this chain. Either anchoring has stopped, or this'); console.error(' so the anchor spacing cannot be measured on this chain. Either anchoring has stopped, or this');
console.error(' endpoint is not serving chain ' + CHAIN_ID + '. Pass ANCHOR_INTERVAL_SCHEDULE=h:interval to state it explicitly.'); console.error(' endpoint is not serving chain ' + CHAIN_ID + '. Pass ANCHOR_INTERVAL_SCHEDULE=h:interval to state it explicitly.');
process.exit(1); stop(1);
} }
const step = seen[0] - seen[1]; const step = seen[0] - seen[1];
if (!(step > 0 && step % 32 === 0) || (seen[0] - ANCHOR_FROM) % step !== 0) { if (!(step > 0 && step % 32 === 0) || (seen[0] - ANCHOR_FROM) % step !== 0) {
console.error('FAIL: the two most recent anchors (' + seen[0] + ', ' + seen[1] + ') are ' + step + ' apart, which is not a'); console.error('FAIL: the two most recent anchors (' + seen[0] + ', ' + seen[1] + ') are ' + step + ' apart, which is not a');
console.error(' multiple of 32 on the grid from ' + ANCHOR_FROM + '. The chain is not anchoring on a regular grid.'); console.error(' multiple of 32 on the grid from ' + ANCHOR_FROM + '. The chain is not anchoring on a regular grid.');
process.exit(1); stop(1);
} }
DERIVED_INTERVAL = step; DERIVED_INTERVAL = step;
INTERVAL_SCHEDULE.push([seen[0] - Math.floor((seen[0] - ANCHOR_FROM) / step) * step, step]); INTERVAL_SCHEDULE.push([seen[0] - Math.floor((seen[0] - ANCHOR_FROM) / step) * step, step]);
@ -314,11 +326,11 @@ if (DERIVED_INTERVAL) {
} else if (INTERVAL_SCHEDULE.length) { } else if (INTERVAL_SCHEDULE.length) {
console.log('interval schedule (given): ' + INTERVAL_SCHEDULE.map(([h, iv]) => h + ':' + iv).join(',') + ' (in force at ' + anchor + ': every ' + intervalAt(anchor) + 'th block)'); console.log('interval schedule (given): ' + INTERVAL_SCHEDULE.map(([h, iv]) => h + ':' + iv).join(',') + ' (in force at ' + anchor + ': every ' + intervalAt(anchor) + 'th block)');
} }
if (anchor < ANCHOR_FROM) { console.error('FAIL: no anchor at or below ' + top + ' (anchors start at ' + ANCHOR_FROM + ')'); process.exit(1); } if (anchor < ANCHOR_FROM) { console.error('FAIL: no anchor at or below ' + top + ' (anchors start at ' + ANCHOR_FROM + ')'); stop(1); }
{ {
const b = await rpc('eth_getBlockByNumber', ['0x' + anchor.toString(16), false]); const b = await rpc('eth_getBlockByNumber', ['0x' + anchor.toString(16), false]);
const vanity = rlpRead(hex(b.extraData), 0).item[0].item; const vanity = rlpRead(hex(b.extraData), 0).item[0].item;
if (vanity.length !== 32 || Buffer.from(vanity).includes(Buffer.from('besu'))) { console.error('FAIL: block ' + anchor + ' does not look like an anchor (vanity is the client string, not a digest)'); process.exit(1); } if (vanity.length !== 32 || Buffer.from(vanity).includes(Buffer.from('besu'))) { console.error('FAIL: block ' + anchor + ' does not look like an anchor (vanity is the client string, not a digest)'); stop(1); }
} }
// AIP-22 stage 3 (2026-09-23): from the identity height a chain's validator set holds post-quantum ids, not ECDSA addresses. // AIP-22 stage 3 (2026-09-23): from the identity height a chain's validator set holds post-quantum ids, not ECDSA addresses.
@ -343,27 +355,27 @@ if (!IDENTITY_OFF) {
if (sources.length) { if (sources.length) {
let reg; let reg;
try { reg = await import('./registry-v3.mjs'); } try { reg = await import('./registry-v3.mjs'); }
catch (e) { console.error('FAIL: this chain publishes v3 registry epochs, and registry-v3.mjs (published beside this tool) cannot be loaded: ' + e.message); process.exit(1); } catch (e) { console.error('FAIL: this chain publishes v3 registry epochs, and registry-v3.mjs (published beside this tool) cannot be loaded: ' + e.message); stop(1); }
let previous = null, link = new Map(); let previous = null, link = new Map();
for (const s of sources) { for (const s of sources) {
const text = await loadText(s.loc); const text = await loadText(s.loc);
if (s.sha && Buffer.from(sha256(Buffer.from(text, 'utf8'))).toString('hex') !== s.sha) { console.error('FAIL: v3 epoch ' + s.file + ': sha256 does not match the index'); process.exit(1); } if (s.sha && Buffer.from(sha256(Buffer.from(text, 'utf8'))).toString('hex') !== s.sha) { console.error('FAIL: v3 epoch ' + s.file + ': sha256 does not match the index'); stop(1); }
const json = JSON.parse(text); const json = JSON.parse(text);
let r; let r;
try { r = reg.load(json, CHAIN_ID, schemesAt(json.bindHeight), previous, previous === null); } try { r = reg.load(json, CHAIN_ID, schemesAt(json.bindHeight), previous, previous === null); }
catch (e) { console.error('FAIL: v3 epoch ' + s.file + ' does not verify: ' + e.message); process.exit(1); } catch (e) { console.error('FAIL: v3 epoch ' + s.file + ' does not verify: ' + e.message); stop(1); }
const km = manifestAt(r.bindHeight); const km = manifestAt(r.bindHeight);
// from H_k (the first epoch the index marks keysFrom) a later epoch may rotate keys under the old keys' signatures, which // from H_k (the first epoch the index marks keysFrom) a later epoch may rotate keys under the old keys' signatures, which
// registry-v3.mjs has verified; the equality with the key manifest is required up to and including the H_k epoch // registry-v3.mjs has verified; the equality with the key manifest is required up to and including the H_k epoch
const rotating = V3_EPOCHS.some(x => x.keysFrom) || (process.env.KEYS_FROM && r.bindHeight > Number(process.env.KEYS_FROM)); const rotating = V3_EPOCHS.some(x => x.keysFrom) || (process.env.KEYS_FROM && r.bindHeight > Number(process.env.KEYS_FROM));
if (!km || km.count !== r.count) { console.error('FAIL: v3 epoch bound at ' + r.bindHeight + ' has ' + r.count + ' rows; the key manifest in force there has ' + (km ? km.count : 'none')); process.exit(1); } if (!km || km.count !== r.count) { console.error('FAIL: v3 epoch bound at ' + r.bindHeight + ' has ' + r.count + ' rows; the key manifest in force there has ' + (km ? km.count : 'none')); stop(1); }
for (const row of (rotating ? [] : r.rows)) { for (const row of (rotating ? [] : r.rows)) {
const k = km.rows[row.index]; const k = km.rows[row.index];
const f = row.keys.find(x => x.name === 'falcon-512'), sl = row.keys.find(x => x.name === 'slh-dsa-sha2-128s'); const f = row.keys.find(x => x.name === 'falcon-512'), sl = row.keys.find(x => x.name === 'slh-dsa-sha2-128s');
const falconSame = f && Buffer.concat([Buffer.from([0x09]), Buffer.from(k.pk)]).equals(Buffer.from(f.pk)); const falconSame = f && Buffer.concat([Buffer.from([0x09]), Buffer.from(k.pk)]).equals(Buffer.from(f.pk));
const slhSame = !k.keys['slh-dsa-sha2-128s'] ? !sl : (sl && Buffer.from(k.keys['slh-dsa-sha2-128s']).equals(Buffer.from(sl.pk))); const slhSame = !k.keys['slh-dsa-sha2-128s'] ? !sl : (sl && Buffer.from(k.keys['slh-dsa-sha2-128s']).equals(Buffer.from(sl.pk)));
const addrSame = !row.ecdsa || ('0x' + row.ecdsa.addr.toString('hex')) === k.addr.toLowerCase(); const addrSame = !row.ecdsa || ('0x' + row.ecdsa.addr.toString('hex')) === k.addr.toLowerCase();
if (!(falconSame && slhSame && addrSame)) { console.error('FAIL: v3 epoch bound at ' + r.bindHeight + ', row ' + row.index + ': ' + (falconSame ? '' : 'falcon key ') + (slhSame ? '' : 'slh-dsa key ') + (addrSame ? '' : 'ecdsa address ') + 'differ from the key manifest that signs there'); process.exit(1); } if (!(falconSame && slhSame && addrSame)) { console.error('FAIL: v3 epoch bound at ' + r.bindHeight + ', row ' + row.index + ': ' + (falconSame ? '' : 'falcon key ') + (slhSame ? '' : 'slh-dsa key ') + (addrSame ? '' : 'ecdsa address ') + 'differ from the key manifest that signs there'); stop(1); }
} }
// the ecdsa link is inherited by id (a later epoch may omit it), exactly as both clients resolve a message's // the ecdsa link is inherited by id (a later epoch may omit it), exactly as both clients resolve a message's
// author: an id that this epoch links again drops its older address, and an id no longer registered drops its link // author: an id that this epoch links again drops its older address, and an id no longer registered drops its link