From 223d073fa9d5ca485111cf350bbc0c465777142a Mon Sep 17 00:00:00 2001 From: igneum-labs <337424239+igneum-labs@users.noreply.github.com> Date: Mon, 5 Oct 2026 23:07:00 +0000 Subject: [PATCH] Ledger close round 2: X14 signing concentration from block payloads; X5 observer columns on a branch (O-X.1) 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 --- docs/bench-log.md | 46 +++ docs/fud-ledger.md | 6 +- tools/finality-attacks/lib/blake2b.mjs | 86 ++++++ tools/finality-attacks/x14-concentration.mjs | 156 ++++++++++- tools/observer/README.md | 13 + tools/observer/asn-table.txt | 6 + tools/observer/lib/concentration.mjs | 278 +++++++++++++++++++ tools/observer/lib/nightly.mjs | 75 +++++ tools/observer/observer.mjs | 164 ++++++++++- tools/observer/pool-statements/README.md | 10 + tools/observer/pool-statements/registry.json | 4 + tools/observer/test/concentration.test.mjs | 186 +++++++++++++ 12 files changed, 1022 insertions(+), 8 deletions(-) create mode 100644 tools/finality-attacks/lib/blake2b.mjs create mode 100644 tools/observer/asn-table.txt create mode 100644 tools/observer/lib/concentration.mjs create mode 100644 tools/observer/lib/nightly.mjs create mode 100644 tools/observer/pool-statements/README.md create mode 100644 tools/observer/pool-statements/registry.json create mode 100644 tools/observer/test/concentration.test.mjs diff --git a/docs/bench-log.md b/docs/bench-log.md index 62659d289..a767e024a 100644 --- a/docs/bench-log.md +++ b/docs/bench-log.md @@ -1717,3 +1717,49 @@ Owed: the tampered pack against the real worker on PC 2's RTX 5090 (the job tool **Not run.** The X20 fast-time simnet timing (a simnet chain is a few hundred blocks; the unit test's step count is the evidence, the 10^6-block chain is still the owed experiment) and the X19 two-node skew run (needs an `attack-switches` build of the node). + +## 5 October 2026 (night), ledger close round 2: X14 signing concentration from block payloads + +Machine: the Mac observer node (`vendor/igneum-node-0310/target-integration/release/igneumd`, fork commit 21d4c73c, `getInfo.serverVersion` 2.1.0, appdir `observer-v4`, wRPC `ws://127.0.0.1:28640`, exec RPC `http://127.0.0.1:26790`), read-only, nothing submitted. The node was restarted onto the 0.3.10 build at 21:49:38 UTC (`ps -o lstart`), about DAA 134,276 (approximate: the DAA at the reading minus the process age at one DAA per second), so the window below straddles that restart and every reading carries a second row from DAA 134,300. Command: `tools/lock/with-lock.sh run node tools/finality-attacks/x14-concentration.mjs --node-build "vendor/igneum-node-0310 21d4c73c target-integration, observer-v4 restarted 22:49:38 local" --since-daa 134300` (the round-1 script extended; the chain walk, the voter-table rebuild and the payload decoder are in `tools/observer/lib/concentration.mjs`, the key hash in `tools/finality-attacks/lib/blake2b.mjs`), run slot run-1, 10 s. Fleet at the reading (`node tools/console.mjs machines`, read-only, 23:03 UTC): Mac app 0.3.10 on node 2.1.0-a24ab01a (its own node, not the observer's), PC 1 the same and stopped 33 min before, PC 2 app 0.3.10 on node 2.1.0 (the 0.3.10 build, its app restarted inside the window: accepted count reset), PC 37ba0461 the same, Sam's Mac app 0.3.9 stopped 2 h before. The rollout was mid-flip; the bytes a block carries are the same whichever node reads them. + +**Method.** No RPC exposes a certificate's signer set (round 1). Every block carries its certificates and votes in the coinbase extra data as `items || len_le32 || "IGNF"` (spec 03 C2, C3, Q2; `consensus/core/src/finality.rs` `encode_section`, `Certificate::write`: `index_le64 || checkpoint || voter_count_le32 || bitmap_len_le32 || bitmap || signature || aggregator || proof`; `Vote::write`: `index || checkpoint || pubkey || signature || sortition`, 280 bytes; identical on 0.3.6 and 0.3.10). The bitmap indexes the canonical voter list at the certificate's checkpoint (keys above dust, not stripped, sorted by key hash), which `getFinalityWeights` reports for the latest checkpoint only, so the script rebuilds it at every checkpoint in the window from headers the way `compute_weights` does (the checkpoint plus every chain block's mergeset blues back along the selected chain, counted when `window_start < daa <= daa(C)`, dust 5, sorted): 13,171 chain blocks walked (`getBlock` with transactions, from the sink to DAA 120,542, 1 s), 4,831 non-chain blue headers read, 18,002 blue blocks. Votes carry the public key; the key hash is BLAKE2b-256 keyed `IgneumVoteKeyHash` (`crypto/hashes/src/hashers.rs`), implemented in `lib/blake2b.mjs` and checked against the RFC 7693 vectors and the BLAKE2 keyed KAT. Checks on the live data: 240 of 240 rebuilt voter lists equal the node's `voters` count per checkpoint; 194 of 194 certificates mapped (every `voter_count` equals the rebuilt list's length and every certificate names the node's checkpoint block at its index); 27 `IGNK` reveals (pubkey to header `vote_key_hash`) against BLAKE2b, 0 mismatches; 0 evidence items in the window; 0 items naming a checkpoint outside the window. Only chain blocks are parsed (the task's scope); the node's block reading also credits votes carried by red blocks. + +**Reading 2 (the one cited), 23:00:44 UTC, DAA 138,542 at the start and 138,555 at the end, window 7,200 DAA, weights at checkpoint 4,522 (DAA 138,514), 5,731 chain blocks in the window (DAA 131,343 to 138,542).** + +| quantity | source | keys | total | top-1 | top-3 | top-10 | +|---|---|---|---|---|---|---| +| hashing (blue blocks per vote key; 27 of 27 above dust 5) | `getFinalityWeights` | 27 | 7,200 blocks | 6.4% | 19.2% | 60.1% | +| signing, heaviest certificate per index (the certificate the lock test reads; 126 indices) | block payloads | 27 | 732,650 signed weight | 10.0% | 29.1% | 77.3% | +| signing, every distinct certificate (194 certificates at 126 indices, carried 250 times) | block payloads | 27 | 1,080,056 | 10.5% | 30.5% | 77.6% | +| signing, every carriage (the same certificates once per carrying block) | block payloads | 27 | 1,443,739 | 10.1% | 29.5% | 76.3% | +| signing, votes carried (4,469 distinct (index, key, block) votes, weighted by the key's weight at C_i; 138 by keys under dust at their checkpoint) | block payloads | 27 | 1,234,377 | 8.4% | 24.4% | 70.0% | +| signing, votes unweighted (votes per key) | block payloads | 27 | 4,469 votes | 4.7% | 13.5% | 42.5% | +| aggregation (certificates built per named aggregator over 132 certified or locked checkpoints; 63 anonymous) | `getFinalityCheckpoints.certificateAggregator` | 10 | 69 certificates | 44.9% | 84.1% | 100% | +| aggregator sortition (keys named eligible, 240 checkpoints) | `getFinalityCheckpoints.aggregators` | 27 | 1,365 eligibilities | 8.7% | 24.9% | 69.7% | +| proving (paid proof records per prover key hash, 5,756 chain blocks from DAA 131,314; 84 carried, 32 rejected) | `igneum_getSegment.proofRecords` | 1 | 52 paid shards | 100% | 100% | 100% | +| signing since DAA 134,300, heaviest per index (88 indices, 149 certificates carried 202 times by 3,323 chain blocks) | block payloads | 26 | 543,555 | 9.9% | 28.7% | 75.9% | +| signing since DAA 134,300, every distinct certificate | block payloads | 26 | 856,271 | 10.5% | 30.8% | 77.4% | +| signing since DAA 134,300, votes weighted (3,018 votes) | block payloads | 27 | 773,597 | 6.9% | 20.3% | 62.3% | + +The node's own aggregate over the same 132 locked checkpoints: `signedWeight` over `totalWeight` p50 74.6%, min 66.7%, max 98.3%; `votesSeen` p50 18 of 27 voters. Signed weight is the sum, over the certificates counted, of each signer's blue blocks at that certificate's checkpoint. + +Signing per key, heaviest certificate per index (top 12 of 27): + +| key (first 8) | signed weight | share | certificates signed of 126 | weight at checkpoint 4,522 | +|---|---|---|---|---| +| a72f7b7c | 73,473 | 10.0% | 126 | 462 | +| ab8a9b21 | 71,886 | 9.8% | 126 | 460 | +| 6dfe55b3 | 67,941 | 9.3% | 126 | 442 | +| ea53bd41 | 67,854 | 9.3% | 126 | 447 | +| 7b8ef6fd | 67,688 | 9.2% | 126 | 435 | +| fe40e510 | 61,005 | 8.3% | 116 | 448 | +| 987175f0 | 57,620 | 7.9% | 104 | 457 | +| c41e4cc2 | 51,610 | 7.0% | 98 | 414 | +| a405c3e6 | 24,800 | 3.4% | 92 | 204 | +| e15fe94b | 22,601 | 3.1% | 87 | 171 | +| b7e3c337 | 20,735 | 2.8% | 87 | 381 | +| 00cec3ae | 20,142 | 2.7% | 85 | 381 | + +**Reading 1, 22:58:20 UTC, DAA 138,377 at the start and 138,385 at the end, weights at checkpoint 4,516 (both rows kept by the rollout-night rule).** Hashing 27 keys, 7,199 blocks: 6.5%, 19.5%, 60.8%. Aggregation 12 keys, 76 certificates over 139 certified or locked checkpoints (63 anonymous): 42.1%, 77.6%, 97.4%. Sortition 27 keys, 1,389 eligibilities: 8.7%, 24.7%, 69.8%. Proving 1 key, 52 paid shards: 100%. Signing: 200 distinct certificates at 131 indices carried 256 times by 5,738 chain blocks, 200 mapped; 239 of 239 tables agreed; 4,506 votes; the five share rows printed as NaN (the script passed the per-key map where the shares function expected its summary; fixed, the second reading taken 2 minutes later), the per-key table was right (a72f7b7c 75,531 signed weight, 10.0%, 131 of 131). + +**What the numbers mean, and what follows.** Signing is more concentrated than hashing (top-10 77.3% against 60.1%, top-3 29.1% against 19.2%) because a certificate carries a median 18 of 27 voters: five keys signed every one of the 126 heaviest certificates and eight signed 98 or more, and those eight hold 70.8% of the lock weight; the other 19 keys appear in 85 to 92 of 126 and split the rest. Every certificate still locked at or above two thirds of total (min 66.7%), so the missing votes cost nothing tonight; on a chain where the eight were one party, that party would be 71% of every lock. The phase 5 gate ("top-10 share of window weight under 50%", `site/journey.json`) fails tonight on all four measures (hashing 60.1%, signing 77.3%, aggregation 100%, proving 100%), as a devnet of 27 keys on 5 machines (3 live at the reading; 5.4 to 9 keys per machine, approximate from the console) must; the gate is a public-testnet test, and the columns to read it from stored data are on branch `ledger-observer` (ledger X5, round 2; not deployed). For the operator page a `getFinalityCertificate(index)` RPC that returns the bitmap with the voter list at C_i would make this a few calls instead of a 13,171-block walk; the walk takes 1 s on the devnet window and would take about 6 min of RPC at the mainnet window (2,592,000 blocks at the same rate, approximate), so the RPC is the mainnet form. Raw output: `/tmp/igneum-x14-results.md` (overwritten by each run). diff --git a/docs/fud-ledger.md b/docs/fud-ledger.md index fc459b0fc..9f8071d1f 100644 --- a/docs/fud-ledger.md +++ b/docs/fud-ledger.md @@ -837,6 +837,8 @@ Evidence: `site/journey.json` phase 5 gate. Cross-reference (external review, 3 October 2026, night): the definition and the four concentration metrics are X14, O-X.1. +Round 2 (5 October 2026, night), the observer columns (O-X.1), built on branch `ledger-observer` and NOT deployed; the running observer is untouched and the definition stays this decision (item 3). `tools/observer/observer.mjs` on that branch adds four tables and three timers (`tools/observer/README.md`, "Independence columns and the nightly table"): (a) `live_peer_asn`, the autonomous system per announcing peer address from `getConnectedPeerInfo` against an offline prefix table (`tools/observer/asn-table.txt`, empty tonight, to be filled from a dated BGP dump: RIPEstat, Team Cymru bulk whois or a pyasn dump of RouteViews; no third party is asked at run time; private ranges read `local`, a miss stays `source = 'none'`); (b) `live_key_machines`, the machine fingerprint per vote key from the log intake (`miner_logs.machine`, the id8 every STATUS line's run id carries, joined to the miner's `identity N 'label' vote_key_hash=...` line in the same upload); (c) `live_pool_statements`, the pool statement format: a signed JSON `{format: "igneum-pool-statement-1", pool, keys[], signed_at, pubkey, sig}`, Ed25519 over canonical bytes, verified against `pool-statements/registry.json` (pool label to key; empty tonight), refused when signed by another key, older than 35 days, with repeated keys or a malformed shape, and stored with the reason when it fails; (d) `live_concentration`, one row a day at 00:05 UTC with top-1, top-3 and top-10 shares for hashing (`getFinalityWeights`), signing (the stored `live_certificates` bitmaps over their voter tables), proving (`live_proofs` paid shards per prover) and aggregation (`certificateAggregator` over the window's checkpoints), plus `n_ind` with `n_ind_definition = 'proposed'`: distinct (ASN, fingerprint, pool) classes among keys above dust, the silent-key rule of the decision request applied (a key with no fingerprint is its own class only when its ASN is used by no other key), and `n_ind_unattributed` counting keys with no attribute at all. Pure functions in `tools/observer/lib/concentration.mjs` and `lib/nightly.mjs`; 9 unit tests under node's test runner (`node --test 'tools/observer/test/*.test.mjs'`: the payload decoder against hand-built fixtures, the voter-table rebuild rule, shares, N_ind on the 21-keys-5-machines reading of this entry (5) and the silent-fleet rule, the pool statement's signing and every refusal, the ASN table's longest prefix, the nightly row), all passing on the Mac. Open: a block names its producer by vote key and a peer announces an address, and nothing ties the two, so the ASN per key is null until the app reports its public address with its uploads (an app-owner item); the ASN table and the pool registry are empty until filled by hand. Tonight's reading from X14's round 2 (23:00 UTC, DAA 138,542): 27 keys above dust over 5 machines in the window, 3 of them live at the reading, so N_ind by fingerprint is at most 5 (approximate, from the console; the fingerprint column will give the exact figure once deployed) and the top-10 share of window weight is 60.1%, against the gate's "under 50%". + ### X6. The one-click app is a honeypot vector "An installer that creates a wallet, holds keys and mines, promoted to people who have never run a miner. Fake copies will be the first Google result. Defender flags every miner as malware." @@ -1559,7 +1561,7 @@ Evidence: `site/journey.json` phases 4 and 5; `docs/commercial/prover-customer-b ### X14. Concentration is unmeasured in four places "Define independent for the 1,000-miner gate, then report concentration in hashing, checkpoint signing, proving and aggregation, because a thousand miners behind two pools is two." -Status: Answered with evidence for hashing, aggregation and proving, Open for signing (5 October 2026, night, ledger close round 1: no RPC exposes a certificate's signer set, so the signing concentration needs the observer extract of O-X.1; bench-log "5 October 2026 (night), ledger close round 1: X14 concentration in hashing, signing, proving and aggregation from the Mac node's RPCs, and E16 one live block"); the independence definition stays the project lead's (X5). Was: Open, measurement scheduled (O-X.1); extends X5. Sweep (5 October 2026): hashing concentration computed from the devnet record (`sim/difficulty/records/live-2026-10-04.csv`, 8,090 blocks, 18 vote keys): top-1 12.8%, top-3 34.5%, top-9 98.0%, the nine being devnet v4's nine identities run by three machines (approximate), so by machine the devnet is three parties. Signing, proving and aggregation concentration need certificate and proof-record extracts the observer does not keep yet (O-X.1). +Status: Answered with evidence for all four (5 October 2026, night, ledger close round 2: signing concentration read from the certificates and votes the chain blocks carry in their coinbase finality section, top-1 10.0%, top-3 29.1%, top-10 77.3% of signed weight over the heaviest certificate at each of 126 indices in the window; bench-log "5 October 2026 (night), ledger close round 2: X14 signing concentration from block payloads"); the independence definition stays the project lead's (X5). Was: Answered with evidence for hashing, aggregation and proving, Open for signing (5 October 2026, night, ledger close round 1: no RPC exposes a certificate's signer set, so the signing concentration needs the observer extract of O-X.1; bench-log "5 October 2026 (night), ledger close round 1: X14 concentration in hashing, signing, proving and aggregation from the Mac node's RPCs, and E16 one live block"); the independence definition stays the project lead's (X5). Was: Open, measurement scheduled (O-X.1); extends X5. Sweep (5 October 2026): hashing concentration computed from the devnet record (`sim/difficulty/records/live-2026-10-04.csv`, 8,090 blocks, 18 vote keys): top-1 12.8%, top-3 34.5%, top-9 98.0%, the nine being devnet v4's nine identities run by three machines (approximate), so by machine the devnet is three parties. Signing, proving and aggregation concentration need certificate and proof-record extracts the observer does not keep yet (O-X.1). Answer: Correct. X5 conceded that "independent" needs a measurable definition (distinct ASNs, benchmark hardware fingerprints, pool attestations) and left it to phase 4. The four concentrations can each be computed from chain data: blue blocks per vote key and per pool (hashing), signed weight per key in certificates (checkpoint signing), proof records per prover key (proving, once P12's fix puts the key in the statement), and certificates and proof records per aggregator key (aggregation). The gate reports all four as top-1, top-3 and top-10 shares over 30 days, beside the independence count, on the live page. Transaction choice inside pools is a separate measurement and is already scheduled: whether members of the reference pool use declared templates (spec 9.4.2, mode C) is recorded under O-9.5. @@ -1567,6 +1569,8 @@ Evidence: X5, F10, P12, spec 9.4.2. Experiment: O-X.1. Review: external, point 6 Run (5 October 2026, night): `tools/lock/with-lock.sh run node tools/finality-attacks/x14-concentration.mjs` (new; the Mac observer node's wRPC and exec RPC, read-only, 7 s) at DAA 125,005, window 7,200 DAA, plus `node tools/console.mjs machines`. Hashing (`getFinalityWeights`, 29 keys, 25 above dust, 6,734 blocks): top-1 25.9%, top-3 37.3%, top-10 67.0%. Aggregation (`certificateAggregator` over 210 certified or locked checkpoints, 187 named, 23 anonymous, 20 keys): top-1 17.1%, top-3 43.3%, top-10 85.0%; sortition eligibility (1,492 namings over 225 checkpoints, 27 keys): top-1 7.1%, top-3 20.4%, top-10 61.9%. Proving (`igneum_getSegment.proofRecords` over 4,474 chain blocks, 275 records carried, 131 paid, 144 rejected): one key and one payout address hold 100% of the paid shards (PC 2's prover; 519 shards paid on the chain in all). Signing: NOT exposed; `RpcCheckpoint` carries `signedWeight`, `votesSeen`, `voters`, `aggregators` and `certificateAggregator` and no signer set or bitmap, so per-key signing concentration cannot be computed from any RPC on this line; what is exposed over the 210 locked checkpoints is `signedWeight` over `totalWeight` p50 71.1%, min 38.8%, max 100% (a locked checkpoint at 38.8% shows the two fields are not the pair the lock test used) and `votesSeen` p50 16 of 25. Key-to-machine ratio: 29 keys against 4 machines on the console at 19:14 UTC (5.8 to 7.3 keys per machine, depending on whether the evening's fifth machine is counted). Bench-log heading: "5 October 2026 (night), ledger close round 1: X14 concentration in hashing, signing, proving and aggregation from the Mac node's RPCs, and E16 one live block". +Round 2 (5 October 2026, night): the signing half, from block payloads. No RPC exposes a certificate's signer set, but every block carries its certificates and votes in the coinbase extra data (spec 03 C2, C3, Q2: `items || len_le32 || "IGNF"` at the end of the payload; `consensus/core/src/finality.rs` `encode_section`, `Certificate::write`, `Vote::write`, the same bytes on 0.3.6 and 0.3.10), and a certificate's bitmap indexes the canonical voter list at its checkpoint. `getFinalityWeights` reports that list for the latest checkpoint only, so `tools/finality-attacks/x14-concentration.mjs` (extended, not a second script) rebuilds the list at every checkpoint in the window from headers the way the node does (`compute_weights`: the checkpoint plus every chain block's mergeset blues back along the selected chain, `window_start < daa <= daa(C)`, dust, sorted by key hash) and maps every bitmap through it; votes carry the public key and the key hash is BLAKE2b-256 keyed `IgneumVoteKeyHash` (`lib/blake2b.mjs`, RFC vectors plus every `IGNK` reveal in the window as the check). Run: `tools/lock/with-lock.sh run node tools/finality-attacks/x14-concentration.mjs --node-build ... --since-daa 134300`, read-only, 10 s, at 23:00:44 UTC on the Mac observer node (`vendor/igneum-node-0310` 21d4c73c, `target-integration/release/igneumd`, `serverVersion` 2.1.0, restarted for 0.3.10 at 21:49:38 UTC, about DAA 134,276, so the window straddles that restart and the reading carries a second row from DAA 134,300), DAA 138,542 at the start and 138,555 at the end, window 7,200 DAA, weights at checkpoint 4,522. Fleet at the reading (console, read-only): Mac and PC 1 apps on node 2.1.0-a24ab01a (PC 1 stopped 33 min before), PC 2 and PC 37ba0461 on node 2.1.0 (the 0.3.10 build), Sam's Mac stopped 2 h before; the chain bytes are the same whichever node reads them. Checks: 240 of 240 rebuilt voter lists agree with the node's `voters` count, 194 of 194 certificates mapped (every `voter_count` equals the rebuilt list), 27 reveals against BLAKE2b with 0 mismatches, 0 evidence items, 0 items naming a checkpoint outside the window. Result, signed weight per key over the heaviest certificate at each of 126 indices (the certificate the lock test reads; 194 distinct certificates carried 250 times by 5,731 chain blocks): 27 keys, top-1 10.0%, top-3 29.1%, top-10 77.3%; over every distinct certificate 10.5%, 30.5%, 77.6%; over every carriage 10.1%, 29.5%, 76.3%; votes carried (4,469 distinct, weighted by the key's weight at C_i) 8.4%, 24.4%, 70.0%, unweighted 4.7%, 13.5%, 42.5%. Beside the other three at the same reading: hashing (27 keys, 7,200 blocks) 6.4%, 19.2%, 60.1%; aggregation (10 named keys, 69 certificates over 132 certified or locked checkpoints, 63 anonymous) 44.9%, 84.1%, 100%; proving (52 paid shards) 100%, 100%, 100%. Since DAA 134,300 (after the observer node's restart, 88 indices): 9.9%, 28.7%, 75.9%. Signing is more concentrated than hashing (top-10 77.3% against 60.1%) because the node sees a median 18 of 27 voters per checkpoint: five keys signed all 126 certificates and eight signed 98 or more, and those eight hold 70.8% of the lock weight (per-key table in the bench-log); the other 19 keys sign 85 to 92 of 126 and carry the rest. What it means: the phase 5 gate ("top-10 share of window weight under 50%") fails tonight on all four measures, as a four-live-machine devnet must (27 keys against 3 machines live at the reading, 5 over the window: 5.4 to 9 keys per machine, approximate from the console); the gate is a public-testnet test and the observer now has the columns to read it (X5, round 2). A `getFinalityCertificate(index)` RPC returning the bitmap and the voter list at C_i would make the reading a few calls instead of a 13,171-block walk; not needed for the ledger, noted for the operator page. An earlier reading at 22:58:20 UTC (DAA 138,377) gave hashing 6.5%, 19.5%, 60.8% and aggregation 42.1%, 77.6%, 97.4% (12 keys, 76 certificates); its signing rows were unreadable (a formatting fault in the script, fixed before the second reading) and both readings are kept in the bench-log. + ### X15. Remove the founders from a test network and show what continues "'The chain runs without its founders' is a sentence. Take the team's miners, provers, aggregators, seed nodes, observer and site off a running testnet and show what keeps producing blocks, proofs and locks." diff --git a/tools/finality-attacks/lib/blake2b.mjs b/tools/finality-attacks/lib/blake2b.mjs new file mode 100644 index 000000000..edb0a695f --- /dev/null +++ b/tools/finality-attacks/lib/blake2b.mjs @@ -0,0 +1,86 @@ +// 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 }; +} diff --git a/tools/finality-attacks/x14-concentration.mjs b/tools/finality-attacks/x14-concentration.mjs index 871a45acd..e2bd930bf 100644 --- a/tools/finality-attacks/x14-concentration.mjs +++ b/tools/finality-attacks/x14-concentration.mjs @@ -14,12 +14,32 @@ // concentration by paid shards per key hash over the window's chain blocks // getBlock(hash, true) the coinbase outputs: the 20% pool output with its OP_RETURN tag (E16) // Never submits anything. +// +// Round 2 (5 October 2026, night, ledger close round 2): the SIGNING half from block payloads. No RPC exposes a +// certificate's signer set, but every block carries its certificates and votes in the coinbase extra data (spec 03 +// C2, C3, Q2; `items || len_le32 || "IGNF"` at the end of the payload, consensus/core/src/finality.rs +// encode_section, Certificate::write, Vote::write). A certificate's bitmap indexes the canonical voter list at its +// checkpoint (keys above dust, not stripped, sorted by key hash), and `getFinalityWeights` reports that list for the +// latest checkpoint only, so the list at every checkpoint in the window is rebuilt here from headers exactly as the +// node does it (consensus/src/processes/finality.rs compute_weights: C itself plus every chain block's mergeset +// blues back along the selected chain, counted when `window_start < daa <= daa(C)`, dust applied, sorted). The +// rebuilt list is checked against the node's `voters` count per checkpoint and against every certificate's +// `voter_count`. Votes carry the public key; the key hash is BLAKE2b-256 keyed `IgneumVoteKeyHash` (lib/blake2b.mjs), +// checked against every `IGNK` reveal the window's coinbases carry. +// --node-build