S7 — Consume LNbits sidecar bunker (was: NIP-46 bunker option) #21

Closed
opened 2026-05-15 18:09:46 +00:00 by padreug · 2 comments
Owner

Part of #13. Closes gap G6 in full (Account.prvkey readable from DB) and the residual part of G5.

2026-05-26 — no longer "future / optional"

aiolabs/lnbits#9 was reframed 2026-05-25: the bunker is now the steady-state architecture, not a sovereignty escape valve. aiolabs/lnbits#18 (filed same day) is the concrete phase-2 plan: sidecar nsecbunkerd on every lnbits host, RemoteBunkerSigner as the default signer_type for new accounts, LocalSigner retained only as a transitional shim for migrating existing rows.

S7 in this repo therefore becomes "consume the bunker once #18 ships." Our role shrinks dramatically — the heavy lifting (NIP-46 client, admin client, scoped-token issuance, NIP-05 publication) lives upstream.

Architecture (revised)

Operator's human admin nsec  — stays on Amber / hardware signer (not LNbits)
            │
            │ admin RPC (kind:24134, NIP-44 v2 over internal relay)
            ▼
nsecbunkerd  — sidecar on the lnbits host
            │   holds: M_lnbits (admin), X_alice, X_bob, ...
            │   per-target connection tokens with SigningConditions
            ▼
LNbits      — holds: pubkey + signer_type=RemoteBunkerSigner + signer_config (token + perms)
            │   NO nsec material
            ▼
satmachineadmin — issues per-ATM scoped tokens via the lnbits admin client
            │   (sign_event:21000 only, 30-day expiry)
            ▼
ATM         — NIP-46 client; connects via bunker:// URL from the pairing seed

Daily ATM operations are unchanged in pattern from the original S7 sketch: the ATM signs kind-21000 with its own ephemeral keypair (the connection token's client identity), and the bunker mediates the operator-side signing of long-lived events (delegations are gone — replaced by token rows in nsecbunkerd's Prisma DB).

Changes in this repo

Satmachineadmin backend

  • POST /api/v1/dca/machines/:id/pair builds the seed URL by calling the lnbits admin client's create_token API (introduced by aiolabs/lnbits#18):
    token = await bunker_admin.create_token(
        target_pubkey=operator_pubkey,
        client_name=f"satmachine-{machine.id}",
        perms=["sign_event:21000"],
        expires_at=int(time.time()) + 86400 * 30,
    )
    
    The seed URL embeds the returned bunker_url.
  • dca_machines gets a bunker_connection_id column so we can revoke_user / re-issue.
  • All operator-authored long-lived events (fleet roster updates, super-fee policy, NIP-78 config) route through resolve_signer(operator_account).sign_event(event). No direct access to operator's prvkey.

Satmachineadmin frontend

  • Fleet tab "Pair ATM" wizard already exists from S0; no UI change needed beyond showing the bunker round-trip status during seed issuance.
  • New "Revoke ATM access" button per machine row → revoke_user admin RPC.

LNbits side

  • Everything in aiolabs/lnbits#18. We consume; we don't implement.

Acceptance

  • Operator pairs a new ATM → wizard calls bunker_admin.create_token → seed URL embeds the resulting bunker://...?secret=... → ATM redeems on first boot → kind-21000 round-trip works.
  • Revoke from operator dashboard → bunker rejects subsequent requests; satmachineadmin reflects revoked state in the Fleet UI.
  • Super-fee config change requires bunker round-trip (operator account is RemoteBunkerSigner post-migration).
  • Daily cash-out path is not slowed by the bunker — the ATM holds the connection token locally; only operator-authored events incur the bunker round-trip.
  • Post-migration: zero accounts.prvkey rows for any operator that owns satmachineadmin machines.

Sequencing

Hard-blocked on aiolabs/lnbits#18. Sprint 3, after Sprint 1 (S0+S1+S5) + Sprint 2 (S2+S3+S4) land. Practically: once lnbits#18 ships nsecbunkerd-on-aio-demo and RemoteBunkerSigner.sign_event works end-to-end, S7 here becomes a 1-week consumer-side task instead of a 4-6 week build.

