Demo images / Build & push demo images (push) Failing after 2m22s
Replaces the registry host across 86 files: 309 references, covering all 40 app manifests, the orchestrator and container crates, the release and catalog scripts, both demo-images workflows, the ISO builder, demo-deploy, and the frontend marketplace data. Verified the domain actually serves the registry before rewriting anything, rather than assuming the web host implies the registry: - TLS verifies clean, HTTP/2 on the web root - an anonymous token grants a manifest fetch (HTTP 200) with no credentials - skopeo inspect --no-creds resolves an image and lists its tags That last check is the one that matters: an outside developer with no account can now pull, which was the functional blocker for publishing at all. Plain-HTTP references become HTTPS in the same pass, so OTA downloads stop crossing the network in the clear. Deliberately NOT rewritten: - The public FIPS anchor on port 8444. It is a functional network endpoint every node dials to bootstrap the mesh — closer to Bitcoin Core's hardcoded seeds than to leaked infrastructure. The domain does resolve to the same host, so it could become a hostname, but that adds a DNS dependency to the path used precisely when things are broken. Worth a deliberate decision, not a side effect of this change. - The companion APK on port 2100. The domain returns 404 for that path, so rewriting it would swap a working URL for a broken one. The Releases page does serve (200), which is where the plan already wants those binaries. - releases/app-catalog.json, releases/manifest.json and release-manifest.json. These carry `signature` and `signed_by`; editing their contents invalidates the signature and the fleet refuses artifacts that fail verification. They were rewritten in a first pass and reverted — they must be regenerated and re-signed through the signing ceremony instead, which needs the mnemonic. So the catalog still advertises the old host until that ceremony runs. Nodes resolve images through the signed catalog, not the on-disk manifests, so this commit alone does not change what a node pulls. Verified: archipelago-container 75/75; every manifest still parses with a top-level app block; no signed artifact modified. Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
301 lines
17 KiB
Markdown
301 lines
17 KiB
Markdown
# Bitcoin Multi-Version Support — Design
|
||
|
||
<!-- ════════════════════════════════════════════════════════════════════
|
||
PROGRESS TRACKER / RESUME POINT (keep this current — update each session)
|
||
════════════════════════════════════════════════════════════════════
|
||
**Branch/worktree:** `bitcoin-multi-version` @ `/home/archipelago/Projects/archy-btcver`
|
||
(isolated — never touch `main` or the other agent's branch). All work UNCOMMITTED on
|
||
that branch as of last update.
|
||
|
||
**Last updated:** 2026-06-28 (session 2 — software end-to-end implemented)
|
||
|
||
**Motivation refresh:** BIP-110 signalling makes per-node version *choice* a real
|
||
requirement — runners must be able to pick / pin / switch Core & Knots versions.
|
||
|
||
**User direction this session:** finish the SOFTWARE end-to-end (Phase 1–3 + UI),
|
||
DEFER the Phase 0 image build pipeline. Downgrade policy = **warn + confirm + allow**.
|
||
|
||
### Status by phase
|
||
- [x] **Phase 1 — catalog schema** (`app_catalog.rs`): `CatalogVersion` struct +
|
||
`versions[]` + `catalog_versions()` / `catalog_default_version()` /
|
||
`catalog_image_for_version()` (same-repo guard) DONE. Pin suppresses update badge
|
||
in `available_update_for_app()` DONE. `versions[]` now EMITTED by
|
||
`scripts/generate-app-catalog.sh` (curated `VERSIONS` map) → `releases/app-catalog.json`
|
||
regenerated; bitcoin-core carries its one built version (28.4.0, default). **Knots
|
||
versions[] intentionally empty** (only floating `:latest` exists; design forbids
|
||
advertising floating). More versions light up automatically once Phase 0 builds
|
||
tagged images and they're appended to the `VERSIONS` map.
|
||
- [x] **Phase 2 — install-time selection**: `version_config.rs` (pin/auto-update
|
||
persistence + `is_downgrade()` + `auto_update_apps()`, unit-tested) DONE;
|
||
`install.rs` `persist_install_version_selection()` DONE; `prod_orchestrator.rs`
|
||
pinned-wins resolution DONE. **UI:** `MarketplaceAppDetails.vue` install panel shows
|
||
a version `<select>` (latest pre-selected) when the app offers ≥2 versions — passes
|
||
the choice to `package.install`. (Hidden today since only 1 version exists.)
|
||
- [x] **Phase 3 — in-app switch + auto-update toggle**:
|
||
- `package.versions` RPC (read) + `package.set-config` RPC (write, downgrade-gated)
|
||
→ new `api/rpc/package/set_config.rs`, wired in `mod.rs` + `dispatcher.rs`.
|
||
- Auto-update tick: `run_update_scheduler` now takes the orchestrator + calls
|
||
`apply_per_app_auto_updates()` hourly (opt-in, pin-respecting, catalog-driven).
|
||
- UI: "Version & Updates" card in `appDetails/AppSidebar.vue` (version switch +
|
||
auto-update toggle + downgrade warn/confirm); `rpc-client.ts` + types added.
|
||
- [x] **Phase 0 — image build pipeline**: `scripts/build-bitcoin-image.sh` —
|
||
downloads the OFFICIAL upstream tarball + SHA256SUMS(.asc), verifies SHA-256 **and**
|
||
the OpenPGP signature (fail-closed; pinned release-key fingerprints), builds a
|
||
minimal **rootless** image (debian-slim + verified `bitcoind`/`bitcoin-cli`),
|
||
smoke-tests `--version`, tags + pushes `:<version>`. Validated on Core 31.0
|
||
(pinned-GPG pass, smoke `v31.0.0`). **Published curated set** (registry
|
||
`lfg2025`): Core **31.0, 30.2, 29.3, 27.2, 26.2, 25.2** (28.4 already present —
|
||
kept, not overwritten) + Knots **29.3.knots20260508**. `VERSIONS` map in
|
||
`generate-app-catalog.sh` lists them; catalog regenerated. Adding a future release
|
||
= run the script for it, then prepend it to the map + regenerate.
|
||
|
||
### Verification status
|
||
- `cargo check -p archipelago` GREEN (backend). Frontend `npm run build` GREEN
|
||
(vue-tsc typecheck passes; new RPC strings confirmed in `web/dist`).
|
||
- Unit tests: `version_config` had a pre-existing parallel-test race (shared
|
||
process-global `ARCHIPELAGO_DATA_DIR`) — FIXED with an `ENV_LOCK` mutex + unique
|
||
per-test dirs. `set_config` `image_tag` test added.
|
||
- **Phase 0 images verified end-to-end**: SHA-256 + pinned-maintainer OpenPGP
|
||
signature (deterministic VALIDSIG check), built rootless, smoke-tested, **pushed
|
||
to the live registry** — confirmed remotely: `bitcoin` tags
|
||
{25.2,26.2,27.2,28.4,29.3,30.2,31.0} + `bitcoin-knots:29.3.knots20260508`.
|
||
- **NOT yet verified on `.228`** (CLAUDE.md invariant — do before any tag): install
|
||
bitcoin-core, open its page, switch/pin a version, confirm recreate. All code
|
||
UNCOMMITTED on the branch.
|
||
|
||
### Gotchas captured (for resume)
|
||
- `gpg --verify` exit code is unreliable on multi-sig `SHA256SUMS` — must parse
|
||
`--status-fd` VALIDSIG and require a pinned maintainer fpr (script does this).
|
||
- `podman push` needs the sandbox disabled (`/var/tmp` is RO under the harness
|
||
sandbox) and `--tls-verify=false` (registry serves HTTP). Persistent keyring
|
||
(`BITCOIN_KEYRING_DIR`) avoids flaky per-build keyserver fetches.
|
||
|
||
### Next action when resuming
|
||
1. Re-verify: `cd archy-btcver/core && CARGO_INCREMENTAL=0 cargo check -p archipelago`
|
||
and `cargo test -p archipelago -- version_config set_config`; `cd neode-ui && npm run build`.
|
||
2. Live-verify on `.228`: install bitcoin-core, open its detail page → "Version &
|
||
Updates" card; exercise `package.versions` / `package.set-config` via RPC.
|
||
3. Commit on the branch (checkpoint).
|
||
4. **Phase 0** when greenlit: build+push tagged Core/Knots images, then extend the
|
||
`VERSIONS` map in `scripts/generate-app-catalog.sh` and regenerate the catalog.
|
||
|
||
### Decisions still needed from user (see §6 open questions)
|
||
Curated version set + storage budget (defaulted to current+~3 majors); when to do
|
||
Phase 0 image pipeline; pruned-node downgrade policy refinement (currently warn+confirm
|
||
for all). Auto-update default = OFF (opt-in), as recommended.
|
||
════════════════════════════════════════════════════════════════════ -->
|
||
|
||
**Status:** design (2026-06-22)
|
||
**Goal:** let a user choose *which* version of Bitcoin Core / Bitcoin Knots to
|
||
install (latest pre-selected, older versions in a dropdown), and later switch
|
||
versions or opt into auto-update — all manifest/catalog-driven, all served from
|
||
**our signed registry**, rootless, with **zero data loss** across version
|
||
changes.
|
||
|
||
See also: [`docs/registry-manifest-design.md`](registry-manifest-design.md)
|
||
(catalog distribution + signing this builds on),
|
||
the production test gate (which must be
|
||
green first), `MEMORY → project_decoupled_app_updates`,
|
||
`MEMORY → project_manifest_driven_north_star`.
|
||
|
||
> **Scheduling:** this is net-new scope. It lands **after** the production test
|
||
> gate (`tests/lifecycle/run-20x.sh`) is green on `.228` + `.198`. The data-
|
||
> preservation invariant (downgrade vs. chainstate) is the highest risk here.
|
||
|
||
---
|
||
|
||
## 1. Where we are today
|
||
|
||
### Image source / build
|
||
| Thing | Today |
|
||
|-------|-------|
|
||
| `apps/bitcoin-core/Dockerfile` | `FROM bitcoin/bitcoin:24.0` — a **community** image, **stale** (manifest says 28.4), no project-official Docker image exists |
|
||
| `apps/bitcoin-knots/` | **no Dockerfile** — `:latest` is built/pushed by hand |
|
||
| Registry | `scripts/image-versions.sh` → `ARCHY_REGISTRY="source.archipelago-foundation.org/lfg2025"`; only `BITCOIN_KNOTS_IMAGE=…/bitcoin-knots:latest` pinned, no Core pin |
|
||
| Tags in registry | **one tag per image**. No historical versions. |
|
||
|
||
### Version pinning
|
||
- `apps/bitcoin-core/manifest.yml` → `…/bitcoin:28.4` (pinned).
|
||
- `apps/bitcoin-knots/manifest.yml` → `…/bitcoin-knots:latest` (**floating** — a
|
||
liability for reproducibility and for "switch back to the version I had").
|
||
- `core/archipelago/src/container/app_catalog.rs` + `app-catalog/catalog.json`:
|
||
signed, hourly-fetched, carries `version` (badge text) + `image`.
|
||
`catalog_image_override()` overrides the manifest image **only if same-repo**.
|
||
`available_update_for_app()` already ignores floating tags for update
|
||
detection.
|
||
|
||
### Install path
|
||
- `prod_orchestrator.rs::install_fresh()` resolves the image as
|
||
**manifest image → catalog override → pull**. There is **no per-install
|
||
version parameter** — `orchestrator.install(app_id)` takes only the id.
|
||
- RPC `package.install` (`api/rpc/package/install.rs`) *accepts* `dockerImage` /
|
||
`version` params but for orchestrator-managed apps (bitcoin-core / bitcoin-knots
|
||
are allowlisted) it **ignores them** and lets the orchestrator resolve.
|
||
- **Conflict guard** (`prod_orchestrator.rs` ~1306–1325): core and knots may not
|
||
run simultaneously. Must be preserved by everything below.
|
||
|
||
### UI
|
||
- Install is **one-click, no modal** (`MarketplaceAppDetails.vue::installApp()`).
|
||
- Update badge + "Update to X" already exist (`appDetails/AppHeroSection.vue`,
|
||
RPC `package.update`).
|
||
- **No** Bitcoin-specific settings panel; all apps share `AppSidebar.vue`.
|
||
- Per-app config persisted **only at install time** as `containerConfig` →
|
||
`/var/lib/archipelago/app-configs/<id>.json`. **No post-install set-config RPC.**
|
||
|
||
---
|
||
|
||
## 2. Source-of-truth decision: official upstream → our registry
|
||
|
||
We use the **official releases** as upstream provenance, but nodes only ever pull
|
||
from our registry. Nodes do **not** fetch bitcoin.org / GitHub at install time —
|
||
that would break rootless/offline installs and the signed-registry trust model,
|
||
and neither project publishes an official Docker image anyway.
|
||
|
||
**Official sources (verified):**
|
||
|
||
| Impl | Index | Per-version asset pattern |
|
||
|------|-------|---------------------------|
|
||
| Bitcoin Core | [bitcoincore.org/en/releases](https://bitcoincore.org/en/releases/) · [github bitcoin/bitcoin](https://github.com/bitcoin/bitcoin/releases) | `https://bitcoincore.org/bin/bitcoin-core-<ver>/bitcoin-<ver>-x86_64-linux-gnu.tar.gz` + `SHA256SUMS` + `SHA256SUMS.asc` |
|
||
| Bitcoin Knots | [github bitcoinknots/bitcoin](https://github.com/bitcoinknots/bitcoin/releases) · [bitcoinknots.org/files](https://bitcoinknots.org/) | `https://bitcoinknots.org/files/<maj>.x/<ver>/bitcoin-<ver>-x86_64-linux-gnu.tar.gz` (`<ver>` e.g. `29.3.knots20260508`) |
|
||
|
||
Both ship **signed binary tarballs** with multi-builder Guix attestations
|
||
(`SHA256SUMS.asc`). The build pipeline verifies these **once, at build**; our DHT
|
||
Phase 0 registry signature then carries provenance to the fleet.
|
||
|
||
> Knots version strings embed a build date (`29.3.knots20260508`). Treat the full
|
||
> string as the tag; surface a friendly `29.3` + date in the UI.
|
||
|
||
---
|
||
|
||
## 3. Design
|
||
|
||
### Phase 0 — Reproducible, verified image pipeline *(prerequisite)*
|
||
|
||
New `scripts/build-bitcoin-image.sh <impl> <version>` that, per version:
|
||
|
||
1. Downloads the official tarball + `SHA256SUMS(.asc)` (GitHub release assets are
|
||
an identical mirror → fallback).
|
||
2. Verifies SHA256 **and** the Guix/builder GPG signatures. **Fail closed.**
|
||
3. Builds a minimal **rootless** image: pin a small base, unpack
|
||
`bitcoind`/`bitcoin-cli`. Keep the existing entrypoint probe
|
||
(`command -v bitcoind || find /opt -path '*/bin/bitcoind'`) so per-version
|
||
layout differences don't break startup.
|
||
4. Tags + pushes `:<version>` **and** updates the default pin (`:latest` /
|
||
`:28.4`-style) to the registry.
|
||
|
||
**Curate, don't mirror everything.** Publish a bounded set (proposal: current +
|
||
last ~3 majors), e.g. Core `31.0, 30.0, 29.3, 28.4, 27.2` and Knots
|
||
`29.3.knots…, 28.1.knots…, 27.1.knots…`. **`log` / document dropped versions** —
|
||
silent truncation reads as "all versions supported" when it isn't.
|
||
|
||
Also fixes existing debt: replaces the stale community `FROM bitcoin/bitcoin:24.0`
|
||
and gives Knots a real Dockerfile + non-floating tags.
|
||
|
||
### Phase 1 — Version catalog (signed, registry-distributed)
|
||
|
||
Extend `AppCatalogEntry` (forward-compatible — no `deny_unknown_fields`, old nodes
|
||
ignore it):
|
||
|
||
```jsonc
|
||
"bitcoin-core": {
|
||
"version": "31.0", // default / latest (existing field)
|
||
"image": "…/bitcoin:31.0", // existing
|
||
"versions": [ // NEW
|
||
{ "version": "31.0", "image": "…/bitcoin:31.0", "default": true },
|
||
{ "version": "30.0", "image": "…/bitcoin:30.0" },
|
||
{ "version": "28.4", "image": "…/bitcoin:28.4", "deprecated": true, "eol": "2026-...." }
|
||
]
|
||
}
|
||
```
|
||
|
||
Published to `releases/app-catalog.json`, signed by the existing release-root
|
||
mechanism. This is the **single source of truth** the UI reads for "what can I
|
||
install / switch to," and third-party-registry apps inherit the capability for
|
||
free. `version`/`image` stay as the default for back-compat.
|
||
|
||
### Phase 2 — Install-time version selection
|
||
|
||
- **Orchestrator:** add `install_with_image(app_id, Option<image_tag>)` (or an
|
||
optional arg on `install`). When a tag is supplied, **validate same-repo**
|
||
against the manifest (reuse `image_without_registry_or_tag()`), then override in
|
||
`install_fresh()`. Default path unchanged. Preserve the core/knots conflict
|
||
guard.
|
||
- **RPC:** thread the selected version/image from `package.install` into the
|
||
orchestrator for the allowlisted apps (the param is already received — just not
|
||
forwarded).
|
||
- **UI:** the first **install modal** in the app — latest pre-selected, dropdown
|
||
of `versions[]`, deprecated/EOL badges on old entries. On confirm, pass the
|
||
chosen version to `package.install`.
|
||
|
||
### Phase 3 — In-app version switch + auto-update toggle
|
||
|
||
- **UI:** a Bitcoin **"Version & Updates"** card (conditional in `AppSidebar.vue`
|
||
for `bitcoin-core` / `bitcoin-knots`): current version, a switch dropdown, and
|
||
an **auto-update-to-latest** toggle.
|
||
- **Switch = controlled re-pull/recreate** reusing the `package.update`
|
||
machinery but targeting an arbitrary (incl. older) tag → effectively
|
||
`package.set-version`.
|
||
- **Persistence:** new `package.set-config` RPC writing the existing
|
||
`app-configs/<id>.json` (`{ pinnedVersion, autoUpdate }`).
|
||
- **Auto-update:** the existing hourly catalog check, when `autoUpdate:true`,
|
||
triggers `package.update` to the catalog default. A pinned version **suppresses
|
||
the update badge**.
|
||
|
||
---
|
||
|
||
## 4. Invariants & safety rails
|
||
|
||
- **Rootless only.** Pipeline images and run path stay rootless; no Docker-socket,
|
||
no privileged.
|
||
- **No data loss across version change.** Preserve `/var/lib/archipelago/bitcoin`,
|
||
secrets (`bitcoin-rpc-password`, `…-rpcauth`), ports, and the adoption container
|
||
name on every install / switch / update.
|
||
- **⚠️ Downgrade vs. chainstate (highest risk).** Bitcoin Core refuses to start on
|
||
a chainstate written by a *newer* version unless reindexed (expensive, or data
|
||
loss on a pruned node). The UI **must** warn loudly on downgrade; the
|
||
orchestrator should gate/confirm it and never silently wipe. Pruned nodes can't
|
||
simply `-reindex`.
|
||
- **Core ⇄ Knots switch** stays governed by the existing conflict guard; treat an
|
||
impl switch as distinct from a version switch.
|
||
- **Floating tags** (`latest`) are never advertised as a selectable "version" and
|
||
never counted as an available update (already handled by
|
||
`available_update_for_app`).
|
||
- **Verify on a real node** (`.228` then `.198`) and pass `run-20x` before any
|
||
tag.
|
||
|
||
---
|
||
|
||
## 5. Files / seams (no code yet)
|
||
|
||
| Concern | File |
|
||
|---------|------|
|
||
| Image build/push | new `scripts/build-bitcoin-image.sh`; `apps/bitcoin-core/Dockerfile`; new `apps/bitcoin-knots/Dockerfile`; `scripts/image-versions.sh` |
|
||
| Catalog schema | `core/archipelago/src/container/app_catalog.rs`; `releases/app-catalog.json` (+ `app-catalog/catalog.json`) |
|
||
| Install override | `core/archipelago/src/container/prod_orchestrator.rs` (`install` / `install_fresh`); `api/rpc/package/install.rs`; `api/rpc/dispatcher.rs` |
|
||
| Switch / set-config RPC | `api/rpc/package/update.rs`; new `package.set-config` handler; `app-configs/<id>.json` |
|
||
| Install modal | `neode-ui/src/views/MarketplaceAppDetails.vue`; new `…/marketplace/AppInstallModal.vue` |
|
||
| Version & Updates card | `neode-ui/src/views/appDetails/AppSidebar.vue`; `neode-ui/src/api/rpc-client.ts`; `neode-ui/src/types/api.ts` |
|
||
|
||
---
|
||
|
||
## 6. Open questions
|
||
|
||
1. **Curated version set** — how many majors back do we host, and storage budget
|
||
on the registry?
|
||
2. **Multi-arch** — fleet is x86_64 today; do any nodes need arm64 images?
|
||
3. **Pruned-node downgrade policy** — block outright, or allow with an explicit
|
||
"this will require re-sync / may lose pruned data" confirmation?
|
||
4. **Auto-update default** — off (opt-in) for a consensus-critical app like
|
||
Bitcoin? (Recommended: **off**, explicit opt-in.)
|
||
5. **Knots date-suffix UX** — how to display `29.3.knots20260508` cleanly.
|
||
|
||
---
|
||
|
||
## Sources
|
||
|
||
- [Bitcoin Core releases](https://bitcoincore.org/en/releases/)
|
||
- [bitcoin/bitcoin releases](https://github.com/bitcoin/bitcoin/releases)
|
||
- [bitcoinknots/bitcoin releases](https://github.com/bitcoinknots/bitcoin/releases)
|
||
- [Bitcoin Knots](https://bitcoinknots.org/)
|
||
- [bitcoin.org version history](https://bitcoin.org/en/version-history)
|