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>
75 lines
4.1 KiB
JavaScript
75 lines
4.1 KiB
JavaScript
// The nightly concentration row (ledger X5 and X14, O-X.1): top-1, top-3 and top-10 shares for the four
|
|
// concentrations over the weight window, plus N_ind under the PROPOSED independence definition (decision item 3
|
|
// for the project lead, docs/plans/ledger-decisions.md). Pure: takes what the observer has read and returns the row; the
|
|
// observer stores it in live_concentration once a day. No RPC and no database here.
|
|
//
|
|
// Inputs:
|
|
// weights the getFinalityWeights answer (keys[] with keyHash, blocks, voter; params.dust; checkpointIndex, daaScore)
|
|
// checkpoints the window's checkpoints as the node reports them (certificateAggregator per certified or locked one)
|
|
// certificates stored certificates [{ index, bitmap_hex, voter_count, voters: [{pubkey, weight}] }] (live_certificates:
|
|
// the voter table as the node reported it when the lock landed; signed weight = sum of voters[p].weight
|
|
// over the set bits); only those whose checkpoint is in the window
|
|
// proofs paid shards per prover id over the window: Map prover -> count (live_proofs rows in state paid)
|
|
// attributes Map keyHash -> { asn, fingerprint, pool } from live_peer_asn, live_key_machines, live_pool_statements
|
|
// meta { nodeVersion, daa, day }
|
|
|
|
import { topShares, independenceClasses } from './concentration.mjs';
|
|
import { voteKeyHash } from '../../finality-attacks/lib/blake2b.mjs';
|
|
|
|
const ZERO = '0'.repeat(64);
|
|
// live_certificates.voters rows carry the public key (what a verifier needs); the key hash is derived here
|
|
const keyOf = (v) => v.keyHash || (v.pubkey ? voteKeyHash(v.pubkey) : null);
|
|
|
|
export function signedWeightFromStored(certificates) {
|
|
const weight = new Map();
|
|
let used = 0, skipped = 0;
|
|
for (const c of certificates) {
|
|
const voters = Array.isArray(c.voters) ? c.voters : [];
|
|
if (voters.length !== Number(c.voter_count)) { skipped++; continue; }
|
|
used++;
|
|
const bm = Buffer.from(String(c.bitmap_hex || ''), 'hex');
|
|
for (let i = 0; i < bm.length; i++) for (let bit = 0; bit < 8; bit++) {
|
|
const p = i * 8 + bit;
|
|
if (p < voters.length && (bm[i] & (1 << bit))) {
|
|
const k = keyOf(voters[p]);
|
|
if (k) weight.set(k, (weight.get(k) || 0) + Number(voters[p].weight));
|
|
}
|
|
}
|
|
}
|
|
return { weight, used, skipped };
|
|
}
|
|
|
|
const shares = (s) => ({ keys: s.keys, total: s.total, top1: s.top1, top3: s.top3, top10: s.top10 });
|
|
|
|
export function nightlyRow({ weights, checkpoints = [], certificates = [], proofs = new Map(), attributes = new Map(), meta = {} }) {
|
|
const dust = Number(weights?.params?.dust ?? 100);
|
|
const keys = (weights?.keys || []).map(k => ({ keyHash: String(k.keyHash), blocks: Number(k.blocks) }));
|
|
const hashing = topShares(new Map(keys.map(k => [k.keyHash, k.blocks])));
|
|
const agg = new Map();
|
|
let anonymous = 0;
|
|
for (const c of checkpoints) {
|
|
if (c.state !== 'locked' && c.state !== 'certified') continue;
|
|
const a = String(c.certificateAggregator || ZERO);
|
|
if (a === ZERO) anonymous++; else agg.set(a, (agg.get(a) || 0) + 1);
|
|
}
|
|
const aggregation = topShares(agg);
|
|
const signed = signedWeightFromStored(certificates);
|
|
const signing = topShares(signed.weight);
|
|
const proving = topShares(proofs);
|
|
const ind = independenceClasses(keys, attributes, dust);
|
|
return {
|
|
day: meta.day || new Date().toISOString().slice(0, 10),
|
|
daa: meta.daa ?? Number(weights?.daaScore ?? 0),
|
|
checkpoint_index: Number(weights?.checkpointIndex ?? 0),
|
|
node_version: meta.nodeVersion || null,
|
|
hashing: { ...shares(hashing), unit: 'blue blocks', above_dust: keys.filter(k => k.blocks >= dust).length },
|
|
signing: { ...shares(signing), unit: 'signed weight', certificates: signed.used, skipped: signed.skipped },
|
|
proving: { ...shares(proving), unit: 'paid shards' },
|
|
aggregation: { ...shares(aggregation), unit: 'certificates', anonymous },
|
|
n_ind: ind.nInd,
|
|
n_ind_definition: 'proposed', // ledger X5, decision item 3: not adopted until the project lead decides
|
|
n_ind_eligible: ind.eligible,
|
|
n_ind_unattributed: ind.unattributed,
|
|
computed_at: new Date().toISOString(),
|
|
};
|
|
}
|