Defensive: route inbound payment by ATM-npub fallback when wallet lookup fails #31
Loading…
Add table
Add a link
Reference in a new issue
No description provided.
Delete branch "%!s()"
Deleting a branch is permanent. Although the deleted branch may continue to exist for a short time before it actually gets removed, it CANNOT be undone in most cases. Continue?
Problem
tasks._handle_paymentresolves the target machine viaget_active_machine_by_wallet_id(payment.wallet_id). If no machine matches that wallet, the handler silently returns and the payment is never recorded as adca_settlementsrow.This is correct for "random unrelated payments that happen to land on a wallet that isn't ours." But it's wrong for a real failure mode demonstrated end-to-end on 2026-05-30: a kind-21000 RPC from a known ATM credits the wrong wallet (because lnbits' nostr-transport auto-account-from-npub flow created an account for the ATM's own pubkey, and the invoice routed to that account's wallet rather than the operator's). The ATM dispensed cash on the strength of the Lightning payment notification (its own view is "invoice paid, dispense"), and satmachineadmin lost the settlement entirely. No
dca_settlementsrow, no DCA distribution legs, no commission split, no super_fee, no LP payouts. Sats sit in the auto-account wallet, recoverable but unaccounted.Reproducer in coord-log
archive/2026-05-31-pre-rotation.md(2026-05-30T21:33Zentry + thread). $20 cash-out from Sintra to Greg's machine; payment landed on auto-accounta94b564f8a8d4a768972ad7e2364fdb9(pubkey = the ATM's npub) instead of Greg's registered wallet9927f101....Why "proper fix lands in S6" isn't enough by itself
#20(S6 — roster-gated auto-account on the lnbits side) is the architectural fix: prevent the auto-account from being created in the first place, so the invoice routes to the operator's wallet instead. That's the right long-term answer.But:
#13epic + has a pending design refresh (see the#20comment thread post-dcd0874). Realistic ETA is post-S0/S2/S7. Multiple weeks.A satmachineadmin-side defensive lookup is cheap, local, additive, and fail-safe — and crucially, makes the silent-drop failure mode loud rather than invisible. The pubkey-collision root cause is tracked separately at
#32; this issue addresses the symptom-side mitigation.Proposed fix
In
tasks._handle_payment, whenget_active_machine_by_wallet_id(payment.wallet_id)returns None, attempt a fallback lookup:get_active_machine_by_atm_pubkey_hexalready exists incrud.py(used by the cassette consumer). The lookup is O(N over active machines) which is fine for any realistic operator fleet.Once a machine is resolved via the fallback path, the rest of
_handle_paymentproceeds identically: attribution check (the existing check pairssender_pubkeyagainstmachine.machine_npub— passes by construction here),parse_settlement, idempotent insert, distribution. The distribution legs draw frompayment.wallet_idwhich is the auto-account wallet, so the operator's commission_split + DCA payouts come from the auto-account's balance (which has the just-credited sats). Operationally functional even though the wallet ownership is "wrong" from the operator's perspective.Acceptance
dca_machinesrow, butpayment.extra.nostr_sender_pubkeymatches a registered ATM'smachine_npub, the listener resolves the machine via the fallback path and records the settlement as if the payment had landed on the registered wallet.payment.wallet_id(the wallet the sats actually landed in). Test on regtest with both a registered-wallet payment and a fallback-resolved payment.get_active_machine_by_atm_pubkey_hex.Out of scope
#20work (lnbits-side roster gating). This issue is the satmachineadmin-side complement; both can land independently. When S6 ships and disables auto-account-from-npub for bunker-registered ATMs, the fallback path here will fire less often but stays as a safety net for any future routing drift.#32. Together with this issue they form the two layers of defence:#32prevents the dependency that breaks; this issue catches the symptom when the dependency does break.Sequencing
Ship-able today; no upstream dependency. Half-day max.
Cross-references
aiolabs/satmachineadmin#20— S6 long-term proper fix on the lnbits side.aiolabs/satmachineadmin#32— pubkey-collision detection (root-cause-side guard); pairs with this issue's symptom-side guard.aiolabs/satmachineadmin#13— epic.archive/2026-05-31-pre-rotation.md2026-05-30T21:33Z entry — the failure-mode reproducer + diagnosis.crud.get_active_machine_by_atm_pubkey_hex— the lookup helper the fallback consumes (already lives there for the cassette consumer's identical sender-resolution concern).➡️ Migrated to aiolabs/spirekeeper#17 (aiolabs/spirekeeper#17).
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.)