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:
parent
944dc0bba6
commit
089dc9803c
@ -328,7 +328,7 @@ if (IDENTITY_OFF) console.log('identity: IDENTITY_BLOCK=none, v3 epochs ignored
|
|||||||
if (!IDENTITY_OFF) {
|
if (!IDENTITY_OFF) {
|
||||||
const sources = process.env.REGISTRY_V3
|
const sources = process.env.REGISTRY_V3
|
||||||
? [{ loc: process.env.REGISTRY_V3, sha: null, file: 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) {
|
if (sources.length) {
|
||||||
let reg;
|
let reg;
|
||||||
try { reg = await import('./registry-v3.mjs'); }
|
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); }
|
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); process.exit(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
|
||||||
|
// 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')); process.exit(1); }
|
||||||
for (const row of 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));
|
||||||
@ -351,9 +354,22 @@ if (!IDENTITY_OFF) {
|
|||||||
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'); 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);
|
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'));
|
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 +
|
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');
|
' 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;
|
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))');
|
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) => {
|
const memberOf = (addr, h) => {
|
||||||
if (h < IDENTITY_BLOCK || !V3_EPOCHS.length) return addr;
|
if (h < IDENTITY_BLOCK || !V3_EPOCHS.length) return addr;
|
||||||
const e = [...V3_EPOCHS].reverse().find(x => x.bindHeight <= h) || V3_EPOCHS[0];
|
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++;
|
sealsTotal++;
|
||||||
const name = SCHEME_NAMES[scheme];
|
const name = SCHEME_NAMES[scheme];
|
||||||
if (!name) { unknownScheme.push(scheme); continue; }
|
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 (!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 (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 {
|
} else {
|
||||||
if (!falconIdx.has(idx)) { unbound.push(name + '@' + idx); continue; }
|
if (!falconIdx.has(idx)) { unbound.push(name + '@' + idx); continue; }
|
||||||
const pk = row.keys && row.keys[name];
|
const pk = row.keys && row.keys[name];
|
||||||
|
|||||||
Loading…
Reference in New Issue
Block a user