NIP-67: EOSE Completeness Hint#2317
Conversation
|
Can this be used for when there is content behind AUTH, for example? It would come in very handy for #2156. We could just add another hint that says "auth" or something like that. Do you think this makes sense? |
In this case, I would propose an object here instead of a field, with |
|
No, I think just a string is enough, we can leave the object for the next item, if necessary. |
Another reason is that you may have |
|
Makes sense, but I don't think having |
What about the simple query Or even drop the kinds. A client may want to download everything mentioning someone, and you can have both |
|
Instead of changing the third element into an array/object (which changes its type and breaks positional indexing), I'd keep it a plain string but define it as a comma-separated list of tokens from the start: Fully backward compatible, and it covers @Semisol's |
ad2bfbe to
b1eb49c
Compare
|
Makes sense, but then I think it is better to do the array. We already have various types of things in these messages anyway. |
|
Edit: went ahead and updated the spec to the array form (array-only). |
|
Oh, I need |
|
But let's merge this as it is and add |
|
Sounds good to me. |
|
Don't you think it makes more sense to have a |
Doesn't not having Also, I think it was a bad idea to have a max-limit and default-limit listed in NIP-11. Relays might have more events, but return less than their advertised max limit if the query exceeded a hidden limit, like total scanned index entries. |
|
I am not sure this adds much. The way I got around this was to always paginate queries and re-sub a modified filter until it hits the number of records I want, or the subscription returns EOSE without any event. This PR seems to inform if "there is more", but you still have to paginate to get them, so just paginate until nothing is left on the relay. I am not sure if the added information is doing much. |
This allows you to not have to have another round trip, when your limit is higher than the number of events on the relay (which is a lot of the time for reactions, etc.). It also opens up the door to a much more useful use case, which is to tell clients that you can do this REQ without AUTH, but you will see less results. For example, a filter mixing non-public and public kinds, or aggregators, pay-for-read relays, etc. |
Isn't that already solved on a |
Yes, you have, from my relay! The problem with notices is that they are unstructured and are not attached to any specific subscription. Clients can instead be told to AUTH for certain subscriptions instead, and clients can then show the AUTH request to be approved depending on the context. (for a feed / for DMs / etc.) |
That workaround is the cost this PR removes. "Paginate until an empty EOSE" forces one extra It also helps clients that don't paginate like you do. Many use the One optional string, fully backward compatible. (And per Semisol, the same mechanism generalizes to a per-subscription |
No, it could be just a relay that doesn't implement this.
I tried to remove that once but tons of people, you included, said those were very important fields. Just ignore that. NIP-11 is mostly useless. |
Shouldn't they? |
To be fair, the cost of that final EOSE is virtually irrelevant. Parsing and verifying the previous 300 or 500 events (~20 ms in native code) will take more time than another round trip, which happens in parallel. This will be an optional NIP, so I don't think that it will change a lot in practice. To use this PR, devs will need to code two solutions: my way and this PR's new way when available. I don't know... this does feel like another NIP-11-like thing that is not reliable, and thus nobody uses them. It's just more bureaucracy to the spec with minimal gains. |
|
@fiatjaf fair point that absence is ambiguous (indistinguishable from a relay that doesn't implement this), so I'll add
MAY because @vitorpamplona I don't think the round trip is negligible. That measures only the client-side parse cost and ignores the server side. That final empty REQ makes the relay run its whole pipeline: receive the WS message, parse JSON, acquire a DB connection, issue SQL, scan an empty result, build and send the EOSE. All for zero events, and it repeats for every completed subscription across every client. Small on one client, far from negligible on the relay. On top of that, what pagination can't replicate is the auth/partial signal: a loop that stops on an empty EOSE concludes "got everything" even when auth-gated events it never saw exist. That's correctness, not latency, so it belongs on the subscription rather than a NOTICE. |
|
If it is correctness, not latency, then it should not be an optional NIP. |
|
It's optional for backward compatibility. Appending the third element doesn't break older clients (they keep indexing EOSE by position). That's protocol-level compatibility, not "unimportant, ignore at will." |
|
Still, if it is correctness, this should be on NIP-01 or NIP-42 |
|
I'm not familiar with the nips conventions, but as far as I know EOSE itself started as its own NIP (NIP-15) and was merged into NIP-01 later, so starting this as its own NIP seems natural to me. That said, if putting it directly into NIP-01 is better, I'm fine with that too. Either way, nostr has no versioning on the protocol spec like internet drafts do, so client and relay implementers have to keep watching the latest spec regardless. |
Relays that do not implement this do not send an array. This could be a mandatory hint for this spec, and relays could send |
They should not. Relays may return less events than requested for many reasons |
Good point, actually. I think some of my relays do this in some special cases:
|
|
@fiatjaf I think you should fix that and return 450 + 50 new events. If the client is asking for 500, you should try to deliver it, not just cut it short. You can even send the 450 in parallel while you gather more 50 before the EOSE. |
|
Yes, in the ideal world I should, but these clients are quite obnoxious aren't they? |
If you prefer to be hit by another query just to get the next 50 events... be my guest... |
|
Your "minimal gains" holds only when the client always sends a 1.
Here the hint only clearly pays off at the boundary (records == cap). But this whole table assumes 2.
For completeness detection the 300-records and 800-records rows are indistinguishable without the hint: the returned events differ, but the only signal the client can use is the same in both, 300 events plus a bare 3. Without a
So |
|
What if, instead of doing this is an optional nip hack, we fully develop pagination into nip01? We have other issues that we are not addressing, like when all created_at's are all the same within the limit. There is no way to filter by anything before "created at + id". Client either gets stuck downloading the same events over and over again or skips some events to unstuck itself That, to me, is more problematic than the gains of this discussion.. Then in that pagination solution, we can make the "finish", "auth" and other replies a first class citizen. |
|
@fiatjaf Keep this as NIP-67, or fold it into NIP-01 (with |
|
Let's do this for now. I agree we should also have better pagination primitives, but that's independent from this, and can wait more. |
For pagination primitives, there is the issue that there could be more than I think we can fix that mostly with Negentropy. |
NIP-67: EOSE Completeness Hint
Problem
EOSEin NIP-01 marks the boundary between stored and real-time events for a subscription. It does not tell the client whether every stored event matching the filter has been delivered, or whether the relay stopped partway through because of an internal limit.In practice, relays enforce a per-subscription cap (commonly 300–1000 events) that is independent of the client's
limit. Clients today cannot observe this cap directly, so they fall back to a heuristic: compare the number of events received against thelimitthey requested, and paginate withuntilif the counts appear to match.This heuristic is both unreliable and wasteful.
Concrete failure cases
1. Silent data loss when the relay's cap is below the client's
limit.A client asks for the last 500 notes. The relay caps responses at 300 and returns 300 events. Because
300 < 500, the client concludes the result is complete and stops. The 201st through Nth oldest matching notes — which the relay does hold — are never retrieved. The client has no way to know.2. Mandatory extra round trip when the cap is ≤ the total number of matching events.
A relay caps at 300 events. Any subscription that exhausts the cap requires at least one redundant
REQ:If the filter happens to match exactly 300 events in total, the second
REQis pure waste — for the client (latency, bandwidth) and for the relay (another full filter scan to return zero events). Every subscription against such a relay pays this cost.3. Boundary tie ambiguity.
Multiple events can share the oldest
created_atin a batch. Paginating withuntil = oldest_created_atrisks either missing events with that timestamp or double-fetching them, depending on the relay's comparison semantics.Change
This PR adds NIP-67, which extends
EOSEwith an optional third element:Only the positive "done" signal is specified. A relay that advertises NIP-67 support but omits the hint is telling the client there is more; a relay that does not advertise support falls through to the existing legacy heuristic. One optional string — a single flag — is enough to eliminate both the silent-truncation bug and the guaranteed-wasted round trip.
The third element is optional. Legacy relays omit it, legacy clients ignore trailing array elements, so the change is backward compatible in both directions.
Why this matters
limitagainst a cap-enforcing relay can silently lose data. This is not a client bug — the protocol provides no way to detect the truncation.REQto confirm" pattern is paid by every client on every exhausted subscription. Across the network this is a significant amount of avoidable query load on relays, which are the constrained resource.limitis a client-side hint, not a completeness contract.EOSEsignals a phase boundary, not a completeness contract either. Nothing in the core protocol currently lets a relay communicate "I have more for you" or "that's everything." This small addition closes that gap.supported_nips.Non-goals
EOSEas the stored/real-time boundary.until-based pagination remains the mechanism; this NIP only tells the client whether pagination is worth attempting.Note: I used AI to help translate this pull request into English, but I have reviewed the content myself.