v0.0.9:2
Build Service / BuildPackage (push) Has been cancelled

This commit is contained in:
2026-05-23 02:37:38 -04:00
parent e41ff31221
commit b308d0ceaa
17 changed files with 911 additions and 493 deletions
+101
View File
@@ -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,
}
},
)
+37
View File
@@ -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,
}
},
)
+37
View File
@@ -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,
}
},
)
+8
View File
@@ -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)
+3 -1
View File
@@ -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,
+58
View File
@@ -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,
}
},
)
+4
View File
@@ -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(
+52
View File
@@ -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,
+4
View File
@@ -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,
)
+34
View File
@@ -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 })
}
})
+13
View File
@@ -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 },
)
})
+97 -2
View File
@@ -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,
]
})
+263 -388
View File
@@ -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: '<prebuilt-indexer-image>' }, mounts, 'indexer-sub')
// const explorerSub = await sdk.SubContainer.of(
// effects, { imageId: '<prebuilt-explorer-image>' }, 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<string, string> },
)
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) ||
'<no output>',
}),
}
}
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
})
+13 -4
View File
@@ -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:<tag>' }, 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:<tag>' }, 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'],
},
},
+36
View File
@@ -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.
+5 -1
View File
@@ -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.