feat(transport): get_machine_config RPC — server-delivered machine config (#41) #42

Merged
padreug merged 1 commit from feat/get-machine-config-rpc into main 2026-07-02 21:52:50 +00:00
Owner

Closes #41. Client half: aiolabs/bitspire#71 (+ its PR).

What

A paired ATM pulls its operator pubkey + fee config over the already-authenticated kind-21000 transport, instead of the operator pubkey being provisioned into the machine's .env and the fee config learned only from an operator-signed kind-30078 broadcast. This lets a seed-only machine (blank .env, scanned spire-seed) leave "awaiting configuration" with zero per-machine provisioning — closing the gap surfaced by bitspire#70.

How

machine_config_transport.py registers a get_machine_config RPC (mirrors the reviewed cashin_transport pattern):

  • Resolves the machine purely from the verified request.sender_pubkey (the ATM's bunker-minted spire key == dca_machines.machine_npub) — same lookup as cashin_transport, no client-supplied trust; only ever returns the caller's own config.
  • Returns {operator_pubkey, fee_config, fiat_code, machine_npub, wallet_id, created_at}. fee_config is None until the operator has a super-config; created_at is a freshness watermark (max of machine + super-config updated_at) for the consumer's replay guard.
  • Reuses build_fee_payload + get_account; AUTH_ACCOUNT; the reply is NIP-44 v2 encrypted to the sender by the transport (no operator-key encryption here).
  • Registration soft-fails on older lnbits (no register_rpc), like the roster hook — spirekeeper still boots; ATMs fall back to env/kind-30078.

The kind-30078 push path stays dual-run for live mid-run fee updates.

Trust rationale (per #41)

Delivering the operator pubkey over the transport means LNbits introduces the operator identity. On this stack that doesn't widen the trust base — spirekeeper is an LNbits extension in the same trust domain, and the ATM already trusts LNbits with the wallet and server-side fee computation. So this aligns config trust with the trust already extended for money. (Full seed-shape / NWC / Lightning.Pub analysis is in #41.)

Tests

6 new tests (test_machine_config_transport.py) — returns config, watermark-is-max, no-super-config, unpaired sender, operator-without-pubkey, missing-sender. Full suite 235 pass; black + ruff clean.

Validated on hardware

Against a patched aio-demo, a Sintra (machine FqnYZ…) that was sitting at "awaiting configuration" pulled its config and reached idle — server log: get_machine_config machine=FqnYZ… operator=8b2498…​ fee_config=present; ATM log: Operator pubkey(s): … (server-delivered, #70 P1)Server-delivered fee config: appliedState: idle.

Process

Money-adjacent extension → merge via the Forgejo UI after review (I don't merge). Additive + soft-failing, so it's safe to ship ahead of the bitspire client.

🤖 Generated with Claude Code

Closes #41. Client half: aiolabs/bitspire#71 (+ its PR). ## What A paired ATM pulls its **operator pubkey + fee config** over the already-authenticated kind-21000 transport, instead of the operator pubkey being provisioned into the machine's `.env` and the fee config learned only from an operator-signed kind-30078 broadcast. This lets a **seed-only** machine (blank `.env`, scanned `spire-seed`) leave "awaiting configuration" with **zero per-machine provisioning** — closing the gap surfaced by bitspire#70. ## How `machine_config_transport.py` registers a `get_machine_config` RPC (mirrors the reviewed `cashin_transport` pattern): - Resolves the machine purely from the **verified `request.sender_pubkey`** (the ATM's bunker-minted spire key == `dca_machines.machine_npub`) — same lookup as `cashin_transport`, no client-supplied trust; only ever returns the caller's own config. - Returns `{operator_pubkey, fee_config, fiat_code, machine_npub, wallet_id, created_at}`. `fee_config` is None until the operator has a super-config; `created_at` is a freshness watermark (max of machine + super-config `updated_at`) for the consumer's replay guard. - Reuses `build_fee_payload` + `get_account`; `AUTH_ACCOUNT`; the reply is NIP-44 v2 encrypted to the sender by the transport (no operator-key encryption here). - Registration soft-fails on older lnbits (no `register_rpc`), like the roster hook — spirekeeper still boots; ATMs fall back to env/kind-30078. The kind-30078 push path stays **dual-run** for live mid-run fee updates. ## Trust rationale (per #41) Delivering the operator pubkey over the transport means LNbits *introduces* the operator identity. On this stack that doesn't widen the trust base — spirekeeper is an LNbits extension in the same trust domain, and the ATM already trusts LNbits with the wallet and server-side fee computation. So this aligns config trust with the trust already extended for money. (Full seed-shape / NWC / Lightning.Pub analysis is in #41.) ## Tests 6 new tests (`test_machine_config_transport.py`) — returns config, watermark-is-max, no-super-config, unpaired sender, operator-without-pubkey, missing-sender. Full suite **235 pass**; black + ruff clean. ## Validated on hardware Against a patched aio-demo, a Sintra (machine `FqnYZ…`) that was sitting at "awaiting configuration" pulled its config and reached `idle` — server log: `get_machine_config machine=FqnYZ… operator=8b2498…​ fee_config=present`; ATM log: `Operator pubkey(s): … (server-delivered, #70 P1)` → `Server-delivered fee config: applied` → `State: idle`. ## Process Money-adjacent extension → **merge via the Forgejo UI** after review (I don't merge). Additive + soft-failing, so it's safe to ship ahead of the bitspire client. 🤖 Generated with [Claude Code](https://claude.com/claude-code)
feat(transport): get_machine_config RPC — server-delivered machine config (#41)
Some checks failed
ci.yml / feat(transport): get_machine_config RPC — server-delivered machine config (#41) (pull_request) Failing after 0s
1f5652b425
A paired ATM pulls its operator pubkey + fee config over the already-
authenticated kind-21000 transport, instead of the operator pubkey being
provisioned into the machine's .env and the fee config learned only from an
operator-signed kind-30078 broadcast. This lets a seed-only machine (blank
.env, scanned spire-seed) leave "awaiting configuration" with zero per-machine
provisioning — closing the gap surfaced by bitspire#70. Client half: bitspire#71.

The transport is already per-machine authenticated (the ATM signs with its
bunker-minted spire key == dca_machines.machine_npub), so the handler resolves
the exact machine → operator from the verified request.sender_pubkey — same
lookup as cashin_transport, no client-supplied trust — and returns only the
caller's own config. Returns {operator_pubkey, fee_config, fiat_code,
machine_npub, wallet_id, created_at}; fee_config is None until the operator has
a super-config. Reuses build_fee_payload + get_account; AUTH_ACCOUNT; registers
soft-failing (older lnbits) like the roster hook. kind-30078 push stays
dual-run for live mid-run updates.

6 tests; full suite 235 pass; black + ruff clean.

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
padreug deleted branch feat/get-machine-config-rpc 2026-07-02 21:52:50 +00:00
Sign in to join this conversation.
No reviewers
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/spirekeeper!42
No description provided.