S3 — NIP-57-style signed settlement receipts (preimage attestation) #17

Closed
opened 2026-05-15 18:08:39 +00:00 by padreug · 3 comments
Owner

Part of #13. Closes gaps G2 (Payment.extra is unauthenticated) and G7 (no signed attestation).

Problem

Today dca_settlements is the source of truth. If the LNbits DB is ever tampered with — by a super skim, an insider, or a hostile host — there's no out-of-band record. Payment.extra is mutable JSON; auditors have no cryptographic anchor.

Fix

After every settlement's three leg groups complete, LNbits publishes a signed event (NIP-57-style zap-receipt shape, possibly a custom kind in the 21001–21002 range) carrying:

{
  "kind": 9735,
  "pubkey": "<lnbits_server_pubkey>",
  "tags": [
    ["e", "<kind-21000 RPC event id>"],
    ["p", "<operator_pubkey>"],
    ["P", "<atm_pubkey>"],
    ["bolt11", "<invoice>"],
    ["preimage", "<32-byte hex>"],
    ["amount", "<msat>"],
    ["fiat", "EUR:20.00"]
  ],
  "sig": "<schnorr sig by lnbits server key>"
}

The preimage is the unforgeable anchor: it hashes to payment_hash on every dca_settlements row. Mismatch ⇒ forged settlement.

Changes

LNbits side (aiolabs/lnbits)

  • After internal pay_invoice legs complete, publisher composes + signs the receipt event using the LNbits server's identity key (the same one nostr-transport already exposes via kind:13194).
  • New service: lnbits.core.services.nostr_attestation.publish_settlement_receipt(payment, sender_pubkey, operator_pubkey).
  • Configurable relay list — defaults to the operator's NIP-65 relays once S4 lands; until then a global default.

This repo (aiolabs/satmachineadmin)

  • New subscription manager in tasks.py: subscribes to kind:9735 with #P=<atm_npub> filter for every active machine.
  • Stores receipt event ids on dca_settlements.receipt_event_id and receipt_relays (JSON).
  • Operator-dashboard column: "Receipt" with a green ✓ when found + verified, em-dash if absent. Click → modal showing the event JSON + verification status.
  • Reconciliation job: for every dca_settlements row, verify sha256(preimage_from_receipt) == payment_hash. Mismatch ⇒ alert.

Open question

Use kind:9735 (NIP-57) verbatim or a new kind? NIP-57's receipt shape fits, but the semantics are "this was a zap of a Nostr event." We're attesting a payment, not a zap. Suggest: new kind 21001 ("bitSpire settlement attestation") with the same tag shape. Decision in design review before implementation.

Acceptance

  • Cash-out completes → kind:9735 (or :21001) event present on operator's relays within 2s.
  • Receipt's preimage hashes to dca_settlements.payment_hash.
  • Operator dashboard shows ✓ next to the row.
  • After artificial DB tamper (UPDATE dca_settlements SET gross_sats=…), reconciliation flags the row as drifted from the receipt.

Reference

NIP-57 spec: ~/dev/nostr-protocol/nips/57.md.
Design doc: docs/security-pathway-v1.md §5.1 (end-state diagram), §6.S3.

Part of #13. Closes gaps G2 (Payment.extra is unauthenticated) and G7 (no signed attestation). ## Problem Today `dca_settlements` is the source of truth. If the LNbits DB is ever tampered with — by a super skim, an insider, or a hostile host — there's no out-of-band record. Payment.extra is mutable JSON; auditors have no cryptographic anchor. ## Fix After every settlement's three leg groups complete, LNbits publishes a signed event (NIP-57-style zap-receipt shape, possibly a custom kind in the 21001–21002 range) carrying: ```json { "kind": 9735, "pubkey": "<lnbits_server_pubkey>", "tags": [ ["e", "<kind-21000 RPC event id>"], ["p", "<operator_pubkey>"], ["P", "<atm_pubkey>"], ["bolt11", "<invoice>"], ["preimage", "<32-byte hex>"], ["amount", "<msat>"], ["fiat", "EUR:20.00"] ], "sig": "<schnorr sig by lnbits server key>" } ``` The **preimage** is the unforgeable anchor: it hashes to `payment_hash` on every `dca_settlements` row. Mismatch ⇒ forged settlement. ## Changes **LNbits side (`aiolabs/lnbits`)** - After internal pay_invoice legs complete, publisher composes + signs the receipt event using the LNbits server's identity key (the same one nostr-transport already exposes via `kind:13194`). - New service: `lnbits.core.services.nostr_attestation.publish_settlement_receipt(payment, sender_pubkey, operator_pubkey)`. - Configurable relay list — defaults to the operator's NIP-65 relays once S4 lands; until then a global default. **This repo (`aiolabs/satmachineadmin`)** - New subscription manager in `tasks.py`: subscribes to kind:9735 with `#P=<atm_npub>` filter for every active machine. - Stores receipt event ids on `dca_settlements.receipt_event_id` and `receipt_relays` (JSON). - Operator-dashboard column: "Receipt" with a green ✓ when found + verified, em-dash if absent. Click → modal showing the event JSON + verification status. - Reconciliation job: for every `dca_settlements` row, verify `sha256(preimage_from_receipt) == payment_hash`. Mismatch ⇒ alert. ## Open question **Use `kind:9735` (NIP-57) verbatim or a new kind?** NIP-57's receipt shape fits, but the semantics are "this was a zap of a Nostr event." We're attesting a payment, not a zap. Suggest: new kind `21001` ("bitSpire settlement attestation") with the same tag shape. Decision in design review before implementation. ## Acceptance - [ ] Cash-out completes → kind:9735 (or :21001) event present on operator's relays within 2s. - [ ] Receipt's preimage hashes to `dca_settlements.payment_hash`. - [ ] Operator dashboard shows ✓ next to the row. - [ ] After artificial DB tamper (`UPDATE dca_settlements SET gross_sats=…`), reconciliation flags the row as drifted from the receipt. ## Reference NIP-57 spec: `~/dev/nostr-protocol/nips/57.md`. Design doc: `docs/security-pathway-v1.md` §5.1 (end-state diagram), §6.S3.
Author
Owner

