diff --git a/docs/fud-fixes.md b/docs/fud-fixes.md index ab45fa0e0..beec6f06c 100644 --- a/docs/fud-fixes.md +++ b/docs/fud-fixes.md @@ -248,6 +248,26 @@ Checked against the files this evening. "present" means the quoted text is still | 77 | HP and LP | present | no contact route anywhere | | 78 | LP not claimed | missing | the first of the three items needs the cache result, not "can be shortcut 110x" | +### 2.4 Round 4 entries (4 October 2026, afternoon) + +Added after `docs/review/round-4-2026-10-04.md`. Rows continue the numbering of section 2. Effort is the review's estimate in hours. + +| # | Ledger | Issue | Fix | Who | When | EXPOSES | +|---|---|---|---|---|---|---| +| 104 | X23 | Either relay secret queues administrator PowerShell on the PCs; the intake key is the relay key, is in 6 tracked files and ships in every package; the relay-clients zip with both secrets is hosted behind the dl token | Zip off the host; rotate the relay token; a relay key of its own; a second per-machine secret or a signature for `run`; rotate the intake key and repackage (2 h) | Josh (rotation), relay owner | now | EXPOSES: the PCs are the founder's own machines | +| 105 | G13 | The OTA manifest is signed over an installer whose node and worker binaries arrived unsigned from the dl host; signing is automatic from the latest green run | Sign `payload-inputs.zip` on the Mac, verify in CI; `OTA_SKIP=1` by default, the signing step names the run (2 h) | release engineer | now | no | +| 106 | G12, X18 | `IGNEUM_POW_*` sets the schedule on every network; no params digest or genesis hash in the handshake; a finality mismatch is a WARN; `rollout-v2.sh` drops the finality block | Delete the fallback; digest plus genesis hash in the version message, refused on mismatch; the WARN becomes a refusal (4 h) | consensus engineer (code) | before testnet | no | +| 107 | F23, F24 | Node-local equivocation ban time; checkpoint determination never revisited | Carrier-DAA bans; re-determine on a reorg deeper than d; two-node tests (4 h) | consensus engineer, cryptographer (code) | before testnet | no | +| 108 | M26, M27, X21 | Frozen fault-guard baseline loops; no prepare rate limit; mismatches never acted on and invisible in the app | Baseline updated on a trip; one prepare per pair per epoch; stop at 3 mismatches, shown on the card (3 h) | miner-community-lead, app owner (code) | now | no | +| 109 | E16 | The 20% pool is an OP_RETURN on the live chain; LP 396 and 414, HP 306 and 446 say it pays provers | "Burned until the payout ships; no prover is paid today" in both places; burned-so-far on the live page | Claude | now | no | +| 110 | L9 | HP "100% to miners and provers", "0% anyone else"; no devnet-value statement beside Get the miner or on the live page | The disclaimer beside the button and on /live; the tiles match the LP dev-fee sentence | Claude | now | no | +| 111 | M29 | LP 424 describes earnings in currency, a hardware wallet and proving the app does not have | Rewrite to 0.3.3; earnings, currency and the hardware wallet become a roadmap sentence | Claude | now | no | +| 112 | F21 | LP 511 says a third of the blocks is needed to split finality in a partition | The round-4 section 1 (b) sentence | Claude | now | no | +| 113 | X24, X25, X26, X27 | Token in the URL path and printed; agent self-installs at every start; permanent feed with the dl token in bodies; free-text `from` and no clean rotation | Header token in every client, HSTS, masked prints; arm only on `reboot_continue`; retention and a cap; sender binding; documented rotation (3 h) | relay owner | now | no | +| 114 | G14 | The intake key (8 commits), the dl token (1), the review files, 51 files with the first name, `+0100` stamps | Section 5 step 4's rewrite list extended; `TZ=UTC` now | Josh, Claude | before public repo | EXPOSES: yes, the whole row | +| 115 | X19, X20, M25, M28, X22, X28, X29, E17 | The minors of round 4 (node knobs and silences, cold-sync cost, miner day length, kernel commitment, restart paths, relay hygiene, host and file modes, unlogged economics inputs) | As each ledger entry says; the stray token-named file and the 0644 modes today | consensus engineer, miner-community-lead, relay owner, Claude | when convenient | no | +| 116 | X30 | Bench page private strings; /api/live addresses, key hashes, payout addresses; the mobile menu | Fixed 4 October 2026: `ac89a37`, `6b644a6`, `2d8f09c`, `621f5cc` | Claude | done | no | + ## 4. What the 3 October decisions close or change Closed: diff --git a/docs/fud-ledger.md b/docs/fud-ledger.md index b98599acd..40fc64c09 100644 --- a/docs/fud-ledger.md +++ b/docs/fud-ledger.md @@ -1587,3 +1587,249 @@ Status: Conceded, contained by rule, review scheduled. Answer: Correct. Design 6 says it in those words. The containment: a job output cannot mint IGN or touch system contracts (R12), jobs are gated behind the proof-system version with the three-month overlap and the 90% signal (design 5.6), and the emergency path for a live soundness bug is the version bump of spec 5.7. The rule for an app, the same one the customer brief gives rollups: anything that acts irreversibly on a job output keeps its own fallback. R12 is reviewed with the cryptographer before devnet v2. Evidence: `docs/design/execution-layer.md` 6, 5.6, 9.1 R12; spec 5.7; ledger P7; `docs/commercial/prover-customer-brief.md` "Risks". + +## Round 4 entries (4 October 2026, afternoon): what is live + +Review: `docs/review/round-4-2026-10-04.md`. Scope: devnet v4 through `a21ff239` (HEAD `3bfe346f`), the prebuilt workers, the Igneum Miner app 0.3.1 to 0.3.3, the relay, the Windows CI, the downloads host, the live site and the economics after the day's measurements. No secret value appears in any entry; comparisons were count-only. + +### X23. One shipped key is an administrator channel to the founder's PCs +"Your relay accepts either the URL token or the `x-igneum-key` header for everything, including queuing PowerShell that the PC agent runs as administrator. The key is the log-intake key, a literal in six tracked files and inside every Windows and prover package you have handed out. And the zip with both relay secrets sits on the downloads host behind the dl token, which is in your git history and in every installed app. One token, four hops, no privilege boundary." + +Status: Open, fatal as an operational fact (4 October 2026). The agent was live and elevated on PC 1 at 13:41 UTC (`GET machines`, read-only). + +Answer: Correct. `relay/lib/relay.mjs:33-42` returns a truthy value for either secret and `relay/api/relay.mjs:111-124` accepts `kind: run` with `flags.elevated` from it; `relay/clients/igneum-agent.ps1:165-166` runs every item returned, as administrator, within 20 s. `README.md:7` and `make-clients.sh:8` make `RELAY_KEY` the intake key. The hosted `igneum-relay-clients.zip` carries the relay token and the key in four files; the dl token that guards it is 0644 on the Mac, in commit `c47ff03`, and in `igneum-app.json` of every install. Fix, in order: the zip off the host; rotate the relay token; a relay key of its own; a second per-machine secret or an Ed25519 signature over `{id, to, body}` for `run`; rotate the intake key and repackage. Review ids R4.4.1, R4.4.2, R4.5.1. + +Evidence: the files above; `docs/review/round-4-2026-10-04.md` sections 4 and 5. Experiment: after the fix, `POST task` with the old key and with a key-only header must return 401, and `GET machines` must show the rotated agent on PC 1. + +### X24. The relay token rides in the URL on every request +"Every poll of every agent and every page refresh puts the token in the path, so it is in Vercel's request logs, in browser history and in every terminal that ran `tools/relay.mjs`. You built an `x-relay-token` header and nobody uses it." + +Status: Open (4 October 2026). + +Answer: Correct. `relay/vercel.json:6` rewrites `/r//api/` to a query string; `igneum-agent.ps1:14`, `send.ps1:26`, `send.sh:11`, `agent.sh:10` and `tools/relay.mjs:24` all build the tokened URL, and `tools/relay.mjs:83,88,125` print it. `Referrer-Policy: no-referrer` and `X-Robots-Tag` are set (`vercel.json:12-13`); there is no HSTS. Fix: the header in every client, the URL token kept for the phone's page only, HSTS, and the print lines masked. Review id R4.4.3. + +Evidence: the files above. Experiment: the Vercel log view for `igneum-relay` after the change shows no token in any path. + +### X25. The PC agent installs itself at every logon, at highest privilege, on every start +"Double-click once and the agent writes a scheduled task with `/RL HIGHEST` and a RunOnce key. Your README says it only re-arms for a reboot. Closing the window does nothing." + +Status: Open (4 October 2026). + +Answer: Correct. `Arm-Restart` (`igneum-agent.ps1:68-79`) runs at `:154` on every start; `README.md:42` describes it as the `reboot_continue` path. Fix: arm only when a task asks for a reboot, and remove the task and the key on a clean exit. Review id R4.4.4. + +Evidence: `relay/clients/igneum-agent.ps1`. Experiment: `schtasks /Query /TN IgneumRelayAgent` on PC 1 before and after. + +### X26. The feed is a permanent transcript, and it holds the dl token by design +"One secret pages the whole history: every task body and every result transcript, usernames, hostnames, the folder that holds the secrets. And `tools/relay.mjs` writes the tokened download URL into task bodies before posting, so a relay leak is a dl-token leak." + +Status: Open (4 October 2026). + +Answer: Correct. `ITEM_COLS` (`relay/lib/relay.mjs:92`) includes `body`; `feed` returns up to 500 per call with no retention and no cap; `delete` leaves blobs. `tools/relay.mjs:76-80` substitutes `__DL_BASE__` with the tokened base and `playbooks/miner-v4.ps1:12` and `prover-setup.ps1:12` print it into the transcript. Today's 12 items hold 0 copies (count-only). Fix: retention (30 days), a cap on `feed`, the dl base passed as an environment value the agent holds rather than text in the body, blob deletion with the row. Review id R4.4.5. + +Evidence: the files above. Experiment: `feed?limit=500&before=` after the change returns nothing older than the retention. + +### X27. The relay has no clean rotation and no sender binding +"Rotate the token and the key path stays open; rotate the key and every shipped package stops uploading logs. Any holder posts a `result` from any machine name, or registers a machine, and the Mac's watch prints it as truth." + +Status: Open (4 October 2026). + +Answer: Correct. `authed()` has two independent secrets with equal power; `insertItem` takes `from` as free text (`relay/api/relay.mjs:30`); `register` creates rows for any hostname (`:148-162`). Fix: the relay's own key (X23), `from` bound to the registered machine for `result` and `register` by a per-machine secret, and a documented rotation (token, relay key, intake key) with what each breaks. Review ids R4.4.6, R4.4.7. + +Evidence: the files above. Experiment: a `result` posted with a `from` that does not match the caller's machine secret is refused. + +### X28. Relay hygiene, minor +"`===` on secrets, no HSTS, a GET that acks, a reboot on any output containing `RELAY-REBOOT`, orphaned blobs, no rate limit anywhere, a WSL user `igneum`/`igneum` with NOPASSWD sudo, the username and secret folder posted on register, and a file in `~/.config/igneum` whose name is a token." + +Status: Open, minor (4 October 2026). + +Answer: Correct on each point: `relay/lib/relay.mjs:38,40`; `relay/vercel.json:8-16`; `relay/api/relay.mjs:85`; `igneum-agent.ps1:133`; `:145`; no limiter in either function; `relay/playbooks/wsl-setup.ps1:39-40` and `prover-setup.ps1:21`; `igneum-agent.ps1:41, :113`; the stray file next to `desec-token` (3 Oct 19:33). Fix when convenient; the stray file today. Review ids R4.4.9 to R4.4.13. + +Evidence: the files above. + +### G12. The PoW schedule comes from the environment on every network, including mainnet +"Your mainnet gate refuses the override file. It does not refuse `IGNEUM_POW_EPOCH_BLOCKS`. A node without a file installs the schedule from the environment and `Params.pow_epoch_blocks` is never consulted." + +Status: Open (4 October 2026). + +Answer: Correct. `daemon.rs:339-340` installs the file's schedule only when the file names one; otherwise `PowSchedule::from_env()` (`consensus/core/src/igneum.rs:137-145`) installs on first read; the difficulty manager takes the global (`services.rs:113`); `daemon.rs:316-319` gates the file only. Fix: delete the environment fallback and install `Params.pow_epoch_blocks` from the network params on every start. Review id R4.1.1. + +Evidence: the files above. Experiment: start a node with `IGNEUM_POW_EPOCH_BLOCKS=60` and no file; its template's `epoch_blocks` must be the network's value. + +### G13. The update signature covers binaries that nobody signed +"The runner fetches `payload-inputs.zip` and its sha256 from the same host, builds the installer, and the Mac signs the manifest over whatever the latest green run produced. A compromised dl project or runner ships as a genuine update." + +Status: Open (4 October 2026). + +Answer: Correct. `.github/workflows/windows.yml` (step "payload inputs") checks the zip against a sha256 served beside it; `packaging/windows/fetch-ci-artifacts.sh` takes the latest green run and calls `packaging/ota/publish-manifest.sh` by default; the node fork is not in the repository the runner builds, so spec 08 item 4 has nothing to reproduce from. Fix: a detached Ed25519 signature over `payload-inputs.zip` made on the Mac and verified in CI before the build; `OTA_SKIP=1` by default with the signing step naming the run id it signs. Review id R4.5.2. + +Evidence: the files above. Experiment: alter one byte of a hosted `payload-inputs.zip` on a test folder and run the workflow; it must fail before the build. + +### G14. Secrets and identity in the history of a repository with a public date +"The intake key is in six files across eight commits, the dl token in one, the review and ledger files are tracked, 51 tracked files carry the founder's first name, and every commit today is stamped `+0100`. The 3 October sweep said zero hits." + +Status: Open (4 October 2026); extends `docs/fud-fixes.md` section 5. + +Answer: Correct, count-only. The key: `packaging/mac/packaged-config.sh`, `infra/gpu-bench/upload.sh`, `proving/windows-wsl2/prove-block.sh`, `prove-shard.sh`, `proto-cuda/windows-miner/upload-log.bat`, `proto-cuda/windows-app/upload-log.bat`, commits `78df757` to `4c9810f`. The token: `docs/plans/morning-2026-10-04.md:49`, commit `c47ff03`. `git check-ignore` returns nothing for the ledger, fixes and review files. The CI identity grep covers the public export list, by design. Fix: both secrets join section 5 step 4's rewrite list (and are rotated regardless); `TZ=UTC` in the commit path now. Review ids R4.4.8, R4.5.3, R4.5.4. + +Evidence: `git ls-files | xargs grep -lF ` counts, `git log -S`. Experiment: section 5 step 7 re-run after the rewrite returns nothing. + +### X18. Two nodes with two override files connect, and only some mismatches fork +"Your handshake compares the network name and nothing else. A PoW or difficulty mismatch forks and bans; a `finality` mismatch is a WARN; `rollout-v2.sh` throws the finality block away when it writes the file; the app rewrites the packaged file on every start." + +Status: Open (4 October 2026). + +Answer: Correct. `protocol/flows/src/flow_context.rs:833` compares `network`; the version message has no params digest and no genesis hash. `pre_pow_validation.rs:37` and `pow_guard.rs:25-42` fork and ban on PoW and difficulty fields; `processes/finality.rs:669-688` only warns on a finality mismatch; `infra/cloud-devnet/rollout-v2.sh:20,35` rewrites the file as two fields; `app/igneum-app/src/engine.rs:742-760` rewrites `override-params.json` each start. Fix: a digest of the effective consensus params plus the genesis hash in the version message, refused on mismatch; the finality WARN becomes a refusal with the reason; `rollout-v2.sh` merges rather than replaces. Review ids R4.1.2, R4.1.12. + +Evidence: the files above. Experiment: two nodes on different files; the handshake must fail with the field named. + +### F23. The equivocation ban is node-local, so honest nodes refuse each other's certificates +"Evidence detected from an RPC vote stamps the sink's DAA; evidence carried in a block stamps the carrier's DAA. Two honest nodes hold different `until` for the same key, their voter lists differ by one at every checkpoint between the two expiries, and `voter_count` refuses the other's certificate for good." + +Status: Open (4 October 2026). + +Answer: Correct. `ingest_evidence` (`processes/finality.rs:600-612`); `ingest_certificate` (`:680-688`). Certificate validity is not a function of the DAG, the same defect R3.9 found in the execution veto. Fix: stamp every ban with the DAA of the block that carries the evidence; evidence seen by RPC is only acted on once carried. Review id R4.1.3. + +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. + +### 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." + +Status: Open (4 October 2026); extends F7 and C4. + +Answer: Correct. `on_virtual_changed` (`processes/finality.rs:404-440`) inserts once and advances `next_index`; `:661-666` refuses any certificate whose checkpoint is not the node's record. Fix: re-determine an unlocked index when the virtual's chain at that blue score changes; refuse only a certificate that conflicts with a lock. Review id R4.1.4. + +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. + +### 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." + +Status: Open, minor (4 October 2026). + +Answer: Correct. `blockrelay/flow.rs:201`, `router.rs:215-224`, `flow_context.rs:819`, `peer.rs:13`; `virtual_processor/processor.rs:1663-1670` (merged in `baa8bc8a`); `pow_guard.rs:28`. The 10 s bound itself is right in shape (two constants, both directions, not overridable). Fix: one WARN from `time_offset` at handshake, the attack switches behind a feature flag, the dead field refused. Review ids R4.1.6 to R4.1.8. + +Evidence: the files above. Experiment: `igneumd` under `faketime -60s` logs one clear line and does not churn. + +### X20. Cold-sync checkpoint determination is indices times chain length +"A fresh node starts `next_index` at 0 and walks the selected chain from the sink for every index. At mainnet length it never finishes its first resolution." + +Status: Open, minor now (4 October 2026). + +Answer: Correct. `processes/finality.rs:413-421`. About 3 x 10^12 store reads at a 10^7-block chain, approximate. Fix: start from the last certified index carried in headers, walk once. Review id R4.1.10. + +Evidence: the file above. Experiment: a fresh node against a 10^6-block simnet chain, time to first resolution. + +### M25. The miner takes the day length from its environment, and the schedule global can tear +"The template carries epoch and lead but not the day; the miner reads `IGNEUM_POW_DAY_MS` from its environment. And `install_pow_schedule` stores day, lead, epoch while `pow_schedule` loads epoch, lead, so a miner switched between schedules can wrap `pow_epoch_seed_score`." + +Status: Open, minor (4 October 2026). + +Answer: Correct. `igneum/miner/src/main.rs:590-596`; `consensus/core/src/igneum.rs:156-172, 198-204`. Fix: the day length in the template; one atomic struct swap. Review id R4.1.9. + +Evidence: the files above. Experiment: `igneum-miner` with `IGNEUM_POW_DAY_MS=1440000` against a default node accepts zero blocks and says why. + +### M26. The interval fault guard freezes its baseline and loops +"On a trip you skip the STATUS print, so the baseline it would have updated stays frozen, and you roll the counters back to it. Any healthy rate over ten times a slow first interval trips again every interval, forever, with no STATUS line and no `faults=` for the app to read." + +Status: Open (4 October 2026). + +Answer: Correct. `igneum/miner/src/main.rs:1404-1418` with the update at `:1452-1454` skipped by `continue`; the restart has no cap and no growing back-off (`:1213-1216`). A slow first interval (a game on the GPU, a foreground self-heal build on a slow card) is enough. Fix: update the baseline on a trip, or compare to the previous interval; cap restarts with a growing back-off. Review id R4.2.1. + +Evidence: the file above. Experiment: `--status-secs 10` with a GPU stress tool for the first 15 s; one fault, one restart and a STATUS line afterwards. + +### M27. A flapping node makes the worker rebuild once per template +"A prepare goes out whenever the wanted pair differs from the prepared one. No count, no interval, no once-per-epoch. Each one writes a pack on the CPU with the job loop stalled and costs the worker a full build; `prepare-failed` resends on the next fill." + +Status: Open (4 October 2026). + +Answer: Correct. `igneum/miner/src/main.rs:1113-1165, 1337-1339`; `worker.cpp:644-658`; `host.c:1208-1225`. Estimated loss 30 to 60 percent against a node that alternates seeds per template; a stale home node on a fork is the realistic trigger. Fix: at most one prepare per pair per epoch and none within 30 s of the last. Review id R4.2.2. + +Evidence: the files above. Experiment: a fast-time simnet node patched to flip `next_epoch_seed` per template; the miner's `now=` rate within 5 percent of steady. + +### M28. The kernel text is bound only to its own directory +"The worker compiles whatever `kernel_bound.cu` it finds in a directory whose `seeds.txt` matches; the self-test checks the GPU against a `vectors.h` from the same directory. Nothing commits the text to what the generator would emit for the seed. 'Source check PASS' is a prefix equality." + +Status: Open, minor (4 October 2026). The writer is the local miner and the directory is the user's own, so no privilege boundary is crossed. + +Answer: Correct. `packfile.h:~262-283, 306-345`; `worker.cpp:414-416, 596-620`; `emu/test.sh:57-66`. The CPU re-check (`main.rs:1250-1256`) stops a wrong program from earning, not from running. Fix: a hash of the emitted kernel in `program.json`, derived from the seed by the miner and checked by both workers; one sentence in the docs that the chain commits to the seed, not the text. Review id R4.2.4. + +Evidence: the files above. Experiment: a tampered pack with matching vectors must be refused. + +### X21. A wrong program burns power with a green rate +"The CPU re-check counts mismatches and does nothing: no threshold, no stop. The app reads `hash`, `now`, `template_age` and `synced` from STATUS and nothing else, so `mismatched=` and `WORKER FAULT` never reach the card." + +Status: Open (4 October 2026). + +Answer: Correct. `igneum/miner/src/main.rs:1250-1261`; `app/igneum-app/src/engine.rs:~1762-1780` (zero matches for either string). The PowerShell launcher matches them (`igneum-common.ps1:843`), which is what README.txt and TEST.md describe. Fix: stop the worker after 3 consecutive mismatches and show it on the card; the app reads both fields. Review id R4.2.3. + +Evidence: the files above. Experiment: one constant edited in `kernel_bound.cu` outside the vector warps; the card must go red within a minute. + +### X22. Worker restart paths, minor +"Two miners truncate the same `packs\devnet` while workers read it; every restart begins on the stale first pack; the restart loop has no cap; the 60 s stall guard wraps the self-heal build; a node can crash the miner through an `expect`; a worker without prepare support loops on export during IBD." + +Status: Open, minor (4 October 2026). + +Answer: Correct on each: `engine.rs:~2047-2055` and `emit.rs:1156-1163`; `main.rs:1206-1228`; `worker.cpp:684-703`; `main.rs:~581, 893`; `main.rs:1373-1380` with `engine.rs:~1405-1411` (the 14:20 export during IBD, bench-log). Each costs a restart, none loses the run. Review ids R4.2.5 to R4.2.10. + +Evidence: the files above. Experiment: time from worker restart to first accepted job on both PCs. + +### E16. The 20% pool is burned on the live chain, and the text says it pays provers +"Your coinbase sends the 20% to an OP_RETURN tagged `igneum-proving-pool-v0`. Your own comment says provably burned. Your cap test counts it as supply. Your litepaper says, present tense, that it pays a standing prover population." + +Status: Open (4 October 2026). + +Answer: Correct for the live devnet-v4 line. `coinbase.rs:112-113`; `consensus/core/src/igneum.rs:86-98, 329-333`; evidence row 21. Proving v0 on the `proving` branch carries payouts in the shard statement (P21, P22) and the live chain does not run it. Until it does, a prover earns 0 from the pool and the spendable cap is 3.2 billion. Fix: one sentence in the litepaper Economics table (`site/litepaper.html:396, 414`) and under the homepage bar (`site/index.html:306, 446-448`): burned until the payout ships, no prover paid today; a burned-so-far figure on the live page; a decision on whether the payout reclaims the burned share or pays from new emission. Review ids R4.7.1, R4.7.2. + +Evidence: the files above; `docs/review/round-4-2026-10-04.md` section 7. Experiment: one live devnet block whose pool output reaches a named prover key. + +### L9. "100% to miners and provers", "0% anyone else", and no word that devnet coins have no value +"The homepage says 100% to miners and provers and 0% to anyone else, beside a 1% client dev fee disclosed only in the litepaper. Nowhere on the site does it say the devnet's coins have no value or that the chain may be reset, and the Get the miner button is live." + +Status: Open (4 October 2026); extends E5. + +Answer: Correct. `site/index.html:306, 382, 446-448`; `grep` for "no value", "reset", "wiped", "test coins" across `site/*.html` finds nothing; the only disclaimer is the app's welcome screen (`app/igneum-app/ui/index.html:45`). Fix: "devnet: coins have no value and the chain may be reset" beside the button and on the live page; the homepage tiles match the litepaper's dev-fee sentence. Review ids R4.7.3, R4.7.5. + +Evidence: the files above. + +### M29. The litepaper's app paragraph describes an app that does not exist +"'Press one button, and the card is mining and proving to a wallet the app made for you, with earnings shown in IGN and in your currency ... offers a hardware wallet for your earnings.' Version 0.3.3 shows a hash rate and block counts." + +Status: Open (4 October 2026). + +Answer: Correct. `site/litepaper.html:424`; the app's sources have no earnings, currency or hardware-wallet path (`grep` of `src/*.rs` and `ui/app.js`); the prover service exists for the `proving` branch's chain. Fix: rewrite the paragraph to 0.3.3 (hash rate, blocks, devnet label, power cap, the prover setting where it applies) and move earnings, currency and the hardware wallet to a roadmap sentence. When an IGN figure is ever shown, derive it from accepted blocks over the last hour times the per-block subsidy, never from hash share, and label the network. Review id R4.7.4. + +Evidence: the files above. + +### 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." + +Status: Open, minor (4 October 2026). + +Answer: Correct on each point; `docs/review/round-4-2026-10-04.md` section 7 holds the arithmetic. Fix: one `nvidia-smi` draw line per setting with the rate beside it on both PCs; one `powermetrics` line on the Mac; the sim rerun at 124 MH/s with the comparison stated as a function of fleet size; "a million 100,000-gas calls a day" in the litepaper. Review ids R4.7.7 to R4.7.13. + +Evidence: the files above. Experiment: the three draw lines. + +### X29. Host and file hygiene, minor +"The Mac's live node binds its gRPC to every interface. Four secrets or pointers in `~/.config/igneum` are world-readable, one token is a filename, and the intake key rides on `curl`'s command line. The manifest answers CORS `*` and the clock source is a cacheable page's Date header." + +Status: Open, minor (4 October 2026). + +Answer: Correct. `--rpclisten=0.0.0.0:26610` on pid 33114 (no `--unsafe-rpc`, `--disable-upnp`); `ls -la ~/.config/igneum`; `app/igneum-app/src/update.rs` (`https_time`, `upload_log`); the dl host's headers. Vercel rewrote `Date` to now on a cache hit today, so the cached-Date failure did not show. Fix: RPC on loopback with PC 2 on a tunnel or its own node; `chmod 600`; the stray file removed; the key passed to `curl` through `-K` or a header file; an uncacheable path for the clock source. Review ids R4.5.5 to R4.5.7. + +Evidence: `lsof`, `ls`, `curl -I`. + +### X30. The live page and the bench page exposed operational detail +"The engineering log page rendered the bench log with private strings; `/api/live` returned peer addresses, full key hashes and payout addresses; the mobile menu did not open." + +Status: Fixed (4 October 2026): `ac89a37` and `6b644a6` (a scrubbed copy, the build fails on any private string), `2d8f09c` (none of the three in the public API), `621f5cc` (the menu). Review ids R4.6.1, R4.6.2, R4.6.8. + +Answer: Correct at discovery; fixed before this document was written. The evidence page was brought up to the day's measurements in `86e5857` and `bf4d7ec` (rows 29 and 30, rows 10, 12 and 15 restated). + +Evidence: the commits above. Experiment: `curl https://igneum.network/api/live` holds no address, no 64-hex key hash and no payout address. + +## Status updates, 4 October 2026 (round 4) + +- **F21** (the long-partition fork). Extended: a side locks alone when its own share of its own table reaches two thirds, at `t = W (2/3 - s) / (1 - s)`: 50/50 at 2,400 DAA s on the devnet (about 40 minutes), 10 days on mainnet; the 60 side of 60/40 at 1,200 DAA s (20 minutes), 5 days; the ledger's measured `W/(3R)` = 200 s is this formula at s = 1/2. At HEAD a second certificate at an index is kept, logged and ignored (`processes/finality.rs:650-655, 661-666`) and `fork_choice_lock` (`:886-901`) pins the node. Public text: `site/litepaper.html:511` says a third of the blocks is needed to split finality in a partition; the partition alone does it. Replacement sentence in `docs/review/round-4-2026-10-04.md` section 1 (b). Review id R4.1.5. +- **M20** (pruning proofs under the stub). Extended: `validate_trusted_header` (`processor.rs:330-337`) skips `pre_pow_validation`, so trusted and pruning-proof headers are never checked for bits, DAA score or the v2 activation. Review id R4.1.11. +- **M13** (Macs mine too). Extended: the ratio is measured, 26.7 / 124, about a fifth, and the litepaper (`:420`) still carries no ratio; the app shows no projected earnings, which for a devnet is the right outcome. Review id R4.7.6. +- **E5** (the client dev fee). Still open on the homepage; see L9. +- **D2** (the app share). The "Why build here" text is applied at `site/litepaper.html:355` and states the number; two gaps noted under E17. diff --git a/docs/review/round-4-2026-10-04.md b/docs/review/round-4-2026-10-04.md new file mode 100644 index 000000000..e080b85be --- /dev/null +++ b/docs/review/round-4-2026-10-04.md @@ -0,0 +1,473 @@ +# Igneum hostile review, round 4: what is live today + +Date: 4 October 2026, afternoon. Round 3 reviewed the specification and the running code of the evening before. This round reviews what is live now, not branches: devnet v4 at `vendor/igneum-node-v4` (commits through `a21ff239`, HEAD `3bfe346f`), the prebuilt Windows workers (`proto-cuda/nvrtc/worker.cpp`, `proto-opencl/host.c` generic mode), the Igneum Miner app (0.3.1 when the reading began, 0.3.3 on master by the time of writing), the relay (`relay/`), the Windows CI (`.github/workflows/windows.yml`), the downloads host, the public spec repository, the live site with `/api/live` and the observer, and the economics after today's measurements. The seven roles each wrote a section in the first person; where a role read code rather than ran it, the section says "reviewed by reading". What was reproduced on the Mac this afternoon is listed at the end. + +Two roles (app security, public face) had not reported when this document was assembled. Their sections carry what the parent reviewer verified alone and the findings the coordinator confirmed as fixed on master this afternoon; a dated addendum follows when they report. + +Every finding carries a rank from round 3's table (fatal stops the roadmap as written; serious must be fixed before the public testnet; minor when convenient) and a ledger id. No secret value is printed here or in the ledger; where a value was compared, the text says "count-only". + +The count: 1 fatal, 16 serious, 8 minor, 1 serious fixed before this document was written, 3 ledger entries extended (F21, M20, M13). + +## The fixes, in order + +| # | Fix | Ledger | Owner | Effort | State | +|---|---|---|---|---|---| +| 1 | Take `igneum-relay-clients.zip` off the downloads host; rotate the relay token; give the relay its own key, never the intake key; require a second, per-machine secret (or an Ed25519 signature over `{id, to, body}`) for `kind: run`; rotate the intake key and repackage | X23, X24 | Josh (rotation), relay owner | 2 h | Open | +| 2 | Sign `payload-inputs.zip` on the Mac (detached Ed25519, the OTA key or a second one) and verify it in CI before the build; make `publish-manifest.sh` a separate, deliberate step that names the CI run it signs (`OTA_SKIP=1` by default in `fetch-ci-artifacts.sh`) | G13 | release engineer | 2 h | Open | +| 3 | Delete the `IGNEUM_POW_*` environment fallback and install `Params.pow_epoch_blocks` on every network; put a digest of the effective consensus params and the genesis hash in the p2p version message and refuse a mismatch; turn the finality "names N voters" WARN into a refusal with a reason | G12, X18 | consensus-engineer | 4 h | Open | +| 4 | Stamp equivocation bans with the carrying block's DAA only; re-determine a checkpoint when a reorg deeper than `checkpoint_depth` moves it; add the two-node tests | F23, F24 | consensus-engineer, cryptographer | 4 h | Open | +| 5 | Miner: update the interval-guard baseline on a trip; at most one `prepare` per pair per epoch and none within 30 s of the last; stop a worker whose `mismatched` passes 3 in a row and show it on the card; the app reads `mismatched=` and `WORKER FAULT` | M26, M27, X21 | miner-community-lead, app owner | 3 h | Open | +| 6 | Three sentences: the 20% pool is burned on the live chain until the payout ships (litepaper Economics table, homepage bar); "devnet: coins have no value, the chain may be reset" beside "Get the miner" and on the live page; the litepaper app paragraph rewritten to what 0.3.3 does | E16, L9, M29 | Claude (site) | 1 h | Open | +| 7 | Relay: token out of the URL path (`x-relay-token` in every client), retention and a cap on `feed`, `Arm-Restart` only on `reboot_continue`, `from` and `kind: result` bound to the registered machine | X24, X25, X26, X27 | relay owner | 3 h | Open | +| 8 | The history: the intake key (6 files, 8 commits) and the dl token (1 commit) join the rewrite list of `docs/fud-fixes.md` section 5; `TZ=UTC` in the commit path; review, ledger and fixes files out of the tree before the public date | G14 | Josh, Claude | 2 h, before public | Open | +| 9 | Live page and bench page: peer addresses, key hashes, payout addresses and private strings removed | X30 | Claude (site) | | Fixed `ac89a37`, `2d8f09c`, `621f5cc` | + +Minors (X19, X20, X22, X28, X29, M25, M28, E17) are listed in the ranking table with their experiments; none blocks a gate. + +--- + +## 1. Consensus engineer + +I own the rules that decide whether two nodes running the same binary stay on one chain. I read the live devnet-v4 line at `3bfe346f` (consensus through `a21ff239`). Reviewed by reading; the unit tests I name were run by the parent (section 8). + +### Attack 1. Fork two nodes with two files, and nobody tells them + +What I do. I run a node with a different `override-params.json` from yours, or with none, and let the p2p layer connect us. + +What the code does. The handshake compares one string, the network name (`protocol/flows/src/flow_context.rs:833`). The version message carries no digest of the effective params and no genesis hash. Two nodes with different override files connect, relay, and diverge at the first header whose validity depends on the differing field. Detection is per field and uneven: + +| Overridable field (`consensus/core/src/config/params.rs:222-288`) | On mismatch | Detected how | Peer outcome | +|---|---|---|---| +| `difficulty_v2_activation_daa` | fork at the height | `UnexpectedDifficulty` (`pre_pow_validation.rs:37`) | strike (`pow_guard.rs:42`), 1 h IP ban after 2 strikes in an hour (`pow_guard.rs:25,34`) | +| `difficulty_rule` | fork at once | same | same | +| `pow_epoch_blocks`, `pow_epoch_lead`, `pow_day_ms` | fork at the next epoch or day | `InvalidPoW` after a program compile or a 256 MiB cache build (`processor.rs:346-348`, `pre_ghostdag_validation.rs:131`) | strike, ban | +| `blockrate` (k, merge depth, parents, mergeset, finality depth) | fork at once | wrong blue score or blue work | strike, ban | +| `genesis_bits` | different chain | never syncs | no ban, no warning | +| `finality` (9 fields) | finality diverges, blocks agree | WARN only: "names N voters, this node counts M" (`processes/finality.rs:680-688`) or "below min_daa" (`:669-673`) | nothing; no `finality_active` change; no ban | +| `skip_proof_of_work` | the node accepts PoW-less blocks | never, on that node | its own chain is free to mine | +| `timestamp_deviation_tolerance` | nothing | no consumer left in `consensus/src` | dead field, still accepted | + +A mismatch in a PoW or difficulty field forks and bans within a few blocks; a mismatch in the `finality` block never forks and is never surfaced beyond a WARN. `docs/plans/cutover-2026-10-04.md:112-121` says as much. `infra/cloud-devnet/rollout-v2.sh:20,35` rewrites the whole file as `{genesis_bits, difficulty_v2_activation_daa}` and drops any finality block an operator had. + +Keying and the boundary. The switch is keyed on the header's own DAA score: `pre_pow_validation.rs:27-38` checks `daa_window.daa_score == header.daa_score`, then `calculate_difficulty_bits` reaches `reference_window` at `difficulty.rs:332-337`, `v2 = daa_score >= v2_activation_daa`, inclusive. A header's DAA score is a function of its past, so every node evaluates the same side; a mergeset can skip the exact value and that is still deterministic. The first v2 header uses the 600-DAA window at once; the 3% and 10% clamps bound the jump (`difficulty.rs:432-454`). The bench-log's node 3 rejecting at exactly DAA 900 is this rule working. Nothing re-validates stored headers after a late activation, and the trusted path (`processor.rs:330-337`) skips `pre_pow_validation` for pruning-proof headers entirely (extends R3.3). + +The environment hole. The PoW schedule the node runs is a process-wide global (`consensus/core/src/igneum.rs:150-172`). `daemon.rs:339-340` installs the file's schedule only when the file names one; otherwise the first reader installs `PowSchedule::from_env()` (`igneum.rs:137-145`): `IGNEUM_POW_EPOCH_BLOCKS`, `IGNEUM_POW_EPOCH_LEAD`, `IGNEUM_POW_DAY_MS`. The mainnet gate at `daemon.rs:316-319` blocks the file only. `MAINNET_PARAMS.pow_epoch_blocks` (`params.rs:721`) is never installed on a file-less node, and the difficulty manager takes the global (`services.rs:113`). An environment variable sets the epoch length on every network including mainnet. + +Where the file comes from. The packages carry `node_override_params` from `packaging/mac/packaged-config.sh:17` (empty today) into `igneum-app.json`; the app writes it to `/override-params.json` on every start (`app/igneum-app/src/engine.rs:742-760`), so an operator edit is reverted silently. The devnet's own nodes take hand-written files. + +Severity. Serious for the environment hole (R4.1.1, G12). Serious for the silent `finality` mismatch and the missing digest (R4.1.2, X18). Minor for the rest. + +Reproduction. Start any node with `IGNEUM_POW_EPOCH_BLOCKS=60` and no file and read its template's `epoch_blocks`. For finality: two nodes, one with the cutover doc's 1,800 window, one without; grep both logs for "names .* voters". + +Evidence missing. No test starts two nodes with different files and asserts what each logs. No version-message digest exists to test. + +### Attack 2. The two-thirds floor, equivocators in the denominator, and the partition that needs no attacker + +Arithmetic. `consensus/core/src/finality.rs:109-116`: `signed * 3 >= 2 * total` in u128, inclusive, `total > 0`. No division, no rounding; exactly two thirds locks. Correct. Total is the blocks of voters above dust and not stripped (`processes/finality.rs:317-329`). The window is DAA-denominated (`:266,272`), not past-median time as W2 says; spec 3.10 records that. + +Equivocators are out of the denominator, and the ban time is node-local. `ingest_evidence` (`:600-612`) sets `until = detected_daa + equivocation_ban`, where a node-local detection stamps the sink's DAA and a block-carried one stamps the carrier's DAA. Two honest nodes that detected the same evidence at different DAA scores hold different `until`, so at every checkpoint between the two expiries their voter lists differ by one key, and `ingest_certificate` refuses the other node's certificate outright: `cert.voter_count != table.voters.len()` (`:680-688`). The refusal is permanent for that index on that node. Certificate validity is not a function of the DAG. R3.9 made the same complaint about the execution veto; this is the finality copy of it. R4.1.3, F23, serious. + +Determination is never revisited. `on_virtual_changed` (`:404-440`) inserts a checkpoint once and only advances `next_index`. After a reorg deeper than `checkpoint_depth` (20 blue score on devnet) the node's record for that index names a block off its new chain; every certificate the network forms for that index hits `cp.hash != cert.checkpoint` (`:661-666`), is pushed to `conflicting_certificates` and refused, with no equivocation anywhere. A liveness gap at that index on that node and a false CONFLICTING line. C4 is implemented as "reject any certificate that does not name my own determination". R4.1.4, F24, serious. + +F21 at HEAD. Not resolved, and the code says so: a second certificate at an index is kept, logged and ignored (`:650-655`, `:661-666`); `fork_choice_lock` (`:886-901`) pins the node to its own locks; nothing clears `finality_active`. A side locks alone when its own share of its own table reaches two thirds, at `t = W (2/3 - s) / (1 - s)`: + +| Split (s for the side) | Devnet, W = 7,200 DAA s | Mainnet, W = 30 d | +|---|---|---| +| 50 / 50, either side | 2,400 DAA s, about 40 min at 1 block/s per side | 10 days | +| 60 side of 60 / 40 | 1,200 DAA s, about 20 min | 5 days | +| 40 side of 60 / 40 | 3,200 DAA s, about 53 min | 13.3 days | +| 70 side of 70 / 30 | 0, locks at once | 0 | +| 30 side of 70 / 30 | 3,771 DAA s, about 63 min | 15.7 days | + +The merge depth (3,600 DAA s) is crossed after the 50/50 lock, so at the heal the other side's blocks are merged red, which is the share jump the ledger measured (`W/(3R)` = 200 s at W 1,800, R 3 is this formula at s = 1/2). R4.1.5 extends F21; conceded, unchanged. + +Reproduction. For the ban: `tools/finality-attacks` with one equivocation detected on node A by RPC vote and on node B from the carrying block 30 DAA later; count certificates refused with "names N voters" between the two expiries. For determination: a 25-block reorg on a 3-node devnet (a 30 s cut at d = 20); grep CONFLICTING on the losing side with no EQUIVOCATION line. + +### Attack 3. The epoch length is a process global, not a chain rule + +Header path: `processor.rs:346-348` derives the epoch from `pow_epoch_index(header.daa_score)`, the global. Template path: `consensus/mod.rs:809-827` reports `epoch_blocks` and `epoch_lead` from the same global. Miner: `igneum/miner/src/main.rs:592-596` installs epoch and lead from the template, but `day_ms` from its own environment (`:590-591`). Two nodes that disagree do not accept each other's blocks: the seeds differ, the engine returns `InvalidPoW`, strike and ban. The mismatch forks loudly, after a compile or a cache build per foreign header, which the M15 cap bounds. The file is refused on mainnet (`daemon.rs:316-319`); the environment is not (attack 1). The `clamped` floor (`igneum.rs:129-132`) stops the divide-by-zero, not the fork. A miner-side torn read: `install_pow_schedule` stores day, lead, then epoch; `pow_schedule` loads epoch, then lead (`igneum.rs:156-172`); a miner switched between schedules mid-run can compute `pow_epoch_seed_score` on old epoch and new lead and wrap. R4.1.9, M25, minor. + +Reproduction. Start `igneum-miner` with `IGNEUM_POW_DAY_MS=1440000` against a default node and count accepted blocks: zero. + +### Attack 4. Ten seconds of clock + +The future bound is 10 s against the local clock, in isolation, before anything else: `pre_ghostdag_validation.rs:50-57`, `FUTURE_TOLERANCE_MS` (`igneum.rs:270`). The lower bounds are chain-relative: strictly above the sampled past median (`post_pow_validation.rs:31-32`) and at least the selected parent minus 10 s (`:37-39`). Kaspa's 132 s is gone as a rule. Neither 10 s is a parameter, so two nodes cannot disagree on them through the file. + +The ban question. `TimeTooFarIntoTheFuture` is not a strike (`pow_guard.rs:38-47`), but the relay flow returns on any rule error (`blockrelay/flow.rs:201`), the flow ends, and the router closes the connection (`router.rs:215-224`). A slow node disconnects every peer on every relayed block, redials, repeats; no ban, no backoff, no line a person can read. The block comes back as a missing parent through `process_orphan` (`flow.rs:166-168`) once the clock allows. The handshake already computes `time_offset` (`flow_context.rs:819`) and never uses it, so the WARN the bench-log filed is a free change. + +The template stamps `max(max(past_median + 1, parent - 10 s), now)` (`virtual_processor/processor.rs:1449-1467`). A fast node's blocks are refused by a receiver when `x + y > 10 s + d`: + +| Sender skew | Receiver skew | Propagation | Refused by that receiver | +|---|---|---|---| +| +6 s | -5 s | 1 s | no, 11 s against 11 s | +| +6 s | -5 s | 0.5 s | yes, until 0.5 s passes | +| +12 s | 0 | 1 to 3 s | yes, for 9 to 11 s, then fetched as a parent | +| 0 | -60 s | any | every block, the PC 2 case | + +With NTP-disciplined clocks the honest refusal rate is zero; for the unsynced box the failure is total, not fractional. The 10 s bound is the right shape; the gap is operational. `IGNEUM_ATTACK_TS_OFFSET_MS` (`virtual_processor/processor.rs:1663-1670`, "branch diff-attacks only", merged in `baa8bc8a`) and `IGNEUM_POW_STRIKES` (`pow_guard.rs:28`) are compiled into the live node. R4.1.6, R4.1.7, R4.1.8: X19, minor. + +### Attack 5. What else the last ten commits leave open + +`on_virtual_changed` walks from the sink to the target by selected parent for every undetermined index (`processes/finality.rs:413-421`); on a cold sync `next_index` starts at 0, so the first resolution is indices times chain length in store reads, about 3 x 10^12 at a 10^7-block mainnet (approximate). A fresh mainnet node never finishes it. R4.1.10, X20, minor now. `igneum_difficulty_bits` unwraps store reads on chain ancestors (`difficulty.rs:258,271-277`), safe while the 600-step walk stays above the pruning point. `6457ca95` kept `total > 0` in `FinalityParams::locks`. No new unwrap on untrusted input in `a21ff239` or `a5ef8b07`. + +### (b) The sentence I would quote back + +`site/litepaper.html:511`: "A miner holding a third of the last 30 days of blocks can split finality during a network partition". F21 needs no miner: a 50/50 partition splits finality by itself after ten days of mainnet time, 40 minutes of devnet time. Fix: "A network partition that lasts a third of the window splits finality by itself, and a miner holding a third of 30-day weight can split it at once; both leave two locks that only an operator can reconcile today." + +### (c) What would change my mind + +A version message with a digest of the effective params and the genesis hash, refused on mismatch, with a two-node test. The environment fallback deleted. Evidence bans stamped with the carrier's DAA only, with a two-node test that forms one certificate across the expiry. A 25-block reorg on the devnet with finality on, showing the losing node accepts the network's certificate. A skew survey of the devnet machines. + +--- + +## 2. Miner and worker security + +Scope as read: `vendor/igneum-node-v4/igneum/miner/src/main.rs` at `3bfe346f`, `proto-cuda/nvrtc/worker.cpp` 1.0, `packfile.h`, `proto-opencl/host.c` generic mode, `app/igneum-app/src/engine.rs` and `procs.rs`, `igneum-pow/src/emit.rs`. Reviewed by reading; round 3 section 4 (protocol, one worker per card, efficiency gap) is not repeated. + +### Attack 1. Drive the miner from the node + +The template's PoW fields are typed, not text: `epoch_seed: RpcHash`, `next_epoch_seed: Option`, `epoch_blocks`, `epoch_lead`, `boundary_daa_score`, `day_index` as `u64` (`rpc/core/src/model/message.rs:196-215`). The only path built from them is `--prepare-packs/-` (`main.rs:1127`): no traversal surface. The program text is never carried by the node; `export-pack` and the prepare path regenerate it from the seed through `igneum_pow::emit::export_pack` (`main.rs:1130-1140`, `1494-1535`; `emit.rs:1167-1205`). The worker is spawned with `Command::new(path).arg("--serve")` and no shell (`main.rs:842-854`); the app launches the miner the same way (`procs.rs:45-51`). NVRTC options are a fixed array (`worker.cpp:334`); `clBuildProgram` options come from the command line, never the pack (`host.c:419-426`, `1082`). The template's schedule is clamped (`consensus/core/src/igneum.rs:129-132`), so `unwrap_or(header.daa_score / pow_epoch_blocks())` (`main.rs:1084`) cannot divide by zero. Worker lines parse with `parse().unwrap_or(..)` behind length guards (`main.rs:1245-1348`). Residual: a node can crash the miner through `try_into().expect("block convert")` (`main.rs:~581`) and `connect(..).expect("connect")` (`:893`); the app restarts it in 5 to 60 s, forever. Minor (R4.2.9, folded into X22). + +### Attack 2. Compile my own kernel + +The worker does not generate the program. It reads `kernel.cu`, `kernel_bound.cu`, `program.h` and `memhard.h` from the directory it is given (`worker.cpp:414-416`) and hands them to NVRTC; the OpenCL worker reads `kernel_bound.cl` (`host.c:1075-1079`). On a mismatch, any subdirectory of `packs\prepare` or `packs\` whose `seeds.txt` matches the job's seeds is accepted (`findPackFor`, `worker.cpp:596-620`). What binds the text to the seed is thin: `pf_load` checks that the seed bytes hash to the constants in `program.h` (`packfile.h:~262-283`) and the self-test compares the GPU against `vectors.h` (`packfile.h:306-345`), all from the same directory. There is no hash of the kernel text in the pack and nothing compares it to what the crate's generator would emit for that seed. The bench-log's "source check PASS for 4 files" is `emu/test.sh:57-66` confirming the bytes handed to NVRTC equal the pack file's own prefix, not a commitment. The one backstop is the miner's CPU re-check of every found nonce (`main.rs:1250-1256`), which stops a wrong program from earning, not from running. The writer is the local miner; `%LOCALAPPDATA%\Igneum\packs` is the user's own directory, so this is not a privilege boundary. No pack is served from `dl.igneum.network`. R4.2.4, M28, minor; worth one line in the docs because "compiles the chain's program" reads as if the chain commits to the text. + +### Attack 3. Prepared packs, hot swap and the 14:20 case + +Prepared packs go to `packs\prepare\-` (`main.rs:1127`); nothing deletes them (12 files of about 80 KB each, 2 MB a day). Both miners of a two-vendor PC export to the same `packs\devnet` on independent threads (`engine.rs:~961-979`, `~2047-2055`) with truncating writes (`emit.rs:1156-1163`); a worker reading mid-write fails `pf_load` and exits 1; the miner restarts it in 5 to 60 s. The 14:20 case is handled by `3bfe346f` (`main.rs:1352-1366`); the CUDA worker also self-heals from `packs\prepare` (`worker.cpp:686-703`). Every worker restart starts on `packs\devnet` as exported at the miner's start, which after the first boundary is the previous epoch: one wasted build, 3 mismatches, a forced prepare. The app's "synced" gate (`engine.rs:~1437-1445`) did not stop the 14:20 export during IBD. R4.2.5, R4.2.10: X22, minor. + +### Attack 4. Trip the fault guards with honest behaviour + +| Guard | Threshold | Baseline | On trip | Loop possible | +|---|---|---|---|---| +| job time per hash (miner, `main.rs:1288-1300`) | 20x faster | EMA, window 50, reset on trip | kill, roll back, restart 5 to 60 s | no | +| interval rate (miner, `main.rs:1404-1418`) | 10x faster | cumulative at the last printed STATUS, frozen on trip | kill, roll back, restart | yes, whenever the healthy rate exceeds 10x the frozen baseline | +| no `done` for 60 s (miner, `main.rs:1382-1398`) | 60 s | last ready or done | kill, restart | only if a self-heal build exceeds 60 s; none measured | +| chunk time (worker, `host.c:1316-1320`) | 20x faster | chunk EMA, window 64 | exit 3 | no | + +The interval guard has a bug. On a trip the STATUS print is skipped by `continue`, so the `(last_report_gpu_ms, last_hashes, last_jobs)` update at `main.rs:1452-1454` never runs, and the counters are rolled back to those same values. After the restart the next interval is compared to the same frozen baseline. If the worker's true healthy rate is more than 10 times that baseline, the guard trips again every status interval, forever: no STATUS line is printed again, `faults=` is never shown, the app sees only "no status from the miner for a while" after 4 intervals (`engine.rs:~1427-1433`). A slow first interval poisons the baseline: a GPU shared with a game for the first minutes, or the CUDA self-heal build, which runs inside a job (`worker.cpp:684-703`). On the RTX 5090 the self-heal is 0.9 s against 140 ms jobs and a 10 s interval, safe; on a card where one slow job fills the first interval, the loop starts. The restart itself has no cap and no growing back-off (`main.rs:1213-1216`). R4.2.1, M26, serious. + +Reproduction. `--status-secs 10` on a card whose first job is slow (a GPU stress tool for the first 15 s, then stop it); watch `WORKER FAULT ... over 10x` repeat after every restart with no STATUS line between. + +### Attack 5. Make the worker rebuild forever with a flapping next_epoch_seed + +A prepare is sent whenever `prepared_pair != Some(want)` (`main.rs:1113-1118`), with no count, no minimum interval and no once-per-epoch rule. Each send writes a pack on the CPU inside `spawn_blocking` that the main loop awaits (`main.rs:1130-1143`): about 0.3 to 0.5 s with no job sent. Each accepted prepare costs the worker a full build on its second stream, and the single `prepared` slot drops the previous build (`worker.cpp:644-646`, `host.c:1208-1210`). A prepare that arrives while one is running is refused; the miner answers `prepare-failed` by clearing `prepared_pair` (`main.rs:1337-1339`), which re-sends it at the next job fill. + +| Node behaviour | Pack writes per minute | GPU builds per minute | Miner loop stalled | Estimated hash loss | +|---|---|---|---|---| +| honest, one next seed per hour | 1 per hour | 1 per hour | 0.5 s per hour | 0 | +| alternating A, B per template | about 60 | up to 60 on the RTX 5090 (0.85 s each) | 20 to 30 s | 30 to 60 percent, estimate | +| a new seed per template | about 60 | up to 60 | 20 to 30 s | the same, plus 0.4 s CPU per entry | + +The engine's cache is bounded (`KEEP = 4`, `MAX_INFLIGHT_BUILDS = 2`, `consensus/pow/src/igneum.rs:59-63, 274`). A stale or forked home node is the realistic trigger. R4.2.2, M27, serious as robustness. + +### Attack 6. Mine with a wrong program and let it burn + +The CPU re-check runs on every `found`: a hash that differs or sits above target increments `mismatched`, prints `WORKER MISMATCH` and drops the share (`main.rs:1250-1261`). Nothing acts on it: no threshold, no kill. `hashes_total` keeps growing, so the reported MH/s stays healthy. The app's STATUS parser reads `hash`, `now`, `template_age` and `synced` only (`engine.rs:~1762-1780`); `mismatched` and `WORKER FAULT` have zero matches in `engine.rs`. The PowerShell launcher matches them (`igneum-common.ps1:843`), which is what README.txt and TEST.md describe; the app does not. A card can run at full power for hours with zero yield and a green rate. R4.2.3, X21, serious. + +Reproduction. Hand-edit one constant in `packs\devnet\kernel_bound.cu` that leaves the three vector warps untouched; restart; watch `mismatched=` climb while the app shows the rate. + +### (c) What would change my mind + +A test that feeds `mine_worker` a slow first interval and shows one fault, one restart and a printed STATUS line. A prepare rate limit with a fast-time run against a flapping node. A `mismatched` threshold surfaced in the app. A kernel-text commitment derived from the seed and checked by both workers. + +--- + +## 3. App security + +Pending the role's report. What the parent verified this afternoon, on the Mac, against a scratch instance of the 0.3.1 engine with `IGNEUM_APP_DATA` in a scratch directory and peers pointed at an unreachable port (no node joined, nothing posted): + +| Check | Result | Where | +|---|---|---| +| Dashboard bind | `127.0.0.1:`, never `0.0.0.0` | `lsof`; `app/igneum-app/src/server.rs:1` | +| Token | 32 characters from `getrandom` (`main.rs:83`), in the path `/t//`; `GET /` 404, wrong token 404 on GET and POST, right token 200; `..` and `..%2f` traversal 404 | `curl` against the scratch instance | +| Response headers | `Cache-Control: no-store`, `X-Content-Type-Options: nosniff`, `Referrer-Policy: no-referrer`; no `Access-Control-Allow-Origin` echoed for a foreign `Origin` | `curl -D -` | +| Files created | `app/machine-id` 0600, `app/app.url` 0600; the real install also holds `wallet.json` 0600 and `settings.json` 0644 | `find -exec ls -la` | +| Packaged config | `igneum-app.json` 0644 in the bundle carries the update-manifest URL with the dl token and the intake key (count-only grep: 1 line each) | `/Applications/Igneum Miner.app/Contents/Resources/` | +| Manifest code | 13 unit tests pass, including `signature_verifies_and_tampering_fails`, `sha256_of_file_is_checked_by_the_caller`, `versions`, `fork_closeness_and_support` | `cargo test --release` in `app/igneum-app` | +| Clock source | `curl -sI https://dl.igneum.network/` on `PATH`, 10 s timeout, `Date` at 1 s resolution; the worst of three sources decides; over 10 s Start is blocked | `update.rs:10-14`, `engine.rs:615, 1559-1612` | +| Installer on master | 0.3.3 (`1d54787`): per-user, `PrivilegesRequired=lowest`, `{localappdata}\Programs`, the firewall rule asked once; the "silent elevated installer" of 0.3.1 is gone | `packaging/windows/Igneum-Miner.iss:32,50` | + +What is still owed from the role: whether the manifest's `published_at` or version is checked against the last seen manifest (a signed old manifest replayed), the Windows download-then-run path on 0.3.3, the macOS bundle swap and quarantine, the rollback trigger, the elevated `nvidia-smi` invocation (full path or `PATH`, quoting of the slider value), and whether the per-launch token reaches any log or upload. The clock-skew detection the coordinator names as fixed is `7c794df` and `0d4498e` (in 0.3.1 and after); the parent's reading of it is in the table. + +--- + +## 4. Relay security + +Read-only review of `relay/` at `131f2ad` on master. One read-only `GET` was sent by the parent (`machines`, `feed`), nothing was posted. No secret is printed; where a tracked literal was compared with a live secret, the comparison was count-only. + +### Attack 1. One secret tier, every power + +What I do. I hold either the relay token (from the URL) or the `x-igneum-key` value. I `POST /r//api/task` with `{"to":"PC1","kind":"run","title":"x","body":"","flags":{"elevated":true}}`. Within 20 s the agent on PC1 runs it as administrator. + +| Endpoint (`relay/api/relay.mjs`) | Method | Auth | Does | +|---|---|---|---| +| `feed` :47-58 | GET | token or key | items newest first, full bodies, up to 500 per call, every machine | +| `machines` :59-62, `item` :63-67, `file` :68-76 | GET | same | names, hostnames, roles, GPU and WSL info; one item; 302 to the public Blob URL | +| `inbox` :77-88 | GET | same | unread tasks for a machine; `ack=1` marks read as a side effect of a GET (:85) | +| `drop` :95-125, `task` :111-125 | POST | same | any item, any `from`, any `kind`; `kind:'run'` with `flags.elevated` and `flags.reboot_continue` (:113-114) is the admin command queue | +| `upload`, `ack`, `done`, `delete` :126-147 | POST | same | one-hour Blob client token, 50 MB; marks; deletion | +| `register`, `name`, `role` :148-191 | POST | same | create, rename and re-role machines (renaming reroutes queued `run` items, :171-180) | + +The only check is `authed()` at `relay/lib/relay.mjs:33-42`: `q.token` (from the path by the rewrite at `relay/vercel.json:6`) or `x-relay-token`, OR `x-igneum-key` equal to `RELAY_KEY`. The handler uses the result for truthiness only (`api/relay.mjs:39-40`). No read, write or command split. The key, documented in three shipped files as "only authorises log uploads", queues admin commands. Compare is `===` (minor). Token: about 100 bits (README.md:7); key: 24 characters. R4.4.1, fatal. + +Handled, with lines: `Referrer-Policy: no-referrer` (`vercel.json:13`); `robots.txt` and `X-Robots-Tag` (`vercel.json:12`); `ui.html` escapes every server string (`esc()` at :163); parametrised SQL (`api/relay.mjs:48,53`); no TLS validation bypass in any client, TLS 1.2 forced (`igneum-agent.ps1:10`); random Blob paths (`lib/relay.mjs:69-71`); placeholders in the repo clients; the relay token never committed; `.env.local` and `.vercel` ignored; one-agent mutex (`igneum-agent.ps1:37-38`). The relay has no `Strict-Transport-Security` (minor). + +Token in the path. Every call from every client goes to `/r//api/` and is rewritten to `/api/relay?token=&fn=`, so Vercel's request logs carry the token on every 20 s poll of every agent. `x-relay-token` exists (`lib/relay.mjs:37`) and no client uses it (`igneum-agent.ps1:14`, `send.ps1:26`, `send.sh:11`, `agent.sh:10`, `tools/relay.mjs:24`). The Mac tool prints the full tokened URL on every `list`, `url` and `watch` (`tools/relay.mjs:83,88,125`), so every transcript that ran it holds the token. R4.4.3, X24, serious. + +### Attack 2. The PC agent + +`relay/clients/igneum-agent.ps1`: polls every 20 s (:21, :172) with `GET inbox?machine=&kind=run&ack=1` (:165). Checks before running: none; every item returned is run in order (:166); dedupe is server-side only. It writes the body to `%LOCALAPPDATA%\igneum-relay\tasks\task-.ps1` (:99, :106), wraps it with `Start-Transcript` (:109-120), runs `powershell -NoProfile -ExecutionPolicy Bypass -File ` (:121, :128) as the agent's user, which is elevated (`igneum-agent.bat:5-10` re-launches with `-Verb RunAs`). `Arm-Restart` (:68-79) creates scheduled task `IgneumRelayAgent` at logon with `/RL HIGHEST` (:72) plus an HKCU `RunOnce` (:76-77), at EVERY agent start (:154), not only before a reboot as README.md:42 says. Closing the window does not remove it. If the transcript contains `RELAY-REBOOT` anywhere (:133), `shutdown.exe /r /t 10` (:139, :148). Uploads back: the last 64 KB of the transcript to `all` (:81-94), the full log as a file when longer, `register` posts username, admin flag, OS build, GPU list, WSL distros and the folder holding the secret-bearing files (:40-66). TLS validated. Rotation: the URL, key and token are baked as literals (:11-13) by `make-clients.sh:12`; changing `RELAY_TOKEN` on the Vercel project kills every client at once, but the key path stays open until `RELAY_KEY` changes too. + +Live state, from the parent's read-only `GET machines` at 13:41 UTC: `PC1` role `miner`, `agent igneum-agent.ps1 v1`, `admin true`, last seen 13:41:33 UTC; `PC2` registered, never seen; the feed holds 12 items (1 `run`, done, not elevated). The agent is live and elevated on PC 1. R4.4.4, X25, serious. + +### Attack 3. The x-igneum-key + +Same value everywhere: README.md:7 says `RELAY_KEY` is "the same value as `~/.config/igneum/log-intake-key`"; `make-clients.sh:8` bakes it in; the site's intake checks it as `LOG_INTAKE_KEY` (`site/api/log.mjs:43-47`). It is a literal in 6 tracked files (count-only match, 1 line each): `infra/gpu-bench/upload.sh:8`, `packaging/mac/packaged-config.sh:7`, `proto-cuda/windows-app/upload-log.bat:9`, `proto-cuda/windows-miner/upload-log.bat:9`, `proving/windows-wsl2/prove-block.sh:29`, `prove-shard.sh:30`. It ships inside every Windows miner package, the prover WSL package, the Mac DMG and the Windows app config (`packaged-config.sh:32` writes it to `igneum-app.json`, read by `config.rs:106`, sent by `update.rs:88`). On the site it gates `POST /api/log` only (`log.mjs:39-47`); there is no GET on `miner_logs` by key, so a holder cannot read other PCs' uploads; a holder can post fake rows (`label`, `machine`, `run_id`, 256 KB of `lines`) that `tools/logs.mjs` prints as truth, with no rate limit. On the relay a holder has every power of attack 1. The comments saying the key "only authorises log uploads" (`upload-log.bat:6`, `upload.sh:2-3`, `packaged-config.sh:3-4`) stopped being true at `8f9f950` today. R4.4.2, fatal. With R4.4.1 and R4.5.1 this is one ledger entry, X23. + +### Attack 4. Storage and data + +Neon tables `relay_items` and `relay_machines` (`lib/relay.mjs:15-29`); files in Vercel Blob `access: 'public'` with a random path. Retention: none; `delete` removes the row and leaves the blob. The feed returns full bodies (`ITEM_COLS` at `lib/relay.mjs:92`), so one secret yields every task body and every transcript ever posted. The dl token is written into task bodies by design: `tools/relay.mjs:76-80` substitutes `__DL_BASE__` with the tokened base URL before posting (:108), and `playbooks/miner-v4.ps1:12` and `prover-setup.ps1:12` print the full URL into the transcript. (Today's 12 items hold 0 copies, count-only.) R4.4.5, X26, serious. + +### Attack 5. The playbooks + +| File | Secrets | Remote code | Defender or firewall | Persistence | +|---|---|---|---|---| +| `miner-v4.ps1` | dl token via `__DL_BASE__` (:5), printed (:12) | downloads a zip and runs its `START-IGNEUM.bat` (:13, :20) | none | none; `Remove-Item C:\igneum-v4 -Recurse -Force` (:14) | +| `prover-setup.ps1` | dl token (:8, :12); password `igneum` echoed to `sudo -S` (:21) | zip, `setup-wsl.sh` inside WSL (:14, :22) | none | none | +| `wsl-setup.ps1` | creates user `igneum`/`igneum` with `NOPASSWD:ALL` (:39-40) | `wsl --install` (:33) | none | `bcdedit`, dism, `.wslconfig`; prints `RELAY-REBOOT` (:25) | +| `prove-block.ps1`, `oneclick-test.ps1`, `mac-smoke.sh` | none in the file | WSL script | none | none | + +No `irm | iex`, no Defender or firewall change, no Run key or service beyond the agent's own `Arm-Restart`. Every header and README.md:53 say untested; the feed shows one `run` executed. R4.4.11, minor (folded into X28). + +### Attack 6. The repository's own history + +Relay token: 0 tracked files, 0 commits. Key: 6 tracked files, 8 commits from `78df757` (3 Oct) to `4c9810f` (4 Oct). dl token: `docs/plans/morning-2026-10-04.md:49`, commit `c47ff03`. `relay/.env.local` and `relay/.vercel` ignored. The remote is private today and slated to go public. R4.4.8, G14, serious. + +### Blast radius + +| If the relay token leaks | If the intake key leaks | +|---|---| +| Who can act: anyone with the link: PC disk (`send.ps1:25`, `igneum-agent.ps1:13`), the hosted zip (section 5), Vercel request logs, browser history, `tools/relay.mjs` output, a screenshot | every recipient of any Windows miner package, prover package, Mac DMG or app payload; anyone with read access to the repository or its history | +| What: queue PowerShell as administrator on every named PC, reboot them, read every task and transcript ever posted, delete items, rename and re-role machines, post forged results; lift the dl token from stored bodies | identical on the relay, plus forged `miner_logs` rows | +| How fast: next poll, 20 s | 20 s | +| Revoke: change `RELAY_TOKEN` on the Vercel project (every client stops at once), then a new zip to each PC by hand; the key path stays open until `RELAY_KEY` changes too | change `RELAY_KEY`; to close the intake too, change `LOG_INTAKE_KEY`, which breaks uploads from every shipped package until repackaged | + +### (c) What would change my mind + +Attacks 1 and 3 drop to serious if the relay project's `RELAY_KEY` is a different value from `LOG_INTAKE_KEY` today (README.md:7 and `make-clients.sh:8` say it is the same). Attack 2's persistence drops to minor if `schtasks /Query /TN IgneumRelayAgent` returns nothing on both PCs (the `machines` read says the agent has run on PC 1, so it will not). + +--- + +## 5. Infrastructure and secrets + +I hold the keys for a living. Everything below was checked on the Mac this afternoon with `ls`, `git`, `curl -I`, `nc -z` and one read-only `GET`; nothing was posted, no state changed, no token value is printed. + +### Attack 1. Start from one token and walk to administrator on PC 1 + +| Step | Where the secret sits | Mode or exposure | Checked | +|---|---|---|---| +| dl token | `~/.config/igneum/dl-token` | 0644 | `ls -la` | +| dl token | `docs/plans/morning-2026-10-04.md:49`, tracked, commit `c47ff03` | in the history of a tree meant to go public | count-only grep of the live value: 1 tracked file, 3 history lines | +| dl token | `igneum-app.json` in every install (the `update_manifest` URL), 0644 in the Mac bundle and inside `igneum-windows-app.zip`; on 0.3.3 under `%LOCALAPPDATA%\Programs` | every install, every user on the machine | grep count 1 in the bundle file and in the zip | +| the relay clients | `https://dl.igneum.network/dl//igneum-relay-clients.zip` (14,551 bytes, hosted 4 Oct 11:00) | anyone with the dl token; Vercel serves the folder with no listing, `robots.txt` disallow, HSTS | listed from the local `dlsite` folder, which is what Vercel serves | +| relay token and intake key | inside that zip: `agent.sh`, `send.ps1`, `send.sh`, `igneum-agent.ps1`, one line each | the holder of the dl token now holds both relay secrets | count-only grep inside the zip | +| intake key | inside every hosted Windows and prover zip (`igneum-windows-app.zip`, `igneum-windows-v4.zip`, `igneum-node-windows-v4.zip`, `igneum-prove-wsl2.zip`) | every package recipient, including the outsider the morning plan names | counts | +| the PC agent | `GET machines`: `PC1` live, elevated, 13:41 UTC | the channel is open now | one read-only GET | + +The chain: dl token (0644 on the Mac, in git history, in every installed app) opens the hosted relay-clients zip; the zip holds the relay token and the intake key; either is full authority on the relay (section 4); `POST task {kind: run, flags: {elevated: true}}` to `PC1` runs as administrator within 20 s. Four hops, no privilege boundary on any of them. The intake key alone shortens it to one hop for anyone who has unzipped a Windows package. R4.5.1, X23, fatal as an operational fact today. + +### Attack 2. What the downloads host trusts, and what the signer signs + +| Link in the chain | Who authenticates it | File | +|---|---|---| +| `payload-inputs.zip` (igneumd.exe, igneum-miner.exe, both GPU workers, NVRTC DLLs) built on the Mac and uploaded with the Vercel CLI | the Vercel login in `~/.config/igneum/vercel/auth.json` (0600) | `packaging/windows/push-inputs.sh` | +| CI fetches the zip and `payload-inputs.sha256` from the same host with `DL_TOKEN` and checks one against the other | nobody: a sha256 from the same host is a transfer check, not an authentication | `.github/workflows/windows.yml`, step "payload inputs" | +| CI builds the engine and host from git and packs the installer | GitHub's runner; `workflow_dispatch` enabled | same | +| the Mac pulls "the latest green run on master" and copies the installer into the dl folder | `gh` as igneum-josh; no diff, no second build, no check of who triggered the run | `packaging/windows/fetch-ci-artifacts.sh` | +| the Mac signs the manifest over that installer's sha256, by default, in the same script | the Ed25519 key in `~/.config/igneum/ota-signing-key` (0600) | `fetch-ci-artifacts.sh` calls `packaging/ota/publish-manifest.sh` unless `OTA_SKIP=1` | +| the app verifies the manifest signature and the installer's sha256 | the compiled-in public key | `app/igneum-app/src/manifest.rs` | + +The signature is real and covers whatever the pipeline produced; the node binary inside the installer was never signed by anything, and the one check on it is served by the host it came from. A compromised Vercel account for the dl project, or a compromised runner, yields a correctly signed malicious installer that the app reports as genuine. Spec 08 item 4 ("version, hash and reproduction instructions") has nothing to reproduce from, because the fork is not in the repository the runner builds. R4.5.2, G13, serious. + +### Attack 3. The hosts + +| Host | 22 | 26611 p2p | 26610 RPC | 28610 JSON RPC | forwarded 27002 to 27012 | Reads as | +|---|---|---|---|---|---|---| +| seed (Hetzner fsn1) | open | open | closed | closed | closed | as documented: RPC on loopback, SSH from anywhere with the ops key, root | +| EU gateway `igneum-01` | open | open | closed | closed | 27002, 27006, 27007, 27011, 27012 open; 27001 closed | DNAT to private nodes' p2p only, as `net/gateway.sh` writes it | +| `igneum-03` (ash, public) | open | open | closed | | 27008 open | as documented | +| the Mac live node 1 (pid 33114) | | `*:26611` | `*:26610` | | | `--rpclisten=0.0.0.0:26610`, no `--unsafe-rpc`, `--disable-upnp` | +| the Mac observer node (pid 4584) | | loopback | loopback | loopback | | loopback only | +| the Mac app node (pid 75697) | | `*:26621` | loopback | | | RPC and EVM RPC on loopback | + +`net/gateway.sh` leaves the `FORWARD` policy at Debian's default (ACCEPT) under its `IGNEUM-FWD` chain; the private range is not routable from outside, so the exposure is the DNAT ports. SSH is open to the world on every host by instruction (`infra/seed-nodes/config.sh`, `SSH_SOURCE=any`). The Mac's live node is the one host whose RPC is reachable from the LAN: any home-network device can read every RPC method and submit blocks and transactions; PC 2 mines through it by design. R4.5.5, X29, minor. + +### Attack 4. What is in the tree that should not be, as of today + +| Item | State today | Rule it breaks | +|---|---|---| +| `docs/fud-ledger.md`, `docs/fud-fixes.md`, `docs/review/*` | tracked, not ignored | `docs/fud-fixes.md` section 5 step 1: out of the tree before any public push | +| the intake key | 6 tracked files, 8 commits | section 5 step 7: the "zero hits" secrets grep of 3 Oct is false now | +| the dl token | 1 tracked file, 1 commit | same | +| identity terms on the full tracked tree | `josh` 51 files, `malone` 3, `vivanmn` 3, `peasehill` 3, `thrsty` 3, `godaddy` 5, `soft-voice` 2, `/Users/` 6 | section 5 steps 2 and 7; the CI grep covers the public export list only, by design | +| commit timestamps | every commit today at `+0100` | section 5 step 4 | +| `~/.config/igneum` | `dl-token`, `desec-token`, `dlsite-dir`, `ota-signing-key.pub` at 0644; a second copy of the deSEC token whose file NAME is a 28-character string of the token's length and whose content equals `desec-token` (a paste accident, 3 Oct 19:33) | a filename is readable by every process regardless of mode | +| the app's shell-outs | `curl` on `PATH`; the intake key rides on its command line (`update.rs`, `upload_log`) | visible to every local user in the process list during an upload | +| the dl host | `access-control-allow-origin: *` on the manifest; the clock source is `curl -sI https://dl.igneum.network/`; Vercel rewrote `Date` to now on a cache hit today (age 302 s) so the cached-Date failure did not show, and nothing guards against it | | + +R4.5.3 and R4.5.4: G14, serious (the public-date rule). R4.5.6 and R4.5.7: X29, minor. + +### (c) What would change my mind + +The relay-clients zip off the host and the relay token rotated; the intake key rotated and split from the relay key; `payload-inputs.zip` signed on the Mac and verified in CI; a history rewrite before the public date; `TZ=UTC` in the commit path; the Mac node's RPC on loopback with PC 2 on a tunnel or its own node. + +--- + +## 6. Public face + +Pending the role's report. Recorded here from the coordinator and from master: + +| Finding | State | Commit | +|---|---|---| +| R4.6.1 The engineering log page rendered the bench log with private strings | Fixed: the site build renders a scrubbed copy and fails on any private string | `ac89a37`, `6b644a6` | +| R4.6.2, R4.6.8 `/api/live` exposed peer addresses, full key hashes and payout addresses | Fixed: none of the three in the public API | `2d8f09c` | +| The mobile menu button | Fixed | `621f5cc` | +| The evidence page against today's measurements | Rows 29 (12-node propagation) and 30 (one click) added; rows 10, 12 and 15 restated on the live lock, the oscillation and the GPU proof | `86e5857`, `bf4d7ec` | + +Together these are X30, serious at discovery, fixed. Still owed from the role: the public spec repository's tree and commit identities, the "Identities" stat's label, the litepaper's "Why build here" text against D2, and whether the live page's LIVE badge now shows the observer lag after the 78-minute stall. Two public-text findings from other roles stand on master as of this writing: `site/litepaper.html:511` (section 1, (b)) and the three economics sentences of section 7. + +--- + +## 7. Economics + +I have kept books for GPU miners since the 2021 run. Nothing here carries a price; where dollars appear they use the three inputs of `docs/analysis/security-budget.md` section 1 (USD 0.005, 0.02 and 0.10 per IGN, USD 0.12 per kWh in section 6) and are labelled as that document's assumptions. + +### Attack 1. What a card earns at devnet parameters, and what it earns once anyone else shows up + +| Input | Value | Source | +|---|---|---| +| Base subsidy per DAA second | 3,168,808,781 sompi = 31.688 IGN | `consensus/core/src/igneum.rs:27`, test at `:321` | +| Devnet block rate target | 1 block per second | `params.rs` DEVNET_PARAMS; `coinbase.rs:261` | +| Day-0 ramp | 10% = 3.169 IGN per block; about 10.5% four hours after the v4 genesis | `igneum.rs:34, 72`; bench-log line 604 | +| Producer share | 80% | `igneum.rs:37, 83` | +| PC 1 RTX 5090 | 121.8 to 123.4 MH/s | bench-log, "first hourly program swap" | +| PC 2 RTX 5090 | 124.2 MH/s (NVRTC worker); 117 to 119 inside the app | bench-log, gfx1036 entry and "first machine on the app" | +| 5090 at the 80% cap | 110 MH/s, the brief's number, not in the bench-log | commit `01d1c06` sets the cap; no rate at the cap is logged | +| Mac M5 Max | 26.7 MH/s (20 to 27 per the brief) | bench-log, swap table | +| AMD gfx1036 | 2.74 to 3.3 MH/s | bench-log | +| Network today | 275.5 MH/s | sum | +| Emission is paid per block | the chain minted at about 2 blocks/s with the two PCs until difficulty caught up | spec 2.5; bench-log line 607 | + +| Card | MH/s | Share today | IGN/day, full rate, 1 block/s | IGN/day, day 0 | IGN/day in a 10,000-card scenario (1.24 TH/s), full rate | USD/day in that scenario at 0.005 / 0.02 / 0.10 | +|---|---|---|---|---|---|---| +| RTX 5090 | 124 | 45.0% | 985,825 | 98,583 | 219.0 | 1.10 / 4.38 / 21.90 | +| RTX 5090, 80% cap | 110 (approximate) | 39.9% | 874,522 | 87,452 | 194.3 | 0.97 / 3.89 / 19.43 | +| Mac M5 Max | 27 | 9.8% | 214,655 | 21,466 | 47.7 | 0.24 / 0.95 / 4.77 | +| Mac M5 Max | 20 | 7.3% | 159,004 | 15,900 | 35.3 | 0.18 / 0.71 / 3.53 | +| AMD gfx1036 | 3 | 1.1% | 23,851 | 2,385 | 5.3 | 0.03 / 0.11 / 0.53 | + +Electricity for the 5090 at 0.35 kW (the brief, approximate): 8.4 kWh a day, USD 1.01 at 0.12 per kWh. At the low price input a 5090 in a 10,000-card network clears USD 0.09 a day after power; at the base input USD 3.37. The per-card figure collapses 4,500x between today's devnet and that scenario, which is arithmetic. The finding is what the public text does with it (attack 4). R4.7.11: any per-day figure shown should be per block and labelled (E17, minor). + +### Attack 2. The 20% proving pool pays nobody, and it is coded as a burn + +| Item | Value | Source | +|---|---|---| +| Where the 20% goes | one output per coinbase to `proving_pool_script_public_key()` | `coinbase.rs:112-113` | +| What that script is | `OP_RETURN OP_DATA_22 "igneum-proving-pool-v0"` | `igneum.rs:90-98` | +| The code's own comment | "The coins are provably burned on devnet v0" | `igneum.rs:86-88` | +| Evidence row 21 | "the payout from the pool against proof records is unwritten" | `docs/evidence.md` | +| Cap accounting | the cap test sums the whole subsidy, pool included | `igneum.rs:329-333` | +| Pool per block, full rate / day 0 | 6.338 / 0.634 IGN | arithmetic | +| Pool per day, full rate | 547,570 IGN | arithmetic | + +Minted, counted toward the 4 billion cap, sent to a provably unspendable script: burned, not escrowed. On the live devnet-v4 line a prover earns exactly 0 IGN from the pool. Proving v0 on the `proving` branch carries payouts in the shard statement and the ledger's P21 and P22 describe its limits; the live chain does not run it. Until the payout ships the spendable cap is 3.2 billion. + +GPU time per block versus the pool: block 78 (2 transactions, 626,876 cycles) proved on the RTX 5090 in 1.4 s core, 2.7 s compressed (bench-log line 585 onward); the shard-scale row is pending; 2.7 s x 0.35 kW = 0.00026 kWh, USD 0.00003 at 0.12 per kWh, against 6.34 IGN per block, USD 0.032 at the low input. Power is not the problem. Per GPU-second, mining beats the pool 4.9x on today's devnet (30.8 against 6.34 IGN per 2.7 s at a 45% share) and loses 930x at 10,000 cards (0.0068 against 6.34 if the card wins the shard); E12's answer depends on fleet size and no document says so. The economy sim's one measured input is a 229 MH/s 5090; today's generator v2 card does 122 to 124 (R4.7.8, R4.7.9: E17). + +What the text says. `site/index.html:306`: "100% to miners and provers"; `:446-448`: "80% miners / 20% provers / 0% anyone else". `site/litepaper.html:396`: "The proving pool: shard provers and aggregators. Pays a standing prover population that does not have to hash"; `:414`: "In-chain proving. The 20% proving pool plus the proving share of every block's gas. Yes, mostly". On the code as it runs: 80% to miners, 20% to an OP_RETURN, 0% to provers, in the present tense on a page with a live devnet strip. R4.7.1 and R4.7.2, E16, serious. + +### Attack 3. The app share: the number is now stated, and it is small + +Transfer receipt: 21,000 gas, 1 gwei tip, 16,800 gwei to the miner, 4,200 gwei developer share burned as unregistered (`docs/evidence.md` row 22; bench-log line 287). Per 100,000-gas call at a 1 gwei tip the app share is 20,000 gwei; a million calls a day is 7,300 IGN a year, USD 36.5 / 146 / 730 at the three inputs; ledger D2's figure matches. `site/litepaper.html:355` states it: "a million calls a day at a 1 gwei tip pays about 7,300 IGN a year. It grows with traffic and nothing else." That is honest; the Canto and Blast sentence is gone. Two gaps: it does not say "100,000-gas calls", and 1 gwei = 1e-9 IGN depends on the open base unit (O-2.6). R4.7.10, E17, minor. + +### Attack 4. What the miner is shown, and what the litepaper says the miner is shown + +The app shows hash rate, accepted and rejected blocks, identities, the chain label "devnet v4", "nothing is bought or sold" on the welcome screen (`ui/index.html:45`), the power cap and telemetry. `grep` for `earn`, `per day`, `revenue`, `IGN/day`, `projected` across `src/*.rs` and `ui/app.js` returns nothing: no projected earnings, which for a devnet is the right outcome. PC 2 at 118 MH/s would be 37,830 to 73,347 blocks a day by three different bases (share at 1 block/s, the true 139M difficulty, the first minute's 34 accepted) that disagree by 2x because the devnet did not run at 1 block/s. + +What the text says. `site/litepaper.html:424`: "press one button, and the card is mining and proving to a wallet the app made for you, with earnings shown in IGN and in your currency, and mining paused while you game ... offers a hardware wallet for your earnings". 0.3.3 shows no IGN, no currency, no hardware wallet; its prover service exists on the `proving` branch's chain only. `:420`: "Macs mine too", no ratio; today's is 26.7 / 124, about a fifth, the sentence M13 asked for. No public page says the devnet's coins have value, and none says they do not: `grep` for "no value", "reset", "wiped", "test coins" across `site/*.html` finds nothing, beside a "Get the miner" button (`index.html:382`) and a live strip. R4.7.4, M29, serious; R4.7.3 and R4.7.5, L9, serious; R4.7.6 extends M13. + +### Attack 5. Electricity: the cap, the Mac, the iGPU + +| Item | Value | Label | +|---|---|---| +| Cap default | 80% of the card's default limit, slider 60 to 100 (`detect.rs:98`; `engine.rs:1073, 1086`) | implemented, for stability ("PC 2's 5090 hard-crashed at full power") | +| 5090 default limit, 80% of it | 575 W, 460 W | public spec, approximate, not in the repo | +| Rate at full / at the cap | 124.2 / 110 MH/s | measured / the brief's number, not logged | +| MH/s per W, full / cap | 0.216 / 0.239, about 11% | two unlogged numbers divided | +| Measured draw, either setting | none; the app logs "draw p95" every 5 min and no line reached the bench-log | missing | +| Mac draw at 26.7 MH/s | not logged | missing | +| iGPU | 1.0% of today's network; off by default with a reason line (`detect.rs:88-91`); its worker went silently dead at 600 s | keep the default | + +A memory-bound kernel often does not pull the full limit, in which case the cap changes the crash rate and nothing else; the honest claim today is stability, which the commit message makes. R4.7.7, R4.7.12, R4.7.13: E17, minor. + +### (c) What would change my mind + +A coinbase that pays a prover set from the 20%, with one live devnet block where a named key received it. The 5090 shard-scale row filled in. One `nvidia-smi` line at full and at 80% with the rate beside each. The litepaper's app paragraph rewritten to 0.3.3, and "devnet, no value" on the download button. The economy sim rerun at 124 MH/s with mining-versus-pool stated as a function of fleet size. + +--- + +## 8. What was reproduced on the Mac this afternoon + +| Check | Result | +|---|---| +| `igneum-pow` `cargo test --release` | 11 passed (vectors, packs, chain derivation) | +| `app/igneum-app` `cargo test --release` | 13 passed (manifest signature and tamper, sha256, versions, fork closeness, key vector, state machine, HTTP date) | +| `vendor/igneum-node-v4` `cargo test --release -p kaspa-consensus-core --lib params` | 7 passed (override file carries the PoW schedule and the v2 activation; unknown fields rejected; 60x file is the devnet at 60x) | +| `cargo test --release -p kaspa-consensus --lib difficulty` | 15 passed, including `reference_window_switches_at_the_activation_height`, `v1_and_v2_agree_in_a_steady_epoch`, `igneum_clock_steps_pay_a_forgery_back` | +| `cargo test --release -p kaspa-consensus --lib finality` | 2 passed | +| A scratch app instance against unreachable peers | loopback bind, 32-character path token, 404 without it, traversal 404, 0600 on `machine-id` and `app.url` | +| Secrets sweep, count-only | dl token: 1 tracked file, 3 history lines; intake key: 6 tracked files, 8 commits; relay token, Hetzner token, deSEC token, OTA private key: 0 | +| Hosted packages, count-only | relay token in 4 files of the hosted relay-clients zip; intake key in 4 hosted zips; dl token in the app payload zip and the installed bundle | +| Port probes (`nc -z`) | seed, EU gateway, ash node: 22 and 26611 open, RPC closed, forwarded 27002 to 27012 open as documented | +| Live sockets (`lsof`) | the Mac live node's gRPC on every interface; observer and app nodes on loopback | +| Downloads host (`curl`) | no listing, `robots.txt` disallow, HSTS, CORS `*` on the manifest, `Date` rewritten to now on a cache hit | +| Relay (`curl`, read-only) | 401 without a token; `noindex`, `no-referrer`, `no-store`, no HSTS; `machines`: PC1 live and elevated; feed 12 items | +| The dashboard of the live app (pid 73591) | loopback, 404 on every path without the token | + +Not reproduced, reviewed by reading: the Windows paths (installer, agent, `nvidia-smi`), the worker fault guards on a real card, the finality two-node scenarios, the partition arithmetic, the Vercel log retention. + +## Ranking of every finding + +| Id | Finding | Role | Rank | Ledger | +|---|---|---|---|---| +| R4.4.1, R4.4.2, R4.5.1 | One secret tier on the relay; the intake key is the relay key, is a literal in 6 tracked files and ships in every package; the hosted relay-clients zip under the dl token carries both secrets; the PC agent is live and elevated | Relay, infra | Fatal (operational) | X23 (new) | +| R4.1.1 | PoW schedule from `IGNEUM_POW_*` on every network including mainnet; the file gate does not cover it | Consensus | Serious | G12 (new) | +| R4.1.2, R4.1.12 | No params digest or genesis hash in the handshake; a `finality` mismatch is a WARN only; `rollout-v2.sh` overwrites the finality block; the packaged file is rewritten on every start | Consensus | Serious | X18 (new) | +| R4.1.3 | Equivocation ban `until` is node-local; `voter_count` refuses the other node's certificate for good | Consensus | Serious | F23 (new) | +| R4.1.4 | Checkpoint determination is never revisited after a deep reorg; false CONFLICTING | Consensus | Serious | F24 (new) | +| R4.1.5 | F21 extended with the lock-alone formula and the devnet times | Consensus | Serious, conceded | F21 (extended) | +| R4.2.1 | Interval fault guard freezes its baseline on a trip: a permanent silent restart loop | Miner | Serious | M26 (new) | +| R4.2.2 | No rate limit on `prepare`: a flapping or stale node forces a pack write and a GPU rebuild per template | Miner | Serious | M27 (new) | +| R4.2.3 | CPU re-check mismatches are counted, never acted on; the app does not read `mismatched=` or `WORKER FAULT` | Miner | Serious | X21 (new) | +| R4.4.3 | Token in the URL path on every request and printed by `tools/relay.mjs`; `x-relay-token` unused | Relay | Serious | X24 (new) | +| R4.4.4 | Agent installs a HIGHEST-privilege logon task and RunOnce at every start; README says only with `reboot_continue` | Relay | Serious | X25 (new) | +| R4.4.5 | Feed returns full history, no retention; the dl token is written into `run` bodies and transcripts by design | Relay | Serious | X26 (new) | +| R4.4.6, R4.4.7 | No clean rotation (the key path survives a token rotation); `from`, `kind: result` and `register` are free text | Relay | Serious | X27 (new) | +| R4.4.8, R4.5.3, R4.5.4 | The intake key (8 commits), the dl token (1), the review files, identity terms in 51 files and `+0100` stamps in the history of a repository with a public date | Relay, infra | Serious | G14 (new) | +| R4.5.2 | The OTA signature covers an installer whose node and worker binaries arrived unsigned from the dl host and were signed automatically from "the latest green run" | Infra | Serious | G13 (new) | +| R4.7.1, R4.7.2 | The 20% pool is minted to an OP_RETURN on the live chain; provers earn 0; the litepaper and homepage say it pays a standing prover population | Economics | Serious | E16 (new) | +| R4.7.3, R4.7.5 | Homepage "100% to miners and provers" and "0% anyone else"; no devnet-value statement beside "Get the miner" or on the live page | Economics | Serious | L9 (new) | +| R4.7.4 | Litepaper line 424 describes an app with earnings in currency, a hardware wallet and proving; 0.3.3 shows MH/s and block counts | Economics | Serious | M29 (new) | +| R4.6.1, R4.6.2, R4.6.8 | Bench page and `/api/live` exposed private strings, peer addresses, key hashes and payout addresses | Public face | Serious, fixed | X30 (new, fixed) | +| R4.1.6, R4.1.7, R4.1.8 | Slow-clock disconnect churn with no node line; `time_offset` unused; attack switches compiled into the live node; dead override fields | Consensus | Minor | X19 (new) | +| R4.1.9 | Miner takes the day length from its environment; a torn read of the schedule global can wrap | Consensus | Minor | M25 (new) | +| R4.1.10 | Cold-sync checkpoint determination is indices times chain length | Consensus | Minor now | X20 (new) | +| R4.1.11 | Trusted and pruning-proof headers skip bits, DAA and activation checks | Consensus | Minor | M20 (extended) | +| R4.2.4 | Kernel text bound only to its own directory; no commitment to the generator | Miner | Minor | M28 (new) | +| R4.2.5 to R4.2.10 | Shared pack directory with truncating writes; every restart on a stale pack; no restart cap; the 60 s guard wraps the self-heal build; a node can crash the miner; the IBD export loop | Miner | Minor | X22 (new) | +| R4.4.9 to R4.4.13 | `===` compare; no HSTS; `inbox?ack=1` side effect; `RELAY-REBOOT` substring; orphaned blobs; no rate limit; `igneum`/`igneum` WSL user with NOPASSWD sudo; username and folder posted on register; the token-named file in `~/.config/igneum` | Relay | Minor | X28 (new) | +| R4.5.5, R4.5.6, R4.5.7 | The Mac live node's gRPC on every interface; four secrets at 0644 and a token in a filename; the key on `curl`'s command line; CORS `*` and the cached-Date clock source | Infra | Minor | X29 (new) | +| R4.7.6 to R4.7.13 | "Macs mine too" without the ratio; the cap's rate and draw unlogged; the economy sim 1.85x stale on its one input; mining-versus-pool as a function of fleet size unstated; the app-share text's two gaps; emission at 2x schedule unlabelled; shard-scale prover cost unmeasured; iGPU default right | Economics | Minor | E17 (new); M13 (extended) | + +Already answered, cited and not repeated: F21 (the long-partition fork, conceded today and extended here), M15 (cache-build cap, the bound that makes R4.1.2's forks cheap), M20 and R3.3 (pruning proofs), R3.9 (validity as a function of the past, the pattern F23 and F24 repeat), R3.14 and F17 (keys per card, now drawn by weight), G7 and G9 (the release key and its steward; G13 is the pipeline in front of it), X6 (the one-click app as a honeypot vector; X23 is its first live instance), D2 (the app share, now stated honestly), E5 (the client dev fee on the homepage, still open), M13 (Macs, extended).