Gift-wrap redelivery on restart re-sends "Order already received" to every past customer and inflates unread counts #11

Open
opened 2026-10-09 17:12:00 +00:00 by padreug · 0 comments
Owner

The kind-1059 subscription deliberately has no since (nostr/nostr_client.py:152-161, correct per NIP-59), so every (re)subscribe redelivers all stored wraps for the merchant. Dedup is a 1000-entry in-memory set that starts empty on each process start (nostr_client.py:73-80). _handle_incoming_dms (services.py:656-682) then runs non-idempotent side effects regardless: increment_customer_unread_messages at :663, and after _persist_dm silently returns the already-stored row (crud.py:768 ON CONFLICT(event_id) DO NOTHING + :781-786 lookup by event_id) it still enters _handle_incoming_structured_dm → _handle_new_order, whose duplicate-order branch (services.py:803-814) returns a payment-request DM saying "Order already received and processed" that reply_to_structured_dm (:680) publishes to the customer. A merchant with N processed orders spams N customers on every deploy/restart; the 120 s stale-subscription monitor adds more redeliveries. Also is_duplicate_event marks the id seen before processing (services.py:518), so a transient bunker failure during unwrap_message loses the event for the cache lifetime.

Fix direction: check get_direct_message_by_event_id before incrementing unread / dispatching structured handling and return early if the row already exists; mark events seen only after successful processing; consider persisting seen ids so restarts don't depend on relay behavior.

Found during reforge run #1 (sandbox nostrmarket#13).

The kind-1059 subscription deliberately has no `since` (`nostr/nostr_client.py:152-161`, correct per NIP-59), so every (re)subscribe redelivers all stored wraps for the merchant. Dedup is a 1000-entry in-memory set that starts empty on each process start (`nostr_client.py:73-80`). `_handle_incoming_dms` (`services.py:656-682`) then runs non-idempotent side effects regardless: `increment_customer_unread_messages` at `:663`, and after `_persist_dm` silently returns the already-stored row (`crud.py:768` `ON CONFLICT(event_id) DO NOTHING` + `:781-786` lookup by event_id) it still enters `_handle_incoming_structured_dm` → `_handle_new_order`, whose duplicate-order branch (`services.py:803-814`) returns a payment-request DM saying "Order already received and processed" that `reply_to_structured_dm` (`:680`) publishes to the customer. A merchant with N processed orders spams N customers on every deploy/restart; the 120 s stale-subscription monitor adds more redeliveries. Also `is_duplicate_event` marks the id seen before processing (`services.py:518`), so a transient bunker failure during `unwrap_message` loses the event for the cache lifetime. Fix direction: check `get_direct_message_by_event_id` before incrementing unread / dispatching structured handling and return early if the row already exists; mark events seen only after successful processing; consider persisting seen ids so restarts don't depend on relay behavior. Found during reforge run #1 (sandbox nostrmarket#13).
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/nostrmarket#11
No description provided.