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

Open
opened 2026-06-14 07:08:44 +00:00 by padreug · 0 comments
Owner

Migrated from aiolabs/satmachineadmin#17 (2026-06-13). Issue numbers were reassigned in this repo; cross-refs updated.

Part of #8. 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.

> _Migrated from aiolabs/satmachineadmin#17 (2026-06-13). Issue numbers were reassigned in this repo; cross-refs updated._ Part of #8. 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.
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/spirekeeper#11
No description provided.