feat(pairing): seed-URL pairing — operator-side producer (S0 / #9) #21

Merged
padreug merged 4 commits from feat/seed-url-pairing into main 2026-06-18 17:03:34 +00:00
2 changed files with 44 additions and 0 deletions
Showing only changes of commit bb473f5385 - Show all commits

feat(pairing): m010 schema — bunker pairing columns on dca_machines

Schema checkpoint for seed-URL pairing (S0 / #9; spire-side bitspire#52),
model A1 — the spire's signing key lives in the operator's nsecbunkerd,
not on the spire's disk.

dca_machines gains:
  - bunker_spire_key_name — the spire's key name in the bunker
    (spire-<machine_id>); used to re-issue connect tokens on re-pair.
  - paired_at — last successful pair; NULL = never paired.

Both nullable, idempotent column-probe add (m009 pattern). Machine model
gains the matching optional fields. Validated on the regtest dev db
(columns present, migrations clean); 191 tests green.

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
Padreug 2026-06-16 22:47:42 +02:00

View file

@ -735,3 +735,44 @@ async def m009_split_fee_fractions_by_direction(db):
await db.execute( await db.execute(
"ALTER TABLE spirekeeper.super_config DROP COLUMN super_fee_fraction" "ALTER TABLE spirekeeper.super_config DROP COLUMN super_fee_fraction"
) )
async def m010_add_machine_bunker_pairing(db):
"""Add NIP-46 bunker-pairing columns to dca_machines for seed-URL
pairing (S0 / aiolabs/spirekeeper#9; spire-side aiolabs/bitspire#52).
Under the chosen model (A1, decided 2026-06-16), the spire's signing
key lives inside the operator's nsecbunkerd rather than on the spire's
disk. `pair_machine` mints a per-spire key in the bunker, issues a
scoped NIP-46 connect token, and hands the spire a one-shot seed URL
embedding a `bunker://` connection. The spire then self-signs all its
events (kind-21000 RPC, kind-30078 beacon/cassette-state) as its own
bunker-held key; lnbits' path-B roster routes that npub to the
operator's wallet.
("spire" = a bitSpire machine; the legacy Lamassu term was "ATM".)
- bunker_spire_key_name the spire's key name inside the bunker
(`spire-<machine_id>`). Used to re-issue a connect token on
re-pair and (once the admin client grows a revoke RPC) to revoke
spire access.
- paired_at timestamp of the last successful pair. NULL = the
machine row exists but no bunker key has been minted yet.
Both nullable: machines created before this migration, and registered
-but-never-paired machines, carry NULL until first pair. Idempotent
column-probe pattern (same shape as m009).
"""
additions = [
("dca_machines", "bunker_spire_key_name", "TEXT"),
("dca_machines", "paired_at", "TIMESTAMP"),
]
for table, col, coltype in additions:
try:
await db.fetchone(f"SELECT {col} FROM spirekeeper.{table} LIMIT 1")
continue
except Exception:
pass
await db.execute(
f"ALTER TABLE spirekeeper.{table} ADD COLUMN {col} {coltype}"
)

View file

@ -56,6 +56,9 @@ class Machine(BaseModel):
is_active: bool is_active: bool
operator_cash_in_fee_fraction: float = 0.0 operator_cash_in_fee_fraction: float = 0.0
operator_cash_out_fee_fraction: float = 0.0 operator_cash_out_fee_fraction: float = 0.0
# NIP-46 bunker pairing (S0 / #9). NULL until the spire is first paired.
bunker_spire_key_name: str | None = None
paired_at: datetime | None = None
created_at: datetime created_at: datetime
updated_at: datetime updated_at: datetime