Lets a node runner choose which Bitcoin Core / Knots version to install (latest pre-selected), then switch, pin, or opt into auto-update from the app's interface — all manifest/catalog-driven, rootless, signed-registry, zero-data-loss. Motivated by upcoming BIP-110 signalling: runners need a real choice of software version. Backend: - version_config.rs: per-app pin + auto-update persistence (atomic, merge- preserving), downgrade detection, auto-update enumeration (+ unit tests). - app_catalog.rs: CatalogVersion / versions[] schema, catalog_versions(), catalog_image_for_version() (same-repo guard); a pin suppresses the update badge. - prod_orchestrator.rs: pinned version wins over the catalog default on every install/recreate. - install.rs: install-time `version` param persisted (default = unpinned). - set_config.rs: package.versions (read) + package.set-config (write) RPCs; downgrade is gated behind explicit confirm (warn + confirm + allow). - update.rs/main.rs: hourly per-app auto-update tick via the orchestrator (opt-in, pin-respecting); fix handle_package_update to be non-fatal for orchestrator-managed apps lacking a catalog primary image (bitcoin-core). UI: - MarketplaceAppDetails.vue: install-time version selector (shown when an app offers >=2 versions). - appDetails/AppSidebar.vue: "Version & Updates" card (switch / pin / auto- update toggle / downgrade warning), per app. - rpc-client.ts + en.json: RPC methods, types, strings. Phase 0 image pipeline: - scripts/build-bitcoin-image.sh: download official tarball + SHA256SUMS(.asc), verify SHA-256 + pinned-maintainer OpenPGP signature (fail-closed), build a minimal rootless image, smoke-test, tag + push. - apps/bitcoin-core/Dockerfile rewritten (drops stale community base); apps/bitcoin-knots/Dockerfile added. - generate-app-catalog.sh: emit curated versions[]; published + catalog now offers Core 25.2/26.2/27.2/28.4/29.3/30.2/31.0 + Knots 29.3.knots20260508. docs/bitcoin-multi-version-design.md: live progress tracker. Co-Authored-By: Claude Opus 4.8 (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),
|
||
[`docs/PRODUCTION-MASTER-PLAN.md`](PRODUCTION-MASTER-PLAN.md) (gate that 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="146.59.87.168:3000/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)
|