archy/scripts/build-bitcoin-image.sh
archipelago 6aa74c7386 feat(bitcoin): multi-version support for Core & Knots (install/switch/pin/auto-update)
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>
2026-06-28 18:46:17 -04:00

167 lines
6.9 KiB
Bash
Executable File
Raw Blame History

This file contains ambiguous Unicode characters

This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.

#!/usr/bin/env bash
# build-bitcoin-image.sh — reproducible, verified, rootless Bitcoin image builder
# (docs/bitcoin-multi-version-design.md §3 Phase 0).
#
# Downloads an OFFICIAL upstream release tarball + SHA256SUMS(.asc), verifies the
# SHA-256 AND the OpenPGP signature (fail-closed), then builds a minimal rootless
# image and tags/pushes it to our registry as :<version>. Nodes only ever pull
# from our registry — they never fetch bitcoincore.org / bitcoinknots.org. The
# DHT Phase-0 catalog signature then carries provenance to the fleet.
#
# Usage:
# scripts/build-bitcoin-image.sh core 31.0
# scripts/build-bitcoin-image.sh knots 29.3.knots20260508
# NO_PUSH=1 scripts/build-bitcoin-image.sh core 31.0 # build + verify only
#
# Env:
# NO_PUSH=1 build + verify, do not push
# ALLOW_UNSIGNED=1 skip the GPG signature check (NOT for production)
# REQUIRE_PINNED=1 additionally require a signature from a pinned release key
# ARCHY_REGISTRY overrides the push registry (default from image-versions.sh)
set -euo pipefail
IMPL="${1:?usage: build-bitcoin-image.sh <core|knots> <version>}"
VERSION="${2:?usage: build-bitcoin-image.sh <core|knots> <version>}"
ROOT="$(cd "$(dirname "${BASH_SOURCE[0]}")/.." && pwd)"
# shellcheck disable=SC1091
source "$ROOT/scripts/image-versions.sh"
REGISTRY="${ARCHY_REGISTRY:?ARCHY_REGISTRY unset}"
# Pinned upstream release-signing fingerprints (REQUIRE_PINNED=1 enforces these).
# Bitcoin Core SHA256SUMS for 25.x31.x are signed by these maintainers; Knots by
# Luke Dashjr. Verified against the live signatures at build time.
# SHA256SUMS is a MULTI-signature file (every Guix builder signs it). We require
# a valid signature from at least one of these well-known release maintainers —
# the ones who sign every Bitcoin Core / Knots SHA256SUMS — and ignore builder
# sigs whose keys we don't hold. Both the primary fpr and the signing-subkey fpr
# that may appear in VALIDSIG are listed.
CORE_SIGNERS=(
"0CCBAAFD76A2ECE2CCD3141DE2FFD5B1D88CA97D" # fanquake (primary)
"E777299FC265DD04793070EB944D35F9AC3DB76A" # fanquake (subkey)
"152812300785C96444D3334D17565732E08E5E41" # achow101
"71A3B16735405025D447E8F274810B012346C9A6" # laanwj (older releases)
)
KNOTS_SIGNERS=(
"1A3E761F19D2CC7785C5502EA291A2C45D0C504A" # Luke Dashjr
)
case "$IMPL" in
core)
TARBALL="bitcoin-${VERSION}-x86_64-linux-gnu.tar.gz"
BASEURL="https://bitcoincore.org/bin/bitcoin-core-${VERSION}"
IMAGE_REPO="bitcoin"
SIGNERS=("${CORE_SIGNERS[@]}")
;;
knots)
MAJOR="${VERSION%%.*}"
TARBALL="bitcoin-${VERSION}-x86_64-linux-gnu.tar.gz"
BASEURL="https://bitcoinknots.org/files/${MAJOR}.x/${VERSION}"
IMAGE_REPO="bitcoin-knots"
SIGNERS=("${KNOTS_SIGNERS[@]}")
;;
*) echo "impl must be 'core' or 'knots'" >&2; exit 2 ;;
esac
TAG="${REGISTRY}/${IMAGE_REPO}:${VERSION}"
WORK="$(mktemp -d)"
trap 'rm -rf "$WORK"' EXIT
cd "$WORK"
# podman/skopeo stage image copies under TMPDIR (default /var/tmp). Point it at a
# writable dir so `podman push` works in sandboxes where /var/tmp is read-only.
export TMPDIR="$WORK/tmp"; mkdir -p "$TMPDIR"
echo "==> [$IMPL $VERSION] downloading from $BASEURL"
curl -fsSL -o "$TARBALL" "${BASEURL}/${TARBALL}"
curl -fsSL -o SHA256SUMS "${BASEURL}/SHA256SUMS"
curl -fsSL -o SHA256SUMS.asc "${BASEURL}/SHA256SUMS.asc"
echo "==> verifying SHA-256"
# SHA256SUMS lists every platform; check only our tarball line. Fail-closed.
grep " ${TARBALL}\$" SHA256SUMS | sha256sum -c - \
|| { echo "FATAL: SHA-256 mismatch for ${TARBALL}" >&2; exit 1; }
if [[ "${ALLOW_UNSIGNED:-0}" == "1" ]]; then
echo "==> WARNING: ALLOW_UNSIGNED=1 — skipping GPG verification (NOT production)"
else
echo "==> verifying OpenPGP signature on SHA256SUMS"
# A persistent, pre-seeded keyring (BITCOIN_KEYRING_DIR) makes verification
# reliable across many builds — keyserver fetches are flaky when each build
# starts from an empty keyring. Falls back to a per-build keyring + fetch.
if [[ -n "${BITCOIN_KEYRING_DIR:-}" ]]; then
export GNUPGHOME="$BITCOIN_KEYRING_DIR"
else
export GNUPGHOME="$WORK/gnupg"
fi
mkdir -p "$GNUPGHOME"; chmod 700 "$GNUPGHOME"
# Ensure each pinned maintainer key is present (best-effort fetch).
for kid in "${SIGNERS[@]}"; do
gpg --list-keys "$kid" >/dev/null 2>&1 && continue
for ks in hkps://keys.openpgp.org hkps://keyserver.ubuntu.com hkp://keyserver.ubuntu.com; do
gpg --keyserver "$ks" --recv-keys "$kid" >/dev/null 2>&1 && break || true
done
done
# SHA256SUMS carries many builder signatures; `gpg --verify`'s exit code is
# unreliable for multi-sig files (one unheld key flips it). Instead collect the
# VALIDSIG fingerprints via --status-fd and REQUIRE at least one from a pinned
# maintainer. Fail-closed otherwise.
# `|| true`: gpg exits non-zero on multi-sig files even with good sigs; we
# judge trust from VALIDSIG below, not the exit code (and set -e/pipefail would
# otherwise abort here).
VALID_FPRS="$(gpg --status-fd=1 --verify SHA256SUMS.asc SHA256SUMS 2>/dev/null \
| awk '/^\[GNUPG:\] VALIDSIG/ {print $3; print $NF}' | sort -u || true)"
ok=0; matched=""
for fpr in $VALID_FPRS; do
for want in "${SIGNERS[@]}"; do
[[ "$fpr" == "$want" ]] && { ok=1; matched="$fpr"; }
done
done
if [[ "$ok" != "1" ]]; then
echo "FATAL: no valid signature from a pinned release maintainer on SHA256SUMS" >&2
echo " valid signers seen: ${VALID_FPRS:-none}" >&2
exit 1
fi
echo " verified: valid maintainer signature ($matched)"
fi
echo "==> extracting binaries"
tar -xzf "$TARBALL"
SRC="bitcoin-${VERSION}"
[[ -x "${SRC}/bin/bitcoind" ]] || { echo "FATAL: bitcoind missing in tarball" >&2; exit 1; }
mkdir -p ctx/bin
cp "${SRC}/bin/bitcoind" "${SRC}/bin/bitcoin-cli" ctx/bin/
echo "==> building rootless image $TAG"
cat > ctx/Containerfile <<'EOF'
FROM debian:bookworm-slim
RUN set -eux; \
apt-get update; \
apt-get install -y --no-install-recommends ca-certificates; \
rm -rf /var/lib/apt/lists/*; \
useradd -m -u 1000 -s /bin/bash bitcoin; \
mkdir -p /home/bitcoin/.bitcoin; \
chown -R bitcoin:bitcoin /home/bitcoin
COPY bin/bitcoind /usr/local/bin/bitcoind
COPY bin/bitcoin-cli /usr/local/bin/bitcoin-cli
RUN chmod 0755 /usr/local/bin/bitcoind /usr/local/bin/bitcoin-cli
USER bitcoin
WORKDIR /home/bitcoin
VOLUME ["/home/bitcoin/.bitcoin"]
EXPOSE 8332 8333
ENTRYPOINT ["bitcoind"]
EOF
podman build -t "$TAG" ctx
echo "==> smoke test (bitcoind --version)"
podman run --rm --entrypoint bitcoind "$TAG" --version | head -1
if [[ "${NO_PUSH:-0}" == "1" ]]; then
echo "==> NO_PUSH=1 — built + verified $TAG (not pushed)"
else
echo "==> pushing $TAG"
# The lfg2025 registry serves plain HTTP (matches image_uses_insecure_registry
# in the Rust runtime). PODMAN_PUSH_TLS_VERIFY=true forces TLS for HTTPS regs.
podman push --tls-verify="${PODMAN_PUSH_TLS_VERIFY:-false}" "$TAG"
echo "==> pushed $TAG"
fi