docs/security/keys.md: every key the project depends on (the folder, the gh
keyring, the Vercel env of three projects, the GitHub secrets) with where it
lives, what it unlocks, the blast radius lost and leaked, who rotates it and
the rotation status, written from the files and the scripts that read them.
No value, no private fingerprint. Section 4: the second OTA signing key kept
offline, the app change (a key list plus revocation in the manifest), 0.3.9
as the carrier, and the emergency path if the one key leaks today (a manifest
signed with a new key is useless to 0.3.x apps; the mitigation in order).
tools/keys/backup.sh: ~/Desktop/igneum-keys-<date>.dmg, AES-256, hdiutil's
own prompt (never argv, history or a file), the folder minus build-slots,
dlsite-dir and pytools/, plus a README; attached read-only, every file
compared by sha256, listed, detached. --dry-run lists. restore.sh: --check
compares the image against the live folder without printing values, --to
copies back with 0600/0644 and 0700. test-backup.sh: the end-to-end test on a
scratch folder with a throwaway passphrase, 8 steps, passed.
tools/ci/no-secrets-check.sh, in ci.yml: no tracked file named like a key of
~/.config/igneum, no 64-hex value assigned to a token/key/secret name outside
tests and the allowlist (the OTA public key, the published Hardhat and Anvil
accounts); a --self-test fires on a known-bad tree first. 776 files, 0 hits.
Also: ~/.config/igneum, vercel/ and txgen/ are 0700 now (were 0755).
Co-Authored-By: Claude Fable 5.1 <noreply@anthropic.com>
Downloads: packaging/ota/publish-public.sh publishes the current installers, the HiveOS package and the two signed
manifests into dl/public/ with no token in any URL, writes the four /public/ aliases as vercel.json rewrites and an
unsigned index for the site; publish-manifest.sh --public and ship-app.mjs --public run it on every release (dry run
and self-test cover it). Nothing removed from the token folders.
Site: the miner and wallet buttons link the public aliases and show the version and size from the index, read at
build time (site/downloads.json is the offline snapshot); TESTNET_OPEN in build.mjs drops the "Public testnet: not yet
open" line on the go; the HiveOS Flight Sheet install line on the miner page; /faucet page.
HiveOS: igneum-hive-0.3.8.tar.gz from the 0.3.8 node (2b6d23ef, PC build job) and the zig-built Linux workers.
Faucet: site/api/faucet.mjs (10 IGN per address and per IP per day, Neon table faucet_grants, EIP-1559 transfer signed
by site/lib/eth.mjs with no dependencies: keccak, RLP, secp256k1 with RFC 6979), FAUCET_KEY and FAUCET_RPC from the
Vercel env only; 15 unit tests with a fake database and node, run in CI.
Co-Authored-By: Claude Fable 5.1 <noreply@anthropic.com>
On 5 October 2026 the Mac's host (shard program id 0x0559759b...) rejected every
proof from PC 2's host (0x05db1aca...). Both were built from the same guest
sources: host/build.rs compiled the guests on each machine and the ELF depends
on where it is built (cargo's -C metadata for a path crate includes the checkout
path; a worktree on the same Mac gave a third id, 0x0dfade07...). The node's
verifier also spent 114 s to 138 s per proof in the prover client and both key
setups before a 0.1 s to 0.4 s verify.
- elf/: both guest ELFs, their verifying keys and manifest.json (sha256, ids);
host/src/pinned.rs embeds and checks them at every start; the prove modes
refuse when SP1's setup does not derive the manifest's id
- --mode verify: LightProver with the pinned key, no prover client, no key
setup; prints the proof's own program id next to ours ("IS NOT OURS")
- --mode id; igneum-prove-pin and pin-guests.sh to re-pin; build.rs builds a
guest only under IGNEUM_BUILD_GUESTS=1
- tools/ci/pinned-guests-check.sh: elf/ must match its manifest, no script
builds a guest outside pin-guests.sh; make-package.sh and build-dmg.sh print
the pinned ids
- unit tests on the pinned set; bench-log entry with the three ids, the cause
and the timing: 127.0 s wall per verify before, 1.8 s to 2.4 s after
- rollout order in proving/README.md: every prover and verifier moves together
Co-Authored-By: Claude Fable 5.1 <noreply@anthropic.com>
The strip under the header stays; the card is the first sight of an update. The mark with a progress ring, "Igneum
Ember 0.3.7", one line (is available, is downloading with the percent, is ready to install, Installing. The app
restarts itself., did not install with the one-line cause and Try again), up to three lines of release notes from the
manifest with the rest behind "What changed", the size, Install now and Later. Escape and the backdrop are Later.
Reduced motion is honoured.
Rules (UpdateCard, pure, app/igneum-app/ui/update-card.test.mjs): the card never opens while a job runs, in the
engine's first 60 s, while the key sheet is up or while the app quits; it waits and comes once the block lifts.
Later hides this version at this stage and leaves the strip; the card comes back for a newer version, or when the
download is ready and automatic updates are off (with them on it installs by itself). An open card follows its
update through downloading, ready, installing and failed; installing and failed never open a card by themselves.
?update=<kind>[&auto=0][&card=1] and ?uptime= on the page show every state without an engine. CI runs the new test
file next to notices.test.mjs.
Co-Authored-By: Claude Fable 5.1 <noreply@anthropic.com>
GET /wake?since=<stamp> is public (the apps hold no token) and rate limited (30 a minute per IP). It holds up to
45 s, re-reading the stamp every 2 s, and answers {stamp, at, added, changed, held_ms} the moment the stored stamp
differs from since, else the unchanged stamp at the deadline. POST /r/<token>/wake {stamp, added} (the relay's
auth, also x-relay-token or x-igneum-key on /wake) records a stamp; one row per stamp in relay_wake, created by the
first POST. maxDuration 60 s for api/wake.mjs in vercel.json. api/relay.mjs is untouched.
The handler lives in lib/wake.mjs with its dependencies injected; relay/test/wake.test.mjs drives it with a fake
database, a fake clock and a fake sleep (the hold, the change, the deadline, the rate limit, the hold cap, auth, a
database error). CI's site job runs it with the other relay tests.
Co-Authored-By: Claude Fable 5.1 <noreply@anthropic.com>
One notice at a time, the most important first (app/igneum-app/ui/app.js, Notices; pure, unit tested):
0 update installing, urgent or failed 1 job failed 2 clock 3 job running
4 update available, downloading, ready, waiting for permission, manual 5 job done 6 updated
Lower notices wait their turn. Every notice has a close control; closing hides that notice's key until the
state moves on (a new status, version or job id).
States and their rules:
update available "Igneum Miner X is available." Install now, Later. Key update:X:pending.
update downloading "Downloading Igneum Miner X: 43%." (no percent when unknown), progress bar; same key as
available and checking, so Later hides the whole download until it is ready.
update checking "Checking Igneum Miner X." (the engine's staging step).
update ready "Igneum Miner X is ready. It installs by itself at a quiet moment." (auto on) or just
"... is ready." Install now, Later. Key update:X:ready.
update waiting Windows, nobody answered the administrator prompt: "... is waiting for permission. It
installs the next time someone is at this PC. Mining continues."
update manual "... is downloaded. Open it and drag the app over the old one." Open the download.
update installing "Installing Igneum Miner X. The app restarts itself. Mining continues until then."
(on a Mac, where the engine quits at once: "The app restarts itself in a moment.")
Also while the engine says "installing now" after Install now.
update urgent the engine's consensus-deadline text, ember, downloading percent when it downloads.
update failed "The update to X failed." plus one line of cause and Try again; rolled back:
"Igneum Miner X did not stay up and was rolled back." A dev build with no manifest
configured shows nothing (Settings still says it).
updated "Updated to Igneum Miner X from Y." Gone 60 s after the new version started.
job running "Job: <title> running, N min. <Stage>." with the last RESULT line underneath.
job done "Job: <title> done after N min. Report uploaded." Gone after 5 minutes.
job failed "Job: <title> failed after N min, exit C. Report not uploaded." plus the first error
line (BUILD FAILED / error / failed / panic among the result lines, else the summary).
Stays until closed. Timeout and aborted are "hit its time cap" and "was stopped".
clock as before: the engine's words, Sync clock, the manual hint; on the setup screens only
(the node card carries it on the dashboard). Jobs show on the dashboard only.
Layout: the strip reserves no height while empty; when a notice appears or goes, main's top moves once with a
150 ms transition (none under prefers-reduced-motion). Existing tokens only, nothing newer than 2022 CSS.
Screenshots: ?update=<state> as before, ?job=running|done|failed added (packaging/ota/README.md).
Test: node --test app/igneum-app/ui/notices.test.mjs (ordering, dismissed keys, wording, the 5-minute and
60-second timers); added to the CI site job. Built once with cargo (include_str) and checked against the
ui-mock scenarios and a scratch engine instance (IGNEUM_APP_DATA in a temp dir, fake worker).
Co-Authored-By: Claude Fable 5.1 <noreply@anthropic.com>
GitHub Actions workflow (.github/workflows/ci.yml) on push and pull_request with three jobs on the free runners:
igneum-pow `cargo test --release` and the igneum-census build; the two Python simulators' --quick modes under a
120-second timeout; the site build, an internal link check of site/*.html (tools/ci/link-check.mjs) and a gh-free
identity grep of the public export list (tools/ci/identity-check.sh over tools/ci/forbidden-strings.txt: machine
names, LAN and overlay addresses, home paths, local time zones, the log-intake key pattern; never a key or a name).
The node fork is too big for CI today and the workflow says so.
sim/finality_v2.py --quick is now a genuine smoke run (one day or hour per scenario, one partition and one eclipse
setting): 149 s at nice 19 on a loaded Mac, was 745 s. sim/difficulty/sim.py gains --quick (up50 and warmup-hard,
kaspa and igneum controllers, 36 s). One bench-log time-zone label reworded so the identity grep passes.
Co-Authored-By: Claude Fable 5.1 <noreply@anthropic.com>