+263
-388
@@ -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
|
||||
})
|
||||
|
||||
Reference in New Issue
Block a user