2026-05-26 — ATM-side prerequisite is now ready

The ATM-side receipt-subscription path planned in the Changes section of this issue will use LnbitsClient.subscribePayments. Per the bitspire session, that path's hash-based dedup primitive just shipped at aiolabs/lamassu-next#50 (commit 8c4be01):

  • Client-global seenEventIds (FIFO 1000) in LnbitsClient.handleReply — catches exact-replay.
  • Per-ActiveSubscription seenPaymentHashes (FIFO 500) — catches logical duplicates from server fan-out.

When S3 work begins, the ATM's subscribePayments({kinds: [9735], '#P': [atm_npub]}) callback will get duplicate-safe processing for free. The bitspire session confirmed this dependency was the blocker; it's no longer one.

Other state:

  • Open question on kind choice (9735 vs 21001) still needs a design call before implementation.
  • Relay defaulting ("operator's NIP-65 relays once S4 lands; until then a global default") is blocked on S4 (#18) which is in progress now on the satmachineadmin side.
  • Cross-codebase reach: when this lands, the lamassu-next ATM will need a small consumer-side issue filed against the aiolabs/lamassu-next repo — the bitspire session offered to wire it once the kind/event-shape is locked (per their 2026-05-26 review note on #50).

This issue is not yet unblocked overall — still needs the LNbits-server-side publisher work (the actual settlement-receipt emission). But the ATM-side hot path is ready.

## 2026-05-26 — ATM-side prerequisite is now ready The ATM-side receipt-subscription path planned in the **Changes** section of this issue will use `LnbitsClient.subscribePayments`. Per the bitspire session, that path's hash-based dedup primitive just shipped at `aiolabs/lamassu-next#50` (commit `8c4be01`): - Client-global `seenEventIds` (FIFO 1000) in `LnbitsClient.handleReply` — catches exact-replay. - Per-`ActiveSubscription` `seenPaymentHashes` (FIFO 500) — catches logical duplicates from server fan-out. When S3 work begins, the ATM's `subscribePayments({kinds: [9735], '#P': [atm_npub]})` callback will get duplicate-safe processing for free. The bitspire session confirmed this dependency was the blocker; it's no longer one. **Other state:** - **Open question on kind choice** (9735 vs 21001) still needs a design call before implementation. - **Relay defaulting** ("operator's NIP-65 relays once S4 lands; until then a global default") is blocked on S4 (#18) which is in progress now on the satmachineadmin side. - **Cross-codebase reach:** when this lands, the lamassu-next ATM will need a small consumer-side issue filed against the `aiolabs/lamassu-next` repo — the bitspire session offered to wire it once the kind/event-shape is locked (per their 2026-05-26 review note on #50). This issue is not yet unblocked overall — still needs the LNbits-server-side publisher work (the actual settlement-receipt emission). But the ATM-side hot path is ready.
Author
Owner

2026-05-26 — producer-side issue filed at aiolabs/lnbits#22

The S3 work has two halves; the LNbits-server-side publisher half is now a separate tracking issue: aiolabs/lnbits#22 — "nostr-transport: publish signed settlement-receipt events (NIP-57-style preimage attestation)".

This consumer-side issue (#17) waits until that producer issue ships. There's nothing to subscribe to without receipts on the wire.

When lnbits#22 lands the events, the consumer side here is:

  1. New subscription manager in tasks.pysubscribePayments-style filter {"kinds": [9735 or 21001], "#p": ["<operator_pubkey>"]} per active machine.
  2. Schema additions: dca_settlements.receipt_event_id + receipt_relays (JSON).
  3. Reconciliation job: sha256(preimage_from_receipt) == payment_hash on every row.
  4. Operator dashboard column: "Receipt" with ✓ when present + verified, em-dash when absent. Click → modal with event JSON + sig verification.

The hash-dedup primitive that this'll piggyback on already shipped in aiolabs/lamassu-next#50 (commit 8c4be01).

Closing as paused-on-upstream; will reopen / re-engage when aiolabs/lnbits#22 ships.

## 2026-05-26 — producer-side issue filed at `aiolabs/lnbits#22` The S3 work has two halves; the LNbits-server-side publisher half is now a separate tracking issue: **`aiolabs/lnbits#22`** — "nostr-transport: publish signed settlement-receipt events (NIP-57-style preimage attestation)". This consumer-side issue (`#17`) **waits** until that producer issue ships. There's nothing to subscribe to without receipts on the wire. When `lnbits#22` lands the events, the consumer side here is: 1. New subscription manager in `tasks.py` — `subscribePayments`-style filter `{"kinds": [9735 or 21001], "#p": ["<operator_pubkey>"]}` per active machine. 2. Schema additions: `dca_settlements.receipt_event_id` + `receipt_relays` (JSON). 3. Reconciliation job: `sha256(preimage_from_receipt) == payment_hash` on every row. 4. Operator dashboard column: "Receipt" with ✓ when present + verified, em-dash when absent. Click → modal with event JSON + sig verification. The hash-dedup primitive that this'll piggyback on already shipped in `aiolabs/lamassu-next#50` (commit `8c4be01`). Closing as **paused-on-upstream**; will reopen / re-engage when `aiolabs/lnbits#22` ships.
Author
Owner

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

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/11 (aiolabs/spirekeeper#11).** 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#17
No description provided.