Reference

  • aiolabs/lnbits#9 — operator-IdP framing (the why).
  • aiolabs/lnbits#18 — sidecar bunker integration (the how). §F is our alignment.
  • github.com/kind-0/nsecbunkerd — the bunker we'll consume; see OAUTH-LIKE-FLOW.md.
  • NIP-46 spec: ~/dev/nostr-protocol/nips/46.md.
  • Design doc: docs/security-pathway-v1.md §5.1, §6.S7 — needs follow-up edit reflecting that the bunker is standard, not optional.
Part of #13. Closes gap G6 in full (`Account.prvkey` readable from DB) and the residual part of G5. ## 2026-05-26 — no longer "future / optional" `aiolabs/lnbits#9` was reframed 2026-05-25: the bunker is now the **steady-state architecture**, not a sovereignty escape valve. `aiolabs/lnbits#18` (filed same day) is the concrete phase-2 plan: sidecar `nsecbunkerd` on every lnbits host, `RemoteBunkerSigner` as the default `signer_type` for new accounts, `LocalSigner` retained only as a transitional shim for migrating existing rows. S7 in this repo therefore becomes **"consume the bunker once #18 ships."** Our role shrinks dramatically — the heavy lifting (NIP-46 client, admin client, scoped-token issuance, NIP-05 publication) lives upstream. ## Architecture (revised) ``` Operator's human admin nsec — stays on Amber / hardware signer (not LNbits) │ │ admin RPC (kind:24134, NIP-44 v2 over internal relay) ▼ nsecbunkerd — sidecar on the lnbits host │ holds: M_lnbits (admin), X_alice, X_bob, ... │ per-target connection tokens with SigningConditions ▼ LNbits — holds: pubkey + signer_type=RemoteBunkerSigner + signer_config (token + perms) │ NO nsec material ▼ satmachineadmin — issues per-ATM scoped tokens via the lnbits admin client │ (sign_event:21000 only, 30-day expiry) ▼ ATM — NIP-46 client; connects via bunker:// URL from the pairing seed ``` Daily ATM operations are **unchanged in pattern** from the original S7 sketch: the ATM signs kind-21000 with its *own* ephemeral keypair (the connection token's client identity), and the bunker mediates the operator-side signing of long-lived events (delegations are gone — replaced by token rows in nsecbunkerd's Prisma DB). ## Changes in this repo **Satmachineadmin backend** - `POST /api/v1/dca/machines/:id/pair` builds the seed URL by calling the lnbits admin client's `create_token` API (introduced by `aiolabs/lnbits#18`): ```python token = await bunker_admin.create_token( target_pubkey=operator_pubkey, client_name=f"satmachine-{machine.id}", perms=["sign_event:21000"], expires_at=int(time.time()) + 86400 * 30, ) ``` The seed URL embeds the returned `bunker_url`. - `dca_machines` gets a `bunker_connection_id` column so we can `revoke_user` / re-issue. - All operator-authored long-lived events (fleet roster updates, super-fee policy, NIP-78 config) route through `resolve_signer(operator_account).sign_event(event)`. No direct access to operator's `prvkey`. **Satmachineadmin frontend** - Fleet tab "Pair ATM" wizard already exists from S0; no UI change needed beyond showing the bunker round-trip status during seed issuance. - New "Revoke ATM access" button per machine row → `revoke_user` admin RPC. **LNbits side** - Everything in `aiolabs/lnbits#18`. We consume; we don't implement. ## Acceptance - [ ] Operator pairs a new ATM → wizard calls `bunker_admin.create_token` → seed URL embeds the resulting `bunker://...?secret=...` → ATM redeems on first boot → kind-21000 round-trip works. - [ ] Revoke from operator dashboard → bunker rejects subsequent requests; satmachineadmin reflects revoked state in the Fleet UI. - [ ] Super-fee config change requires bunker round-trip (operator account is `RemoteBunkerSigner` post-migration). - [ ] Daily cash-out path is *not* slowed by the bunker — the ATM holds the connection token locally; only operator-authored events incur the bunker round-trip. - [ ] Post-migration: zero `accounts.prvkey` rows for any operator that owns satmachineadmin machines. ## Sequencing **Hard-blocked on `aiolabs/lnbits#18`.** Sprint 3, after Sprint 1 (S0+S1+S5) + Sprint 2 (S2+S3+S4) land. Practically: once lnbits#18 ships nsecbunkerd-on-aio-demo and `RemoteBunkerSigner.sign_event` works end-to-end, S7 here becomes a 1-week consumer-side task instead of a 4-6 week build. ## Reference - `aiolabs/lnbits#9` — operator-IdP framing (the why). - `aiolabs/lnbits#18` — sidecar bunker integration (the how). §F is our alignment. - `github.com/kind-0/nsecbunkerd` — the bunker we'll consume; see `OAUTH-LIKE-FLOW.md`. - NIP-46 spec: `~/dev/nostr-protocol/nips/46.md`. - Design doc: `docs/security-pathway-v1.md` §5.1, §6.S7 — needs follow-up edit reflecting that the bunker is standard, not optional.
padreug changed title from S7 — NIP-46 bunker option (operator nsec off LNbits' DB) to S7 — Consume LNbits sidecar bunker (was: NIP-46 bunker option) 2026-05-25 23:01:13 +00:00
Author
Owner

Partial shipment — 2026-05-31

The runtime nip44 path of S7 just shipped on v2-bitspire via PR #30 commit dcb7de0 (refactor(v2): cassette transport — signer.nip44_* migration). What's done, what's left:

Shipped

  • cassette_transport.publish_to_atm + decrypt_and_parse_state_event no longer read account.prvkey directly. They go through resolve_signer(account).nip44_encrypt(...) / signer.nip44_decrypt(...) per the lnbits.core.signers.base.NostrSigner ABC.
  • Hybrid LocalSigner-to-bunker pathway preserved: bunker-backed accounts route through RemoteBunkerSigner and the nsec never leaves the bunker process; the transitional LocalSigner-with-prvkey path uses the hand-rolled nip44.py impl via _nip44_decrypt_via_signer's fallback. Same wire output either way.
  • _sign_as_operator simplified to await signer.sign_event(event) — previously had a broken-but-permanent ImportError fallback masquerading as the pre-#17 compatibility path (the SignerError import actually fails at the package root; the errors live in .base).
  • Error hierarchy properly handled: NsecBunkerTimeoutErrorCassetteEventTransientError (consumer retries on next poll, does NOT advance state_event_id); NsecBunkerRpcError / SignerUnavailableError → terminal CassetteEventDecodeError (log + skip).
  • 155 passed (19 new tests for the migrated transport), 1 pre-existing async-plugin failure unchanged.
  • Live smoke against Greg's bunker-migrated account confirms the round-trip wire is correct end-to-end (bunker timeout fired during smoke, surfaced cleanly as transient; bunker NDK upgrade unblocks once Greg's bunker is unlocked).

