4.6 KiB
Peer-request replay protection
A relay poll can return the same signed encrypted request many times. Rejecting or cancelling a request must not make that old event a new pending request. Conversely, rejecting one request is not a permanent block on the sender.
Storage and admission
The receiver verifies the Nostr envelope before carrying its event ID and signed creation time into inbound request admission. A production insertion requires that metadata; the metadata-free insertion helper exists only for legacy test fixtures. Approval, membership and DID verification remain separate operations.
pending_requests.json now retains request-event receipts alongside its request
rows. Both are written together using the existing locked, atomic persistence
path. Receipts survive approval, rejection, cancellation/deletion and subsequent
request-store writes. Different events coalesced into an existing pending request
are recorded too, so those events cannot reappear after the decision.
The existing 30-day request lifetime also bounds replay-receipt retention. Events older than that window cannot acquire a fresh lifetime by being polled again. Future-dated events are deferred until their signed creation time arrives; no approval or new pending row is inferred from them.
Receipt storage admits at most 256 distinct events per sender and 4096 overall inside that window. Saturation returns a rate-limit error, rather than evicting unexpired receipts and reopening old requests. These are defensive admission limits, not a guarantee of delivery during request flooding. Existing valid receipts still suppress replay at capacity. Expired receipts are pruned on subsequent event admission checks.
Existing data and compatibility
Old request files remain readable. Rows created without an event ID use a conservative migration check: events from the same sender/DID created no later than that row's recorded receipt time are not reopened. Invalid legacy timestamps fail closed and retain the prior data for diagnosis. This cannot reconstruct an unknown historical event ID or prove remote clock correctness. Once a verified event is suppressed by this migration check, its ID and signed creation time are saved as a receipt before successful suppression is reported. Removing the legacy row then preserves suppression of that known event. This uses the same capacity checks as new-event admission; saturation returns an error without claiming a receipt was saved. Invalid legacy data remains unchanged. New event-aware rows can distinguish different events even within the same second.
The new fields are additive, but an older binary does not implement replay protection and may discard receipts when it rewrites the store. Do not claim mixed-writer or downgrade replay safety. Deployment/rollback plans must preserve the exact private store backup and identify the old-version behavioural limit; never clear request history to make a rollout appear successful.
Unresolved decision-message correlation
Request-event receipts do not bind a peer-cancel or peer-reject to the request
it decides. Existing wire messages carry only an optional reason, and handlers
match sender plus state. An old signed decision can therefore affect a newer
request, including after restart or on delayed first delivery. Exact-generation
correlation needs an approved wire and legacy/mixed-version policy; this storage
correction does not change or solve that contract. C01 late-response acceptance
remains blocked. A timestamp-only cutoff cannot distinguish same-second requests.
Qualification boundaries
The restoration correction has only source-assertion RED/GREEN evidence. Its three added Rust regressions cover legacy suppression/delete/disk reload and same-second distinct events, both capacity limits, and invalid Pending legacy timestamps. They have NOT been compiled or executed in this packet. The storage delete test uses the function called by PeerCancel; it is not a signed RPC test.
Regression tests exercise persistent rejection, same-second new requests, coalesced requests, deletion, unrelated writes, concurrent delivery, expiry, invalid metadata, legacy rows and capacity refusal. A signed NIP-44 loopback relay test exercises actual polling, rejection RPC and handler recreation, then admits a new event from the same sender without granting membership.
These tests run through scripts/test-backend-isolated.sh. They do not establish
installed-node, public-relay, mixed-version or operator acceptance. Independent
protocol/storage review and installed reject/reload/restart/new-request checks
remain required before declaring the reported issue resolved.