Skip to content

Initial gift-wrap subscription lookback ignores max_notification_age_secs / future_skew, under-fetching on startup relative to the server's own acceptance window and the reconnect path #328

Description

@erskingardner

Summary

initial_subscription_since sets the startup subscription since to now - NIP59_TIMESTAMP_TWEAK_WINDOW_SECS only. It never factors in max_notification_age_secs or max_notification_future_skew_secs, even though (a) the reconnect path does, and (b) the event processor will accept rumors up to max_notification_age_secs old. Because a kind:1059 gift wrap's created_at is randomized up to the tweak window into the past of the rumor's real creation time, the startup subscription requests a strictly narrower window than the server is willing to accept, so a fraction of otherwise-valid events published during downtime are never fetched after a restart.

Location

  • src/nostr/client.rs:156-158initial_subscription_since (used at src/nostr/client.rs:590 in subscribe)
  • Contrast with src/nostr/client.rs:160-176reconnect_subscription_since, which floors the lookback at now - max_notification_age_secs
  • Acceptance side: src/nostr/events/processor.rs (validate_notification_freshness, max_notification_age)

Details

To be guaranteed to receive a rumor created at time T, the subscription since must be <= T - tweak_window (the wrap timestamp can be back-dated by up to the tweak window). The server accepts rumors as fresh while rumor_created_at >= now - max_notification_age_secs. So to catch every still-acceptable event, since must be:

now - max_notification_age_secs - tweak_window - future_skew

The reconnect path computes exactly this (disconnect.max(now - age) - tweak - skew). The initial path computes only now - tweak, dropping the - max_notification_age_secs term. Two consequences:

  1. At defaults (max_notification_age_secs = 3600, tweak ≈ 2 days): an event created shortly before startup whose wrap timestamp was maximally back-dated has a wrap created_at below now - tweak and is never requested. This is a deterministic ~age/tweak (≈2%) band of loss for events published just before a restart — a small but real gap on every restart.
  2. If max_notification_age_secs > NIP59_TIMESTAMP_TWEAK_WINDOW_SECS (a supported config): the startup catch-up window becomes strictly smaller than both the acceptance window and the reconnect lookback, so a large fraction of fresh events published during downtime are silently never fetched after a restart.

The internal inconsistency is the crux: validate_notification_freshness explicitly accepts events the initial subscription never asks the relays for.

Why this is not a duplicate

None note that initial_subscription_since requests a narrower window than the server's acceptance bound.

Recommendation

Make initial_subscription_since take the same SubscriptionLookbackConfig and floor its lookback at now - max_notification_age_secs (then subtract tweak + skew), matching reconnect_subscription_since, so the initial fetch window is never narrower than what the processor will accept.

Impact

Deterministic small loss (~2% of just-before-restart events) at defaults; potentially large loss under max_notification_age_secs > tweak_window. LOW at default config.

Metadata

Metadata

Assignees

No one assigned

    Type

    No type

    Fields

    No fields configured for issues without a type.

    Projects

    No projects

    Milestone

    No milestone

    Relationships

    None yet

    Development

    No branches or pull requests

    Issue actions