Skip to content

Commit 0ef5ce6

Browse files
grunchclaude
andcommitted
fix(pow): filter wait_for_dm notifications so PoW detection actually fires
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) <noreply@anthropic.com>
1 parent 25e07ae commit 0ef5ce6

2 files changed

Lines changed: 40 additions & 3 deletions

File tree

docs/pow_error_handling.md

Lines changed: 20 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -170,6 +170,26 @@ Add an `&Context` parameter? Look at the signature today —
170170
passed. We just need to grant the helper access to `ctx.client` and
171171
`ctx.mostro_pubkey` (already does).
172172

173+
#### 4.3.1 Notification leak from the concurrent probe
174+
175+
`client.notifications()` is a **global** broadcast: every event seen by
176+
any active subscription or fetch lands on the same channel
177+
(`nostr-relay-pool::relay::inner::send_notification`). The spawned PoW
178+
probe issues its own subscription for kind‑38385 events to read the
179+
required difficulty — those info events show up on the same broadcast
180+
the wait loop is consuming. Without an application‑side filter, the
181+
loop returns the info event as the "first event" and short‑circuits the
182+
wait before mostrod has even had a chance to (silently) drop the
183+
request, surfacing further downstream as `"No response received from
184+
Mostro"`.
185+
186+
Fix: the notification loop mirrors the subscription filter explicitly —
187+
only `Kind::GiftWrap` events whose `p` tags contain `trade_keys.public_key()`
188+
escape the loop. Anything else (kind‑38385 info, replaceable
189+
status events, gift wraps for other trade keys) is dropped on the floor
190+
so the wait properly reaches its timeout, where the PoW probe result
191+
escalates to `PowRequirementUnmet`.
192+
173193
### 4.4 `add_bond_invoice` interplay
174194

175195
`add_bond_invoice` treats `WaitForDmTimeout` as the happy path (Mostro pays

src/util/messaging.rs

Lines changed: 20 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -317,11 +317,12 @@ where
317317
F: std::future::Future<Output = Result<()>> + Send,
318318
{
319319
let trade_keys = order_trade_keys.unwrap_or(&ctx.trade_keys);
320+
let trade_pubkey = trade_keys.public_key();
320321
let mut notifications = ctx.client.notifications();
321322
let opts =
322323
SubscribeAutoCloseOptions::default().exit_policy(ReqExitPolicy::WaitForEventsAfterEOSE(1));
323324
let subscription = Filter::new()
324-
.pubkey(trade_keys.public_key())
325+
.pubkey(trade_pubkey)
325326
.kind(nostr_sdk::Kind::GiftWrap)
326327
.limit(0);
327328
ctx.client.subscribe(subscription, Some(opts)).await?;
@@ -340,11 +341,27 @@ where
340341
ctx.mostro_pubkey,
341342
));
342343

343-
// Wait for the DM or gift wrap event
344+
// Wait for the DM or gift wrap event.
345+
//
346+
// `client.notifications()` is the **global** broadcast for every event
347+
// any active subscription / fetch sees — including the kind-38385 info
348+
// event coming back from the spawned PoW probe above. Without an
349+
// application-side filter, that info event would race ahead of the real
350+
// reply and short-circuit the wait, surfacing as "No response received
351+
// from Mostro" further downstream. So mirror the subscription filter
352+
// here and only accept GiftWraps tagged to our trade key.
344353
let waited = tokio::time::timeout(super::events::FETCH_EVENTS_TIMEOUT, async move {
345354
loop {
346355
match notifications.recv().await {
347-
Ok(RelayPoolNotification::Event { event, .. }) => return Ok(*event),
356+
Ok(RelayPoolNotification::Event { event, .. }) => {
357+
if event.kind != nostr_sdk::Kind::GiftWrap {
358+
continue;
359+
}
360+
if !event.tags.public_keys().any(|pk| *pk == trade_pubkey) {
361+
continue;
362+
}
363+
return Ok(*event);
364+
}
348365
Ok(_) => continue,
349366
Err(e) => {
350367
return Err(anyhow::anyhow!("Error receiving notification: {:?}", e));

0 commit comments

Comments
 (0)