verify-anchor.mjs: AIP-22 stage 3b read path, inert until an index marks it - from KEYS_FROM (or the v3 epoch whose index entry carries keysFrom) the key and the id of a seal's index come from the v3 epoch in force at the parent, as both clients read them; the ECDSA link of an id is inherited across epochs that omit it, and an id linked again drops its older address. Without keysFrom anywhere nothing changes: chain 2800 5/5, testnet 28001 5/5 re-run on 2026-09-23.

This commit is contained in:
Aere Network 2026-09-23 16:05:24 +03:00
parent 944dc0bba6
commit 089dc9803c

View File

@ -328,7 +328,7 @@ if (IDENTITY_OFF) console.log('identity: IDENTITY_BLOCK=none, v3 epochs ignored
if (!IDENTITY_OFF) {
const sources = process.env.REGISTRY_V3
? [{ loc: process.env.REGISTRY_V3, sha: null, file: process.env.REGISTRY_V3 }]
: [...v3Entries].sort((a, b) => Number(a.bindHeight) - Number(b.bindHeight)).map(m => ({ loc: join(MANIFESTS, m.file), sha: String(m.sha256).toLowerCase(), file: m.file }));
: [...v3Entries].sort((a, b) => Number(a.bindHeight) - Number(b.bindHeight)).map(m => ({ loc: join(MANIFESTS, m.file), sha: String(m.sha256).toLowerCase(), file: m.file, keysFrom: m.keysFrom === true || m.keysFrom === 'true' }));
if (sources.length) {
let reg;
try { reg = await import('./registry-v3.mjs'); }
@ -342,8 +342,11 @@ if (!IDENTITY_OFF) {
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); }
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); }
for (const row of r.rows) {
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));
@ -351,9 +354,22 @@ if (!IDENTITY_OFF) {
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); }
}
// 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
link = new Map(link);
const idsNow = new Set(r.rows.map(row => '0x' + row.id.toString('hex')));
for (const row of r.rows) if (row.ecdsa) { const idh = '0x' + row.id.toString('hex'); for (const [a, i] of link) if (i === idh) link.delete(a); }
for (const row of r.rows) if (row.ecdsa) link.set('0x' + row.ecdsa.addr.toString('hex'), '0x' + row.id.toString('hex'));
V3_EPOCHS.push({ bindHeight: r.bindHeight, link });
for (const [a, i] of link) if (!idsNow.has(i)) link.delete(a);
// AIP-22 stage 3b: the key set of this epoch by index (Falcon-512 without its 0x09 header, the other schemes by name) and the
// index's id; used for seals from KEYS_FROM on, exactly as both clients read keys from the v3 epoch in force at the parent
const byIndex = r.rows.map(row => {
const keys = {};
for (const k of row.keys) if (k.name !== 'falcon-512') keys[k.name] = new Uint8Array(k.pk);
const f = row.keys.find(k => k.name === 'falcon-512');
return { id: '0x' + row.id.toString('hex'), pk: f ? new Uint8Array(f.pk.subarray(1)) : null, keys };
});
V3_EPOCHS.push({ bindHeight: r.bindHeight, link, byIndex, keysFrom: s.keysFrom === true });
console.log(' v3 epoch ' + String(r.bindHeight).padEnd(10) + ' ' + String(s.file).padEnd(26) + ' rows ' + r.count + '/' + r.count +
' registry hash 0x' + r.hash.toString('hex').slice(0, 16) + '.. every possession proof, ecdsa link and the Merkle hash verified; keys = the key manifest of that height');
previous = r;
@ -362,6 +378,10 @@ if (!IDENTITY_OFF) {
console.log('identity: from block ' + IDENTITY_BLOCK + ' the validator set holds post-quantum ids (' + V3_EPOCHS.length + ' v3 epoch(s))');
}
}
// the height from which the keys that seal come from the v3 epochs (AIP-22 stage 3b): KEYS_FROM, or the epoch the index marks
const KEYS_FROM = process.env.KEYS_FROM ? Number(process.env.KEYS_FROM) : (V3_EPOCHS.find(e => e.keysFrom)?.bindHeight ?? Infinity);
if (Number.isFinite(KEYS_FROM)) console.log('keys: from block ' + KEYS_FROM + ' the keys that seal come from the v3 epoch in force (AIP-22 stage 3b)');
const v3EpochAt = (h) => [...V3_EPOCHS].reverse().find(x => x.bindHeight <= h) || null;
const memberOf = (addr, h) => {
if (h < IDENTITY_BLOCK || !V3_EPOCHS.length) return addr;
const e = [...V3_EPOCHS].reverse().find(x => x.bindHeight <= h) || V3_EPOCHS[0];
@ -423,11 +443,13 @@ for (let i = 0; i < COUNT; i++) {
sealsTotal++;
const name = SCHEME_NAMES[scheme];
if (!name) { unknownScheme.push(scheme); continue; }
const row = man && man.rows[idx];
// AIP-22 stage 3b: from KEYS_FROM the index names the row of the v3 epoch in force at the PARENT (its id and its keys)
const v3k = h - 1 >= KEYS_FROM ? v3EpochAt(h - 1) : null;
const row = v3k ? v3k.byIndex[idx] : (man && man.rows[idx]);
if (!row) { unknownIndex.push(idx); continue; }
if (!vset.has(memberOf(row.addr.toLowerCase(), h))) { notValidator.push(idx); continue; }
if (!vset.has(v3k ? row.id : memberOf(row.addr.toLowerCase(), h))) { notValidator.push(idx); continue; }
if (scheme === 1) {
if (!verifyFalcon512(row.pk, msg, sig)) { invalid.push('falcon@' + idx); continue; }
if (!row.pk || !verifyFalcon512(row.pk, msg, sig)) { invalid.push('falcon@' + idx); continue; }
} else {
if (!falconIdx.has(idx)) { unbound.push(name + '@' + idx); continue; }
const pk = row.keys && row.keys[name];