Merge branch 'redteam' into fud-consensus
# Conflicts: # docs/fud-ledger.md
This commit is contained in:
commit
2ca10df22d
5 changed files with 268 additions and 0 deletions
|
|
@ -1691,6 +1691,8 @@ Answer: Correct. `ingest_evidence` (`processes/finality.rs:600-612`); `ingest_ce
|
|||
|
||||
Evidence: the files above. Experiment: `tools/finality-attacks` with one equivocation detected on node A by RPC and on node B from the carrying block 30 DAA later; count certificates refused with "names N voters" between the two expiries; after the fix, zero.
|
||||
|
||||
Red-team run, 4 October 2026 (evening, the 0.3.4 finality-fixes build with rule v3 on, `docs/review/redteam-2026-10-04.md` row 15): reproduced by the stock scenario 1 (two keys equivocating at every index, 4 honest voters, 3 nodes, fast time). The node that received the equivocators' votes by RPC re-detected at every index (46 detections) and held the ban until DAA 726; the two nodes that saw the evidence only in blocks detected it at indices 1 to 3 (8 detections, ban until 239) and let it expire. The first detection alone stamped `until` 174 and 176 on the RPC node against 176 on the others. From index 9 the voter lists differed by two keys and the nodes refused each other's certificates: 9 refusals "names 6 voters, this node counts 4" on the RPC node, 3 and 4 refusals "names 4 voters, this node counts 6" on the others. Every node still locked 15 of 15 only because each could aggregate its own certificate from the votes it held; with 8 named aggregators on a real network that fallback is `aggregator_fallback` later and a node whose certificate the rest refuse is one more aggregation round behind at every index. Rule v3 does not touch this path. Severity stays serious; the fix above stands.
|
||||
|
||||
### F24. A checkpoint determination is never revisited
|
||||
"After a reorg deeper than `checkpoint_depth`, the node's record for that index names a block off its chain. Every certificate the network forms for that index is refused as conflicting, with no equivocation anywhere, and the node voted for a block that is not on its chain."
|
||||
|
||||
|
|
@ -1700,6 +1702,17 @@ Answer: Correct. `on_virtual_changed` (`processes/finality.rs:404-440`) inserts
|
|||
|
||||
Evidence: the files above. Experiment: a 30 s cut on a 3-node devnet at d = 20; the losing side must accept the network's certificate at that index with no CONFLICTING line.
|
||||
|
||||
Red-team run, 5 October 2026, 00:56 (the 0.3.4 finality-fixes build, rule v3 on, fast time, `docs/review/redteam-2026-10-04.md` row 23b): reproduced on a clean merge. Two nodes with three voting keys each, cut for 16 s (about 50 blue blocks a side, under the 60-DAA merge depth), healed: sinks equal, 64 locks each, no conflicting certificate. Checkpoint 33 was determined during the cut on each side's own chain (two different blocks); after the reorg neither node re-determined it, neither block ever got a certificate (votes split 3/3), and finality went on from 34. A permanent one-index hole on every node, with no equivocation anywhere. The round-4 shape, a false CONFLICTING line, needs the other side's certificate to arrive, which a 3/3 split cannot form; the two 4/2 attempts (rows 22 and 23) showed the stuck indices and the refusals "this node's checkpoint is ..." but overshot merge depth, so they are confounded with F21. Rule v3 does not touch this path. The fix above stands; the two-node test is `rtfin.mjs f24c` in the red-team scratchpad (cut 16 s, 3/3), which should end with index 33 locked on both nodes.
|
||||
|
||||
### F25. The fast-time harnesses cannot start a node, and the timestamp probe tests the old rule
|
||||
"Both attack harnesses rebuild each node's override with `JSON.parse` and `JSON.stringify` of `infra/fast-time/override-60x.json`. That file now carries two `u64::MAX` sentinels (`difficulty_v2_activation_daa`, `proving_v0_activation_daa`); a JavaScript number cannot hold them, the round-trip writes `18446744073709552000`, and `igneumd` refuses the file as a floating point where a u64 is expected. Every `--fast-time` run of `tools/finality-attacks` and `tools/harness` fails at the first node. Scenario 2 of `tools/harness` still probes the 132 s future bound and reports FAIL against the 10 s rule the node has carried since the timestamp fix."
|
||||
|
||||
Status: Open (4 October 2026, red-team run). Tooling, low severity: no consensus effect, but every fast-time attack run is blind until it is fixed.
|
||||
|
||||
Answer: Correct, measured. The red-team run's first scenario errored on it (`docs/review/redteam-2026-10-04.md`, "Tooling defect"); `tools/proving-v0/run.mjs` already edits the file as text for this reason. Scenario 2 live probe: past floor pmt+1, future flip between +130.00 and +130.01 s of the probe's own offsets, every stamp from +10 s rejected; the node is right, the criterion is stale. Smallest fix: in `tools/finality-attacks/lib/net.mjs` and `tools/harness/lib/net.mjs` `overrideParams`, drop the two sentinel fields before `stringify` (absent means never) or splice the extra fields into the file text; in `tools/harness/scenarios/s2-timestamp.mjs`, probe `max(pmt + 1, parent - 10 s)` and the +10 s bound. Also stale: `tools/exec-attacks/scenario3_pgas.mjs` waits for an over-budget transaction to be included and skipped with `BlockProvingBudget`; since F-exec-B the mempool refuses it with the metered pgas, so the check should accept `ProvingGasAboveBlockLimit` from the pool (`docs/review/redteam-2026-10-04.md` row 28). And `tools/finality-attacks` scenario 5 compares the burster's share of the weight window with its share of the whole run, which only agree when the run is shorter than the window (row 17).
|
||||
|
||||
Evidence: the first run's `/tmp/igneum-redteam-fin/n0/node.log` parse line (kept in the session scratchpad `rt/logs/fa_s8/n0/node.log`), `rt/logs/ord_s2.log`.
|
||||
|
||||
### X19. Operational knobs and silences in the shipped node
|
||||
"A slow-clock node disconnects every peer on every relayed block and never says why; the handshake's `time_offset` is computed and unused; `IGNEUM_ATTACK_TS_OFFSET_MS` and `IGNEUM_POW_STRIKES` are compiled into the live binary; `timestamp_deviation_tolerance` is dead and still accepted."
|
||||
|
||||
|
|
@ -1799,6 +1812,24 @@ Answer: Correct. `site/litepaper.html:424`; the app's sources have no earnings,
|
|||
|
||||
Evidence: the files above.
|
||||
|
||||
### M30. A block or transaction flood grows the 0.3.4 node by hundreds of megabytes in a minute
|
||||
"On the 3 October ordering-layer node (no execution layer) the resource-exhaustion scenario grew RSS by 4, 11 and 14 MB and the 50x block flood by 30 MB. On the 0.3.4 build, same harness, same scenarios, same 60 s: template flood +6 MB, submit flood +269 MB, mempool flood +270 MB, block flood 302 to 1,082 MB on both nodes (567 MB at 10 s, 824 MB at 20 s). The harness calls it a pass because its bound is baseline + 512 MB; a peer that keeps going is not bounded by the harness."
|
||||
|
||||
Status: Open (4 October 2026, red-team run). Serious: a single peer at 50 blocks/s or 500 transactions/s is the devnet's own fast-miner event, and a node that grows 13 MB/s under it runs out of memory in minutes on the 2 to 4 GB cloud nodes.
|
||||
|
||||
Answer: Measured, cause not yet isolated. What changed between the two builds is the execution layer, which this node links: `igneum/exec/src/service.rs` keeps every `ChainBlockRecord` in `ExecState.records` (`:304`, `:419`, pushed and never truncated) plus `tx_index` and `inclusions` maps per transaction, and the mempool flood's 14,998 rejected transactions cost 270 MB, so rejected transactions are retained somewhere too. Smallest fix: bound `ExecState.records` to the record window plus the pruning depth and drop `tx_index`/`inclusions` entries with them; discard a rejected transaction's bytes at rejection; then re-run `tools/harness` s6 and s7 and require growth under 50 MB, the 3 October figure.
|
||||
|
||||
Evidence: `/tmp/igneum-redteam-ord/results/s6-exhaustion.json` and `s7-flood.json` (samples carry `rss_a`, `rss_b` every 10 s), kept in the session scratchpad `rt/logs/ord_s6`, `rt/logs/ord_s7`; the 3 October numbers in `docs/bench-log.md`, "consensus attack harness" entry.
|
||||
|
||||
### M31. The 0.3.4 node cannot produce a block template on mainnet, testnet or simnet parameters
|
||||
"`getBlockTemplate` on a `--simnet` node from the finality-fixes build answers every call with `Coinbase payload is above max length (204). Try to shorten the extra data.` and the network never makes a block. The coinbase of this build carries the vote-key reveal, the proof-record section and the finality section; only `DEVNET_PARAMS` was raised to `MAX_COINBASE_PAYLOAD_LEN_WITH_FINALITY` (16,384). `MAINNET_PARAMS`, `TESTNET_PARAMS` and `SIMNET_PARAMS` still carry Kaspa's 204 (`consensus/core/src/config/params.rs:705, 766, 828` against `:900`)."
|
||||
|
||||
Status: Open (4 October 2026, red-team run). Serious for anything that is not the devnet: a mainnet or testnet genesis on these parameters cannot be mined by a voting miner at all; harmless on the live devnet, whose parameters carry the raise.
|
||||
|
||||
Answer: Measured on the execution-layer attack network (`tools/exec-attacks/net.sh` runs `--simnet` with no override): three nodes up, 0 blocks, every template refused with that line (`docs/review/redteam-2026-10-04.md` row 27). Smallest fix: set `max_coinbase_payload_len: MAX_COINBASE_PAYLOAD_LEN_WITH_FINALITY` on the three other networks, and add a unit test that builds a coinbase with a key reveal, the maximum record section and a full certificate and checks it under every network's limit. The red-team run worked around it with `{"max_coinbase_payload_len": 16384}` in an override file.
|
||||
|
||||
Evidence: session scratchpad `rt/logs/exec_b/miner_node1.log` (the template error, repeated once per second), `rt/logs/exec_b/n1/node.log` (28 lines, genesis executed, nothing after).
|
||||
|
||||
### E17. Unlogged inputs behind the economics, minor
|
||||
"The cap's 110 MH/s and its draw are not in the bench-log; the Mac's draw is not logged; the economy sim's one measured input is a 229 MH/s card against today's 124; mining-versus-pool flips from 4.9x for mining on today's devnet to 930x for proving at 10,000 cards and no document says it depends on fleet size; the app-share text omits '100,000-gas calls' and the open base unit; emission ran at up to 2x schedule; shard-scale prover cost is unmeasured on any GPU; the iGPU default off is right."
|
||||
|
||||
|
|
|
|||
129
docs/review/redteam-2026-10-04.md
Normal file
129
docs/review/redteam-2026-10-04.md
Normal file
|
|
@ -0,0 +1,129 @@
|
|||
# Red-team run, 4 October 2026: every attack suite against the 0.3.4 finality-fixes build
|
||||
|
||||
Scope (set this evening): run every attack suite the repo has against the node that ships as 0.3.4 tonight, on
|
||||
fast-time test networks, and report what breaks. The 0.3.4 node is the finality-fixes build: F21 (frozen weight
|
||||
table) and F22 (certificate fold) on top of difficulty v2 and proving v0.
|
||||
|
||||
## Build under test
|
||||
|
||||
- Commit `6aa69a45` (branch `finality-fixes`): "Finality rule v3 behind finality_v3_activation_daa: the frozen weight
|
||||
table (ledger F21) and the certificate fold (F22)". This is devnet-v4 (generator v2, 2/3 floor, difficulty v2,
|
||||
proving v0) plus rule v3.
|
||||
- Built by this agent from a detached worktree at `6aa69a45` (`vendor/igneum-node-redteam`), release, `nice -n 19`,
|
||||
4 cargo jobs, through `tools/lock/with-lock.sh build`. Binaries: `igneumd`, `igneum-miner`, `igneum-p2p-probe`,
|
||||
`igneum-harness-sim` under `vendor/igneum-node-redteam/target/release`. `igneumd --version` = `igneumd
|
||||
2.1.0-b84644c`. The shipping `vendor/igneum-node/target-finality/release/igneumd` carries the same v3 strings; this
|
||||
agent built its own copy to be certain, per CLAUDE.md.
|
||||
- The hostile driver is the `fin-attacks` miner (`vendor/igneum-node-fin-attacks/target/release/igneum-miner`, its
|
||||
test-only flags `--equivocate`, `--sybil`, `--drop-votes`, `--pulse`, `--no-vote`, and `fin-rpc-attack`). The
|
||||
finality-fixes miner has none of those, so the attack flags must come from that worktree; its RPCs are additive and
|
||||
drove the devnet-v4 node before. The node under test is always the finality-fixes build.
|
||||
|
||||
## Method and isolation
|
||||
|
||||
- Fast-time 60x profile (`infra/fast-time/override-60x.json`), rule v3 forced on with
|
||||
`finality_v3_activation_daa: 0` in a copy of that file, `skip_proof_of_work` for the harnessed networks. Weight
|
||||
window and min_daa 120 DAA, equivocation ban 120 DAA, checkpoint every 30 blue, determined 20 later, dust 5,
|
||||
aggregators 8, presence window 1, certificate fold 3 DAA. The node confirms at boot: "Finality v2 ... rule v3
|
||||
(frozen table, certificate fold) from checkpoint DAA 0".
|
||||
- Ports: finality attacks 29650+, custom finality 29650+ (same lib), ordering harness 29750+, execution suite
|
||||
299x0, proof flood 29680+. The live devnet (26610/26611, 26640/26641, 28640) and other agents' ports were never
|
||||
touched. Data under `/tmp/igneum-redteam-*`.
|
||||
- All functional runs went through `tools/lock/with-lock.sh run`; the build and the proving unit tests through
|
||||
`with-lock.sh build`.
|
||||
|
||||
## Machine caveat (reliability of the numbers)
|
||||
|
||||
The Mac was shared with ten to fourteen other agents for the whole session: load average 100 to 180 on 16 cores
|
||||
throughout (the run lines record it). Under that load the functional outcomes a scenario asserts (locks formed,
|
||||
conflicting certificates, disagreeing locked indices, blocks accepted or rejected, node alive) are valid: they are
|
||||
counts the consensus rules produce regardless of wall clock. Any latency or throughput figure here (lock latency,
|
||||
records per second, CPU per record) is taken under that load and is an upper bound, not a measurement, and is
|
||||
labelled so. Per CLAUDE.md, a timing number taken while the box is loaded is not a number.
|
||||
|
||||
## Tooling defect found before any scenario could run
|
||||
|
||||
The fast-time harnesses build their per-node override by `JSON.parse` of `infra/fast-time/override-60x.json` and
|
||||
`JSON.stringify` of the result (`tools/finality-attacks/lib/net.mjs` `overrideParams`, and the same in
|
||||
`tools/harness/lib/net.mjs`). That file now carries `difficulty_v2_activation_daa` and `proving_v0_activation_daa`
|
||||
set to `18446744073709551615` (`u64::MAX`, the "never" sentinel). JavaScript numbers cannot hold that value, so the
|
||||
round-trip writes `18446744073709552000`, and the node refuses the file: "invalid type: floating point
|
||||
`1.8446744073709552e+19`, expected u64". Every fast-time harness run is broken this way today (the first finality
|
||||
scenario this session errored on exactly this, node never started). `tools/proving-v0/run.mjs` avoids it by editing
|
||||
the file as text, not JSON. Worked around here by stripping the two sentinel fields from the override copy (the node
|
||||
then defaults them to "never", which is what the sentinel meant). Logged as a tooling fail below (F25).
|
||||
|
||||
## Results
|
||||
|
||||
One row per scenario. Log paths: `rt/logs/...` is the red-team session scratchpad (`/private/tmp/claude-501/.../scratchpad/rt/logs`, with every node, miner and result file copied per scenario), `results/...` is `/tmp/igneum-redteam-ord/results`. The scenario scripts are in `tools/finality-attacks/redteam/` (`rtfin.mjs` withhold34, part5050, f23, f24, f24b, f24c; `flood.mjs`; the two override files); they run against the patched copies of the harness libraries described above.
|
||||
|
||||
| # | Scenario | Expected | Observed | Pass/Fail | Log |
|
||||
|---|---|---|---|---|---|
|
||||
| 1 | Ordering 1: withheld-block mining, share 0.10/0.25/0.33/0.45, release every 5 (sim, 400 blocks, seed 7) | attacker blue share within hash share + 2 sigma | blue share 6.0 / 23.3 / 29.7 / 43.6%; max honest reorg 8 / 11 / 9 / 13 | PASS (release-every-20 is full-run only, not re-run) | rt/logs/ord_s1.log, /tmp/igneum-redteam-ord/sim/s1-*.json |
|
||||
| 2 | Ordering 3: partition 10 s at 60x, heal (sim) | one chain after merge depth | one chain, blue scores within 5, healed in 2 s, losing-side reorg 4 blocks, 0 rejects | PASS | rt/logs/ord_s3.log |
|
||||
| 3 | Ordering 4: eclipse 10 s at 60x (sim) | victim rejoins within merge depth | rejoined 2 s after reconnection, gap 0, victim reorg 21, adversary's 42 blocks never entered the honest chain | PASS | rt/logs/ord_s4.log |
|
||||
| 4 | Ordering 7A: 50x fast-miner joins at 600 s, leaves at 1,200 s (sim, controller trajectory) | trajectory recorded | peak 5.37 blocks/s, difficulty x61.1, within 25% of 1 BPS after 60 s; trough 0 after the leave, back within 25% after 1,170 s | PASS (recorded; this is rule v1 on a fresh chain, v2 is behind the height switch) | rt/logs/ord_s7.log |
|
||||
| 5 | Ordering 7B: 50 blocks/s from one peer for 60 s (live, 2 nodes) | node responsive, alive, one sink | 197 blocks accepted (3.3/s under load); honest template p95 74.7 ms vs 60.7 baseline; alive, same sink; RSS 302 to 1,082 MB and 305 to 1,085 MB | PASS by criterion; memory growth is a FAIL, ledger M30 | rt/logs/ord_s7.log, results/s7-flood.json |
|
||||
| 6 | Ordering 2: timestamp boundaries (live) and a 33% stretcher (sim) | reject exactly at the rule's bounds; drift measured | past: pmt-1 and pmt rejected, pmt+1 accepted; every future stamp from +10 s rejected (harness probes 130 to 192 s, all rejected); sim: honest 1.005 b/s, ahead -3.2%, oscillate -3.5% | node PASS; harness criterion stale (probes the 132 s rule), ledger F25 | rt/logs/ord_s2.log |
|
||||
| 7 | Ordering 5: 63 malformed and boundary inputs (46 RPC, 17 p2p) | rejected without a crash or a cache build | 63 of 63: 20 blockInvalid, 13 disconnected, 17 RPC errors, 6 accepted sanity cases; `cache_build` false on every case; alive; RSS 297 to 312 MB | PASS (M15 holds) | rt/logs/ord_s5.log, results/s5-malformed.json |
|
||||
| 8 | Ordering 6: template 500/s, submit 50/s, mempool 500/s floods from one peer, 30 s each (live) | honest template p95 under 200 ms, RSS under baseline + 512 MB | p95 worst 71.1 ms (baseline 62); 14,999 templates and 1,499 submits accepted, 14,998 mempool txs rejected; RSS +6 / +269 / +270 MB | PASS by criterion; growth is a FAIL, ledger M30 | rt/logs/ord_s6.log, results/s6-exhaustion.json |
|
||||
| 9 | Proving v0 hostile records (in-crate tests on the 0.3.4 source: tampered statement, wrong payout, prover not assigned inside the exclusive window, shard not in plan, wrong block, outside the record window, before activation; replay of a submitted record; proof bytes that no longer hash to the record) | every one refused with its reason; a replay is deduplicated, not paid twice | `igneum-exec` 4 of 4, `kaspa-consensus-core` 6 of 6 (dust keys dropped, weighted sortition stops at 8 keys, record round-trip and verify); replay returns accepted, new=false; tampered proof refused on proof_hash | PASS | rt/logs/provetests.log |
|
||||
| 10 | Proof pool flood: 6,000 invalid records over `igneum_submitProofRecord` from one client (2,000 each: proof-hash mismatch; good hash and bad version; version 1 garbage reaching the record lookup), 1 node, proving v0 active | 0 accepted, node up, pool empty, cost per record bounded | 0 accepted; node up; pool entries 0 after; node CPU 0.68 / 0.31 / 0.31 ms per record, 89 / 109 / 120 records/s sequential (load 50 to 90: upper bounds) | PASS | rt/logs/flood.log, rt/logs/flood/flood.json |
|
||||
| 11 | Proving v0 end-to-end network run (`tools/proving-v0/run.mjs`) | the happy-path loop on the 0.3.4 node | NOT RUN this session: the runner is the happy path only (export, CPU proof, sign, submit, pay); its hostile cases live in the unit tests of row 9, which ran. A run needs the SP1 host for minutes under the single run lock | not run, stated | tools/proving-v0/run.mjs |
|
||||
| 12 | Finality s8: malformed, mis-signed, wrong-chain, replayed, 2 MB and non-hex votes over `submitFinalityVote` (v3 node, 3 voters) | each refused without a crash, replay deduplicated, node up | control accepted; bad signature, wrong chain id, short garbage, 2 MB, non-hex refused; replay "already known"; getInfo answered after all 8 cases; node up after | PASS | rt/logs/fa_s8.log, /tmp/igneum-redteam-fin/s8-fin-rpc-attack.out |
|
||||
| 13 | Finality s3: dishonest aggregators, 6 voters on 3 nodes, 105 s (v3) | locks on every node, 0 conflicting certs, lock hashes agree, latency under 2 s | 15 / 15 / 15 locks, 0 / 0 / 0 conflicting, hashes agree; median lock latency 1,034 ms (load 150: upper bound). F22 seen working: the fold round rebuilt certificates from 5 of 6 to 6 of 6 voters on 5 / 4 / 2 checkpoints per node ("every voter signed") | PASS | rt/logs/fa_s3.log, rt/logs/fa_s3/n*/node.log |
|
||||
| 14 | Finality s2: Sybil dust, 200 x 4 and 200 x 6 blocks from one process, 3 honest voters (v3) | dust keys zero weight and not voters; above-dust weight = blocks; total = voters' blue blocks; aggregator draw by weight | 22 dust keys seen, none a voter; total weight 36 = sum of the 4 voters' blocks; the Sybil minted only 4 above-dust keys in the 105 s quick scale, under the 9 needed to observe the sortition, so the F17 half was not exercised here (the 4 October F17 re-run stands) | PASS (weights); sortition not exercised at this scale | rt/logs/fa_s2.log |
|
||||
| 15 | Finality s1: two keys equivocating at every index, 4 honest voters, 3 nodes, 105 s (v3; F23 probe) | both keys stripped on every node; 0 conflicting certs; locks continue; honest nodes accept each other's certificates | stripped keys at the end 2 / 0 / 0: n0 (the equivocators' RPC node) re-detected at every index (46 detections) and kept the ban until DAA 726; n1 and n2 saw evidence only at indices 1 to 3 (8 detections, ban until DAA 239) and let it expire. Even the first detection stamped two different `until` on n0 (174, then 176) against 176 on n1/n2. From index 9 the voter lists differed by two keys: n0 refused 9 certificates "names 6 voters, this node counts 4", n1/n2 refused 3 and 4 "names 4 voters, this node counts 6". 0 CONFLICTING, 15 / 15 / 15 locks, hashes agree, because every node could aggregate its own certificate from the votes it held | FAIL on the harness criterion; F23 reproduced on the stock scenario, no new id (ledger F23 updated) | rt/logs/fa_s1.log, rt/logs/fa_s1/n*/node.log |
|
||||
| 16 | Finality s4: a 40% block producer that strips the finality section from its coinbase, 2 nodes (v3) | node B's participation and locks unaffected, delay measured | node B locked 14 with the dropper vs a control run; median lock latency 1,032 vs 1,032 ms, 0 ms added. Side note: 38 of 257 dropper blocks were rejected by the node (the strip tool predates the record section that proving v0 put before the finality section; tooling, F25) | PASS | rt/logs/fa_s4.log |
|
||||
| 17 | Finality s5: one of six voters pulsing 10x for 20 s of every 120 s, 126 s (v3) | weight share = block share over the window (no retarget amplification); cannot lock alone | 0 locks with under 2 votes, 0 conflicting certs; the burster found 281 blocks (31% of the run) but held 15.8% of the 120-DAA window at the end (ratio 0.51): at fast time the window is shorter than the run, so the harness compares a window share with a run share and reports a false amplification the other way; the 4 October full-window run gave 0.999 | PASS (lock-alone); ratio criterion invalid at fast time, F25 | rt/logs/fa_s5.log |
|
||||
| 18 | Finality s6: 3/3 and 4/2 partitions over a cut proxy, 147 s warm-up (window full, 6 voters), 53 s split, 53 s heal (v3; F21 probe) | harness: 0 new locks either side in 3/3, 0 conflicting certificates; 4/2: the 4 side locks, the 2 side does not. Rule v3: no side under two thirds of the frozen table locks until one window of its own DAA has passed without a certified checkpoint | 3/3: 10 new locks during the split, first at 36 s (side 0) and 49 s (side 1); after the heal 8 and 0 conflicting certificates, locks resumed. 4/2: the 4 side's first solo lock at 24 s (its 4 keys hold 4/6 of the frozen table, which is two thirds, allowed); the 2 side locked 4 checkpoints near the end of the split with a table of its own 2 keys (total 120, the other four aged out); 1 and 4 conflicting certificates after the heal. The old bound W/(3R) is about 13 s at these rates; the first solo locks came at 36 and 49 s, which is W minus the age of the last common lock at the side's own DAA rate, as the v3 rule states (`frozen_table` returns None once C_i is a window past the last lock). The fork the heal leaves (conflicting certificates on both sides) is the conceded F21 residual | FAIL on the harness criterion, which asserts more than the rule promises at fast time; node follows the v3 rule; no new id, F21 updated with the numbers | rt/logs/fa_s6.log, rt/logs/fa_s6/results.json |
|
||||
| 19 | 34% miner withholding votes: one producer at share 0.34 that never signs, five voters at 0.132 each, 2 nodes, 300 s (v3) | finality pauses (signing weight under two thirds of total), never forks: 0 conflicting certificates, every lock at or above 2/3 of total, nodes agree | the silent key found 627 of 1,864 blocks (33.6%) but, mining at 2 blocks/s on one node, lost more siblings to red than the voters did, so it held 27 to 33% of the 120-DAA window: 30 of 61 checkpoints locked, 31 stalled; every lock had 66.7 to 74.8% of total signed (median 69.2%); 0 conflicting certificates; both nodes hold the same 30 locks. The node reports the stalled indices as proposed, not paused (the active denominator is the sum of present voters, 88 of 120 here, and "finality active" stays true while any lock is within the presence window) | PASS (pause, not fork; the rule holds at the boundary) | rt/logs/rt_withhold34.log, rt/logs/rt_withhold34/n0/node.log |
|
||||
| 20 | 50/50 partition longer than the old W/(3R) bound, then heal: 3/3 keys over a cut proxy, 200 s warm-up (last common lock 38 at DAA 1,140), 105 s split, 160 s heal (v3; F21) | the script's expectation, taken from the task: 0 locks either side during the split, 0 disagreeing locked indices, 0 conflicting certificates. The v3 rule's own promise: no side locks while the last certified checkpoint on its chain is under one weight window old | both sides locked checkpoint 42 at 29 s (blue score 1,260 = 1,140 + 120, the first checkpoint exactly one window past lock 38; the node logs "no frozen table (no lock on this chain inside the window)" and locks on the sliding table at 81.7% and 82.6% of total, 100% of active); 23 new locks during the split; after the heal 24 disagreeing locked indices and 24 / 24 conflicting certificates; locks resumed on both. At these rates (about 4 blue blocks per second per side) the old bound W/(3R) is 10 s, so the split was ten times the old bound and 3.6 times the frozen window; the frozen table protected checkpoints 39 to 41 only (4 checkpoints = one window at a 30-blue interval) | FAIL on the task's expectation (the rule does not promise it past one window); node follows the v3 rule exactly; the F21 residual fork is unchanged in size (24 indices here, 23 on the 4 October 6A long heal). Ledger F21 updated | rt/logs/rt_part5050.log, rt/logs/rt_part5050/n*/node.log |
|
||||
| 21 | F23 custom: one key equivocating for 40 s then stopping, 5 honest voters on 3 nodes, 360 s so the 120-DAA ban expires and the key's blocks age out (v3) | ban expiry identical on honest nodes; 0 voter-count refusals; 0 CONFLICTING; 0 disagreeing locks | the RPC node (n0) stamped every detection 1 to 2 DAA earlier than n1/n2 (index 1: until 178 then 180 on n0, 180 on n1/n2; index 7: 358/363/369 on n0, 363/369 on the others), the node-local stamp of F23 measured directly. No refusal followed: the burst stopped at index 7 and the key's 38 blocks left the window before any ban expired, so the lists never differed at a checkpoint that was still being evaluated. 60 / 60 / 60 locks, hashes agree. The refusals appear when equivocation continues (row 15) | PASS on this script's criterion; the stamp divergence is F23's mechanism, reproduced; the refusals are in row 15 | rt/logs/rt_f23.log, rt/logs/rt_f23/n*/node.log |
|
||||
| 22 | F24 custom as run: 4/2 keys over a cut proxy, 160 s warm-up, 150 s cut, 200 s heal (v3) | the losing node re-determines the moved indices and accepts the network's certificates; 0 false CONFLICTING without equivocation | the cut was 630 DAA on the majority side against a merge depth of 60 DAA, so the heal was not a reorg: both nodes rejected every block of the other side's chain past the cut ("PoW rejected", 2,970 and 2,282 lines), each finalised its own chain (n0 71 locks, n1 45; 20 disagreeing locked indices, 8 and 16 CONFLICTING, 0 equivocation). n1's three refusals "certificate at index 30..32 is for X, this node's checkpoint is Y" are F24-shaped but confounded by the permanent split. This is the F21 beyond-merge-depth case, not an F24 reproduction; re-run under merge depth as row 23 | not a valid F24 test (the cut was too long); the split itself behaved as F21 states | rt/logs/rt_f24.log, rt/logs/rt_f24/n*/node.log |
|
||||
| 23 | F24 custom, second attempt: 4/2 keys, 160 s warm-up, 24 s cut, 150 s heal (v3) | the losing node re-determines the indices the reorg moved and accepts the network's certificates; 0 false CONFLICTING, 0 stuck indices | n1 determined 30 and 31 on its own chain during the cut; but the majority side made about 100 DAA in those 24 s against a merge depth of 60, so the heal was again a permanent split (PoW-rejected 1,626 / 2,009, sinks differ, 9 disagreeing locked indices, 4 / 5 CONFLICTING, 0 equivocation). What the losing node shows is F24-shaped: indices 30 and 31 stay "determined" for ever while the other node locks them under other hashes, and it refused the three certificates for 30 to 32 with "this node's checkpoint is ..."; but with no merge it cannot be told from F21. With a 4/2 split the minority needs 50 of its own blue blocks to determine one checkpoint, in which time the majority makes 100 DAA, so at the 60x profile a 4/2 reorg deep enough to move a determination is always beyond merge depth; a third attempt uses a 3/3 split (row 23b) | not a valid F24 test (merge depth); suggestive, not conclusive | rt/logs/rt_f24b.log, rt/logs/rt_f24b/n*/node.log |
|
||||
| 23b | F24 custom, third attempt: 3/3 keys over a cut proxy, 160 s warm-up, 16 s cut, 150 s heal, quiet machine (v3) | after a reorg deeper than `checkpoint_depth` the node re-determines the moved index and the network locks it; 0 CONFLICTING | the cut stayed under merge depth: the chains merged (sinks equal, 0 MissingParents, 64 / 64 locks, 0 disagreeing locked indices, 0 CONFLICTING, 0 equivocation). Index 33 was determined during the cut on each side's own chain (n0 `b8629574`, n1 `5cb74f77`), the heal reorged one side past it, and neither node ever revisited it: both still hold their own determination, no certificate ever formed for either block (the votes split 3/3), and finality skipped 33 and locked 34 onwards. That is F24's mechanism measured on the merged network: a determination the reorg moved is never re-made. The round-4 shape (false CONFLICTING) needs the majority's certificate for the index to reach the losing node, which a 3/3 split cannot form; here the symmetric form gives a permanent one-index liveness hole on every node | FAIL (the script passed by its own narrower check; the hole is the defect); F24 reproduced, no new id, ledger F24 updated | rt/logs/rt_f24c.log, rt/logs/rt_f24c/n*/node.log |
|
||||
| 24 | Timestamp forging against difficulty rule v2 (simulator `sim/difficulty/attacks/attacks.py --scenario ts --ts-rules tight`, rules igneum-v2 / igneum / kaspa, seeds 7 to 9, forger at 30% and 50% stamping latest, earliest, alternating, 6 h runs) | block rate after 1 h of forging within a few percent of target; no difficulty excursion; no forger advantage | igneum-v2: rate after forging 0.992 to 1.005 of target (drift -0.8% to +0.5%, worst seed -1.5% to +0.9%), difficulty ratio 1.001 to 1.013, worst gap 9.5 to 10.3 s, forger advantage 0.000 in every cell; igneum (v1) +0.4% to +1.1% (worst +2.7%); kaspa 0.0% to +0.9%. The v2 reference lane (newest 600 blocks of the epoch) changes nothing the forger can use | PASS | rt/logs/diff_ts_v2.log |
|
||||
| 25 | Difficulty v2 timestamp forging on a live 3-node network (`sim/difficulty/attacks/testnet.py`, real CPU miners, `IGNEUM_ATTACK_TS_OFFSET_MS` on the forger's node) | the 4 October fixed-rule result (flat within CPU noise) on the 0.3.4 binary | NOT RUN: two 15-minute runs of 4-thread CPU miners under the single run lock on a machine at load 100 to 180 would give hash-rate-bound numbers, not rule numbers; the simulator row above covers the rule, and the 0.3.4 binary carries the `IGNEUM_ATTACK_TS_OFFSET_MS` hook (strings confirm it) for a quiet-machine re-run | not run, stated | sim/difficulty/attacks/testnet.py |
|
||||
| 26 | `sim/finality_v2.py` scenarios E (partition) and L (L1 silent weight, L2 churn, L3 eclipse, L4 long partition with view-local weight) at the 2/3 floor | the 4 October `sim/results_v2.md` figures; L4 shows the pre-v3 bound (the model has no frozen table: `grep frozen sim/finality_v2.py` finds only the participation ring) | NOT RUN tonight: the first attempt failed on `numpy` missing from the system python (`rt/logs/sim_fin_EL_quick.log`); the re-queue under the conda python was withdrawn to shorten the shared run queue at the coordinator's request. Nothing in it tests the 0.3.4 node: the simulator models rule v2 and cannot see F21's frozen table, so its L4 numbers (50/50 locks alone from day 10, `results_v2.md` L4) stand as the pre-fix model and rows 18 and 20 are the measurement of the fix on the node | not run, stated | sim/results_v2.md L4 |
|
||||
| 27 | Execution-layer suite as shipped (`tools/exec-attacks/run_all.sh`: malformed txs, nonce games, RPC fuzz, pgas, reorgs, registry) on a `--simnet` network of the 0.3.4 node | the 3 October 98-check pass | the suite's stub-engine miner flags are not in the 0.3.4 miner (first attempt: miner usage text, 0 blocks); with the miner swapped for `vmine`, every template was refused: "Coinbase payload is above max length (204)", 0 blocks on all three nodes, every scenario "node not producing blocks". The 0.3.4 coinbase (key reveal, record section, finality section) does not fit Kaspa's 204-byte limit, which only `DEVNET_PARAMS` raised | FAIL: new ledger M31; the suite re-ran with an override (row 28) | rt/logs/exec_run_all_b.log, rt/logs/exec_b/miner_node1.log |
|
||||
| 28 | Execution-layer suite re-run on the 0.3.4 node with `{"max_coinbase_payload_len": 16384, "skip_proof_of_work": true}` and `vmine` producers (scenarios 1, 2, 5, 3, 6, 4; `tools/exec-attacks/run_all.sh`; quiet machine, load 1.6 to 2) | the 3 October 98-check pass, with the 4 October F-exec-A/B rules | scenario 1 malformed txs 30 / 30; 2 nonce games 9 / 9; 5 RPC fuzz 5 / 5; 6 reorgs 30 / 30 (max depth 12, state roots agree); 4 registry 19 / 19; 3 pgas 3 / 4: the three loops under budget executed at 3.45 M, 10.33 M and 20.64 M pgas (1.2, 3.2, 6.0 ms), the three over budget were refused at the mempool with "proving gas above the block proving limit: at least 29,998,593 pgas metered before the abort, limit 30,000,000", which is the F-exec-B rule of 4 October; the one failed check waits for the pre-fix outcome (included and skipped with `BlockProvingBudget`). 96 of 97 checks pass; the 97th asserts the old rule. No executed block carried more than B_p | PASS on the node; one stale harness criterion (F25) | rt/logs/exec_run_all_c.log, rt/exec/results/scenario*.json |
|
||||
| 29 | EVM smoke (`tools/evm-smoke/smoke.mjs`, viem) against the three 0.3.4 simnet nodes of row 28, 21 s | the 4 October 84-of-85 run: chain id, funding, transfers across blocks and nodes, duplicates, deploy, call, receipts, logs, revert, developer share, state roots identical | 83 of 87 checks: chain id 4463, 124 segments, state roots identical across the three nodes, deploy, call, logs, revert and the developer share all pass. The four failed checks are the balances of accounts B and C (60.8 and 48.1 IGN where the smoke expects 5): the exec-attack network pays those two accounts as its miners 2 and 3 (its README says so), so they hold mining rewards; a fixture clash from pairing the two tools on one network, not a node result | PASS (83 of 83 checks the node can be held to) | rt/logs/evm_smoke_c.log, rt/logs/evm_smoke-results_c.json |
|
||||
|
||||
## Fails
|
||||
|
||||
Every fail has a ledger entry in `docs/fud-ledger.md` (new ids F25, M30, M31; F21, F23 updated with the measured
|
||||
numbers). Severity and the smallest fix are in the ledger; one line each here.
|
||||
|
||||
| Ledger | What broke | Severity | Smallest fix |
|
||||
|---|---|---|---|
|
||||
| M31 (new) | The 0.3.4 node cannot produce a block template on mainnet, testnet or simnet parameters: the coinbase with the key reveal, the record section and the finality section does not fit Kaspa's 204-byte `max_coinbase_payload_len`, which only `DEVNET_PARAMS` raised to 16,384 (row 27) | serious for every network that is not the devnet; nothing on the live devnet | raise the three other networks to `MAX_COINBASE_PAYLOAD_LEN_WITH_FINALITY`; a unit test that builds the largest coinbase and checks every network's limit |
|
||||
| M30 (new) | A block or transaction flood grows the 0.3.4 node by 269 to 780 MB in 60 s where the 3 October node grew 11 to 30 MB; the harness bound of baseline + 512 MB hides it (rows 5 and 8) | serious: the devnet's own 50x event against a 2 to 4 GB cloud node | bound `ExecState.records` and its indexes to the record window plus the pruning depth; drop a rejected transaction's bytes at rejection; re-run s6 and s7 and require growth under 50 MB |
|
||||
| F23 (updated) | Reproduced on the stock equivocation scenario: honest nodes stamp the ban at different DAA scores, their voter lists diverge, and they refuse each other's certificates, 9 and 3 and 4 times in 105 s (row 15) | serious (unchanged) | stamp every ban with the carrying block's DAA; act on RPC-seen evidence only once carried |
|
||||
| F21 (updated) | The frozen table holds for one weight window of the side's own DAA after the last common lock, then the sliding table locks the side alone; measured 36 and 49 s against the old 13 s at fast time; the heal leaves the fork (rows 18 and 20) | conceded, stated; bound tripled, not removed | none proposed beyond the ledger's gate-3 option |
|
||||
| F25 (new) | Both fast-time harnesses corrupt the `u64::MAX` sentinels of the override file through a JavaScript round-trip, so no fast-time attack run starts a node; the ordering harness's timestamp probe and the pulse ratio test assert the pre-fix rules (rows 6, 16, 17 and the first run) | tooling, low | splice fields into the file text or drop the sentinels before `stringify`; update the scenario 2 probe to the 10 s rules and the s5 ratio to a full-window comparison |
|
||||
| F24 (updated, reproduced) | Row 23b: a 3/3 split cut for 16 s (under merge depth), clean merge; index 33, determined on each side's own chain, was never re-determined on either node after the reorg and never locked: a permanent one-index hole on every node, finality continued from 34. Rows 22 and 23 (4/2 splits) overshot merge depth and show the same stuck indices plus the refusals, confounded with F21 | serious (unchanged) | as in the ledger: re-determine an unlocked index when the virtual's chain at that blue score changes |
|
||||
|
||||
## Summary
|
||||
|
||||
| | Count |
|
||||
|---|---|
|
||||
| Scenarios in the table | 30 (rows 1 to 29, with 23b) |
|
||||
| Ran against the 0.3.4 node, pass | 19 |
|
||||
| Ran, fail or fail-shaped | 8 (rows 5, 8 memory; 15 F23; 18, 20 F21 bound; 22, 23, 23b F24; 27 M31) |
|
||||
| Harness criterion stale, node right | 4 (rows 6, 16, 17, 28: all F25) |
|
||||
| Not run, stated | 3 (rows 11, 25, 26) |
|
||||
| New ledger ids | F25 (tooling), M30 (memory under flood), M31 (coinbase limit off the devnet) |
|
||||
| Ledger ids updated with measurements | F21, F23, F24 |
|
||||
|
||||
What the night says about the build that ships: the finality rule v3 does what F21 and F22 claim (the frozen table
|
||||
holds for one window, the fold round lifts certificates to every voter), the votes, aggregators, Sybil weights,
|
||||
vote-dropping producers, malformed votes and records, the proof pool under a garbage flood, the ordering layer under
|
||||
withholding, partitions, eclipses, floods and malformed inputs, difficulty v2 under timestamp forging, and the
|
||||
execution layer under its own suite all hold. What does not hold: a node on mainnet, testnet or simnet parameters
|
||||
cannot make a block (M31); a one-minute flood grows the node by hundreds of megabytes (M30); the two round-4 defects
|
||||
F23 and F24 are real on this build and unchanged by v3.
|
||||
|
||||
Not done: the proving-v0 network loop and the live difficulty-v2 forger network (rows 11, 25); the ordering harness's
|
||||
full-length withholding case (45% share, release every 20, the 3 October FAIL) and its finality sub-scenarios, which
|
||||
are stubs there and were run from the finality harness instead; a quiet-machine repeat of every latency number.
|
||||
106
tools/finality-attacks/redteam/flood.mjs
Normal file
106
tools/finality-attacks/redteam/flood.mjs
Normal file
|
|
@ -0,0 +1,106 @@
|
|||
// Red-team: flood of invalid proof records against the proof pool of the finality-fixes build (proving v0 active).
|
||||
// One redteam node (eth RPC), 3 vmine voters to reach activation and assign shards, then a flood of well-formed-length
|
||||
// garbage records through igneum_submitProofRecord. Measures reject throughput and node CPU per rejected record.
|
||||
import { spawn, spawnSync } from 'node:child_process';
|
||||
import { mkdirSync, rmSync, openSync, readFileSync, writeFileSync, existsSync } from 'node:fs';
|
||||
import { createHash, randomBytes } from 'node:crypto';
|
||||
import { connectRpc } from '../lib/rpc.mjs';
|
||||
|
||||
const ROOT = '/Users/joshm/Projects/igneum/';
|
||||
const IGNEUMD = `${ROOT}vendor/igneum-node-redteam/target/release/igneumd`;
|
||||
const MINER = `${ROOT}vendor/igneum-node-fin-attacks/target/release/igneum-miner`;
|
||||
const OVERRIDE = '/tmp/igneum-redteam-override-prove-v3.json';
|
||||
const TMP = '/tmp/igneum-redteam-flood';
|
||||
const BASE = 29680, SUFFIX = 968;
|
||||
const RECLEN = 2 + 32 + 8 + 4 + 48 + 20 + 32 + 32 + 96; // 274
|
||||
const log = (...a) => console.log(new Date().toISOString().slice(11, 23), ...a);
|
||||
const sleep = (ms) => new Promise(r => setTimeout(r, ms));
|
||||
const started = [];
|
||||
for (const b of [IGNEUMD, MINER]) if (!existsSync(b)) { console.error(`missing ${b}`); process.exit(2); }
|
||||
rmSync(TMP, { recursive: true, force: true }); mkdirSync(TMP, { recursive: true });
|
||||
|
||||
class Node {
|
||||
constructor(i, connect = []) { this.i = i; this.grpc = BASE + i * 10; this.p2p = BASE + i * 10 + 1; this.json = BASE + i * 10 + 2; this.evm = BASE + i * 10 + 3; this.connect = connect; this.dir = `${TMP}/n${i}`; this.logFile = `${this.dir}/node.log`; }
|
||||
async start() {
|
||||
mkdirSync(this.dir, { recursive: true });
|
||||
const a = ['--devnet', `--devnet-suffix=${SUFFIX}`, '--nodnsseed', '--disable-upnp', '--nologfiles', '--enable-unsynced-mining', '--utxoindex', '--unsaferpc',
|
||||
`--appdir=${this.dir}`, `--rpclisten=127.0.0.1:${this.grpc}`, `--rpclisten-json=127.0.0.1:${this.json}`, `--evm-rpclisten=127.0.0.1:${this.evm}`,
|
||||
`--listen=127.0.0.1:${this.p2p}`, `--override-params-file=${OVERRIDE}`, '--loglevel=info', '--yes'];
|
||||
if (this.connect.length) a.push(...this.connect.map(c => `--connect=${c}`)); else a.push('--outpeers=0');
|
||||
const out = openSync(this.logFile, 'a');
|
||||
this.proc = spawn(IGNEUMD, a, { stdio: ['ignore', out, out] }); started.push(this.proc);
|
||||
await sleep(900); this.rpc = await connectRpc(`ws://127.0.0.1:${this.json}`);
|
||||
log(`n${this.i} up pid ${this.proc.pid} evm ${this.evm}`); return this;
|
||||
}
|
||||
async eth(method, params = []) {
|
||||
const body = JSON.stringify({ jsonrpc: '2.0', id: 1, method, params });
|
||||
const r = await fetch(`http://127.0.0.1:${this.evm}`, { method: 'POST', headers: { 'content-type': 'application/json' }, body });
|
||||
const j = await r.json(); if (j.error) throw new Error(`${method}: ${JSON.stringify(j.error)}`); return j.result;
|
||||
}
|
||||
}
|
||||
function miner(node, label) {
|
||||
const a = ['vmine', `grpc://127.0.0.1:${node.grpc}`, '600', '--label', label, '--share', String(1 / 3), '--bps', '1'];
|
||||
const out = openSync(`${TMP}/miner-${label}.log`, 'a'); const p = spawn(MINER, a, { stdio: ['ignore', out, out] }); started.push(p); return p;
|
||||
}
|
||||
function cpuOf(pid) { try { return parseFloat(spawnSync('ps', ['-o', '%cpu=,time=', '-p', String(pid)], { encoding: 'utf8' }).stdout.trim().split(/\s+/)[0]) || 0; } catch { return 0; } }
|
||||
function cpuSecs(pid) { try { const t = spawnSync('ps', ['-o', 'time=', '-p', String(pid)], { encoding: 'utf8' }).stdout.trim(); const m = t.match(/(?:(\d+)-)?(\d+):(\d+):(\d+)|(\d+):(\d+)\.(\d+)/); if (!m) return 0; if (m[2] != null) return (+(m[1]||0))*86400 + (+m[2])*3600 + (+m[3])*60 + (+m[4]); return (+m[5])*60 + (+m[6]) + (+('0.'+m[7])); } catch { return 0; } }
|
||||
|
||||
// Build a well-formed-length record with controllable version and a matching/mismatching proof_hash.
|
||||
function craftRecord({ version = 1, proofMatches = false } = {}) {
|
||||
const proof = randomBytes(256);
|
||||
const rec = Buffer.alloc(RECLEN);
|
||||
let o = 0;
|
||||
rec.writeUInt16LE(version & 0xffff, o); o += 2; // version
|
||||
randomBytes(32).copy(rec, o); o += 32; // block
|
||||
rec.writeBigUInt64LE(BigInt(1 + Math.floor(Math.random() * 1000)), o); o += 8; // number
|
||||
rec.writeUInt32LE(0, o); o += 4; // shard
|
||||
randomBytes(48).copy(rec, o); o += 48; // pubkey
|
||||
randomBytes(20).copy(rec, o); o += 20; // payout
|
||||
randomBytes(32).copy(rec, o); o += 32; // statement
|
||||
const ph = proofMatches ? createHash('sha256').update(proof).digest() : randomBytes(32);
|
||||
ph.copy(rec, o); o += 32; // proof_hash
|
||||
randomBytes(96).copy(rec, o); o += 96; // signature
|
||||
return { record: '0x' + rec.toString('hex'), proof: '0x' + proof.toString('hex') };
|
||||
}
|
||||
|
||||
const out = { cases: [] };
|
||||
try {
|
||||
const n0 = await new Node(0).start();
|
||||
['v0', 'v1', 'v2'].forEach(l => miner(n0, l));
|
||||
const status0 = await n0.eth('igneum_getProvingStatus');
|
||||
log(`proving status: activationDaa=${parseInt(status0.activationDaa,16)} verifier=${status0.verifier}`);
|
||||
// wait for activation + a few assigned shards
|
||||
let daa = 0, waited = 0;
|
||||
while (daa < 55 && waited < 180) { await sleep(2000); waited += 2; const s = await n0.eth('igneum_getProvingStatus').catch(() => null); if (s) daa = parseInt(s.tipDaa, 16); if (waited % 10 === 0) log(`daa ${daa}`); }
|
||||
log(`reached daa ${daa}`);
|
||||
|
||||
async function floodCase(name, opts, n) {
|
||||
const c0 = cpuSecs(n0.proc.pid); const t0 = Date.now();
|
||||
let accepted = 0, rejected = 0; const reasons = {};
|
||||
for (let k = 0; k < n; k++) {
|
||||
const { record, proof } = craftRecord(opts);
|
||||
try { const r = await n0.eth('igneum_submitProofRecord', [{ record, proof }]); if (r.accepted) accepted++; else { rejected++; reasons[r.reason] = (reasons[r.reason] || 0) + 1; } }
|
||||
catch (e) { rejected++; const m = String(e.message).slice(0, 60); reasons[m] = (reasons[m] || 0) + 1; }
|
||||
}
|
||||
const wall = (Date.now() - t0) / 1000; const c1 = cpuSecs(n0.proc.pid);
|
||||
const row = { case: name, submitted: n, accepted, rejected, wallSecs: +wall.toFixed(2), rate: +(n / wall).toFixed(1), nodeCpuSecs: +(c1 - c0).toFixed(2), cpuMsPerRecord: +(((c1 - c0) * 1000) / n).toFixed(3), reasons };
|
||||
out.cases.push(row); log(`CASE ${name}: ${JSON.stringify(row)}`);
|
||||
}
|
||||
await floodCase('proof_hash-mismatch (cheapest)', { proofMatches: false, version: 1 }, 2000);
|
||||
await floodCase('bad-version (passes proof_hash)', { proofMatches: true, version: 0xbbbb }, 2000);
|
||||
await floodCase('v1-garbage (reaches record lookup)', { proofMatches: true, version: 1 }, 2000);
|
||||
|
||||
const up = await n0.eth('eth_blockNumber').catch(() => null);
|
||||
const status1 = await n0.eth('igneum_getProvingStatus').catch(() => null);
|
||||
out.nodeUpAfter = up != null;
|
||||
out.poolAfter = status1 && status1.pool;
|
||||
log(`node up after flood: ${out.nodeUpAfter}; pool ${JSON.stringify(out.poolAfter)}`);
|
||||
out.ok = out.nodeUpAfter && out.cases.every(c => c.accepted === 0);
|
||||
} catch (e) { out.error = e.message; log(`FAILED: ${e.message}`); }
|
||||
finally {
|
||||
writeFileSync(`${TMP}/flood.json`, JSON.stringify(out, null, 2));
|
||||
console.log(JSON.stringify(out, null, 2));
|
||||
for (const p of started.reverse()) { try { p.kill('SIGINT'); } catch {} }
|
||||
await sleep(1500); for (const p of started) { try { p.kill('SIGKILL'); } catch {} }
|
||||
process.exit(out.ok ? 0 : 1);
|
||||
}
|
||||
1
tools/finality-attacks/redteam/override-60x-v3.json
Normal file
1
tools/finality-attacks/redteam/override-60x-v3.json
Normal file
|
|
@ -0,0 +1 @@
|
|||
{"timestamp_deviation_tolerance": 132, "past_median_time_window_size": 27, "difficulty_window_size": 661, "min_difficulty_window_size": 150, "difficulty_rule": "igneum-dual", "coinbase_payload_script_public_key_max_len": 150, "max_coinbase_payload_len": 16384, "max_tx_inputs": 1000, "max_tx_outputs": 1000, "max_signature_script_len": 250000, "max_script_public_key_len": 10000, "mass_per_tx_byte": 1, "mass_per_script_pub_key_byte": 10, "mass_per_sig_op": 1000, "block_mass_limits": {"compute": 500000, "storage": 500000, "transient": 1000000}, "block_lane_limits": {"lanes_per_block": 50, "gas_per_lane": 1000000000}, "storage_mass_parameter": 1000000000000, "deflationary_phase_daa_score": 0, "pre_deflationary_phase_base_subsidy": 50000000000, "skip_proof_of_work": false, "max_block_level": 250, "pruning_proof_m": 1000, "blockrate": {"target_time_per_block": 1000, "ghostdag_k": 18, "past_median_time_sample_rate": 10, "difficulty_sample_rate": 4, "max_block_parents": 10, "mergeset_size_limit": 180, "merge_depth": 60, "finality_depth": 720, "pruning_depth": 13838, "coinbase_maturity": 2}, "pre_crescendo_target_time_per_block": 1000, "crescendo_activation": 0, "genesis_bits": 487587840, "finality": {"checkpoint_interval": 30, "checkpoint_depth": 20, "weight_window": 120, "dust": 5, "presence_window": 1, "aggregators": 8, "equivocation_ban": 120, "min_daa": 120, "aggregator_fallback": 1}, "pow_epoch_blocks": 60, "pow_epoch_lead": 10, "pow_day_ms": 1440000, "finality_v3_activation_daa": 0}
|
||||
|
|
@ -0,0 +1 @@
|
|||
{"max_coinbase_payload_len": 16384, "skip_proof_of_work": true}
|
||||
Loading…
Reference in a new issue