X14: the signing half. No RPC exposes a certificate's signer set, so
tools/finality-attacks/x14-concentration.mjs now walks the selected
chain over the window, decodes the coinbase finality section (IGNF
trailer: votes, certificates, evidence, the IGNK reveal), rebuilds the
canonical voter list at every checkpoint from headers the way the node's
compute_weights does, and maps every bitmap through it. Read-only on the
Mac observer node under the run lock, node version and DAA recorded,
two readings kept (the window straddles the 0.3.10 restart). Result at
23:00:44 UTC, DAA 138,542: signed weight over the heaviest certificate
per index, 27 keys, top-1 10.0%, top-3 29.1%, top-10 77.3%; hashing
6.4/19.2/60.1, aggregation 44.9/84.1/100, proving 100/100/100. Checks:
240 of 240 rebuilt voter lists equal the node's count, 194 of 194
certificates mapped, 27 reveals against BLAKE2b with 0 mismatches.
Status moved to Answered with evidence for all four; the old status
kept after "Was:". Bench-log entry appended.
X5 (paragraph only; Status stays Decision owner: the project lead): the observer
columns of O-X.1 on this branch, not deployed, the running observer
untouched: live_peer_asn (offline prefix table, no third-party lookup),
live_key_machines (machine fingerprint per vote key from the log
intake), live_pool_statements (signed JSON {pool, keys[], signed_at},
Ed25519, parser and verifier), live_concentration (nightly top-1/3/10
for the four concentrations plus N_ind labelled "proposed definition").
Pure functions in tools/observer/lib/concentration.mjs and
lib/nightly.mjs; keyed BLAKE2b in tools/finality-attacks/lib/blake2b.mjs;
9 node:test tests, all passing.
Co-Authored-By: Claude Fable 5.1 <noreply@anthropic.com>
86 lines
5 KiB
JavaScript
86 lines
5 KiB
JavaScript
// BLAKE2b (RFC 7693), keyed and with a chosen output length, in plain Node 22 (BigInt words, no dependencies).
|
|
// Needed because the fork's `vote_key_hash` is BLAKE2b-256 keyed with the domain string `IgneumVoteKeyHash`
|
|
// (vendor/igneum-node-036/crypto/hashes/src/hashers.rs, the `blake2b_hasher!` macro: hash_length 32, key = the
|
|
// domain) over the 48-byte compressed G1 public key, and Node's crypto exposes neither the key nor a 32-byte length.
|
|
// Checked against the RFC's unkeyed test vector in `selfTest()` and, in x14-concentration.mjs, against every key
|
|
// reveal the chain carries (reveal pubkey -> header vote_key_hash).
|
|
|
|
const IV = [
|
|
0x6a09e667f3bcc908n, 0xbb67ae8584caa73bn, 0x3c6ef372fe94f82bn, 0xa54ff53a5f1d36f1n,
|
|
0x510e527fade682d1n, 0x9b05688c2b3e6c1fn, 0x1f83d9abfb41bd6bn, 0x5be0cd19137e2179n,
|
|
];
|
|
const SIGMA = [
|
|
[0, 1, 2, 3, 4, 5, 6, 7, 8, 9, 10, 11, 12, 13, 14, 15],
|
|
[14, 10, 4, 8, 9, 15, 13, 6, 1, 12, 0, 2, 11, 7, 5, 3],
|
|
[11, 8, 12, 0, 5, 2, 15, 13, 10, 14, 3, 6, 7, 1, 9, 4],
|
|
[7, 9, 3, 1, 13, 12, 11, 14, 2, 6, 5, 10, 4, 0, 15, 8],
|
|
[9, 0, 5, 7, 2, 4, 10, 15, 14, 1, 11, 12, 6, 8, 3, 13],
|
|
[2, 12, 6, 10, 0, 11, 8, 3, 4, 13, 7, 5, 15, 14, 1, 9],
|
|
[12, 5, 1, 15, 14, 13, 4, 10, 0, 7, 6, 3, 9, 2, 8, 11],
|
|
[13, 11, 7, 14, 12, 1, 3, 9, 5, 0, 15, 4, 8, 6, 2, 10],
|
|
[6, 15, 14, 9, 11, 3, 0, 8, 12, 2, 13, 7, 1, 4, 10, 5],
|
|
[10, 2, 8, 4, 7, 6, 1, 5, 15, 11, 9, 14, 3, 12, 13, 0],
|
|
];
|
|
const M64 = (1n << 64n) - 1n;
|
|
const rotr = (x, n) => ((x >> BigInt(n)) | (x << BigInt(64 - n))) & M64;
|
|
|
|
function compress(h, block, t, last) {
|
|
const m = new Array(16);
|
|
for (let i = 0; i < 16; i++) m[i] = block.readBigUInt64LE(i * 8);
|
|
const v = h.concat(IV);
|
|
v[12] ^= BigInt(t) & M64;
|
|
v[13] ^= (BigInt(t) >> 64n) & M64;
|
|
if (last) v[14] ^= M64;
|
|
const G = (a, b, c, d, x, y) => {
|
|
v[a] = (v[a] + v[b] + x) & M64; v[d] = rotr(v[d] ^ v[a], 32);
|
|
v[c] = (v[c] + v[d]) & M64; v[b] = rotr(v[b] ^ v[c], 24);
|
|
v[a] = (v[a] + v[b] + y) & M64; v[d] = rotr(v[d] ^ v[a], 16);
|
|
v[c] = (v[c] + v[d]) & M64; v[b] = rotr(v[b] ^ v[c], 63);
|
|
};
|
|
for (let r = 0; r < 12; r++) {
|
|
const s = SIGMA[r % 10];
|
|
G(0, 4, 8, 12, m[s[0]], m[s[1]]); G(1, 5, 9, 13, m[s[2]], m[s[3]]);
|
|
G(2, 6, 10, 14, m[s[4]], m[s[5]]); G(3, 7, 11, 15, m[s[6]], m[s[7]]);
|
|
G(0, 5, 10, 15, m[s[8]], m[s[9]]); G(1, 6, 11, 12, m[s[10]], m[s[11]]);
|
|
G(2, 7, 8, 13, m[s[12]], m[s[13]]); G(3, 4, 9, 14, m[s[14]], m[s[15]]);
|
|
}
|
|
for (let i = 0; i < 8; i++) h[i] ^= v[i] ^ v[i + 8];
|
|
}
|
|
|
|
/** BLAKE2b of `data` (Buffer or Uint8Array) with an optional key (at most 64 bytes) and output length 1 to 64. */
|
|
export function blake2b(data, { key = Buffer.alloc(0), outlen = 64 } = {}) {
|
|
if (outlen < 1 || outlen > 64 || key.length > 64) throw new Error('blake2b: bad parameters');
|
|
const h = IV.slice();
|
|
h[0] ^= BigInt(0x01010000 ^ (key.length << 8) ^ outlen);
|
|
const msg = Buffer.from(data);
|
|
const blocks = [];
|
|
if (key.length) { const kb = Buffer.alloc(128); Buffer.from(key).copy(kb); blocks.push([kb, 128]); }
|
|
if (msg.length === 0) { if (!key.length) blocks.push([Buffer.alloc(128), 0]); }
|
|
else for (let i = 0; i < msg.length; i += 128) { const b = Buffer.alloc(128); const n = Math.min(128, msg.length - i); msg.copy(b, 0, i, i + n); blocks.push([b, n]); }
|
|
let t = 0;
|
|
for (let i = 0; i < blocks.length; i++) { t += blocks[i][1]; compress(h, blocks[i][0], t, i === blocks.length - 1); }
|
|
const out = Buffer.alloc(64);
|
|
for (let i = 0; i < 8; i++) out.writeBigUInt64LE(h[i], i * 8);
|
|
return out.subarray(0, outlen);
|
|
}
|
|
|
|
/** The fork's vote key hash: BLAKE2b-256 keyed `IgneumVoteKeyHash` over the 48-byte compressed G1 key (hex in, hex out). */
|
|
export function voteKeyHash(pubkeyHex) {
|
|
return blake2b(Buffer.from(pubkeyHex, 'hex'), { key: Buffer.from('IgneumVoteKeyHash'), outlen: 32 }).toString('hex');
|
|
}
|
|
|
|
/** RFC 7693 appendix A: BLAKE2b-512("abc"); plus the keyed vector of the BLAKE2 reference tests for key = bytes 0..63. */
|
|
export function selfTest() {
|
|
const abc = blake2b(Buffer.from('abc')).toString('hex');
|
|
const want = 'ba80a53f981c4d0d6a2797b69f12f6e94c212f14685ac4b74b12bb6fdbffa2d17d87c5392aab792dc252d5de4533cc9518d38aa8dbf1925ab92386edd4009923';
|
|
if (abc !== want) return { ok: false, detail: `BLAKE2b-512("abc") = ${abc}` };
|
|
const empty = blake2b(Buffer.alloc(0)).toString('hex');
|
|
const wantEmpty = '786a02f742015903c6c6fd852552d272912f4740e15847618a86e217f71f5419d25e1031afee585313896444934eb04b903a685b1448b755d56f701afe9be2ce';
|
|
if (empty !== wantEmpty) return { ok: false, detail: `BLAKE2b-512("") = ${empty}` };
|
|
// blake2b-kat.txt (BLAKE2 reference): in = "", key = 00..3f, out = 10ebb67700b1868efb4417987acf4690ae9d972fb7a590c2f02871799aaa4786b5e996e8f0f4eb981fc214b005f42d2ff4233499391653df7aefcbc13fc51568
|
|
const key = Buffer.from(Array.from({ length: 64 }, (_, i) => i));
|
|
const kat = blake2b(Buffer.alloc(0), { key }).toString('hex');
|
|
const wantKat = '10ebb67700b1868efb4417987acf4690ae9d972fb7a590c2f02871799aaa4786b5e996e8f0f4eb981fc214b005f42d2ff4233499391653df7aefcbc13fc51568';
|
|
if (kat !== wantKat) return { ok: false, detail: `keyed BLAKE2b-512("") = ${kat}` };
|
|
return { ok: true };
|
|
}
|