From 0ef5ce65066085fe0df7f26cafef37c9345cad4c Mon Sep 17 00:00:00 2001 From: grunch Date: Thu, 28 May 2026 14:09:20 -0300 Subject: [PATCH] fix(pow): filter wait_for_dm notifications so PoW detection actually fires MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit The concurrent PoW probe added in #173 spawned a fetch_required_pow_with task whose kind-38385 events flow through the same global notification broadcast wait_for_dm consumes (client.notifications() is global — nostr-relay-pool::relay::inner::send_notification fans every event from every subscription out to it). The notification loop returned the info event as the "first event" and short-circuited the wait before mostrod had a chance to (silently) drop the request, so the wait never timed out, the PoW probe result was never consulted, and downstream print_dm_events surfaced "No response received from Mostro" — exactly the misleading UX the original fix was meant to eliminate. Mirror the subscription filter inside the notification loop: only accept Kind::GiftWrap events whose `p` tags include our trade key. Everything else (kind-38385 info, unrelated gift wraps, status events) is ignored, so the wait reaches its timeout cleanly and the existing PowRequirementUnmet path fires as designed. Co-Authored-By: Claude Opus 4.7 (1M context) --- docs/pow_error_handling.md | 20 ++++++++++++++++++++ src/util/messaging.rs | 23 ++++++++++++++++++++--- 2 files changed, 40 insertions(+), 3 deletions(-) diff --git a/docs/pow_error_handling.md b/docs/pow_error_handling.md index 42bb453..6e7614e 100644 --- a/docs/pow_error_handling.md +++ b/docs/pow_error_handling.md @@ -170,6 +170,26 @@ Add an `&Context` parameter? Look at the signature today — passed. We just need to grant the helper access to `ctx.client` and `ctx.mostro_pubkey` (already does). +#### 4.3.1 Notification leak from the concurrent probe + +`client.notifications()` is a **global** broadcast: every event seen by +any active subscription or fetch lands on the same channel +(`nostr-relay-pool::relay::inner::send_notification`). The spawned PoW +probe issues its own subscription for kind‑38385 events to read the +required difficulty — those info events show up on the same broadcast +the wait loop is consuming. Without an application‑side filter, the +loop returns the info event as the "first event" and short‑circuits the +wait before mostrod has even had a chance to (silently) drop the +request, surfacing further downstream as `"No response received from +Mostro"`. + +Fix: the notification loop mirrors the subscription filter explicitly — +only `Kind::GiftWrap` events whose `p` tags contain `trade_keys.public_key()` +escape the loop. Anything else (kind‑38385 info, replaceable +status events, gift wraps for other trade keys) is dropped on the floor +so the wait properly reaches its timeout, where the PoW probe result +escalates to `PowRequirementUnmet`. + ### 4.4 `add_bond_invoice` interplay `add_bond_invoice` treats `WaitForDmTimeout` as the happy path (Mostro pays diff --git a/src/util/messaging.rs b/src/util/messaging.rs index f6936d2..eeb97cd 100644 --- a/src/util/messaging.rs +++ b/src/util/messaging.rs @@ -317,11 +317,12 @@ where F: std::future::Future> + Send, { let trade_keys = order_trade_keys.unwrap_or(&ctx.trade_keys); + let trade_pubkey = trade_keys.public_key(); let mut notifications = ctx.client.notifications(); let opts = SubscribeAutoCloseOptions::default().exit_policy(ReqExitPolicy::WaitForEventsAfterEOSE(1)); let subscription = Filter::new() - .pubkey(trade_keys.public_key()) + .pubkey(trade_pubkey) .kind(nostr_sdk::Kind::GiftWrap) .limit(0); ctx.client.subscribe(subscription, Some(opts)).await?; @@ -340,11 +341,27 @@ where ctx.mostro_pubkey, )); - // Wait for the DM or gift wrap event + // Wait for the DM or gift wrap event. + // + // `client.notifications()` is the **global** broadcast for every event + // any active subscription / fetch sees — including the kind-38385 info + // event coming back from the spawned PoW probe above. Without an + // application-side filter, that info event would race ahead of the real + // reply and short-circuit the wait, surfacing as "No response received + // from Mostro" further downstream. So mirror the subscription filter + // here and only accept GiftWraps tagged to our trade key. let waited = tokio::time::timeout(super::events::FETCH_EVENTS_TIMEOUT, async move { loop { match notifications.recv().await { - Ok(RelayPoolNotification::Event { event, .. }) => return Ok(*event), + Ok(RelayPoolNotification::Event { event, .. }) => { + if event.kind != nostr_sdk::Kind::GiftWrap { + continue; + } + if !event.tags.public_keys().any(|pk| *pk == trade_pubkey) { + continue; + } + return Ok(*event); + } Ok(_) => continue, Err(e) => { return Err(anyhow::anyhow!("Error receiving notification: {:?}", e));