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:
parent
0b36247051
commit
75b2872e13
@ -52,6 +52,18 @@ import { secp256k1 } from '@noble/curves/secp256k1';
|
||||
import fs from 'node:fs';
|
||||
import path from 'node:path';
|
||||
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 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) }; })]; })
|
||||
.sort((a, b) => a[0] - b[0]);
|
||||
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 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;
|
||||
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); }
|
||||
if (!index || !Array.isArray(index.manifests) || !index.manifests.length) { console.error('FAIL: manifest index has no entries'); process.exit(1); }
|
||||
if (Number(index.chainId) !== CHAIN_ID) { console.error('FAIL: manifest index is for chain ' + index.chainId); 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'); stop(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('');
|
||||
@ -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)
|
||||
.map(p => p.split(':').map(x => Number(x.trim()))).sort((a, b) => a[0] - b[0]);
|
||||
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)
|
||||
@ -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(' 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.');
|
||||
process.exit(1);
|
||||
stop(1);
|
||||
}
|
||||
const step = seen[0] - seen[1];
|
||||
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(' 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;
|
||||
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) {
|
||||
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 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.
|
||||
@ -343,27 +355,27 @@ if (!IDENTITY_OFF) {
|
||||
if (sources.length) {
|
||||
let reg;
|
||||
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();
|
||||
for (const s of sources) {
|
||||
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);
|
||||
let r;
|
||||
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);
|
||||
// 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
|
||||
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)) {
|
||||
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 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 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
|
||||
// author: an id that this epoch links again drops its older address, and an id no longer registered drops its link
|
||||
|
||||
Loading…
Reference in New Issue
Block a user