Pending — full S7 closure still needs

  1. Pairing-wizard endpoint (POST /api/v1/dca/machines/:id/pair per the original AC). Returns a one-shot seed URL embedding a bunker bunker:// connection URL. Depends on aiolabs/lnbits#18 admin-client surface — that's landed for individual RemoteBunkerSigner.provision (Greg got migrated this way), but the per-machine create_token flow with perms=["sign_event:21000"] + 30-day expiry isn't wired through satmachineadmin yet.
  2. dca_machines.bunker_connection_id + bunker_token_expires_at columns. New schema migration (m009?). Lets the operator see the expiry + a "Re-pair" button.
  3. Revoke-ATM-access UI + the corresponding bunker_admin.revoke_user call.
  4. Lamassu-next provision-atm.sh side: stop accepting ATM_PRIVATE_KEY=<operator nsec>; consume the seed URL via --seed-url flag.
  5. All operator accounts on this instance migrated to RemoteBunkerSigner. Today only Greg is migrated (lnbits coord-log 2026-05-30T22:00Z); the LocalSigner transitional fallback in _nip44_*_via_signer covers the rest. The fallback retires when this AC clears.
  6. AC verification: "Post-migration: zero accounts.prvkey rows for any operator that owns satmachineadmin machines." Easy SQL check; meaningful only once item 5 is done.

Reference

  • PR #30 commit: dcb7de0
  • Branch: feat/cassette-config-v1
  • Cross-ref: aiolabs/lnbits PR #38 (the lnbits-side nip44 ABC this consumes)
  • Lnbits coord-log 2026-05-31T07:10Z documents the call-shape contract this migration consumes against.

Keep open until items 1-6 above are addressed.

