S6 — Roster-gated auto-account-from-npub + rate limit (LNbits-side) #20
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?
Part of #13. Closes gaps G8 (no rate limiting) and G9 (no ACL on auto-account).
Primary work is in
aiolabs/lnbitsnostr-transport. Tracked here because it directly mitigates the stale-npub1111…incident on this repo.Problem
Today (per
lnbits/core/services/nostr_transport/auth.py), the first kind-21000 from a previously-unknown npub triggers auto-account creation. There is no allowlist, no roster check, no rate limit. T7 (attacker who knows operator's npub) can spam our auto-account endpoint with arbitrary keys; combined with G3 (operator nsec on disk), this is a real foothold.Fix
NEW_ACCOUNTS_ALLOWED_OPEN=trueexplicitly set (public-provider mode)rate_limitederror and the relay subscription doesn't get torn down.Changes
LNbits side
nostr_transport/auth.py:resolve_nostr_authconsults the roster cache.NOSTR_TRANSPORT_ROSTER_REQUIRED(default true post-merge),NOSTR_TRANSPORT_RATE_LIMIT_PER_PUBKEY_PER_MIN(default 30).This repo
Acceptance
not_in_roster,rate_limited,delegation_invalid, etc.).Reference
Design doc:
docs/security-pathway-v1.md§5.1, §6.S6.2026-05-26 — status sync against lnbits-side trackers
This S6 work is captured on the LNbits side at
aiolabs/lnbits#14Item 3 (roster-gated auto-account + per-pubkey rate limit). That issue was updated 2026-05-26 with:4ce568d8— seeaiolabs/satmachineadmin#19closure.e4b5bcd7— seeaiolabs/satmachineadmin#15closure.aiolabs/lnbits#18(sidecarnsecbunkerdintegration).Sequencing: the bunker pivot doesn't block S6 directly. The roster-gated auto-account work needs the NIP-78 fleet roster to exist (so LNbits has something to gate against), which depends on S4 (#18) publishing the operator's fleet kind:30078 events. S4 is in progress.
Order of operations:
d="bitspire-fleet").resolve_nostr_authgates auto-account-from-npub on roster membership.S6 stays in Sprint 3 per the epic body (#13). When S4 is done, this issue's implementation has the input it needs.
Design refresh — 2026-05-31
The original issue body is broken-as-spec'd post-
dcd0874and partially superseded by the S2 work that's now in-flight on the bunker. Capturing the corrections inline here so the design archaeology is preserved (issue body) alongside the post-dcd0874updated direction (this comment).Two design assumptions that no longer hold
NIP-78 fleet roster as the source of truth for "is this npub a registered ATM?" — the original S6 design says auto-account-from-npub only fires "when the npub appears in some operator's NIP-78 fleet roster (depends on S4)." But S4 was shipped (
131ff92) and reverted (dcd0874) for privacy reasons: the public-relay roster leaked operator fleet composition + ATM locations + fiat codes. The replacement default posture is privacy-first; there is no public roster to consult. S6 needs a different source of truth.S2 supersedes most of S6's gating. Re-reading
aiolabs/satmachineadmin#16(S2 — NIP-46 connection-token enforcement) carefully:Under S2, every inbound kind-21000 must come from a bunker-registered ATM token, and the handler knows the target operator pubkey before crediting any wallet. That collapses S6 from "roster-gated auto-account" to "disable auto-account-from-npub entirely when bunker is configured" + the rate-limit half.
Operational evidence the gap is real, not theoretical
Today (
2026-05-30T21:33Zcoord-log) a $20 cash-out from a Sintra ATM landed on an auto-created account's wallet (a94b564f8a8d4a768972ad7e2364fdb9— pubkey = the ATM's own npub), not the operator's wallet (9927f101...for Greg). Symptom: ATM dispensed cash, customer got bills, satmachineadmin saw nothing, nodca_settlementsrow, no DCA distribution legs ran. Root cause: pre-bunker, the operator'saccounts.pubkeyhappened to match the ATM's npub by collision (manual setup artifact). Postaccounts.pubkeyrefresh, the collision broke + lnbits' nostr-transport auto-account-from-npub fired for the ATM, routing the invoice to the wrong wallet. Reproducer logged; sats sit in the auto-account, recoverable but unaccounted on the operator side.Re-scoped acceptance for S6
Replacing the original AC list:
NOSTR_TRANSPORT_ROSTER_REQUIREDbecomesNOSTR_TRANSPORT_BUNKER_REQUIRED(or similar) — auto-account-from-npub only fires when one of:NEW_ACCOUNTS_ALLOWED_OPEN=trueexplicitly set (public-provider mode)not_in_bunker_roster,rate_limited, etc.).Sequencing
The original sequencing was Sprint 3 (after S0+S2+S7 land in Sprint 2). That's still right — S6 logically follows S2, since S2 introduces the bunker-token-as-roster primitive S6 needs to consult. Once S2 ships, S6's scope is small (a few hundred lines + tests, ~few days) rather than the original "1 week" estimate against the now-defunct NIP-78 roster lookup.
Satmachineadmin-side defensive fallback (separate issue)
Independent of S6 landing, satmachineadmin has a defensive fix that catches the operational failure mode regardless of LNbits timing: when the listener gets a payment on an unknown wallet, fall back to checking
payment.extra.nostr_sender_pubkeyagainstdca_machines.machine_npub. If matched, process the settlement against that machine (sats sit in the auto-account; distribution legs draw from there). Doesn't fix the auto-account-spam attack surface S6 addresses, but does close the silent-drop failure mode. Filed separately so it can land independent of S2/S6 timing.Cross-references
aiolabs/lnbits#14Item 3 (the upstream LNbits-side tracker) — needs same update applied to its body.aiolabs/satmachineadmin#16(S2 — bunker-token enforcement) — the prerequisite that pivots S6's source-of-truth.aiolabs/satmachineadmin#18— closed viadcd0874revert (S4 NIP-78 fleet roster killed for privacy).aiolabs/satmachineadmin#27— post-launch opt-in publishing tracker (the future of any voluntary public roster surface).2026-05-30T21:33Zcoord-log entry (inarchive/2026-05-31-pre-rotation.md) — the wallet-routing miss + diagnosis chain.Cross-link: implementation tracker on the lnbits side is aiolabs/lnbits#42 — feat(nostr-transport): roster-gated auto-account-from-npub (path B).
#20 stays the design archive (post-
dcd0874rescope onto path B via the resolver-callback ABC); #42 carries the lnbits-sideroster.pyregistry +EnvSettings.nostr_transport_roster_requiredenv field + theauth.py/auth_api.py:nostr_logingating.Frozen shape contract
RouteHit(operator_user_id: str, wallet_id: str, source_extension: str)— confirmed across coord-log entries14:40Z(sat-side proposal),15:15Z(lnbits ack),15:25Z(sat-side close-out). Adding a field is a coordination round.Failure-mode posture
Aggressive / fail-closed across the board per the user's
15:20Zdirection:roster_required=true+ no resolver match → reject + ERROR logroster_required=true+ no resolvers registered → reject + ERROR log (NOT permissive fall-through)roster_required=true+ resolver raises → reject + ERROR lognostr_loginentrypoint gated under the same flag (closes the second auto-account-creation door)roster_required=false(default) preserves today's behaviour for back-compatSatmachineadmin-side prep
Resolver-exposure branch (
feat/roster-resolveroffv2-bitspire@44f6c0b) is built + committed locally + held off the remote per coord-log15:25Zsequencing rule. Ready to push the momentaiolabs/lnbits#42lands ondevand the regtest pod picks up the new image.Sequencing
feat/roster-resolver→ satmachineadmin-side PR closes #20LNBITS_NOSTR_TRANSPORT_ROSTER_REQUIRED=true+ cashout from Sintra → confirm sats land in operator's wallet, no auto-account rowrefs: coord-log §
14:40Z→ §17:25Z, aiolabs/lnbits#42, aiolabs/satmachineadmin#31/#32/#33