From bb473f538504c47d7d97c2ee0506569a71df0ce7 Mon Sep 17 00:00:00 2001 From: Padreug Date: Tue, 16 Jun 2026 22:47:42 +0200 Subject: [PATCH] =?UTF-8?q?feat(pairing):=20m010=20schema=20=E2=80=94=20bu?= =?UTF-8?q?nker=20pairing=20columns=20on=20dca=5Fmachines?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit 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-); 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) --- migrations.py | 41 +++++++++++++++++++++++++++++++++++++++++ models.py | 3 +++ 2 files changed, 44 insertions(+) diff --git a/migrations.py b/migrations.py index b8e6ec0..da8ba07 100644 --- a/migrations.py +++ b/migrations.py @@ -735,3 +735,44 @@ async def m009_split_fee_fractions_by_direction(db): await db.execute( "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-`). 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}" + ) diff --git a/models.py b/models.py index c158fba..90df810 100644 --- a/models.py +++ b/models.py @@ -56,6 +56,9 @@ class Machine(BaseModel): is_active: bool operator_cash_in_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 updated_at: datetime