## Partial shipment — 2026-05-31 The **runtime nip44 path** of S7 just shipped on `v2-bitspire` via PR #30 commit `dcb7de0` (`refactor(v2): cassette transport — signer.nip44_* migration`). What's done, what's left: ### Shipped - `cassette_transport.publish_to_atm` + `decrypt_and_parse_state_event` no longer read `account.prvkey` directly. They go through `resolve_signer(account).nip44_encrypt(...)` / `signer.nip44_decrypt(...)` per the `lnbits.core.signers.base.NostrSigner` ABC. - Hybrid LocalSigner-to-bunker pathway preserved: bunker-backed accounts route through `RemoteBunkerSigner` and the nsec never leaves the bunker process; the transitional LocalSigner-with-prvkey path uses the hand-rolled `nip44.py` impl via `_nip44_decrypt_via_signer`'s fallback. Same wire output either way. - `_sign_as_operator` simplified to `await signer.sign_event(event)` — previously had a broken-but-permanent ImportError fallback masquerading as the pre-#17 compatibility path (the `SignerError` import actually fails at the package root; the errors live in `.base`). - Error hierarchy properly handled: `NsecBunkerTimeoutError` → `CassetteEventTransientError` (consumer retries on next poll, does NOT advance `state_event_id`); `NsecBunkerRpcError` / `SignerUnavailableError` → terminal `CassetteEventDecodeError` (log + skip). - 155 passed (19 new tests for the migrated transport), 1 pre-existing async-plugin failure unchanged. - Live smoke against Greg's bunker-migrated account confirms the round-trip wire is correct end-to-end (bunker timeout fired during smoke, surfaced cleanly as transient; bunker NDK upgrade unblocks once Greg's bunker is unlocked). ### Pending — full S7 closure still needs 1. **Pairing-wizard endpoint** (`POST /api/v1/dca/machines/:id/pair` per the original AC). Returns a one-shot seed URL embedding a bunker `bunker://` connection URL. Depends on `aiolabs/lnbits#18` admin-client surface — that's landed for individual `RemoteBunkerSigner.provision` (Greg got migrated this way), but the per-machine `create_token` flow with `perms=["sign_event:21000"]` + 30-day expiry isn't wired through satmachineadmin yet. 2. **`dca_machines.bunker_connection_id`** + **`bunker_token_expires_at`** columns. New schema migration (m009?). Lets the operator see the expiry + a "Re-pair" button. 3. **Revoke-ATM-access UI** + the corresponding `bunker_admin.revoke_user` call. 4. **Lamassu-next `provision-atm.sh`** side: stop accepting `ATM_PRIVATE_KEY=<operator nsec>`; consume the seed URL via `--seed-url` flag. 5. **All operator accounts on this instance migrated to `RemoteBunkerSigner`**. Today only Greg is migrated (lnbits coord-log `2026-05-30T22:00Z`); the LocalSigner transitional fallback in `_nip44_*_via_signer` covers the rest. The fallback retires when this AC clears. 6. **AC verification**: "Post-migration: zero `accounts.prvkey` rows for any operator that owns satmachineadmin machines." Easy SQL check; meaningful only once item 5 is done. ### Reference - PR #30 commit: `dcb7de0` - Branch: `feat/cassette-config-v1` - Cross-ref: `aiolabs/lnbits` PR #38 (the lnbits-side nip44 ABC this consumes) - Lnbits coord-log `2026-05-31T07:10Z` documents the call-shape contract this migration consumes against. Keep open until items 1-6 above are addressed.
Author
Owner

➡️ Migrated to aiolabs/spirekeeper#12 (aiolabs/spirekeeper#12).

The v2-bitspire line of this extension now lives in its own repo, aiolabs/spirekeeper. Tracking for this issue continues there; closing here. (Issue numbers were reassigned in the new repo.)

➡️ **Migrated to https://git.atitlan.io/aiolabs/spirekeeper/issues/12 (aiolabs/spirekeeper#12).** The v2-bitspire line of this extension now lives in its own repo, `aiolabs/spirekeeper`. Tracking for this issue continues there; closing here. (Issue numbers were reassigned in the new repo.)
Sign in to join this conversation.
No labels
No milestone
No project
No assignees
1 participant
Notifications
Due date
The due date is invalid or out of range. Please use the format "yyyy-mm-dd".

No due date set.

Dependencies

No dependencies set.

Reference
aiolabs/satmachineadmin#21
No description provided.