Cuprate: disk-fill gate + Cuprate UI companion dashboard #157

Closed
ssmithx wants to merge 0 commits from cuprate-fixes into main
Collaborator

Why

Cuprate was filling the disk until Archipelago crashed. Upstream cuprate has no on-disk pruning (verified against cuprated/src/config.rs; its pruning crate is Monero's p2p protocol pruning), so it cannot do what bitcoin-knots/core do (self-prune below a disk threshold). The safe equivalent is to refuse to run rather than let an unprunable ~250 GiB chain overrun the filesystem.

Changes

Disk-safety gate (450 GB total, crate::constants::CUPRATE_MIN_DISK_GB — single source)

  • install (sync + async RPC), package.start, package.restart, package.update all refuse with an actionable message; boot reconcile skips an already-installed cuprate on a shrunken disk (Left("cuprate-insufficient-disk"), recorded before ensure_running so desired-state recovery can't undo it)
  • fail-open on unreadable disk at install, fail-closed at boot
  • apps/cuprate/manifest.yml storage/disk_limit raised 300Gi -> 450Gi so manifest-driven surfaces match what is enforced

Cuprate UI companion (cuprate-ui, port 18091)

  • docker/cuprate-ui/: host-networked nginx on 127.0.0.1:18091 (auth: gated, session_passthrough), session-gated /cuprate-rpc/ proxy to the restricted RPC (18090), dark-glass dashboard: sync progress (handles Monero's target_height: 0 = synced sentinel), peers, mempool, chain size, free disk, wallet remote-node endpoint; offline state explains the disk gate
  • auto-provisioned/reaped with cuprate via companion.rs (archy-cuprate-ui); UI_APP_IDS, health tier, image pin, mesh launch port
  • neode-ui: cuprate tiles launch the companion (never the raw RPC runtimeUrl), cuprate -> cuprate-ui alias, icon, vite dev proxy

Review round (see commits): restart/update gate bypasses, target_height: 0 misread, manifest 300Gi vs enforced 450, duplicated 450 constant unified into crate::constants.

Verification

  • cargo test -p archipelago --bins: 1450 passed; the 24 failures are pre-existing podman-environment failures identical on main (dev box has no podman)
  • neode-ui vitest: 1052/1052; production bundle rebuilt and grep-confirmed
  • catalog logical diff vs main: exactly cuprate (450Gi) + cuprate-ui; registry-trust preflight OK; drift check clean

⚠️ Signing notes — REQUIRED before this ships

  1. releases/app-catalog.json is committed UNSIGNED. scripts/generate-app-catalog.sh cannot sign (mnemonic is terminal-only, never stored). Before publishing this catalog to the OVH origin, run scripts/sign-catalog.sh (paste the 24-word release master mnemonic; it preflights registry trust and verifies the expected release-root key did:key:z6Mkfu5…DLWT). Nodes reject an unsigned/invalid-signature catalog — committing without signing is fine, publishing without signing breaks the fleet's catalog.
  2. Publish the companion image: build docker/cuprate-ui and push source.archipelago-foundation.org/lfg2025/cuprate-ui:1.7.123-alpha (the pin in scripts/image-versions.sh). Nodes without the local build context pull this as their fallback; until it exists only local-build nodes get the UI.
  3. Run the production test gate on a real node (tests/lifecycle/run-gate.sh) — install/UI/stop/start/restart/reinstall/reboot-survive — plus a manual check of the disk gate on a <450 GB node (install/start/restart/update must all refuse with the explanatory message).

Do not merge-and-publish the catalog before step 1.

## Why Cuprate was filling the disk until Archipelago crashed. Upstream cuprate has **no on-disk pruning** (verified against `cuprated/src/config.rs`; its `pruning` crate is Monero's p2p *protocol* pruning), so it cannot do what bitcoin-knots/core do (self-prune below a disk threshold). The safe equivalent is to refuse to run rather than let an unprunable ~250 GiB chain overrun the filesystem. ## Changes **Disk-safety gate (`450 GB` total, `crate::constants::CUPRATE_MIN_DISK_GB` — single source)** - install (sync + async RPC), `package.start`, `package.restart`, `package.update` all refuse with an actionable message; boot reconcile skips an already-installed cuprate on a shrunken disk (`Left("cuprate-insufficient-disk")`, recorded before `ensure_running` so desired-state recovery can't undo it) - fail-open on unreadable disk at install, fail-closed at boot - `apps/cuprate/manifest.yml` storage/disk_limit raised 300Gi -> 450Gi so manifest-driven surfaces match what is enforced **Cuprate UI companion (`cuprate-ui`, port 18091)** - `docker/cuprate-ui/`: host-networked nginx on 127.0.0.1:18091 (auth: gated, session_passthrough), session-gated `/cuprate-rpc/` proxy to the restricted RPC (18090), dark-glass dashboard: sync progress (handles Monero's `target_height: 0` = synced sentinel), peers, mempool, chain size, free disk, wallet remote-node endpoint; offline state explains the disk gate - auto-provisioned/reaped with cuprate via `companion.rs` (`archy-cuprate-ui`); UI_APP_IDS, health tier, image pin, mesh launch port - neode-ui: cuprate tiles launch the companion (never the raw RPC runtimeUrl), `cuprate -> cuprate-ui` alias, icon, vite dev proxy **Review round** (see commits): restart/update gate bypasses, `target_height: 0` misread, manifest 300Gi vs enforced 450, duplicated 450 constant unified into `crate::constants`. ## Verification - `cargo test -p archipelago --bins`: 1450 passed; the 24 failures are pre-existing podman-environment failures identical on main (dev box has no podman) - neode-ui vitest: 1052/1052; production bundle rebuilt and grep-confirmed - catalog logical diff vs main: exactly `cuprate` (450Gi) + `cuprate-ui`; registry-trust preflight OK; drift check clean ## ⚠️ Signing notes — REQUIRED before this ships 1. **`releases/app-catalog.json` is committed UNSIGNED.** `scripts/generate-app-catalog.sh` cannot sign (mnemonic is terminal-only, never stored). Before publishing this catalog to the OVH origin, run `scripts/sign-catalog.sh` (paste the 24-word release master mnemonic; it preflights registry trust and verifies the expected release-root key `did:key:z6Mkfu5…DLWT`). Nodes reject an unsigned/invalid-signature catalog — committing without signing is fine, publishing without signing breaks the fleet's catalog. 2. **Publish the companion image**: build `docker/cuprate-ui` and push `source.archipelago-foundation.org/lfg2025/cuprate-ui:1.7.123-alpha` (the pin in `scripts/image-versions.sh`). Nodes without the local build context pull this as their fallback; until it exists only local-build nodes get the UI. 3. **Run the production test gate on a real node** (`tests/lifecycle/run-gate.sh`) — install/UI/stop/start/restart/reinstall/reboot-survive — plus a manual check of the disk gate on a <450 GB node (install/start/restart/update must all refuse with the explanatory message). **Do not merge-and-publish the catalog before step 1.**
Collaborator

Maintainer review in progress. The local Cuprate UI image builds successfully as localhost/cuprate-ui:review (nginx, port 18091). However, cargo fmt --all -- --check currently fails on generated core/archipelago/src/fips/app_ports.rs; please format that file before merge. The targeted disk-gate tests are still running on the review worktree. No merge yet; catalog/signing and node UAT remain required.

Maintainer review in progress. The local Cuprate UI image builds successfully as `localhost/cuprate-ui:review` (nginx, port 18091). However, `cargo fmt --all -- --check` currently fails on generated `core/archipelago/src/fips/app_ports.rs`; please format that file before merge. The targeted disk-gate tests are still running on the review worktree. No merge yet; catalog/signing and node UAT remain required.
Collaborator

The targeted Cuprate test compile was not completed in this review window (the isolated worktree triggered a long cold Rust build and was stopped to avoid leaving a runaway compiler). This is not a pass; please include the test result with the formatting fix before merge.

The targeted Cuprate test compile was not completed in this review window (the isolated worktree triggered a long cold Rust build and was stopped to avoid leaving a runaway compiler). This is not a pass; please include the test result with the formatting fix before merge.
Collaborator

Additional local check: the review image serves HTTP 200 on port 18091 when run with host networking, as declared by the manifest. A normal bridged -p run cannot reach nginx because its config intentionally binds 127.0.0.1 inside the container; this is expected for the host-network design, but should remain documented. Still blocked on formatting and targeted test result.

Additional local check: the review image serves HTTP 200 on port 18091 when run with host networking, as declared by the manifest. A normal bridged `-p` run cannot reach nginx because its config intentionally binds 127.0.0.1 inside the container; this is expected for the host-network design, but should remain documented. Still blocked on formatting and targeted test result.
Collaborator

I pushed commit cb567056 to this PR for pre-manifest UAT: the dashboard now follows the Bitcoin UI header/card rhythm, keeps only Cuprate/Monero-accurate fields, and removes the cross-node pruning/disk-policy comparison text. The rebuilt image is running temporarily on the review node at port 28091 for visual testing.

I pushed commit cb567056 to this PR for pre-manifest UAT: the dashboard now follows the Bitcoin UI header/card rhythm, keeps only Cuprate/Monero-accurate fields, and removes the cross-node pruning/disk-policy comparison text. The rebuilt image is running temporarily on the review node at port 28091 for visual testing.
chaum force-pushed cuprate-fixes from 5441415a3d to 5441415a3d 2026-09-12 19:49:15 +00:00 Compare
chaum force-pushed cuprate-fixes from 6b8535fca7 to 6b8535fca7 2026-09-12 19:52:01 +00:00 Compare
chaum added 16 commits 2026-09-12 20:18:07 +00:00
Cuprate has no pruning — verified against upstream main
(binaries/cuprated/src/config.rs): the 'pruning' crate is Monero's p2p
protocol pruning, not on-disk. Unlike the bitcoin apps, which branch on
DISK_GB in their entrypoint and self-prune, a disk-constrained cuprate
can only sync until the filesystem fills and take Archipelago down.

Translate the bitcoin disk-awareness into the only form cuprate can
honor — refuse rather than prune:
- install (sync + async RPC paths) and package.start fail with an
  actionable message below CUPRATE_MIN_DISK_GB (450 GB total: chain
  ~250 GiB + headroom; allows 500 GB-class, refuses the 250 GB VPS)
- boot reconcile skips an already-installed cuprate on a shrunken disk,
  recorded as Left("cuprate-insufficient-disk") before ensure_running
  so desired-state recovery can never undo it (same shape as
  requires-archival-bitcoin)
- df failure fail-opens at install (never block on an unreadable disk),
  fail-closes at boot (never start a doomed sync)

prod_orchestrator also registers cuprate-ui in UI_APP_IDS (its
companion commit follows).
Same companion shape as bitcoin-ui/electrs-ui: host-networked nginx
bound to 127.0.0.1:18091 (auth: gated + session_passthrough), serving
a dark glass status page that polls the node's restricted RPC via a
session-gated /cuprate-rpc/ proxy — sync height/target with progress
bar, peers, mempool, chain size and free disk (from get_info), plus a
wallet 'remote node' endpoint. The offline state explains the disk gate
so a refused node says why.

No secret rendering: the restricted RPC is Monero's safe-for-public
subset, so nginx.conf is baked into the image (no pre_start hook, no
bind mount). companion.rs auto-provisions archy-cuprate-ui alongside
cuprate and reaps it when cuprate goes.

Catalog regenerated (cuprate-ui entry + manifest embed, 18091 into the
mesh launch-port list). NOTE: releases/app-catalog.json is UNSIGNED as
committed — run scripts/sign-catalog.sh before publishing.
cuprate publishes only raw JSON RPC (18090 restricted, 18183 p2p), so
launches must land on the companion on :18091, never on the running
node's runtimeUrl — same root-path special-case bitcoin uses, with the
dev vite proxy for /app/cuprate-ui/. Alias cuprate -> cuprate-ui so the
port-auth lookup finds the gated launch port on HTTPS nodes; pin the
companion icon to the cuprate mark.
Restart and update are stop + recreate — a fresh start by another name —
but only start carried the disk gate, so on a disk that shrank below the
floor after install, either action silently resumed the unprunable
Monero sync: the exact failure the gate exists to close.

Both now call check_cuprate_disk_compatibility after validate_app_id and
BEFORE any state mutation (user-stopped clear / Restarting / Updating
flip), matching handle_package_start's fail-clean contract.
Monero's get_info returns target_height == 0 when the node is FULLY
SYNCED — the field is the height being caught up to, not the chain tip.
The '??' fallback left 0 in place, so every healthy node rendered
"Syncing — 0.00%, 0 blocks behind" forever. Treat 0/absent as
target = own height, the same sentinel electrs_status.rs branches on.
450 existed as two independent Rust constants (RPC gates vs boot
reconciler) linked only by a "keep in lockstep" comment — updating one
would reopen the disk-fill hole. Move it to crate::constants as the
single source of truth both paths import.

Also raise apps/cuprate/manifest.yml storage dependency and disk_limit
from 300Gi to 450Gi so manifest-driven surfaces (store size, pre-checks)
show the number the gate actually enforces — a user provisioning to the
displayed 300 was refused at an unexplained 450. Catalog regenerated
(cuprate entry re-embedded; still unsigned pending sign-catalog.sh).
chaum force-pushed cuprate-fixes from 6b8535fca7 to 00682e6420 2026-09-12 20:18:07 +00:00 Compare
chaum force-pushed cuprate-fixes from 00682e6420 to 00682e6420 2026-09-12 20:18:12 +00:00 Compare
chaum closed this pull request 2026-09-13 10:12:44 +00:00

Pull request closed

Please reopen this pull request to perform a merge.
Sign in to join this conversation.