Source operator pubkey + fee config from LNbits over the transport, not env/seed (#70 P1) #71

Open
opened 2026-07-02 19:15:44 +00:00 by padreug · 1 comment
Owner

Summary

Make the ATM pull its operator pubkey + fee config from LNbits via a get_machine_config kind-21000 RPC right after pairing, instead of reading the operator pubkey from VITE_OPERATOR_PUBKEYS (env) and learning fees only from an operator-signed kind-30078 broadcast. This is the client half of #70 P1; the server RPC is the companion issue on aiolabs/spirekeeper.

Outcome: a truly-fresh, seed-only machine (blank .env, scanned spire-seed) leaves "awaiting configuration" with zero provisioning — and we never add a 3rd/4th npub to the seed.

Background

The pairing seed carries the two channel anchors it must (relay + lnbits_npub for the LNbits channel; spire_npub + bunker_secret for the NIP-46 bunker). The operator pubkey — which the ATM needs to trust the fee config — is config, not a channel anchor, and is currently only sourced from VITE_OPERATOR_PUBKEYS (src/services/lightning.ts:91). A seed-only machine has it empty → startOperatorFeesService / startOperatorConfigService disable themselves (operator-fees.ts:105-108, operator-config.ts:75) → getFeeConfig() null → permanent awaiting-fees maintenance (stores/atm.ts:1074-1081).

The decided fix (see the spirekeeper issue for full rationale — NWC / NIP-46 / Lightning.Pub all keep the pairing secret minimal and let the server advertise the rest) is: LNbits delivers operator pubkey + fee config over the already-authenticated transport.

P0 (remnant hygiene — landed) already stopped provisioning VITE_OPERATOR_PUBKEYS, so post-P0 a fresh machine honestly reports [Lightning] Operator pubkey(s): (none — …#70 P1). P1 closes it.

Why it's a small client change

initializeForProduction deliberately does NOT bail on awaiting-fees — it starts services and unblocks reactively (stores/atm.ts:1067-1081), and applyFeeConfig already does the watermark + range validation + reactive UI unblock (electron/state-store.ts:729). So the RPC reply just needs to flow into paths that already exist.

Proposed implementation

  1. LnbitsClient.getMachineConfig() — add to packages/lnbits/src/client.ts (+ types in packages/lnbits/src/types.ts). Returns { operator_pubkey, fee_config, fiat_code, cassettes?, machine_npub, wallet_id, created_at }.
  2. Call it right after list_wallets in src/services/lightning.ts (~:491), on the same authenticated client.
  3. Populate CONFIG.operatorPubkeys from response.operator_pubkey when VITE_OPERATOR_PUBKEYS is empty (env stays an override; keep the provenance logging added in P0). This re-enables the fees/operator-config/management services.
  4. Feed fee_config into the existing applyFeeConfig IPC path (electron/state-store.ts) using the RPC's created_at as the replay watermark — so the existing guards work unchanged and the reactive awaiting-fees → idle unblock fires.
  5. Seed stays minimal — do NOT add operator_npub (or any new field) to packages/nostr-client/src/seed.ts.

Notes / edges

  • operatorPubkeys is load-bearing in three places (operator-config.ts authors filter + reverse-channel recipient, operator-fees.ts authors filter, lightning.ts:504 kind-21003 management allowlist). Sourcing all three from the RPC value is fine, but verify each once wired.
  • Live updates: keep consuming the kind-30078 push for mid-run fee edits; the RPC handles boot/reconnect. (Server keeps the push dual-run per the spirekeeper issue.)
  • Watermark: use the server created_at so re-pair + resetForRepair (P0) interplay stays correct.

Server RPC: aiolabs/spirekeeper companion issue. Parent: #70 (P0 remnant-hygiene commits landed; P1 is this). Context: the seed-shape + trust-domain analysis is in the spirekeeper issue.

## Summary Make the ATM pull its **operator pubkey + fee config** from LNbits via a `get_machine_config` kind-21000 RPC right after pairing, instead of reading the operator pubkey from `VITE_OPERATOR_PUBKEYS` (env) and learning fees only from an operator-signed kind-30078 broadcast. This is the client half of **#70 P1**; the server RPC is the companion issue on `aiolabs/spirekeeper`. **Outcome:** a truly-fresh, seed-only machine (blank `.env`, scanned `spire-seed`) leaves "awaiting configuration" with **zero provisioning** — and we never add a 3rd/4th npub to the seed. ## Background The pairing seed carries the two channel anchors it must (relay + `lnbits_npub` for the LNbits channel; `spire_npub` + `bunker_secret` for the NIP-46 bunker). The **operator pubkey** — which the ATM needs to trust the fee config — is *config, not a channel anchor*, and is currently only sourced from `VITE_OPERATOR_PUBKEYS` (`src/services/lightning.ts:91`). A seed-only machine has it empty → `startOperatorFeesService` / `startOperatorConfigService` disable themselves (`operator-fees.ts:105-108`, `operator-config.ts:75`) → `getFeeConfig()` null → permanent `awaiting-fees` maintenance (`stores/atm.ts:1074-1081`). The decided fix (see the spirekeeper issue for full rationale — NWC / NIP-46 / Lightning.Pub all keep the pairing secret minimal and let the server advertise the rest) is: **LNbits delivers operator pubkey + fee config over the already-authenticated transport.** P0 (remnant hygiene — landed) already stopped provisioning `VITE_OPERATOR_PUBKEYS`, so post-P0 a fresh machine honestly reports `[Lightning] Operator pubkey(s): (none — …#70 P1)`. P1 closes it. ## Why it's a small client change `initializeForProduction` deliberately does NOT bail on awaiting-fees — it starts services and unblocks reactively (`stores/atm.ts:1067-1081`), and `applyFeeConfig` already does the watermark + range validation + reactive UI unblock (`electron/state-store.ts:729`). So the RPC reply just needs to flow into paths that already exist. ## Proposed implementation 1. **`LnbitsClient.getMachineConfig()`** — add to `packages/lnbits/src/client.ts` (+ types in `packages/lnbits/src/types.ts`). Returns `{ operator_pubkey, fee_config, fiat_code, cassettes?, machine_npub, wallet_id, created_at }`. 2. **Call it right after `list_wallets`** in `src/services/lightning.ts` (~`:491`), on the same authenticated client. 3. **Populate `CONFIG.operatorPubkeys`** from `response.operator_pubkey` when `VITE_OPERATOR_PUBKEYS` is empty (env stays an override; keep the provenance logging added in P0). This re-enables the fees/operator-config/management services. 4. **Feed `fee_config` into the existing `applyFeeConfig` IPC path** (`electron/state-store.ts`) using the RPC's `created_at` as the replay watermark — so the existing guards work unchanged and the reactive `awaiting-fees` → idle unblock fires. 5. **Seed stays minimal** — do NOT add `operator_npub` (or any new field) to `packages/nostr-client/src/seed.ts`. ## Notes / edges - **`operatorPubkeys` is load-bearing in three places** (`operator-config.ts` authors filter + reverse-channel recipient, `operator-fees.ts` authors filter, `lightning.ts:504` kind-21003 management allowlist). Sourcing all three from the RPC value is fine, but verify each once wired. - **Live updates**: keep consuming the kind-30078 push for mid-run fee edits; the RPC handles boot/reconnect. (Server keeps the push dual-run per the spirekeeper issue.) - **Watermark**: use the server `created_at` so re-pair + `resetForRepair` (P0) interplay stays correct. ## Related Server RPC: `aiolabs/spirekeeper` companion issue. Parent: #70 (P0 remnant-hygiene commits landed; P1 is this). Context: the seed-shape + trust-domain analysis is in the spirekeeper issue.
Author
Owner

Companion server RPC issue: aiolabs/spirekeeper#41 (has the full seed-shape + trust-domain rationale).

Companion server RPC issue: aiolabs/spirekeeper#41 (has the full seed-shape + trust-domain rationale).
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/bitspire#71
No description provided.