diff --git a/README.md b/README.md index e39dbec..a309981 100644 --- a/README.md +++ b/README.md @@ -39,8 +39,10 @@ through a browser-based terminal. | --- | --- | | Spaces image | `docker.io/horologger/spaces` | | PostgreSQL image | `docker.io/postgres:16.3` | -| Indexer-Go image | `docker.io/golang:1.23-alpine` | -| Explorer-UI image | `docker.io/node:20-alpine` | +| Subspaces image | `docker.io/horologger/subs:v0.1.0` | +| Certrelay image | `docker.io/horologger/certrelay:v0.2.3` | +| Indexer image | _**TODO** — prebuilt explorer-indexer image, not yet produced_ | +| Explorer image | _**TODO** — prebuilt explorer (SvelteKit) image_ | | Architectures | `linux/amd64`, `linux/arm64` | | Entrypoint | StartOS-managed (image entrypoints are **not** used directly) | @@ -48,10 +50,15 @@ The Spaces image bundles `spaced`, `space-cli`, `bitcoin-cli`, `gotty`, `node`, `npm`, `screen`, and a small shell environment. StartOS ignores the Spaces image's `docker_entrypoint.sh`; daemons are defined in `startos/main.ts`. The PostgreSQL image is the upstream `postgres:16.3`, launched via its official -`docker-entrypoint.sh`. The Indexer-Go image is the upstream -`golang:1.23-alpine`, used both to compile the indexer's `sync` binary + -`goose` migrator (one-time, at install) and to run the compiled binaries on -each start. +`docker-entrypoint.sh`. + +> **Note (build-from-source removed):** earlier builds compiled the Go indexer +> and the SvelteKit explorer from source inside `golang:1.23-alpine` and +> `node:20-alpine` builder images. Those images and their build chains were +> removed to shrink the `.s9pk` and eliminate the multi-minute first-enable +> compile. They will be replaced with prebuilt images (see the **TODO** rows +> above). Until then, enabling the embedded explorer starts **PostgreSQL only** +> — the indexer and explorer-UI daemons are stubbed out in `startos/main.ts`. ## Volume and Data Layout @@ -61,14 +68,13 @@ each start. | `/data/mainnet/.cookie` | `main` | Spaced RPC cookie (auto-generated by `spaced` at startup) | | `/data/store.json` | `main` | StartOS-managed credentials (web-UI password, bitcoind RPC user/password, PostgreSQL user/password/database) | | `/data/postgres` | `main` | PostgreSQL data directory (`PGDATA`). Owned by uid 999 (`postgres`). The whole `main` volume is backed up, so the database is included. | -| `/data/explorer-indexer` | `main` | Spaces-protocol explorer-indexer source (`spacesprotocol/explorer-indexer`), fetched from GitHub on first start. Contains `cmd/sync/`, `pkg/`, `sql/schema/`, `go.mod`, etc. | -| `/data/explorer-indexer/bin/sync` | `main` | Compiled indexer sync binary (`go build ./cmd/sync`). | -| `/data/explorer-indexer/bin/goose` | `main` | Compiled goose migrator (`go install github.com/pressly/goose/v3/cmd/goose`). Used by `indexer-migrate` to apply `sql/schema/*.sql`. | -| `/data/explorer-indexer/.installed-sha` | `main` | Marker file recording the pinned commit + build-ID currently extracted. If the build-ID in the package code differs from the marker, the next start wipes and re-fetches. | -| `/data/explorer-ui` | `main` | SvelteKit explorer source (`randomlogin/explorer`), fetched on first explorer-enable. Contains `src/`, `static/`, `server.js`, `package.json`, etc. | -| `/data/explorer-ui/build` | `main` | SvelteKit node-adapter build output. `node build` (i.e. `node /data/explorer-ui/build`) runs the production server. | -| `/data/explorer-ui/node_modules` | `main` | npm dependencies (~300-500 MB). Re-fetched on `Reset Explorer UI State`. | -| `/data/explorer-ui/.installed-sha` | `main` | Marker file for the pinned explorer commit + build-ID. | +| `/data/explorer-indexer` | `main` | **TODO** — reserved for the prebuilt indexer's runtime data. The build-from-source layout (source, `bin/sync`, `bin/goose`, marker) is gone; `reset-indexer-state` still wipes this path. | +| `/data/explorer-ui` | `main` | **TODO** — reserved for the prebuilt explorer's runtime data. The build-from-source layout (source, `build/`, `node_modules`, marker) is gone; `reset-explorer-state` still wipes this path. | +| `/data/subspaces/data` | `main` | `subs` runtime data (`SUBS_DATA_DIR`) — handles, proofs, config.db. Persists across restarts and enable/disable toggles. | +| `/data/subspaces/prover` | `main` | Working directory for the `subs-prover` daemon. | +| `/data/subspaces/registry` | `main` | Working directory for the `registry-server` daemon. | +| `/data/certrelay/data` | `main` | Certrelay data directory (`CERTRELAY_DATA_DIR`) — anchors, peer state, sqlite. | +| `/data/bin/fabric` | `main` | The `fabric` CLI, copied from the certrelay image on each start so the gotty terminal (which has `/data/bin` on PATH) can resolve handles. | ## Installation and First-Run Flow @@ -92,8 +98,16 @@ On the first install, StartOS: gotty terminal's `spaces` alias and the indexer share a single auth path. 6. Seeds `store.json.enableExplorer = false`. The embedded explorer (PostgreSQL + Go indexer) is **opt-in**. -7. Launches `spaced` as a managed daemon (no `screen`, no shell auto-start) +7. Seeds `store.json` certrelay defaults: `certrelaySelfUrl = + https://certrelay.spacesops.com`, `certrelayBootstrap = false`, + `certrelayHealthcheckHandle = account-digital-useful.genesis@key`. +8. Launches `spaced` as a managed daemon (no `screen`, no shell auto-start) and the `gotty` web terminal once the bashrc oneshot completes. +9. Runs `certrelay-setup` (creates `/data/certrelay/data`, copies the `fabric` + CLI to `/data/bin/fabric`) and launches the **certrelay** daemon on 7778. + Certrelay is **always on**; its `CERTRELAY_SPACED_RPC_URL` uses the local + spaced credentials from `store.spacedAuth`. Configure its self URL etc. via + the **Configure Certrelay** action. The user is **not** prompted to choose a chain or RPC mode — this package is mainnet-only. @@ -101,31 +115,20 @@ mainnet-only. ### Enabling the embedded explorer Run the **Enable Embedded Explorer** action. That flips -`store.json.enableExplorer = true` and the service auto-restarts with the full -daemon graph: +`store.json.enableExplorer = true` and the service auto-restarts: 8. Postgres-chown oneshot creates `/data/postgres` with correct ownership. 9. PostgreSQL daemon starts on loopback `127.0.0.1:5432`. -10. `indexer-fetch` downloads the pinned `spacesprotocol/explorer-indexer` - tarball into `/data/explorer-indexer` and applies two compatibility - patches for our older spaced binary (see Limitations). -11. `indexer-build` runs `CGO_ENABLED=0 go build -o /data/explorer-indexer/bin/sync ./cmd/sync` - and `GOBIN=/data/explorer-indexer/bin go install github.com/pressly/goose/v3/cmd/goose@v3.24.3`. -12. `indexer-cleanup-legacy` checks for and removes leftover TS-indexer schema - if present (from earlier package versions). -13. `indexer-migrate` runs `goose ... up` to apply `sql/schema/*.sql`. -14. `indexer` daemon (`/data/explorer-indexer/bin/sync`) starts polling - bitcoind + spaced and writes blocks, transactions, spaces, rollouts, etc. - to PostgreSQL. -15. `explorer-fetch` downloads the pinned `randomlogin/explorer` tarball into - `/data/explorer-ui` (skipped if marker already matches). -16. `explorer-install` runs `npm install` + `PUBLIC_BTC_NETWORK=mainnet npm run build` - to produce `/data/explorer-ui/build/`. Skipped if `build/index.js` already - exists. First-time install can take 1-3 minutes for the npm download. -17. `explorer-ui` daemon (`node /data/explorer-ui/build`) starts on port 3000. - The SvelteKit app reads exclusively from PostgreSQL — it does not talk to - spaced or bitcoind directly. The UI link will appear in the StartOS - dashboard alongside the gotty terminal. + +> **TODO (prebuilt images pending):** the indexer and explorer-UI daemons are +> currently **stubbed out** in `startos/main.ts`. The build-from-source chain +> (`indexer-fetch` → `indexer-build` → `indexer-migrate` → `indexer`, and +> `explorer-fetch` → `explorer-install` → `explorer-ui`) was removed to shrink +> the `.s9pk` and cut startup time. Until prebuilt images are wired in, +> enabling the explorer starts **PostgreSQL only**; nothing populates the DB +> and the Explorer Web UI (port 3000) will not resolve. The `show-db-credentials`, +> `reset-db-state`, `reset-indexer-state`, and `reset-explorer-state` actions +> remain available. Run **Disable Embedded Explorer** to turn it back off; on-disk data at `/data/postgres`, `/data/explorer-indexer`, and `/data/explorer-ui` is @@ -146,7 +149,11 @@ preserved. | Interface | Port | Protocol | Exposure | Notes | | --- | --- | --- | --- | --- | | Space-CLI Web UI (gotty terminal) | 8080 | HTTP | LAN / `.local` / Tor / clearnet (via StartOS) | Basic auth: `admin:` | -| Explorer Web UI | 3000 | HTTP | LAN / `.local` / Tor / clearnet (via StartOS) | SvelteKit explorer. No built-in auth — relies on StartOS interface exposure controls. Only useful while the embedded explorer is enabled (otherwise nothing is listening). | +| Explorer Web UI | 3000 | HTTP | LAN / `.local` / Tor / clearnet (via StartOS) | SvelteKit explorer. **TODO** — daemon stubbed pending the prebuilt explorer image; the interface is still registered but nothing listens yet. | +| Subspaces Web UI | 7777 | HTTP | LAN / `.local` / Tor / clearnet (via StartOS) | `subs` UI. No built-in auth. Only useful while Subspaces is enabled. | +| Subspaces Prover | 8888 | HTTP | LAN / `.local` / Tor / clearnet (via StartOS) | `subs-prover` RISC Zero prover server (no GPU). Its own StartOS interface. Only useful while Subspaces is enabled. | +| Subspaces Registry | 8081 | HTTP | LAN / `.local` / Tor / clearnet (via StartOS) | `registry-server` for publishing/resolving handles (remapped from its upstream default 8080 to avoid colliding with the gotty terminal on 8080). Its own StartOS interface. Only useful while Subspaces is enabled. | +| Certrelay | 7778 | HTTP | LAN / `.local` / Tor / clearnet (via StartOS) | `certrelay` server (always on). Serves cryptographic proofs binding handles to owner keys. **Setup:** run **Configure Certrelay** and set `CERTRELAY_SELF_URL` to the publicly visible URL StartOS exposes for this interface. | | Spaced RPC | 7225 | HTTP JSON-RPC | **Loopback only** | Internal use, static-cred-authenticated via `SPACED_RPC_USER` / `SPACED_RPC_PASSWORD` from `store.json.spacedAuth` | | PostgreSQL | 5432 | Postgres wire protocol | **Loopback only** (`listen_addresses=127.0.0.1`) | Username/password from `store.json.dbAuth`; full `DB_URL` exported into the web terminal | @@ -168,6 +175,10 @@ preserved. | `reset-db-state` | Reset Database State | Enabled | Any | Deletes `/data/postgres` so the next start re-initializes an empty database. Warning-gated. `store.json` is preserved. | | `reset-indexer-state` | Reset Indexer State | Enabled | Any | Deletes `/data/explorer-indexer` so the next start re-fetches the indexer source and rebuilds the sync + goose binaries. Useful when bumping the pinned commit. PostgreSQL data is preserved. Warning-gated. | | `reset-explorer-state` | Reset Explorer UI State | Enabled | Any | Deletes `/data/explorer-ui` so the next start re-fetches the SvelteKit source and rebuilds the bundle. PostgreSQL data is preserved. Warning-gated. | +| `enable-subspaces` | Enable Subspaces | Enabled (hidden when already on) | Any | Sets `store.enableSubspaces = true` and triggers a service restart so the Subspaces chain (build → wallet → subs-prover → subs) takes effect. | +| `disable-subspaces` | Disable Subspaces | Enabled (hidden when already off) | Any | Sets `store.enableSubspaces = false` and triggers a service restart so the subs and subs-prover daemons stop. On-disk data at `/data/subspaces` is preserved. | +| `reset-subspaces-state` | Reset Subspaces State | Enabled | Any | Deletes `/data/subspaces` (runtime data only — binaries ship in the image). Use to wipe local handles/proofs. The spaces wallet on spaced is preserved. Warning-gated. | +| `configure-certrelay` | Configure Certrelay | Enabled | Any | Sets `CERTRELAY_SELF_URL` (text), `CERTRELAY_BOOTSTRAP` (toggle), and `CERTRELAY_HEALTHCHECK_HANDLE` (text) in `store.json`; prefilled with current values. Saving restarts the service so certrelay picks up the new config. | ## Backups and Restore @@ -182,11 +193,13 @@ the same idempotent init logic runs and reuses the existing credentials in | --- | --- | --- | --- | | `postgres` (daemon `ready`) | Database | 60 s | TCP listen on `127.0.0.1:5432` | | `spaced` (daemon `ready`) | Spaced RPC | 120 s | TCP listen on `127.0.0.1:7225` | -| `indexer` (daemon `ready`) | Indexer Process | 60 s | Daemon liveness only — health is reported by `indexer-sync` below | -| `explorer-ui` (daemon `ready`) | Explorer Web UI | 60 s | TCP listen on `0.0.0.0:3000`. Only present when embedded explorer is enabled. | +| `subs-prover` (daemon `ready`) | Subspaces Prover | 60 s | TCP listen on `0.0.0.0:8888`. Only present when Subspaces is enabled. | +| `subs-registry` (daemon `ready`) | Subspaces Registry | 60 s | TCP listen on `0.0.0.0:8081`. Only present when Subspaces is enabled. | +| `subs` (daemon `ready`) | Subspaces Web UI | 60 s | TCP listen on `0.0.0.0:7777`. Only present when Subspaces is enabled. | +| `certrelay` (daemon `ready`) | Certrelay | 120 s | TCP listen on `0.0.0.0:7778`. Always on. Polled every 30 s. | | `web-terminal` (daemon `ready`) | Web Interface | default | TCP listen on `0.0.0.0:8080` | | `sync` (standalone) | Spaced Sync | 30 s | Exec `space-cli --output-format json getserverinfo`; reports `success` when `ready=true && progress=100%`, otherwise `loading` with progress percentage | -| `indexer-sync` (standalone) | Indexer Sync | 60 s | `psql` the `blocks` table for `MAX(height) WHERE orphan = FALSE AND height >= 0`; cross-check against spaced's current tip via `space-cli getserverinfo`. `success` when within 5 blocks of the tip; otherwise `loading` with explicit lag. | +| `indexer` / `explorer-ui` / `indexer-sync` | — | — | **TODO** — removed with the build-from-source chain. Will return when the prebuilt indexer + explorer images are wired in. | ## Limitations and Differences @@ -195,44 +208,52 @@ the same idempotent init logic runs and reuses the existing credentials in `spaced` inside `screen` would conflict with the StartOS-managed daemon. 3. **Space-CLI Web UI is a terminal, not a graphical app.** All Spaces operations happen via `space-cli` (aliased as `spaces` inside the shell). -4. **Only port 8080 (gotty) is exposed externally.** Spaced RPC (7225) and - PostgreSQL (5432) are loopback-only, and any other ports present in the - image (e.g. 22253, 3000, 5173, 8081) are not bound. +4. **Externally exposed ports** (each a StartOS interface): 8080 (gotty + Space-CLI Web UI, always on), 7778 (Certrelay, always on), and — only while + their feature is enabled — 3000 (Explorer Web UI, currently stubbed), 7777 + (Subspaces Web UI), 8888 (Subspaces Prover), 8081 (Subspaces Registry). + **Loopback-only:** spaced RPC (7225) and PostgreSQL (5432). The Subspaces + registry is remapped from its upstream default 8080 to 8081 to avoid + colliding with the gotty terminal. 5. **Bitcoin Core 31.x is the only supported dependency.** Earlier majors are not allowed by the manifest version range. 6. **The web terminal is independent of spaced.** Gotty stays reachable even when `spaced` is crash-looping, so you can always shell in to diagnose. -7. **The embedded explorer is opt-in.** Fresh installs run in spaces-only - mode (spaced + gotty). Run **Enable Embedded Explorer** to start - PostgreSQL + the Go indexer; **Disable Embedded Explorer** stops them. The - `postgres` and `indexer-go` images are bundled in the .s9pk regardless so - toggling is instant, no re-download. Indexed data is preserved across - toggles. +7. **The embedded explorer is opt-in — and currently PostgreSQL-only.** Fresh + installs run in spaces-only mode (spaced + gotty). Run **Enable Embedded + Explorer** to start PostgreSQL; **Disable Embedded Explorer** stops it. The + indexer and explorer-UI daemons are **stubbed out** pending prebuilt images + (the build-from-source chain was removed to shrink the `.s9pk` and cut + startup time). So enabling the explorer today brings up an empty database + and nothing else. PostgreSQL data is preserved across toggles. 8. **PostgreSQL is embedded, not a separate service.** There is no cross-service dependency on a postgres .s9pk; the database is local to this - package, lives at `/data/postgres`, and is intended for the embedded - indexer (and any future explorer UI). Connect to it from inside the gotty - terminal with `psql $DB_URL` (only meaningful while the explorer is - enabled). -9. **Enabling the explorer requires internet on its first start.** The pinned - commit of `spacesprotocol/explorer-indexer` is downloaded from GitHub into - `/data/explorer-indexer`, and `go build` + `go install github.com/pressly/goose/v3/cmd/goose` - fetch Go modules. After that the indexer runs offline; the only network it - needs is bitcoind (already a StartOS dependency) and the local spaced RPC. -10. **Explorer first-enable latency is ~2-5 minutes.** The Go indexer needs to - download modules and compile from scratch the first time you turn it on. - Subsequent restarts (and toggle off→on) skip `indexer-fetch` + - `indexer-build` because the binaries already exist on the volume. Use the - **Reset Indexer State** action to force a rebuild. -11. **Subspaces pointers data is not indexed.** Our spaced binary predates the - pointers feature. The indexer source is patched during `indexer-fetch` to - treat `ptrs_root` as optional and to skip the `getptrblockmeta` RPC call. - All other spaces protocol data — blocks, transactions, spaces, rollouts, - root anchors with empty pointers root — indexes normally. A future - rebuild of `horologger/spaces` with a newer spaced will let us drop these - patches. - -## What Is Unchanged from Upstream + package, lives at `/data/postgres`, and is intended for the (pending) + prebuilt indexer + explorer UI. Connect to it from inside the gotty + terminal with `psql $DB_URL`. +9. **Subspaces pointers data note.** When the prebuilt indexer is wired back + in, be aware our spaced binary predates the spaces pointers feature; the + prior build-from-source indexer was patched to treat `ptrs_root` as optional + and to skip the `getptrblockmeta` RPC. A future rebuild of + `horologger/spaces` with a newer spaced will let us drop those patches. +10. **Subspaces is opt-in.** Fresh installs do not run Subspaces. Run **Enable + Subspaces** to start it. It ships as the prebuilt `horologger/subs:v0.1.0` + image (no compile step), so enabling is fast — the three daemons start as + soon as the image is on the box. Enabling runs three daemons, each with its + own StartOS interface: **subs** (Web UI, 7777), **subs-prover** (8888, no + GPU), **registry-server** (8081). subs loads the existing `default` spaces + wallet at startup and does **not** create it — create it first from the + Space-CLI Web UI with `spaces createwallet` if you haven't. Subspaces is + independent of the Embedded Explorer toggle — you can run any combination + of {explorer, subspaces}. +11. **Certrelay is always on.** It ships as the prebuilt + `horologger/certrelay:v0.2.3` image (static musl binaries) and runs on + 7778 from service start. It talks to the local spaced over loopback using + the `store.spacedAuth` credentials. The bundled `fabric` CLI is copied to + `/data/bin/fabric` so it's runnable from the Space-CLI Web UI. **You must + set `CERTRELAY_SELF_URL`** (via **Configure Certrelay**) to the publicly + visible URL StartOS exposes for the Certrelay interface, or peers/clients + won't be able to reach this relay at the advertised address. - `space-cli` subcommands and flags work exactly as documented upstream. - `spaced` honours all `SPACED_*` environment variables not otherwise set by @@ -256,8 +277,8 @@ Examples: ```bash spaces getserverinfo -spaces walletcreate default -spaces walletbalance default +spaces createwallet # creates the `default` wallet (--wallet defaults to "default") +spaces balance ``` ## Quick Reference for AI Consumers @@ -268,14 +289,20 @@ upstream_version: subspacesplus images: spaces: docker.io/horologger/spaces:v0.0.9s postgres: docker.io/postgres:16.3 - indexer-go: docker.io/golang:1.23-alpine - explorer-ui: docker.io/node:20-alpine + subspaces: docker.io/horologger/subs:v0.1.0 + certrelay: docker.io/horologger/certrelay:v0.2.3 + # TODO: prebuilt indexer image (was golang:1.23-alpine builder, removed) + # TODO: prebuilt explorer image (was node:20-alpine builder, removed) architectures: [x86_64, aarch64] volumes: main: /data ports: ui: 8080 - explorer_ui: 3000 # external, only useful while explorer enabled + explorer_ui: 3000 # interface registered; daemon stubbed (TODO prebuilt image) + subspaces_ui: 7777 # external, only useful while subspaces enabled + subspaces_prover: 8888 # external (its own interface), subspaces only + subspaces_registry: 8081 # external (its own interface), subspaces only; remapped from upstream 8080 + certrelay: 7778 # external (its own interface), always on spaced_rpc: 7225 # loopback only postgres: 5432 # loopback only dependencies: @@ -307,27 +334,45 @@ postgres_env_vars: explorer: default_enabled: false # opt-in via Enable Embedded Explorer action store_field: enableExplorer - bundled_when_disabled: true # images stay in .s9pk; data on /data preserved + status: postgres_only # indexer + explorer-ui daemons stubbed; see TODOs + reset_actions: [reset-db-state, reset-indexer-state, reset-explorer-state] indexer: - source_repo: spacesprotocol/explorer-indexer - pinned_commit: 00ae1e548734d93f1a8bb9f48d2290f459e12b35 - install_dir: /data/explorer-indexer - bin_dir: /data/explorer-indexer/bin - schema_dir: /data/explorer-indexer/sql/schema - language: Go - build_image: golang:1.23-alpine - migrations_tool: goose - goose_version: v3.24.3 + status: TODO # build-from-source (Go) removed; prebuilt image pending + prior_source_repo: spacesprotocol/explorer-indexer + data_dir: /data/explorer-indexer # reset-indexer-state still wipes this explorer_ui: - source_repo: randomlogin/explorer - pinned_commit: c827da1754c3cba5c5507d2c29f21b8fa231344d - install_dir: /data/explorer-ui - build_dir: /data/explorer-ui/build - language: TypeScript (SvelteKit) - build_image: node:20-alpine - build_command: PUBLIC_BTC_NETWORK=mainnet npm run build - daemon_command: node /data/explorer-ui/build - port: 3000 + status: TODO # build-from-source (SvelteKit) removed; prebuilt image pending + prior_source_repo: randomlogin/explorer + data_dir: /data/explorer-ui # reset-explorer-state still wipes this + port: 3000 # interface registered; daemon stubbed +subspaces: + default_enabled: false # opt-in via Enable Subspaces action + store_field: enableSubspaces + image: horologger/subs:v0.1.0 # prebuilt; no compile step + data_dir: /data/subspaces/data + wallet: default # loaded by subs, NOT created (create via space-cli first) + daemons: # three separate daemons, each its own interface + subs: { bin: /usr/local/bin/subs, port: 7777 } + subs-prover: { bin: /usr/local/bin/subs-prover, port: 8888, gpu: none } + registry-server: { bin: /usr/local/bin/registry-server, port: 8081 } # remapped from upstream 8080 +certrelay: + always_on: true + image: horologger/certrelay:v0.2.3 # prebuilt; static musl binaries + server_bin: /usr/local/bin/certrelay + fabric_bin: /usr/local/bin/fabric # copied to /data/bin/fabric for the CLI + data_dir: /data/certrelay/data + port: 7778 + spaced_rpc_creds: from store.spacedAuth (local spaced) + configurable_via_action: configure-certrelay + env: + CERTRELAY_CHAIN: mainnet + CERTRELAY_BIND: 0.0.0.0 + CERTRELAY_PORT: '7778' + CERTRELAY_REMOTE_IP_HEADER: x-forwarded-for + CERTRELAY_ANCHOR_REFRESH: '300' + CERTRELAY_SELF_URL: https://certrelay.spacesops.com # default; set to your public URL + CERTRELAY_BOOTSTRAP: 'false' # toggle via action + CERTRELAY_HEALTHCHECK_HANDLE: account-digital-useful.genesis@key # via action managed_env_vars: - POSTGRES_URI - BITCOIN_NODE_URI @@ -360,4 +405,8 @@ actions: - reset-db-state - reset-indexer-state - reset-explorer-state + - enable-subspaces + - disable-subspaces + - reset-subspaces-state + - configure-certrelay ``` diff --git a/startos/actions/configureCertrelay.ts b/startos/actions/configureCertrelay.ts new file mode 100644 index 0000000..9bc7bba --- /dev/null +++ b/startos/actions/configureCertrelay.ts @@ -0,0 +1,101 @@ +import { storeJson } from '../fileModels/storeJson' +import { i18n } from '../i18n' +import { sdk } from '../sdk' +import { + CERTRELAY_DEFAULT_BOOTSTRAP, + CERTRELAY_DEFAULT_HEALTHCHECK_HANDLE, + CERTRELAY_DEFAULT_SELF_URL, +} from '../utils' + +const { InputSpec, Value } = sdk + +const inputSpec = InputSpec.of({ + certrelaySelfUrl: Value.text({ + name: i18n('Certrelay Self URL'), + description: i18n( + 'The publicly visible URL clients use to reach this certrelay (CERTRELAY_SELF_URL). Set this to match the external address StartOS exposes for the Certrelay interface (e.g. your .onion or clearnet domain).', + ), + warning: null, + footnote: null, + default: CERTRELAY_DEFAULT_SELF_URL, + required: true, + masked: false, + placeholder: 'https://certrelay.example.com', + minLength: 1, + maxLength: null, + }), + certrelayBootstrap: Value.toggle({ + name: i18n('Certrelay Bootstrap'), + description: i18n( + 'Whether this relay bootstraps from peer relays on startup (CERTRELAY_BOOTSTRAP).', + ), + warning: null, + footnote: null, + default: CERTRELAY_DEFAULT_BOOTSTRAP, + }), + certrelayHealthcheckHandle: Value.text({ + name: i18n('Certrelay Healthcheck Handle'), + description: i18n( + 'A handle the bundled fabric CLI resolves against this relay to verify end-to-end health (CERTRELAY_HEALTHCHECK_HANDLE).', + ), + warning: null, + footnote: null, + default: CERTRELAY_DEFAULT_HEALTHCHECK_HANDLE, + required: true, + masked: false, + placeholder: 'account-digital-useful.genesis@key', + minLength: 1, + maxLength: null, + }), +}) + +export const configureCertrelay = sdk.Action.withInput( + // id + 'configure-certrelay', + + // metadata + async ({ effects }) => ({ + name: i18n('Configure Certrelay'), + description: i18n( + 'Set the certrelay self URL, bootstrap toggle, and healthcheck handle. Saving restarts the service so certrelay picks up the new values.', + ), + warning: null, + allowedStatuses: 'any', + group: null, + visibility: 'enabled', + }), + + // input + inputSpec, + + // prefill — load current values from store + async ({ effects }) => { + const store = await storeJson.read().once() + return { + certrelaySelfUrl: store?.certrelaySelfUrl ?? CERTRELAY_DEFAULT_SELF_URL, + certrelayBootstrap: + store?.certrelayBootstrap ?? CERTRELAY_DEFAULT_BOOTSTRAP, + certrelayHealthcheckHandle: + store?.certrelayHealthcheckHandle ?? + CERTRELAY_DEFAULT_HEALTHCHECK_HANDLE, + } + }, + + // run + async ({ effects, input }) => { + await storeJson.merge(effects, { + certrelaySelfUrl: input.certrelaySelfUrl, + certrelayBootstrap: input.certrelayBootstrap, + certrelayHealthcheckHandle: input.certrelayHealthcheckHandle, + }) + + return { + version: '1', + title: i18n('Success'), + message: i18n( + 'Certrelay configuration saved. The service is restarting to apply the new settings.', + ), + result: null, + } + }, +) diff --git a/startos/actions/disableSubspaces.ts b/startos/actions/disableSubspaces.ts new file mode 100644 index 0000000..7d568fd --- /dev/null +++ b/startos/actions/disableSubspaces.ts @@ -0,0 +1,37 @@ +import { storeJson } from '../fileModels/storeJson' +import { i18n } from '../i18n' +import { sdk } from '../sdk' + +export const disableSubspaces = sdk.Action.withoutInput( + // id + 'disable-subspaces', + + // metadata + async ({ effects }) => { + const current = await storeJson.read((s) => s.enableSubspaces).once() + return { + name: i18n('Disable Subspaces'), + description: i18n( + 'Stop the embedded Subspaces service. The compiled binaries and data at /data/subspaces are preserved and will be reused if Subspaces is re-enabled. Service restarts automatically.', + ), + warning: null, + allowedStatuses: 'any', + group: null, + visibility: current === true ? 'enabled' : 'hidden', + } + }, + + // run + async ({ effects }) => { + await storeJson.merge(effects, { enableSubspaces: false }) + + return { + version: '1', + title: i18n('Success'), + message: i18n( + 'Subspaces disabled. The service is restarting; the Subspaces Web UI is no longer listening. Run "Enable Subspaces" to turn it back on.', + ), + result: null, + } + }, +) diff --git a/startos/actions/enableSubspaces.ts b/startos/actions/enableSubspaces.ts new file mode 100644 index 0000000..a13bba9 --- /dev/null +++ b/startos/actions/enableSubspaces.ts @@ -0,0 +1,37 @@ +import { storeJson } from '../fileModels/storeJson' +import { i18n } from '../i18n' +import { sdk } from '../sdk' + +export const enableSubspaces = sdk.Action.withoutInput( + // id + 'enable-subspaces', + + // metadata + async ({ effects }) => { + const current = await storeJson.read((s) => s.enableSubspaces).once() + return { + name: i18n('Enable Subspaces'), + description: i18n( + 'Turn on the embedded Subspaces service (off-chain Bitcoin handles). Starts three prebuilt daemons — subs (Web UI), subs-prover, and registry-server — each on its own interface. Service restarts automatically. subs loads the existing `default` spaces wallet; create it first via the Space-CLI Web UI (`spaces createwallet`) if you have not already.', + ), + warning: null, + allowedStatuses: 'any', + group: null, + visibility: current === true ? 'hidden' : 'enabled', + } + }, + + // run + async ({ effects }) => { + await storeJson.merge(effects, { enableSubspaces: true }) + + return { + version: '1', + title: i18n('Success'), + message: i18n( + 'Subspaces enabled. The service is restarting; the Subspaces Web UI (7777), Prover (8888), and Registry (8081) interfaces come up once their daemons are healthy.', + ), + result: null, + } + }, +) diff --git a/startos/actions/index.ts b/startos/actions/index.ts index 3a8d6f4..ecdda3e 100644 --- a/startos/actions/index.ts +++ b/startos/actions/index.ts @@ -1,6 +1,9 @@ import { sdk } from '../sdk' +import { configureCertrelay } from './configureCertrelay' import { disableExplorer } from './disableExplorer' +import { disableSubspaces } from './disableSubspaces' import { enableExplorer } from './enableExplorer' +import { enableSubspaces } from './enableSubspaces' import { exportWallet } from './exportWallet' import { importWallet } from './importWallet' import { resetDbState } from './resetDbState' @@ -8,6 +11,7 @@ import { resetExplorerState } from './resetExplorerState' import { resetIndexerState } from './resetIndexerState' import { resetPassword } from './resetPassword' import { resetSpacedState } from './resetSpacedState' +import { resetSubspacesState } from './resetSubspacesState' import { setBitcoinRpc } from './setBitcoinRpc' import { showCredentials } from './showCredentials' import { showDbCredentials } from './showDbCredentials' @@ -29,3 +33,7 @@ export const actions = sdk.Actions.of() .addAction(resetDbState) .addAction(resetIndexerState) .addAction(resetExplorerState) + .addAction(enableSubspaces) + .addAction(disableSubspaces) + .addAction(resetSubspacesState) + .addAction(configureCertrelay) diff --git a/startos/actions/resetExplorerState.ts b/startos/actions/resetExplorerState.ts index fd1feda..d0e3b31 100644 --- a/startos/actions/resetExplorerState.ts +++ b/startos/actions/resetExplorerState.ts @@ -24,7 +24,9 @@ export const resetExplorerState = sdk.Action.withoutInput( async ({ effects }) => { const res = await sdk.SubContainer.withTemp( effects, - { imageId: 'explorer-ui' }, + // TODO(prebuilt-explorer): switch back to the explorer image once it + // exists. Any image with `rm` works for the wipe; `spaces` is always present. + { imageId: 'spaces' }, sdk.Mounts.of().mountVolume({ volumeId: 'main', subpath: null, diff --git a/startos/actions/resetSubspacesState.ts b/startos/actions/resetSubspacesState.ts new file mode 100644 index 0000000..88e3a2b --- /dev/null +++ b/startos/actions/resetSubspacesState.ts @@ -0,0 +1,58 @@ +import { i18n } from '../i18n' +import { sdk } from '../sdk' +import { dataDir, SUBSPACES_DIR } from '../utils' + +export const resetSubspacesState = sdk.Action.withoutInput( + // id + 'reset-subspaces-state', + + // metadata + async ({ effects }) => ({ + name: i18n('Reset Subspaces State'), + description: i18n( + 'Wipe /data/subspaces so the next start re-creates empty Subspaces data, prover, and registry directories.', + ), + warning: i18n( + 'This deletes all local Subspaces state — any handles/proofs stored on disk will be lost. The binaries ship in the image, so nothing needs to be re-downloaded. The spaces wallet on spaced is preserved.', + ), + allowedStatuses: 'any', + group: null, + visibility: 'enabled', + }), + + // run + async ({ effects }) => { + const res = await sdk.SubContainer.withTemp( + effects, + { imageId: 'subspaces' }, + sdk.Mounts.of().mountVolume({ + volumeId: 'main', + subpath: null, + mountpoint: dataDir, + readonly: false, + }), + 'spaces-reset-subspaces', + (subc) => subc.exec(['rm', '-rf', SUBSPACES_DIR], { user: 'root' }), + ) + + if (res.exitCode !== 0) { + return { + version: '1', + title: i18n('Failure'), + message: i18n('Could not wipe Subspaces state: ${error}', { + error: (res.stderr ?? '').toString() || `exit ${res.exitCode}`, + }), + result: null, + } + } + + return { + version: '1', + title: i18n('Success'), + message: i18n( + 'Subspaces cache and data have been wiped. Start (or restart) the service to re-fetch and rebuild from GitHub.', + ), + result: null, + } + }, +) diff --git a/startos/fileModels/storeJson.ts b/startos/fileModels/storeJson.ts index d61d904..b37ec54 100644 --- a/startos/fileModels/storeJson.ts +++ b/startos/fileModels/storeJson.ts @@ -26,6 +26,10 @@ const shape = z.object({ .nullable() .catch(null), enableExplorer: z.boolean().nullable().catch(null), + enableSubspaces: z.boolean().nullable().catch(null), + certrelaySelfUrl: z.string().nullable().catch(null), + certrelayBootstrap: z.boolean().nullable().catch(null), + certrelayHealthcheckHandle: z.string().nullable().catch(null), }) export const storeJson = FileHelper.json( diff --git a/startos/i18n/dictionaries/default.ts b/startos/i18n/dictionaries/default.ts index 966aeb4..39da3fd 100644 --- a/startos/i18n/dictionaries/default.ts +++ b/startos/i18n/dictionaries/default.ts @@ -76,6 +76,58 @@ const dict = { 'Explorer cache has been wiped. Start (or restart) the service to re-fetch and rebuild from GitHub.': 97, 'Could not wipe explorer state: ${error}': 98, + 'Subspaces Prover': 99, + 'subs-prover is ready': 100, + 'subs-prover is not ready': 101, + 'Subspaces Web UI': 102, + 'subspaces UI is ready': 103, + 'subspaces UI is not ready': 104, + 'Off-chain Bitcoin handles via the Spaces protocol (spacesops/subs). Talks to the local spaced over loopback RPC. Only useful while Subspaces is enabled.': + 105, + 'Subspaces Registry': 117, + 'registry-server is ready': 118, + 'registry-server is not ready': 119, + 'RISC Zero prover server for Subspaces (no GPU). Generates the cryptographic proofs the Subspaces UI requests. Only useful while Subspaces is enabled.': + 120, + 'Registry server for publishing and resolving Subspaces handles. Only useful while Subspaces is enabled.': + 121, + 'Enable Subspaces': 106, + 'Turn on the embedded Subspaces service (off-chain Bitcoin handles). Starts three prebuilt daemons — subs (Web UI), subs-prover, and registry-server — each on its own interface. Service restarts automatically. subs loads the existing `default` spaces wallet; create it first via the Space-CLI Web UI (`spaces createwallet`) if you have not already.': + 107, + 'Subspaces enabled. The service is restarting; the Subspaces Web UI (7777), Prover (8888), and Registry (8081) interfaces come up once their daemons are healthy.': + 108, + 'Disable Subspaces': 109, + 'Stop the embedded Subspaces service. The compiled binaries and data at /data/subspaces are preserved and will be reused if Subspaces is re-enabled. Service restarts automatically.': + 110, + 'Subspaces disabled. The service is restarting; the Subspaces Web UI is no longer listening. Run "Enable Subspaces" to turn it back on.': + 111, + 'Reset Subspaces State': 112, + 'Wipe /data/subspaces so the next start re-creates empty Subspaces data, prover, and registry directories.': + 113, + 'This deletes all local Subspaces state — any handles/proofs stored on disk will be lost. The binaries ship in the image, so nothing needs to be re-downloaded. The spaces wallet on spaced is preserved.': + 114, + 'Could not wipe Subspaces state: ${error}': 115, + 'Subspaces cache and data have been wiped. Start (or restart) the service to re-fetch and rebuild from GitHub.': + 116, + Certrelay: 122, + 'certrelay is ready': 123, + 'certrelay is not ready': 124, + 'Certrelay serves cryptographic proofs binding Bitcoin-anchored handles to owner keys. SETUP NOTE: run the "Configure Certrelay" action and set CERTRELAY_SELF_URL to the publicly visible URL StartOS exposes for this interface (your clearnet domain or .onion address) — peers and clients reach this relay at that URL.': + 125, + 'Configure Certrelay': 126, + 'Set the certrelay self URL, bootstrap toggle, and healthcheck handle. Saving restarts the service so certrelay picks up the new values.': + 127, + 'Certrelay Self URL': 128, + 'The publicly visible URL clients use to reach this certrelay (CERTRELAY_SELF_URL). Set this to match the external address StartOS exposes for the Certrelay interface (e.g. your .onion or clearnet domain).': + 129, + 'Certrelay Bootstrap': 130, + 'Whether this relay bootstraps from peer relays on startup (CERTRELAY_BOOTSTRAP).': + 131, + 'Certrelay Healthcheck Handle': 132, + 'A handle the bundled fabric CLI resolves against this relay to verify end-to-end health (CERTRELAY_HEALTHCHECK_HANDLE).': + 133, + 'Certrelay configuration saved. The service is restarting to apply the new settings.': + 134, // interfaces.ts 'Space-CLI Web UI': 12, diff --git a/startos/init/index.ts b/startos/init/index.ts index f614cb6..5cb1fde 100644 --- a/startos/init/index.ts +++ b/startos/init/index.ts @@ -5,8 +5,10 @@ import { setInterfaces } from '../interfaces' import { sdk } from '../sdk' import { versionGraph } from '../versions' import { taskBtcAuth } from './taskBtcAuth' +import { taskSeedCertrelay } from './taskSeedCertrelay' import { taskSeedDb } from './taskSeedDb' import { taskSeedEnableExplorer } from './taskSeedEnableExplorer' +import { taskSeedEnableSubspaces } from './taskSeedEnableSubspaces' import { taskSeedSpacedAuth } from './taskSeedSpacedAuth' import { taskSetPassword } from './taskSetPassword' @@ -20,6 +22,8 @@ export const init = sdk.setupInit( taskSeedDb, taskSeedSpacedAuth, taskSeedEnableExplorer, + taskSeedEnableSubspaces, + taskSeedCertrelay, taskSetPassword, ) diff --git a/startos/init/taskSeedCertrelay.ts b/startos/init/taskSeedCertrelay.ts new file mode 100644 index 0000000..6b2249a --- /dev/null +++ b/startos/init/taskSeedCertrelay.ts @@ -0,0 +1,34 @@ +import { storeJson } from '../fileModels/storeJson' +import { sdk } from '../sdk' +import { + CERTRELAY_DEFAULT_BOOTSTRAP, + CERTRELAY_DEFAULT_HEALTHCHECK_HANDLE, + CERTRELAY_DEFAULT_SELF_URL, +} from '../utils' + +export const taskSeedCertrelay = sdk.setupOnInit(async (effects) => { + const store = await storeJson.read().once() + + const patch: { + certrelaySelfUrl?: string + certrelayBootstrap?: boolean + certrelayHealthcheckHandle?: string + } = {} + + if (store?.certrelaySelfUrl === null || store?.certrelaySelfUrl === undefined) + patch.certrelaySelfUrl = CERTRELAY_DEFAULT_SELF_URL + if ( + store?.certrelayBootstrap === null || + store?.certrelayBootstrap === undefined + ) + patch.certrelayBootstrap = CERTRELAY_DEFAULT_BOOTSTRAP + if ( + store?.certrelayHealthcheckHandle === null || + store?.certrelayHealthcheckHandle === undefined + ) + patch.certrelayHealthcheckHandle = CERTRELAY_DEFAULT_HEALTHCHECK_HANDLE + + if (Object.keys(patch).length > 0) { + await storeJson.merge(effects, patch, { allowWriteAfterConst: true }) + } +}) diff --git a/startos/init/taskSeedEnableSubspaces.ts b/startos/init/taskSeedEnableSubspaces.ts new file mode 100644 index 0000000..3690d80 --- /dev/null +++ b/startos/init/taskSeedEnableSubspaces.ts @@ -0,0 +1,13 @@ +import { storeJson } from '../fileModels/storeJson' +import { sdk } from '../sdk' + +export const taskSeedEnableSubspaces = sdk.setupOnInit(async (effects) => { + const existing = await storeJson.read((s) => s.enableSubspaces).once() + if (existing !== null && existing !== undefined) return + + await storeJson.merge( + effects, + { enableSubspaces: false }, + { allowWriteAfterConst: true }, + ) +}) diff --git a/startos/interfaces.ts b/startos/interfaces.ts index 1fcc642..beba7d2 100644 --- a/startos/interfaces.ts +++ b/startos/interfaces.ts @@ -1,6 +1,13 @@ import { i18n } from './i18n' import { sdk } from './sdk' -import { EXPLORER_PORT, uiPort } from './utils' +import { + CERTRELAY_PORT, + EXPLORER_PORT, + SUBSPACES_PROVER_PORT, + SUBSPACES_REGISTRY_PORT, + SUBSPACES_UI_PORT, + uiPort, +} from './utils' export const setInterfaces = sdk.setupInterfaces(async ({ effects }) => { const uiMulti = sdk.MultiHost.of(effects, 'ui-multi') @@ -43,5 +50,93 @@ export const setInterfaces = sdk.setupInterfaces(async ({ effects }) => { const explorerReceipt = await explorerMultiOrigin.export([explorer]) - return [uiReceipt, explorerReceipt] + const subspacesMulti = sdk.MultiHost.of(effects, 'subspaces-multi') + const subspacesMultiOrigin = await subspacesMulti.bindPort(SUBSPACES_UI_PORT, { + protocol: 'http', + }) + const subspaces = sdk.createInterface(effects, { + name: i18n('Subspaces Web UI'), + id: 'subspaces', + description: i18n( + 'Off-chain Bitcoin handles via the Spaces protocol (spacesops/subs). Talks to the local spaced over loopback RPC. Only useful while Subspaces is enabled.', + ), + type: 'ui', + masked: false, + schemeOverride: null, + username: null, + path: '', + query: {}, + }) + + const subspacesReceipt = await subspacesMultiOrigin.export([subspaces]) + + const proverMulti = sdk.MultiHost.of(effects, 'subspaces-prover-multi') + const proverMultiOrigin = await proverMulti.bindPort(SUBSPACES_PROVER_PORT, { + protocol: 'http', + }) + const prover = sdk.createInterface(effects, { + name: i18n('Subspaces Prover'), + id: 'subspaces-prover', + description: i18n( + 'RISC Zero prover server for Subspaces (no GPU). Generates the cryptographic proofs the Subspaces UI requests. Only useful while Subspaces is enabled.', + ), + type: 'api', + masked: false, + schemeOverride: null, + username: null, + path: '', + query: {}, + }) + + const proverReceipt = await proverMultiOrigin.export([prover]) + + const registryMulti = sdk.MultiHost.of(effects, 'subspaces-registry-multi') + const registryMultiOrigin = await registryMulti.bindPort( + SUBSPACES_REGISTRY_PORT, + { protocol: 'http' }, + ) + const registry = sdk.createInterface(effects, { + name: i18n('Subspaces Registry'), + id: 'subspaces-registry', + description: i18n( + 'Registry server for publishing and resolving Subspaces handles. Only useful while Subspaces is enabled.', + ), + type: 'api', + masked: false, + schemeOverride: null, + username: null, + path: '', + query: {}, + }) + + const registryReceipt = await registryMultiOrigin.export([registry]) + + const certrelayMulti = sdk.MultiHost.of(effects, 'certrelay-multi') + const certrelayMultiOrigin = await certrelayMulti.bindPort(CERTRELAY_PORT, { + protocol: 'http', + }) + const certrelay = sdk.createInterface(effects, { + name: i18n('Certrelay'), + id: 'certrelay', + description: i18n( + 'Certrelay serves cryptographic proofs binding Bitcoin-anchored handles to owner keys. SETUP NOTE: run the "Configure Certrelay" action and set CERTRELAY_SELF_URL to the publicly visible URL StartOS exposes for this interface (your clearnet domain or .onion address) — peers and clients reach this relay at that URL.', + ), + type: 'api', + masked: false, + schemeOverride: null, + username: null, + path: '', + query: {}, + }) + + const certrelayReceipt = await certrelayMultiOrigin.export([certrelay]) + + return [ + uiReceipt, + explorerReceipt, + subspacesReceipt, + proverReceipt, + registryReceipt, + certrelayReceipt, + ] }) diff --git a/startos/main.ts b/startos/main.ts index b7ee76b..9e2a48b 100644 --- a/startos/main.ts +++ b/startos/main.ts @@ -5,32 +5,35 @@ import { APP_USER, BITCOIND_RPC_HOSTNAME, BITCOIND_RPC_PORT, + CERTRELAY_ANCHOR_REFRESH, + CERTRELAY_BIN, + CERTRELAY_BIND, + CERTRELAY_CHAIN, + CERTRELAY_DATA_DIR, + CERTRELAY_DEFAULT_BOOTSTRAP, + CERTRELAY_DEFAULT_HEALTHCHECK_HANDLE, + CERTRELAY_DEFAULT_SELF_URL, + CERTRELAY_DIR, + CERTRELAY_FABRIC_BIN, + CERTRELAY_FABRIC_DEST, + CERTRELAY_PORT, + CERTRELAY_REMOTE_IP_HEADER, dataDir, - EXPLORER_BUILD_DIR, - EXPLORER_BUILD_ID, - EXPLORER_DIR, - EXPLORER_MARKER, - EXPLORER_NETWORK, - EXPLORER_PORT, - EXPLORER_TARBALL_URL, - INDEXER_ACTIVATION_HEIGHT, - INDEXER_BIN_DIR, - INDEXER_BUILD_ID, - INDEXER_DIR, - INDEXER_FAST_SYNC_HEIGHT, - INDEXER_GOOSE_BIN, - INDEXER_MARKER, - INDEXER_MEMPOOL_CHUNK_SIZE, - INDEXER_SCHEMA_DIR, - INDEXER_SYNC_BIN, - INDEXER_TARBALL_URL, - INDEXER_UPDATE_INTERVAL, pgDataDir, - POSTGRES_DB, POSTGRES_PORT, - POSTGRES_USER, SPACED_CHAIN, spacedRpcPort, + SUBSPACES_DATA_DIR, + SUBSPACES_DIR, + SUBSPACES_PROVER_BIN, + SUBSPACES_PROVER_DIR, + SUBSPACES_PROVER_PORT, + SUBSPACES_REGISTRY_BIN, + SUBSPACES_REGISTRY_DIR, + SUBSPACES_REGISTRY_PORT, + SUBSPACES_SUBS_BIN, + SUBSPACES_UI_PORT, + SUBSPACES_WALLET, uiPort, } from './utils' @@ -52,6 +55,7 @@ export const main = sdk.setupMain(async ({ effects }) => { const { password: APP_PASSWORD, btcAuth, dbAuth, spacedAuth } = store const enableExplorer = store.enableExplorer === true + const enableSubspaces = store.enableSubspaces === true const spacedEnv = { SPACED_CHAIN, @@ -75,6 +79,15 @@ export const main = sdk.setupMain(async ({ effects }) => { DB_URL: `postgres://${dbAuth.username}:${dbAuth.password}@127.0.0.1:${POSTGRES_PORT}/${dbAuth.database}`, } + // Certrelay config — user-tunable bits live in store.json (set via the + // "Configure Certrelay" action); the rest are fixed package defaults. + const certrelaySelfUrl = + store.certrelaySelfUrl ?? CERTRELAY_DEFAULT_SELF_URL + const certrelayBootstrap = + store.certrelayBootstrap ?? CERTRELAY_DEFAULT_BOOTSTRAP + const certrelayHealthcheckHandle = + store.certrelayHealthcheckHandle ?? CERTRELAY_DEFAULT_HEALTHCHECK_HANDLE + const mounts = sdk.Mounts.of().mountVolume({ volumeId: 'main', subpath: null, @@ -110,45 +123,77 @@ export const main = sdk.setupMain(async ({ effects }) => { PGDATA: pgDataDir, } - const indexerSub = await sdk.SubContainer.of( + // TODO(prebuilt-indexer / prebuilt-explorer): the Go indexer and SvelteKit + // explorer used to be built from source in dedicated builder images + // (golang:1.23-alpine, node:20-alpine). Those images + their build chains + // were removed to shrink the .s9pk and cut first-enable latency. When the + // prebuilt images exist, re-add their SubContainers + env here, e.g.: + // const indexerSub = await sdk.SubContainer.of( + // effects, { imageId: '' }, mounts, 'indexer-sub') + // const explorerSub = await sdk.SubContainer.of( + // effects, { imageId: '' }, mounts, 'explorer-sub') + // const indexerEnv = { POSTGRES_URI, BITCOIN_NODE_URI/USER/PASSWORD, + // SPACES_NODE_URI, RPC_USER/PASSWORD, ACTIVATION_BLOCK_HEIGHT, + // FAST_SYNC_BLOCK_HEIGHT, UPDATE_DB_INTERVAL, MEMPOOL_CHUNK_SIZE } + // const explorerEnv = { DB_URL (?sslmode=disable), PUBLIC_BTC_NETWORK, PORT } + // Also add the image ids to startos/manifest/index.ts and re-add the daemon + // definitions in the explorer branch below. See git history for prior shapes. + + const subspacesSub = await sdk.SubContainer.of( effects, - { imageId: 'indexer-go' }, + { imageId: 'subspaces' }, mounts, - 'indexer-sub', + 'subspaces-sub', ) - const explorerSub = await sdk.SubContainer.of( - effects, - { imageId: 'explorer-ui' }, - mounts, - 'explorer-sub', - ) - - const explorerEnv = { - DB_URL: `postgres://${dbAuth.username}:${dbAuth.password}@127.0.0.1:${POSTGRES_PORT}/${dbAuth.database}?sslmode=disable`, - PUBLIC_BTC_NETWORK: EXPLORER_NETWORK, - PORT: String(EXPLORER_PORT), - HOME: '/root', + // subs reads all of these from the environment (see subs/src/main.rs #[arg(env=...)]). + const subsEnv = { + SUBS_DATA_DIR: SUBSPACES_DATA_DIR, + SUBS_PORT: String(SUBSPACES_UI_PORT), + SUBS_WALLET: SUBSPACES_WALLET, + SUBS_SPACED_RPC_URL: `http://127.0.0.1:${spacedRpcPort}`, + SUBS_SPACED_RPC_USER: spacedAuth.username, + SUBS_SPACED_RPC_PASSWORD: spacedAuth.password, + SUBS_PROVER_ENDPOINT: `http://127.0.0.1:${SUBSPACES_PROVER_PORT}`, + SUBS_REGISTRY_ENDPOINT: `http://127.0.0.1:${SUBSPACES_REGISTRY_PORT}`, + HOME: SUBSPACES_DATA_DIR, + RUST_LOG: 'subs=info,subs_prover=info,registry_server=info', } - const postgresUri = `postgres://${dbAuth.username}:${dbAuth.password}@127.0.0.1:${POSTGRES_PORT}/${dbAuth.database}?sslmode=disable` + const proverEnv = { + SUBS_PROVER_SERVER: '1', + SUBS_PROVER_PORT: String(SUBSPACES_PROVER_PORT), + HOME: SUBSPACES_PROVER_DIR, + RUST_LOG: 'subs_prover=info', + } - const indexerEnv = { - POSTGRES_URI: postgresUri, - BITCOIN_NODE_URI: `http://${BITCOIND_RPC_HOSTNAME}:${BITCOIND_RPC_PORT}`, - BITCOIN_NODE_USER: btcAuth.username, - BITCOIN_NODE_PASSWORD: btcAuth.password, - SPACES_NODE_URI: `http://127.0.0.1:${spacedRpcPort}`, - RPC_USER: spacedAuth.username, - RPC_PASSWORD: spacedAuth.password, - ACTIVATION_BLOCK_HEIGHT: INDEXER_ACTIVATION_HEIGHT, - FAST_SYNC_BLOCK_HEIGHT: INDEXER_FAST_SYNC_HEIGHT, - UPDATE_DB_INTERVAL: INDEXER_UPDATE_INTERVAL, - MEMPOOL_CHUNK_SIZE: INDEXER_MEMPOOL_CHUNK_SIZE, - PATH: `${INDEXER_BIN_DIR}:/usr/local/go/bin:/usr/local/bin:/usr/bin:/bin`, - HOME: '/root', - GOPATH: '/root/go', - GOBIN: INDEXER_BIN_DIR, + const registryEnv = { + REGISTRY_SERVER_PORT: String(SUBSPACES_REGISTRY_PORT), + HOME: SUBSPACES_REGISTRY_DIR, + RUST_LOG: 'registry_server=info', + } + + const certrelaySub = await sdk.SubContainer.of( + effects, + { imageId: 'certrelay' }, + mounts, + 'certrelay-sub', + ) + + const certrelayEnv = { + CERTRELAY_CHAIN, + CERTRELAY_DATA_DIR, + CERTRELAY_BIND, + CERTRELAY_PORT: String(CERTRELAY_PORT), + CERTRELAY_SELF_URL: certrelaySelfUrl, + // Credentials come from the local spaced server (store.spacedAuth). + CERTRELAY_SPACED_RPC_URL: `http://${spacedAuth.username}:${spacedAuth.password}@127.0.0.1:${spacedRpcPort}`, + CERTRELAY_REMOTE_IP_HEADER, + CERTRELAY_BOOTSTRAP: certrelayBootstrap ? 'true' : 'false', + CERTRELAY_ANCHOR_REFRESH, + CERTRELAY_HEALTHCHECK_HANDLE: certrelayHealthcheckHandle, + HOME: CERTRELAY_DATA_DIR, + RUST_LOG: 'info', } const bashrc = [ @@ -161,18 +206,155 @@ export const main = sdk.setupMain(async ({ effects }) => { '│ spaced is managed by StartOS — do not run it manually. │', '│ Use the `spaces` alias to call space-cli, e.g. │', '│ spaces getserverinfo │', + '│ `fabric` resolves handles against the local certrelay. │', '│ Docs: https://docs.spacesprotocol.org/ │', '└──────────────────────────────────────────────────────────┘', '', 'EOF', ].join('\n') + // Appends the always-on certrelay chain (setup oneshot + certrelay daemon) to + // any existing chain. certrelay serves cryptographic proofs binding handles to + // owner keys anchored to Bitcoin. The `fabric` CLI is copied onto /data/bin so + // the gotty terminal (which has /data/bin on PATH) can resolve handles. + const withCertrelay = (chain: any): any => + chain + .addOneshot('certrelay-setup', { + subcontainer: certrelaySub, + exec: { + command: [ + 'sh', + '-c', + `set -eu; \ + mkdir -p ${CERTRELAY_DATA_DIR} /data/bin; \ + echo "certrelay-setup: installing fabric CLI to ${CERTRELAY_FABRIC_DEST}..."; \ + cp -f ${CERTRELAY_FABRIC_BIN} ${CERTRELAY_FABRIC_DEST}; \ + chmod +x ${CERTRELAY_FABRIC_DEST}; \ + echo "certrelay-setup: done."`, + ], + user: 'root', + }, + requires: [], + }) + .addDaemon('certrelay', { + subcontainer: certrelaySub, + exec: { + command: [CERTRELAY_BIN], + env: certrelayEnv, + // /data is root-owned; the image's USER certrelay can't write it. + user: 'root', + }, + ready: { + display: i18n('Certrelay'), + fn: () => + sdk.healthCheck.checkPortListening(effects, CERTRELAY_PORT, { + successMessage: i18n('certrelay is ready'), + errorMessage: i18n('certrelay is not ready'), + }), + // Match the image's 120s start-period: anchor-refresh/checkpoint work + // on first start can delay the listener. + gracePeriod: 120_000, + trigger: sdk.trigger.cooldownTrigger(30_000), + }, + requires: ['certrelay-setup', 'spaced'], + }) + + // Appends the Subspaces chain (fetch → build → wallet ensure → subs-prover → + // subs daemon) to any existing chain. `as any` because the chain's TS type + // depends on prior IDs and we can't easily express the union here. + const withSubspaces = (chain: any): any => + chain + .addOneshot('subspaces-dirs', { + subcontainer: subspacesSub, + exec: { + command: [ + 'sh', + '-c', + `set -eu; \ + mkdir -p ${SUBSPACES_DATA_DIR} ${SUBSPACES_PROVER_DIR} ${SUBSPACES_REGISTRY_DIR}; \ + echo "subspaces-dirs: ensured data/prover/registry dirs under ${SUBSPACES_DIR}."`, + ], + user: 'root', + }, + requires: [], + }) + .addDaemon('subs-prover', { + subcontainer: subspacesSub, + exec: { + command: [ + 'sh', + '-c', + `cd ${SUBSPACES_PROVER_DIR} && exec ${SUBSPACES_PROVER_BIN} --server --server-port ${SUBSPACES_PROVER_PORT}`, + ], + env: proverEnv, + // Run as root: the horologger/subs image declares USER subs, but our + // /data volume + subdirs are root-owned, so the non-root user can't + // traverse/write them. Root sidesteps the ownership mismatch. + user: 'root', + }, + ready: { + display: i18n('Subspaces Prover'), + fn: () => + sdk.healthCheck.checkPortListening(effects, SUBSPACES_PROVER_PORT, { + successMessage: i18n('subs-prover is ready'), + errorMessage: i18n('subs-prover is not ready'), + }), + gracePeriod: 60_000, + trigger: sdk.trigger.cooldownTrigger(30_000), + }, + requires: ['subspaces-dirs'], + }) + .addDaemon('subs-registry', { + subcontainer: subspacesSub, + exec: { + command: [ + 'sh', + '-c', + `cd ${SUBSPACES_REGISTRY_DIR} && exec ${SUBSPACES_REGISTRY_BIN} --port ${SUBSPACES_REGISTRY_PORT}`, + ], + env: registryEnv, + user: 'root', + }, + ready: { + display: i18n('Subspaces Registry'), + fn: () => + sdk.healthCheck.checkPortListening(effects, SUBSPACES_REGISTRY_PORT, { + successMessage: i18n('registry-server is ready'), + errorMessage: i18n('registry-server is not ready'), + }), + gracePeriod: 60_000, + }, + requires: ['subspaces-dirs'], + }) + .addDaemon('subs', { + subcontainer: subspacesSub, + exec: { + command: [SUBSPACES_SUBS_BIN], + env: subsEnv, + user: 'root', + }, + ready: { + display: i18n('Subspaces Web UI'), + fn: () => + sdk.healthCheck.checkPortListening(effects, SUBSPACES_UI_PORT, { + successMessage: i18n('subspaces UI is ready'), + errorMessage: i18n('subspaces UI is not ready'), + }), + gracePeriod: 60_000, + }, + // Intentionally NOT contingent on subs-prover: the RISC Zero prover can + // take a while to become ready, and subs only needs it on-demand (proof + // generation), not at startup. subs reaches the prover via + // SUBS_PROVER_ENDPOINT once the prover is up. + requires: ['subspaces-dirs', 'spaced', 'subs-registry'], + }) + if (!enableExplorer) { // Spaces-only mode: spaced + gotty terminal. No PostgreSQL, no indexer. // User can flip the toggle via the Enable Embedded Explorer action; // store.json.enableExplorer is read via .const() so the merge triggers // an automatic service restart and the full chain takes over. - return sdk.Daemons.of(effects) + let chain: any = sdk.Daemons.of(effects) .addOneshot('bashrc', { subcontainer: termSub, exec: { @@ -316,11 +498,14 @@ SPACES_BASHRC_EOF`], }, requires: ['spaced'], }) + chain = withCertrelay(chain) + if (enableSubspaces) chain = withSubspaces(chain) + return chain } // enableExplorer = true: full chain — spaced + terminal + embedded // PostgreSQL + Go indexer + indexer-sync health check. - return sdk.Daemons.of(effects) + let chain: any = sdk.Daemons.of(effects) .addOneshot('bashrc', { subcontainer: termSub, exec: { @@ -382,215 +567,22 @@ SPACES_BASHRC_EOF`], }, requires: [], }) - .addOneshot('indexer-fetch', { - subcontainer: indexerSub, - exec: { - command: [ - 'sh', - '-c', - `set -eu; \ - echo "indexer-fetch: image diagnostics..."; \ - cat /etc/os-release 2>/dev/null | head -3 || echo "no /etc/os-release"; \ - echo " go: $(go version 2>/dev/null || echo MISSING)"; \ - echo " git: $(git --version 2>/dev/null || echo MISSING)"; \ - echo " curl: $(curl --version 2>/dev/null | head -1 || echo MISSING)"; \ - echo " wget: $(wget --version 2>/dev/null | head -1 || echo MISSING)"; \ - echo " tar: $(tar --version 2>/dev/null | head -1 || echo MISSING)"; \ - mkdir -p ${INDEXER_DIR} ${INDEXER_BIN_DIR}; \ - if [ -f ${INDEXER_MARKER} ] && [ "$(cat ${INDEXER_MARKER})" = "${INDEXER_BUILD_ID}" ] && [ -x ${INDEXER_SYNC_BIN} ] && [ -x ${INDEXER_GOOSE_BIN} ]; then \ - echo "indexer-fetch: ${INDEXER_BUILD_ID} already present, skipping."; \ - exit 0; \ - fi; \ - echo "indexer-fetch: downloading ${INDEXER_BUILD_ID}..."; \ - rm -rf ${INDEXER_DIR}; \ - mkdir -p ${INDEXER_DIR} ${INDEXER_BIN_DIR}; \ - if command -v curl >/dev/null 2>&1; then \ - curl -fsSL '${INDEXER_TARBALL_URL}' -o /tmp/explorer-indexer.tar.gz; \ - elif command -v wget >/dev/null 2>&1; then \ - wget -q -O /tmp/explorer-indexer.tar.gz '${INDEXER_TARBALL_URL}'; \ - else \ - echo "indexer-fetch: ERROR no curl or wget available"; exit 1; \ - fi; \ - tar -xzf /tmp/explorer-indexer.tar.gz --strip-components=1 -C ${INDEXER_DIR}; \ - rm -f /tmp/explorer-indexer.tar.gz; \ - echo "indexer-fetch: patching types.go to make ptrs_root optional (older spaced compatibility)..."; \ - awk 'BEGIN{patched=0} /if aux.PointersRoot == nil \\{/ {skip=2; patched++; next} skip>0 {skip--; next} /ra\\.PointersRoot = \\*aux\\.PointersRoot/ {print "\\tif aux.PointersRoot != nil { ra.PointersRoot = *aux.PointersRoot }"; next} {print} END{if(patched<2){print "PATCH-FAIL: expected 2 ptrs_root blocks, got " patched > "/dev/stderr"; exit 1}}' ${INDEXER_DIR}/pkg/node/types.go > ${INDEXER_DIR}/pkg/node/types.go.new; \ - mv ${INDEXER_DIR}/pkg/node/types.go.new ${INDEXER_DIR}/pkg/node/types.go; \ - if grep -q 'missing required field: ptrs_root' ${INDEXER_DIR}/pkg/node/types.go; then \ - echo "indexer-fetch: ERROR ptrs_root check still present after patch"; exit 1; \ - fi; \ - echo "indexer-fetch: patching store.go to skip getptrblockmeta (older spaced has no pointer-block RPC)..."; \ - awk 'BEGIN{patched=0} /spacesPtrBlock, err := sc.GetPtrBlockMeta/ {skip=8; patched++; next} skip>0 {skip--; next} {print} END{if(patched<1){print "PATCH-FAIL: expected GetPtrBlockMeta block, got " patched > "/dev/stderr"; exit 1}}' ${INDEXER_DIR}/pkg/store/store.go > ${INDEXER_DIR}/pkg/store/store.go.new; \ - mv ${INDEXER_DIR}/pkg/store/store.go.new ${INDEXER_DIR}/pkg/store/store.go; \ - if grep -q 'GetPtrBlockMeta' ${INDEXER_DIR}/pkg/store/store.go; then \ - echo "indexer-fetch: ERROR GetPtrBlockMeta call still present after patch"; exit 1; \ - fi; \ - echo "${INDEXER_BUILD_ID}" > ${INDEXER_MARKER}; \ - echo "indexer-fetch: done."`, - ], - user: 'root', - }, - requires: [], - }) - .addOneshot('indexer-build', { - subcontainer: indexerSub, - exec: { - command: [ - 'sh', - '-c', - `set -eu; \ - if [ -x ${INDEXER_SYNC_BIN} ] && [ -x ${INDEXER_GOOSE_BIN} ]; then \ - echo "indexer-build: sync + goose already built, skipping."; \ - exit 0; \ - fi; \ - cd ${INDEXER_DIR}; \ - mkdir -p ${INDEXER_BIN_DIR}; \ - echo "indexer-build: building sync binary..."; \ - CGO_ENABLED=0 go build -o ${INDEXER_SYNC_BIN} ./cmd/sync; \ - echo "indexer-build: installing goose..."; \ - CGO_ENABLED=0 GOBIN=${INDEXER_BIN_DIR} go install github.com/pressly/goose/v3/cmd/goose@v3.24.3; \ - ls -l ${INDEXER_BIN_DIR}; \ - echo "indexer-build: done."`, - ], - env: indexerEnv, - user: 'root', - }, - requires: ['indexer-fetch'], - }) - .addOneshot('indexer-cleanup-legacy', { - subcontainer: postgresSub, - exec: { - command: [ - 'sh', - '-c', - `set -eu; \ - PSQL="psql -U $POSTGRES_USER -h 127.0.0.1 -p 5432 -d $POSTGRES_DB -tA"; \ - HAS_LEGACY=$($PSQL -c "SELECT (to_regclass('public.block_stats') IS NOT NULL) OR (to_regclass('public.spaces_history') IS NOT NULL)" 2>/dev/null || echo f); \ - HAS_GOOSE=$($PSQL -c "SELECT to_regclass('public.goose_db_version') IS NOT NULL" 2>/dev/null || echo f); \ - if [ "$HAS_GOOSE" != "t" ] && [ "$HAS_LEGACY" = "t" ]; then \ - echo "indexer-cleanup-legacy: dropping leftover TS indexer schema..."; \ - $PSQL -c "DROP TABLE IF EXISTS block_stats, syncs, spaces_history, spaces, transactions, blocks CASCADE; DROP EXTENSION IF EXISTS pg_trgm;"; \ - echo "indexer-cleanup-legacy: done."; \ - else \ - echo "indexer-cleanup-legacy: nothing to clean (HAS_GOOSE=$HAS_GOOSE HAS_LEGACY=$HAS_LEGACY)."; \ - fi`, - ], - env: { - ...postgresEnv, - PGPASSWORD: dbAuth.password, - }, - user: 'postgres', - }, - requires: ['postgres'], - }) - .addOneshot('indexer-migrate', { - subcontainer: indexerSub, - exec: { - command: [ - 'sh', - '-c', - `set -eu; \ - echo "indexer-migrate: applying goose migrations..."; \ - ${INDEXER_GOOSE_BIN} -dir ${INDEXER_SCHEMA_DIR} postgres "$POSTGRES_URI" up; \ - echo "indexer-migrate: done."`, - ], - env: indexerEnv, - user: 'root', - }, - requires: ['indexer-build', 'postgres', 'indexer-cleanup-legacy'], - }) - .addDaemon('indexer', { - subcontainer: indexerSub, - exec: { - command: [INDEXER_SYNC_BIN], - env: indexerEnv, - }, - ready: { - display: i18n('Indexer Process'), - fn: () => ({ - result: 'success', - message: i18n('indexer process is running'), - }), - gracePeriod: 60_000, - }, - requires: ['indexer-migrate', 'postgres', 'spaced'], - }) - .addOneshot('explorer-fetch', { - subcontainer: explorerSub, - exec: { - command: [ - 'sh', - '-c', - `set -eu; \ - mkdir -p ${EXPLORER_DIR}; \ - if [ -f ${EXPLORER_MARKER} ] && [ "$(cat ${EXPLORER_MARKER})" = "${EXPLORER_BUILD_ID}" ] && [ -d ${EXPLORER_BUILD_DIR} ]; then \ - echo "explorer-fetch: ${EXPLORER_BUILD_ID} already present, skipping."; \ - exit 0; \ - fi; \ - echo "explorer-fetch: downloading ${EXPLORER_BUILD_ID}..."; \ - rm -rf ${EXPLORER_DIR}; \ - mkdir -p ${EXPLORER_DIR}; \ - if command -v curl >/dev/null 2>&1; then \ - curl -fsSL '${EXPLORER_TARBALL_URL}' -o /tmp/explorer-ui.tar.gz; \ - elif command -v wget >/dev/null 2>&1; then \ - wget -q -O /tmp/explorer-ui.tar.gz '${EXPLORER_TARBALL_URL}'; \ - else \ - echo "explorer-fetch: ERROR no curl or wget"; exit 1; \ - fi; \ - tar -xzf /tmp/explorer-ui.tar.gz --strip-components=1 -C ${EXPLORER_DIR}; \ - rm -f /tmp/explorer-ui.tar.gz; \ - echo "${EXPLORER_BUILD_ID}" > ${EXPLORER_MARKER}; \ - echo "explorer-fetch: done."`, - ], - user: 'root', - }, - requires: [], - }) - .addOneshot('explorer-install', { - subcontainer: explorerSub, - exec: { - command: [ - 'sh', - '-c', - `set -eu; \ - cd ${EXPLORER_DIR}; \ - if [ -d node_modules ] && [ -d build ] && [ -f build/index.js ]; then \ - echo "explorer-install: already built, skipping."; \ - exit 0; \ - fi; \ - echo "explorer-install: npm install (this may take a few minutes)..."; \ - npm install --no-audit --no-fund; \ - echo "explorer-install: building (PUBLIC_BTC_NETWORK=${EXPLORER_NETWORK})..."; \ - PUBLIC_BTC_NETWORK=${EXPLORER_NETWORK} npm run build; \ - if [ ! -f ${EXPLORER_BUILD_DIR}/index.js ]; then \ - echo "explorer-install: ERROR build/index.js not produced"; \ - ls -la ${EXPLORER_BUILD_DIR} || true; \ - exit 1; \ - fi; \ - echo "explorer-install: done."`, - ], - env: explorerEnv, - user: 'root', - }, - requires: ['explorer-fetch'], - }) - .addDaemon('explorer-ui', { - subcontainer: explorerSub, - exec: { - command: ['node', EXPLORER_BUILD_DIR], - env: explorerEnv, - }, - ready: { - display: i18n('Explorer Web UI'), - fn: () => - sdk.healthCheck.checkPortListening(effects, EXPLORER_PORT, { - successMessage: i18n('explorer UI is ready'), - errorMessage: i18n('explorer UI is not ready'), - }), - gracePeriod: 60_000, - }, - requires: ['explorer-install', 'postgres'], - }) + // TODO(prebuilt-indexer): the Go indexer build-from-source chain + // (indexer-fetch + indexer-build + indexer-cleanup-legacy + + // indexer-migrate + `indexer` daemon) was removed to shrink the .s9pk + // and cut first-enable latency. Re-add an `indexer` daemon here that runs + // the prebuilt `sync` binary from the future indexer image (image id + + // binary path TBD) against PostgreSQL + spaced. Env it needs is the old + // indexerEnv (POSTGRES_URI, BITCOIN_NODE_*, SPACES_NODE_URI, RPC_USER/ + // PASSWORD, ACTIVATION_BLOCK_HEIGHT, FAST_SYNC_BLOCK_HEIGHT, + // UPDATE_DB_INTERVAL, MEMPOOL_CHUNK_SIZE). A goose migration step must + // run before it. See git history (build-from-source) for the prior shape. + // + // TODO(prebuilt-explorer): the SvelteKit explorer build chain + // (explorer-fetch + explorer-install + `explorer-ui` daemon) was removed + // for the same reason. Re-add an `explorer-ui` daemon that runs the + // prebuilt explorer server from the provided explorer image (image id + + // command TBD) on port 3000, reading DB_URL + PUBLIC_BTC_NETWORK. .addDaemon('web-terminal', { subcontainer: termSub, exec: { @@ -714,128 +706,11 @@ SPACES_BASHRC_EOF`], }, requires: ['spaced'], }) - .addHealthCheck('indexer-sync', { - ready: { - display: i18n('Indexer Sync'), - fn: async () => { - try { - // Go indexer has no syncs table; track progress by max(blocks.height) - // of non-orphan rows. - const psql = await postgresSub.exec( - [ - 'psql', - '-U', - POSTGRES_USER, - '-d', - POSTGRES_DB, - '-h', - '127.0.0.1', - '-p', - String(POSTGRES_PORT), - '-t', - '-A', - '-F', - '|', - '-c', - "SELECT COALESCE(MAX(height), 0) FROM blocks WHERE orphan = FALSE AND height >= 0;", - ], - { env: { PGPASSWORD: dbAuth.password } as Record }, - ) - - if (psql.exitCode !== 0) { - return { - result: 'failure', - message: i18n('indexer psql exited ${code}: ${error}', { - code: String(psql.exitCode), - error: ((psql.stderr ?? '') as string).toString().slice(0, 200) || - '', - }), - } - } - - const out = (psql.stdout ?? '').toString().trim() - if (!out) { - return { - result: 'loading', - message: i18n('indexer has not run a sync cycle yet.'), - } - } - const parts = out.split('|') - const endBlockHeight = parseInt(parts[0] ?? '0', 10) || 0 - const ageSec = -1 // Go indexer has no per-sync timestamp surface - - const probe = await spacedSub.exec( - [ - '/root/.cargo/bin/space-cli', - '--chain', - SPACED_CHAIN, - '--rpc-user', - spacedAuth.username, - '--rpc-password', - spacedAuth.password, - '--output-format', - 'json', - 'getserverinfo', - ], - {}, - ) - let spacedBlocks = 0 - if (probe.exitCode === 0) { - try { - const parsed = JSON.parse((probe.stdout ?? '').toString()) - spacedBlocks = parsed?.chain?.blocks ?? 0 - } catch { - /* ignore */ - } - } - - if (endBlockHeight === 0) { - return { - result: 'loading', - message: i18n( - 'indexer has not yet committed any blocks (spaced tip ${tip}).', - { tip: String(spacedBlocks) }, - ), - } - } - - const lag = Math.max(0, spacedBlocks - endBlockHeight) - if (spacedBlocks > 0 && lag <= 5) { - return { - result: 'success', - message: i18n( - 'indexer caught up at block ${end} (spaced tip ${tip}).', - { - end: String(endBlockHeight), - tip: String(spacedBlocks), - }, - ), - } - } - - return { - result: 'loading', - message: i18n( - 'indexer at block ${end}, ${lag} behind spaced tip ${tip}.', - { - end: String(endBlockHeight), - lag: String(lag), - tip: String(spacedBlocks), - }, - ), - } - } catch (e) { - return { - result: 'failure', - message: i18n('Indexer Sync health check crashed: ${error}', { - error: (e as Error)?.message ?? String(e), - }), - } - } - }, - gracePeriod: 60_000, - trigger: sdk.trigger.cooldownTrigger(30_000), - }, - requires: ['indexer', 'postgres'], - }) + // TODO(prebuilt-indexer): re-add the `indexer-sync` standalone health + // check once the prebuilt indexer daemon is wired back in. It queried + // MAX(blocks.height) (non-orphan) via psql and compared against spaced's + // tip from `space-cli getserverinfo`. See git history for the prior body. + chain = withCertrelay(chain) + if (enableSubspaces) chain = withSubspaces(chain) + return chain }) diff --git a/startos/manifest/index.ts b/startos/manifest/index.ts index 033b7ed..333ad02 100644 --- a/startos/manifest/index.ts +++ b/startos/manifest/index.ts @@ -21,12 +21,21 @@ export const manifest = setupManifest({ source: { dockerTag: 'postgres:16.3' }, arch: ['x86_64', 'aarch64'], }, - 'indexer-go': { - source: { dockerTag: 'golang:1.23-alpine' }, + // TODO(prebuilt-indexer): add the prebuilt explorer-indexer image here once + // it's produced, e.g. 'indexer-go': { source: { dockerTag: + // 'horologger/explorer-indexer:' }, arch: ['x86_64','aarch64'] }. + // The golang:1.23-alpine builder image + build-from-source chain were + // removed to shrink the .s9pk and cut first-enable latency. + // + // TODO(prebuilt-explorer): add the prebuilt explorer (SvelteKit) image here, + // replacing the node:20-alpine builder, e.g. 'explorer-ui': { source: { + // dockerTag: 'horologger/explorer:' }, arch: ['x86_64','aarch64'] }. + subspaces: { + source: { dockerTag: 'horologger/subs:v0.1.0' }, arch: ['x86_64', 'aarch64'], }, - 'explorer-ui': { - source: { dockerTag: 'node:20-alpine' }, + certrelay: { + source: { dockerTag: 'horologger/certrelay:v0.2.3' }, arch: ['x86_64', 'aarch64'], }, }, diff --git a/startos/utils.ts b/startos/utils.ts index 29d6a46..53f48ae 100644 --- a/startos/utils.ts +++ b/startos/utils.ts @@ -52,6 +52,42 @@ export const EXPLORER_MARKER = '/data/explorer-ui/.installed-sha' export const EXPLORER_PORT = 3000 export const EXPLORER_NETWORK = 'mainnet' +// Subspaces ships as the prebuilt horologger/subs image; binaries live at +// these paths inside it. No build-from-source step. +export const SUBSPACES_SUBS_BIN = '/usr/local/bin/subs' +export const SUBSPACES_PROVER_BIN = '/usr/local/bin/subs-prover' +export const SUBSPACES_REGISTRY_BIN = '/usr/local/bin/registry-server' + +export const SUBSPACES_DIR = '/data/subspaces' +export const SUBSPACES_DATA_DIR = '/data/subspaces/data' +export const SUBSPACES_PROVER_DIR = '/data/subspaces/prover' +export const SUBSPACES_REGISTRY_DIR = '/data/subspaces/registry' + +export const SUBSPACES_UI_PORT = 7777 +export const SUBSPACES_PROVER_PORT = 8888 +// 8081 (not the upstream default 8080) to avoid colliding with the gotty +// Space-CLI Web UI on 8080. +export const SUBSPACES_REGISTRY_PORT = 8081 +export const SUBSPACES_WALLET = 'default' + +// Certrelay ships as the prebuilt horologger/certrelay image; both binaries +// are static musl, so `fabric` runs anywhere once copied onto the volume. +export const CERTRELAY_BIN = '/usr/local/bin/certrelay' +export const CERTRELAY_FABRIC_BIN = '/usr/local/bin/fabric' +// Copied here so the gotty terminal (which has /data/bin on PATH) can run it. +export const CERTRELAY_FABRIC_DEST = '/data/bin/fabric' +export const CERTRELAY_DIR = '/data/certrelay' +export const CERTRELAY_DATA_DIR = '/data/certrelay/data' +export const CERTRELAY_PORT = 7778 +export const CERTRELAY_CHAIN = 'mainnet' +export const CERTRELAY_BIND = '0.0.0.0' +export const CERTRELAY_REMOTE_IP_HEADER = 'x-forwarded-for' +export const CERTRELAY_ANCHOR_REFRESH = '300' +export const CERTRELAY_DEFAULT_SELF_URL = 'https://certrelay.spacesops.com' +export const CERTRELAY_DEFAULT_HEALTHCHECK_HANDLE = + 'account-digital-useful.genesis@key' +export const CERTRELAY_DEFAULT_BOOTSTRAP = false + export function randomPassword() { // bitcoind's generate-rpc-dependent action validates the password against // /^[A-Za-z0-9_-]+$/, so the charset must stay in that set. diff --git a/startos/versions/v0.0.9.1.ts b/startos/versions/v0.0.9.1.ts index f39aac3..65a473e 100644 --- a/startos/versions/v0.0.9.1.ts +++ b/startos/versions/v0.0.9.1.ts @@ -3,7 +3,11 @@ import { IMPOSSIBLE, VersionInfo } from '@start9labs/start-sdk' export const v_0_0_9_1 = VersionInfo.of({ version: '0.0.9:1', releaseNotes: { - en_US: `- Embedded explorer (PostgreSQL + Go indexer + SvelteKit web UI) is now **opt-in**. Fresh installs run in spaces-only mode (spaced + gotty terminal). New "Enable Embedded Explorer" action turns the whole bundle on; "Disable Embedded Explorer" turns it back off. Both auto-restart the service so the new daemon graph takes effect. Indexed data on disk is preserved across toggles. + en_US: `- Embedded Certrelay (prebuilt \`horologger/certrelay:v0.2.3\` image, static musl binaries). Always on — runs the \`certrelay\` server on port 7778 from service start, as its own StartOS interface. It serves cryptographic proofs binding Bitcoin-anchored handles to owner keys, talking to the local spaced over loopback using the store.spacedAuth credentials. The bundled \`fabric\` CLI is copied to /data/bin/fabric so it's runnable from the Space-CLI Web UI. New "Configure Certrelay" action sets CERTRELAY_SELF_URL, CERTRELAY_BOOTSTRAP, and CERTRELAY_HEALTHCHECK_HANDLE (stored in store.json; saving restarts the service). Set CERTRELAY_SELF_URL to the publicly visible URL StartOS exposes for the Certrelay interface. +- Removed the build-from-source chains for the Go indexer and the SvelteKit explorer, and dropped their builder images (\`golang:1.23-alpine\`, \`node:20-alpine\`). This shrinks the .s9pk and removes the multi-minute first-enable compile. The indexer + explorer-UI daemons are now stubbed with TODOs in startos/main.ts pending prebuilt images (indexer image not yet produced; explorer image to be provided). Enabling the embedded explorer currently starts PostgreSQL only. All explorer-indexer actions (show-db-credentials, reset-db-state, reset-indexer-state, reset-explorer-state) are retained. +- Embedded Subspaces support (prebuilt \`horologger/subs:v0.1.0\` image). Opt-in via the new "Enable Subspaces" action; "Disable Subspaces" stops it. No compile step — enabling starts three prebuilt daemons, each on its own StartOS interface: **subs** (Web UI, 7777), **subs-prover** (RISC Zero prover, no GPU, 8888), and **registry-server** (handle registry, 8081). subs loads the existing \`default\` spaces wallet at startup (it does not create one). Runtime data persists at /data/subspaces/data across restarts and toggles. New "Reset Subspaces State" action wipes local data. Adds a fifth manifest image (\`horologger/subs:v0.1.0\`, ~84 MB) — switched from building \`spacesops/subs\` from source with \`rust:1-slim\` to shrink the .s9pk and eliminate the multi-minute first-enable compile. +- Five distinct interfaces now appear in the dashboard: **Space-CLI Web UI** (gotty terminal, 8080), **Explorer Web UI** (3000, opt-in), **Subspaces Web UI** (7777, opt-in), **Subspaces Prover** (8888, opt-in), **Subspaces Registry** (8081, opt-in). Internal "Web UI" references renamed to **Space-CLI Web UI** for clarity. +- Embedded explorer (PostgreSQL + Go indexer + SvelteKit web UI) is now **opt-in**. Fresh installs run in spaces-only mode (spaced + gotty terminal). New "Enable Embedded Explorer" action turns the whole bundle on; "Disable Embedded Explorer" turns it back off. Both auto-restart the service so the new daemon graph takes effect. Indexed data on disk is preserved across toggles. - Embedded SvelteKit explorer web UI (randomlogin/explorer @ c827da175). Fetched into /data/explorer-ui on first enable, \`npm install\` + \`npm run build\` produce a SvelteKit node-adapter bundle, and \`node build\` runs as a managed daemon on port 3000. Exposed as a StartOS interface so it appears alongside the gotty terminal in the dashboard. Reads exclusively from the embedded PostgreSQL; does not talk to spaced or bitcoind directly. New "Reset Explorer UI State" action wipes the cache for a clean rebuild. Adds a fourth manifest image \`node:20-alpine\`. - Embedded spaces-protocol explorer indexer (Go, spacesprotocol/explorer-indexer @ 00ae1e548). When enabled, on first start its source tarball is fetched into /data/explorer-indexer, the \`sync\` binary and \`goose\` migrator are built into /data/explorer-indexer/bin, the goose-managed SQL schema is applied to the embedded PostgreSQL, and the sync binary runs as a managed daemon that polls bitcoind + spaced and writes to the database. - Two patches applied to the indexer source during \`indexer-fetch\` to handle our older spaced binary: \`pkg/node/types.go\` makes \`ptrs_root\` optional in RootAnchor, and \`pkg/store/store.go\` skips the \`getptrblockmeta\` RPC call. Subspaces pointer data won't be indexed; everything else (blocks, transactions, spaces, rollouts, root anchors) does.