Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
Show all changes
20 commits
Select commit Hold shift + click to select a range
499fe06
docs(start9): design spec for StartOS adaptation
Jun 25, 2026
e6c6d35
feat(start9): StartOS (.s9pk) package for Broadcast Pool
Jun 26, 2026
4c84801
ci(start9): build ncc bundle before start-cli pack
Jun 26, 2026
6cf80d0
fix(start9): add LICENSE and instructions.md (required s9pk ingredients)
Jun 26, 2026
35af3d1
fix(start9): add assets/ABOUT.md ingredient
Jun 26, 2026
e4cdae0
fix(start9): base image off published binary image, x86_64 v1
Jun 26, 2026
206bf23
docs(start9): record base-image build approach + x86_64 v1
Jun 26, 2026
3ce5ea6
fix(start9): depend on Fulcrum (the node's indexer), not electrs
Jun 26, 2026
ad7d57a
fix(start9): run as root (StartOS mounts /data root-owned)
Jun 26, 2026
3eab062
fix(indexer): resolve hostname indexer addresses via DNS (v0.3.12)
Jun 26, 2026
c7e1ce1
chore(start9): base off broadcast-pool-umbrel:0.3.12 (hostname indexe…
Jun 26, 2026
ade07fd
fix(dashboard): correct wallet URL on StartOS / non-Umbrel (v0.3.13)
Jun 27, 2026
d0fb12d
chore(start9): base off broadcast-pool-umbrel:0.3.13 (dashboard walle…
Jun 27, 2026
c907363
fix(dashboard): ignore stale overlay IP persisted in lan_connect_host…
Jun 27, 2026
5c9e68d
chore(start9): base off broadcast-pool-umbrel:0.3.14
Jun 27, 2026
8345b6d
feat(start9): dedicated Liana Electrum interface (port 50051)
Jun 27, 2026
3bb0377
Revert "feat(start9): dedicated Liana Electrum interface (port 50051)"
Jun 27, 2026
d776ede
fix(electrum): detect Liana on the shared Sparrow port (v0.3.15)
Jun 27, 2026
1d35b47
chore(start9): base off broadcast-pool-umbrel:0.3.15 (Liana shared-po…
Jun 27, 2026
0759273
Merge branch 'master' into start9-adaptation
Jun 27, 2026
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
107 changes: 107 additions & 0 deletions .github/workflows/build-startos-s9pk.yml
Original file line number Diff line number Diff line change
@@ -0,0 +1,107 @@
name: Build StartOS s9pk

# Isolated from the Umbrel image workflow (which triggers on v* tags). This one
# builds the StartOS .s9pk from the start9/ package directory.
on:
workflow_dispatch:
push:
branches: [start9-adaptation]
tags:
- 'startos-v*'

permissions:
contents: write

defaults:
run:
working-directory: start9

jobs:
build:
runs-on: ubuntu-latest
strategy:
matrix:
arch: [x86_64]
steps:
- uses: actions/checkout@v4

- uses: actions/setup-node@v4
with:
node-version: '22'

- name: Install system dependencies
run: |
sudo rm -f /etc/apt/sources.list.d/microsoft-prod.list
sudo apt-get update
sudo apt-get install -y --no-install-recommends \
build-essential \
squashfs-tools-ng \
qemu-user-static \
jq

- uses: docker/setup-qemu-action@v3
- uses: docker/setup-docker-action@v5
- uses: docker/setup-buildx-action@v4

- name: Install start-cli
run: |
ARCH=$(uname -m)
OS=$(uname -s | tr '[:upper:]' '[:lower:]')
ASSET_NAME="start-cli_${ARCH}-${OS}"
DOWNLOAD_URL=$(curl -fsS \
-H "Authorization: token ${{ github.token }}" \
https://api.github.com/repos/Start9Labs/start-os/releases \
| jq -r '[.[].assets[] | select(.name=="'"$ASSET_NAME"'")] | first | .browser_download_url')
if [ -z "$DOWNLOAD_URL" ] || [ "$DOWNLOAD_URL" = "null" ]; then
echo "Error: Could not find asset '$ASSET_NAME' in Start9Labs/start-os releases"
exit 1
fi
curl -fsSL \
-H "Authorization: token ${{ github.token }}" \
-H "Accept: application/octet-stream" \
"$DOWNLOAD_URL" -o /tmp/start-cli
sudo install -m 755 /tmp/start-cli /usr/local/bin/start-cli
start-cli --version

- name: Build s9pk (${{ matrix.arch }})
env:
DEV_KEY: ${{ secrets.DEV_KEY }}
run: |
test -f icon.png
test $(stat -c%s icon.png) -le 40960
npm install
npm run check
# Build the ncc bundle (javascript/index.js) BEFORE packing: start-cli
# s9pk pack/list-ingredients needs it to exist.
npm run build
if [ -n "$DEV_KEY" ]; then
mkdir -p "$HOME/.startos"
printf '%s' "$DEV_KEY" > "$HOME/.startos/developer.key.pem"
else
start-cli init-key
fi
make arch/${{ matrix.arch }}
sha256sum broadcast-pool_${{ matrix.arch }}.s9pk

- name: Upload artifact
uses: actions/upload-artifact@v4
with:
name: broadcast-pool_${{ matrix.arch }}.s9pk
path: start9/broadcast-pool_${{ matrix.arch }}.s9pk
compression-level: 0

release:
if: startsWith(github.ref, 'refs/tags/startos-v')
needs: build
runs-on: ubuntu-latest
steps:
- uses: actions/download-artifact@v4
with:
pattern: broadcast-pool_*
merge-multiple: true

- uses: softprops/action-gh-release@v2
with:
files: |
broadcast-pool_x86_64.s9pk
generate_release_notes: true
2 changes: 1 addition & 1 deletion Cargo.lock

Some generated files are not rendered by default. Learn more about how customized files appear on GitHub.

2 changes: 1 addition & 1 deletion Cargo.toml
Original file line number Diff line number Diff line change
@@ -1,6 +1,6 @@
[package]
name = "broadcast-pool"
version = "0.3.12"
version = "0.3.15"
edition = "2021"
description = "Bitcoin Broadcast Pool - Privacy-preserving transaction scheduling and migration tool"
license = "MIT"
Expand Down
116 changes: 116 additions & 0 deletions docs/superpowers/specs/2026-06-25-start9-adaptation-design.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,116 @@
# Diseño: Adaptación de Broadcast Pool a Start9 (StartOS)

Fecha: 2026-06-25 · Rama: `start9-adaptation`

## Contexto

Broadcast Pool ya funciona empaquetado para Umbrel (`umbrel-app/sparrow-broadcast-pool/`).
El binario Rust `broadcast-pool` es un intermediario Electrum entre wallets (Sparrow/Liana)
y un indexador (electrs/Fulcrum), con mempool virtual para programar/retrasar broadcasts.

Queremos ofrecerlo también en **Start9 (StartOS)** sin afectar al proyecto Umbrel. El binario
es **agnóstico de distro**: su superficie de integración es su contrato de variables de entorno
(`BROADCAST_POOL_*`). Se verificó que funciona en modo **genérico** (sin `BROADCAST_POOL_UMBREL=1`):
`BROADCAST_POOL_INDEXER_URL` se lee directo y la resolución de genesis usa solo `BROADCAST_POOL_RPC_*`.

**Objetivo:** un paquete `.s9pk` que envuelve el mismo binario, declarando dependencias de
`bitcoind` + `electrs` y exponiendo el proxy Electrum (Sparrow/Liana) y el dashboard web.

**Decisiones tomadas (brainstorming):**
- Mismo repo, **rama aislada** `start9-adaptation`, todo bajo `start9/` (no toca archivos Umbrel).
- Nodo de pruebas Start9 disponible con acceso SSH/web, indexador **electrs**, red **mainnet**.
- **Multi-red (testnet4, signet, mainnet) con la red autodetectada desde el nodo Bitcoin.** El
binario ya lo hace: `discovery::apply_network_from_rpc` (incondicional) mapea
`getblockchaininfo.chain` → red. El `entrypoint.sh` **no** fija `BROADCAST_POOL_NETWORK`.
(Se corrigió el orden en `src/main.rs`: detectar red ANTES de abrir la DB, que es por-red.)
- Verificación = **pruebas de humo seguras** (conexión, genesis, handshake, dashboard, que
Sparrow/Liana conecten). **NO** se difundirá una tx de valor real.

## Arquitectura

Patrón espejo de `criptoworld8484/sparrow-frigate-startos` (StartOS TypeScript SDK), que ya
depende de bitcoind+electrs y expone una interfaz Electrum — forma casi idéntica a la nuestra.

```
Sparrow / Liana ──TCP LAN:50050──▶ broadcast-pool (.s9pk)
│ (mismo binario Rust; entrypoint StartOS)
├──▶ electrs.startos:50001 (upstream índice)
└──▶ bitcoind.startos:8332 (genesis/red/altura, cookie auth)
Navegador ──LAN/Tor HTTPS──▶ dashboard web :8080
```

El binario Rust **no se modifica** (salvo un posible ajuste menor: que una `INDEXER_URL`
explícita no sea pisada por el LAN-scan en modo no-Umbrel; se valida en implementación).

## Componentes (todo bajo `start9/`)

Estructura TS SDK (como frigate):
- `start9/startos/manifest/index.ts` — id `broadcast-pool`, imágenes (dockerBuild), deps bitcoind+electrs.
- `start9/startos/dependencies.ts` — bitcoind (running, prune=0) + electrs (running).
- `start9/startos/interfaces.ts` — **dos** interfaces:
- `electrum` (TCP, sin SSL) en 50050 — Sparrow/Liana.
- `ui` (web) en 8080 — dashboard.
- `start9/startos/main.ts` — subcontenedor, monta volumen `main`→`/data` y bitcoind RO→`/mnt/bitcoind`,
daemon `entrypoint.sh`, health-check `checkPortListening(50050)`.
- `start9/startos/utils.ts` — constantes de puerto (`electrumPort=50050`, `webPort=8080`).
- `start9/startos/{index,versions/current,init,backups,actions,sdk,i18n}.ts` — plumbing estándar.
- `start9/Dockerfile` — **base de la imagen ya publicada** `broadcast-pool-umbrel:0.3.11` (que ya
contiene el binario + dashboard) + overlay del entrypoint/env de StartOS. Decisión: el contexto de
build del s9pk es el dir del paquete (`start9/`), que no contiene el crate Rust; empaquetar desde un
artefacto preconstruido es lo idiomático en StartOS (frigate descarga un binario). **v1 es x86_64**
(la imagen base es linux/amd64); aarch64 queda como follow-up (imagen multi-arch o binarios de release).
- `start9/entrypoint.sh` — traduce StartOS → env vars del binario (ver mapeo).
- `start9/{Makefile,s9pk.mk,package.json,tsconfig.json}` + `.github/workflows/build-s9pk.yml`.

## Mapeo de configuración (entrypoint.sh StartOS → binario)

| Binario (env) | Valor en StartOS |
|-----------------------------------|----------------------------------------------------|
| `BROADCAST_POOL_NETWORK` | `mainnet` (red del nodo Start9) |
| `BROADCAST_POOL_DATA_DIR` | `/data` |
| `BROADCAST_POOL_RPC_URL` | `http://bitcoind.startos:8332` |
| `BROADCAST_POOL_RPC_USER`/`PASS` | leídos del cookie `/mnt/bitcoind/.cookie` (`__cookie__:<pass>`) |
| `BROADCAST_POOL_INDEXER_URL` | `tcp://electrs.startos:50001` |
| `BROADCAST_POOL_ELECTRUM_HOST` | `0.0.0.0` |
| `BROADCAST_POOL_ELECTRUM_PORT` | `50050` |
| `BROADCAST_POOL_WEB_HOST`/`PORT` | `0.0.0.0` / `8080` |
| `BROADCAST_POOL_NETWORK` | **no se define** — autodetectada por RPC |
| `BROADCAST_POOL_UMBREL` | **no se define** (modo genérico) |

El cookie de bitcoind se resuelve igual que frigate (`/mnt/bitcoind/.cookie`, con fallback a
`find`), se parte por `:` en usuario/contraseña, y se espera a que exista antes de arrancar.

## Manejo de errores
- Esperar el cookie de bitcoind (bucle con timeout) antes de lanzar el binario.
- Health-check de StartOS sobre el puerto 50050: el servicio aparece "starting" hasta que escucha.
- Si electrs aún sincroniza, el binario ya tolera respuestas lentas/erróneas del indexador
(timeouts + fallback de la mempool virtual); StartOS marca la dependencia electrs por su propio health.

## Pruebas / Verificación (mainnet, sin mover fondos)
1. Build local del `.s9pk` (Makefile) y/o vía workflow `build-s9pk.yml`.
2. Sideload/instalar en el nodo Start9; comprobar que el servicio arranca y la interfaz Electrum
queda **verde** (health).
3. Smoke test del proxy en la dirección LAN:50050:
- `server.version` responde.
- `blockchain.block.header(0)` → hash = genesis mainnet
`000000000019d6689c085ae165831e934ff763ae46a2a6c172b3f1b60a8ce26f`.
- `server.features.genesis_hash` coincide con el del nodo.
4. Apuntar **Sparrow** (y opcionalmente **Liana en mainnet**) a la URL Electrum LAN → conecta y
carga el monedero (solo lectura). **No** difundir una tx real.
5. Dashboard web accesible por la interfaz `ui`.

Tests automatizados del binario Rust: sin cambios (la suite existente cubre la lógica; el
empaquetado no añade lógica Rust nueva salvo el posible ajuste de INDEXER_URL, que llevaría su test).

## Alcance / YAGNI
- v1 expone **un** puerto Electrum (50050) para Sparrow y Liana; el `liana_port` separado
(PoC de altura/anti-fee-sniping) **no** se expone en v1.
- **Multi-red por autodetección RPC** (testnet4/signet/mainnet); sin selector manual en la UI.
- Sin acciones/actions extra más allá de las de plumbing en v1.

## Riesgos
- Posible necesidad de un pequeño ajuste en `discovery.rs`/`main.rs` para que una `INDEXER_URL`
explícita no sea sobrescrita por el LAN-scan en modo no-Umbrel. Se valida al implementar; si
hace falta, es un cambio mínimo y testeado, compartido con Umbrel (no lo rompe).
- Versionado/SDK de StartOS: usar la misma versión del `@start9labs/start-sdk` que frigate para
evitar incompatibilidades.
18 changes: 13 additions & 5 deletions src/api/dashboard.html
Original file line number Diff line number Diff line change
Expand Up @@ -892,7 +892,7 @@ <h2>&#9881; <span data-i18n="cfg_title">Configuration</span></h2>
Configure Sparrow or Liana to connect here (Electrum server)
</div>
<div style="font-family: var(--font-mono); font-size: 15px; color: var(--btc); padding: 10px 14px; background: var(--bg-card); border-radius: 8px; border: 1px solid var(--border); margin-bottom: 8px; cursor: pointer;" id="wallet-url" onclick="copyWalletUrl('wallet-url')" title="Click to copy">-</div>
<div style="font-size: 12px; color: var(--amber); margin-bottom: 8px;" data-i18n="cfg_wallet_warning">Use your Umbrel LAN IP (same as in the browser). Do not use the electrs indexer.</div>
<div style="font-size: 12px; color: var(--amber); margin-bottom: 8px;" data-i18n="cfg_wallet_warning">Use your node’s LAN IP (the same host you use in the browser). Do not use the electrs/Fulcrum indexer.</div>
<div style="font-size: 12px; color: var(--text-dim); margin-top: 4px;" data-i18n="cfg_wallet_url">
Use this URL in your wallet server settings
</div>
Expand Down Expand Up @@ -1216,7 +1216,7 @@ <h4 data-i18n="wizard_settings_title">Settings</h4>
cfg_min_delay: 'Min Delay (hours)', cfg_min_delay_hint: 'Minimum random delay before broadcast',
cfg_max_delay: 'Max Delay (hours)', cfg_max_delay_hint: 'Maximum random delay before broadcast',
cfg_reset: 'Reset', cfg_save: 'Save Settings',
cfg_wallet_title: 'Wallet Connection', cfg_wallet_hint: 'Configure your wallet to connect to Broadcast Pool here', cfg_wallet_url: 'Use this URL in your wallet server settings', cfg_wallet_warning: 'Use your Umbrel LAN IP (same as in the browser). Do not use the electrs indexer. In Sparrow: Settings → Network → disable Tor proxy or broadcasts bypass this pool.',
cfg_wallet_title: 'Wallet Connection', cfg_wallet_hint: 'Configure your wallet to connect to Broadcast Pool here', cfg_wallet_url: 'Use this URL in your wallet server settings', cfg_wallet_warning: 'Use your node’s LAN IP (the same host you use in the browser). Do not use the electrs/Fulcrum indexer. In Sparrow: Settings → Network → disable Tor proxy or broadcasts bypass this pool.', cfg_wallet_startos: 'On StartOS, connect your wallet using the Electrum (TCP) address shown in this service’s Interfaces page — not an internal IP.',
detail_title: 'Transaction Details', detail_pool_id: 'Pool ID', detail_status: 'Status', detail_network: 'Network',
detail_scheduled: 'Scheduled', detail_fee_rate: 'Fee Rate',
detail_utxos: 'UTXOs', detail_broadcast_at: 'Broadcast At', detail_confirmed_at: 'Confirmed At',
Expand Down Expand Up @@ -1326,7 +1326,7 @@ <h4 data-i18n="wizard_settings_title">Settings</h4>
cfg_min_delay: 'Retraso M\u00ednimo (horas)', cfg_min_delay_hint: 'Retraso aleatorio m\u00ednimo antes de transmisi\u00f3n',
cfg_max_delay: 'Retraso M\u00e1ximo (horas)', cfg_max_delay_hint: 'Retraso aleatorio m\u00e1ximo antes de transmisi\u00f3n',
cfg_reset: 'Restablecer', cfg_save: 'Guardar Configuraci\u00f3n',
cfg_wallet_title: 'Conexi\u00f3n Wallet', cfg_wallet_hint: 'Configura tu wallet para conectarse aqu\u00ed', cfg_wallet_url: 'Usar esta URL en la configuraci\u00f3n del servidor de tu wallet', cfg_wallet_warning: 'Usa la IP LAN de Umbrel (la misma del navegador). No uses el indexer electrs. En Sparrow: Ajustes \u2192 Red \u2192 desactiva el proxy Tor o los env\u00edos no llegan al pool.',
cfg_wallet_title: 'Conexi\u00f3n Wallet', cfg_wallet_hint: 'Configura tu wallet para conectarse aqu\u00ed', cfg_wallet_url: 'Usar esta URL en la configuraci\u00f3n del servidor de tu wallet', cfg_wallet_warning: 'Usa la IP LAN de tu nodo (el mismo host que usas en el navegador). No uses el indexer electrs/Fulcrum. En Sparrow: Ajustes \u2192 Red \u2192 desactiva el proxy Tor o los env\u00edos no llegan al pool.', cfg_wallet_startos: 'En StartOS, conecta tu wallet con la direcci\u00f3n Electrum (TCP) que se muestra en la p\u00e1gina de Interfaces de este servicio \u2014 no una IP interna.',
detail_title: 'Detalles de Transacci\u00f3n', detail_pool_id: 'ID del Pool', detail_status: 'Estado', detail_network: 'Red',
detail_scheduled: 'Programada', detail_fee_rate: 'Tarifa',
detail_utxos: 'UTXOs', detail_broadcast_at: 'Transmitida el', detail_confirmed_at: 'Confirmada el',
Expand Down Expand Up @@ -2230,9 +2230,17 @@ <h4 data-i18n="wizard_settings_title">Settings</h4>
}

function updateWalletUrls(cfg) {
const walletUrl = cfg.wallet_connect_url || cfg.sparrow_connect_url;
const walletEl = document.getElementById('wallet-url');
if (walletEl && walletUrl) walletEl.textContent = walletUrl;
if (walletEl) {
if (cfg.startos_mode) {
// On StartOS the wallet address comes from the service's Interfaces page,
// not an auto-detected container IP. Show guidance instead of a bogus URL.
walletEl.textContent = t('cfg_wallet_startos');
} else {
const walletUrl = cfg.wallet_connect_url || cfg.sparrow_connect_url;
if (walletUrl) walletEl.textContent = walletUrl;
}
}

appUmbrelMode = !!cfg.umbrel_mode;
const externalGroup = document.getElementById('cfg-external-indexer-group');
Expand Down
2 changes: 2 additions & 0 deletions src/api/mod.rs
Original file line number Diff line number Diff line change
Expand Up @@ -121,6 +121,7 @@ fn config_to_response(config: &Config, network_changed: bool) -> ConfigResponse
indexer_is_manual: is_manual,
network_editable: !umbrel,
umbrel_mode: umbrel,
startos_mode: crate::discovery::is_startos_mode(),
network: config.network.network_type.data_dir_name().to_string(),
broadcast_mode: config.schedule.broadcast_mode.to_string(),
default_delay_hours: config.schedule.default_delay_hours,
Expand Down Expand Up @@ -409,6 +410,7 @@ struct ConfigResponse {
indexer_is_manual: bool,
network_editable: bool,
umbrel_mode: bool,
startos_mode: bool,
network: String,
broadcast_mode: String,
default_delay_hours: u64,
Expand Down
Loading
Loading