nips icon indicating copy to clipboard operation
nips copied to clipboard

Add Private Event Kind Range to NIP-42

Open arthurfranca opened this issue 4 months ago • 26 comments

This is an alternative to #1029. It simply uses A tags that are set to pubkeys that are allowed to download the event after authenticating with NIP-42. Private events also must be in a specific kind range and I explain why below.

– Why define a kind range when the A tag alone would be enough to know an event requires authentication? Because a filter like { kinds: [1] } may or may not require authentication, which would need relay to get events from DB to figure out by checking if some of these events include A tags. While { kinds: ["<a-number-in-the-new-range>"] } clearly requires authentication.

– What if I want to add access control to a public event such as a kind:1 one? Imo best way would be using #1033 to point clients to an auxiliary event in the new kind range. Or you could define a new event in the new range such as 40001 that is exactly like a kind:1 and that also must have A tag(s).

– Why use an indexable tag? For a relay to be able to know if a currently authenticated user is authorized to download the requested events while simultaneously filtering them just by looking at a filter like { #A: ["<someone>"], ... }

In a nutshell, this approach should make relay life easy.

@monlovesmango @fiatjaf @staab @vitorpamplona

arthurfranca avatar Feb 14 '24 18:02 arthurfranca

I don't like that it couples access control to kind. This means we'd have to create duplicate kinds for anything we wanted this behavior for.

staab avatar Feb 14 '24 19:02 staab

I don't like that it couples access control to kind. This means we'd have to create duplicate kinds for anything we wanted this behavior for.

I had suggested something like this in @vitorpamplona's original PR and this was his response immediately, to which I agree.

fiatjaf avatar Feb 14 '24 19:02 fiatjaf

agree. but I think the tag based access control is worse for the network (bc then relays need to run queries first before knowing whether auth is needed, and then after auth obtained need to run the query again).

it seems our two options are to either do tag based access control or kind based access control. neither are perfect, but kind based seems less harmful imo. is there another option I am not considering? what rationale is there for tag based access control being the less harmful option?

monlovesmango avatar Feb 14 '24 19:02 monlovesmango

Event kind ranges were a mistake.

We could have added the tags ephemeral and checked the presence of the d tag to make any event kind ephemeral or replaceable. With tag notations, kinds could have been ephemeral and non-ephemeral at the same time. After all, ephemeral is just the extreme case of expiration.

In fact, you can turn any event into ephemeral by adding a tag expiration with the value of .created_at

vitorpamplona avatar Feb 14 '24 20:02 vitorpamplona

agree that event kind ranges maybe aren't necessary for ephemeral and replaceable events, but that doesn't automatically make it a mistake.

its also not relevant to considerations of private events, which is the topic at hand. whats your rationale for preferring tag based access control over kind based access control? if relays having to run queries twice is not a concern to you, can you walk me through your thought process on why not?

monlovesmango avatar Feb 14 '24 20:02 monlovesmango

Though I wish we had made a separate .lifecycle tags field to receive relay-oriented instructions for the event, like a keep-history for replaceables, the expiration, auth_by the can_be_deleted_by and delegation tags, leaving the tags field for features managed by each kind.

vitorpamplona avatar Feb 14 '24 20:02 vitorpamplona

whats your rationale for preferring tag based access control over kind based access control?

I don't prefer tags. I still prefer root fields and breaking serializations as in https://github.com/nostr-protocol/nips/pull/1029 :)

vitorpamplona avatar Feb 14 '24 20:02 vitorpamplona

root field based access control suffers from the same flaw as tag based access control though. namely, that a relay has to run a query first before it can ascertain that the results require authentication. then after authentication it will likely have to run the query again (or cache the results from before). do you not see this as a concern? if not, why not?

monlovesmango avatar Feb 14 '24 20:02 monlovesmango

But the difference is clear:

  • Lifecycle/Relay policy by kind ranges can be set and forget by the relay. It automatically applies to new NIPs.
  • Lifecycle/Relay policy by each new NIP/kind applies to all instances of the kind, independent if the client wants it to happen or not. But each new kind must be manually added to the relay's setup.
  • Lifecycle/Relay policy by tags: delegates the decision to the client. Every time the client signs an event, any event, if the client includes the tag, it takes one behavior, if the Client doesn't include it behaves as a regular event.

vitorpamplona avatar Feb 14 '24 20:02 vitorpamplona

we'd have to create duplicate kinds for anything we wanted this behavior for

@staab @fiatjaf Have you checked how #1033 tries to solve this in some cases? It uses an auxiliary event that mimicks the event structure of the event referencing it, to avoid having to duplicate kinds. Please read this small section

arthurfranca avatar Feb 14 '24 20:02 arthurfranca

root field based access control suffers from the same flaw as tag based access control though. namely, that a relay has to run a query first before it can ascertain that the results require authentication. then after authentication it will likely have to run the query again (or cache the results from before). do you not see this as a concern? if not, why not?

Sure, but this happens only once per connection. It shouldn't be a big deal.

The relay should be running unauthenticated and only request AUTH when an event that requires auth and matches the filter shows up. Meanwhile, the relay should keep running unauthenticated until the AUTH comes back. Clients will reset all subscriptions after the AUTH is sent, so no need to worry about missing events.

vitorpamplona avatar Feb 14 '24 20:02 vitorpamplona

thanks @vitorpamplona, I suppose this is true and its not as big of a deal as I imagine.

I still think spec's should be designed to use relay resources as efficiently as possible, even if its to the detriment of the client implementation experience. relays should be first class citizens in nostr, similar to nodes in bitcoin. imo any private/protected event nip we adopt should allow relays to fail fast and fail early. relays shouldn't have to:

  • expend more resources on authorization flow
  • bear the burden of users requesting content that they aren't authorized to access
  • deliberate between ending inactive connections or preserving them to prevent having to re-authenticate the user

so clearly I still think kind ranges are better, but you succeeded in convincing me that root fields(/tags) aren't that bad. :)

monlovesmango avatar Feb 14 '24 21:02 monlovesmango

Event kind ranges were a mistake.

We could have added the tags ephemeral and checked the presence of the d tag to make any event kind ephemeral or replaceable. With tag notations, kinds could have been ephemeral and non-ephemeral at the same time. After all, ephemeral is just the extreme case of expiration.

In fact, you can turn any event into ephemeral by adding a tag expiration with the value of .created_at

we didn't do (P)REs as a tag as they would introduce too much complexity on how to handle events for (P)REs and ephemeral events there's also the fact that you don't want kind 1s to be ephemeral or replaceable, so it's an explicit opt in, in a way

The relay should be running unauthenticated and only request AUTH when an event that requires auth and matches the filter shows up.

I disagree. this introduces additional roundtrips. just send auth on connection and clients should authenticate immediately, or when they ask for something that they know will need for ACL before sending the REQ for automatically connected ones not authing does not improve privacy as your filters already leak your identity.

the entire auth-required thing is a mistake, okay so a client is doing a global req and some events may need auth, maybe someone is trying to be annoying by protecting events unnecessarily, now do you want to outright reject the REQ? no, you would ideally hide auth requiring events, and process the REQ as normal. adding a new message for "you may get more events if you auth" is a waste of time compared to "relays ask for auth on connection, clients respond immediately (or use the query param based spec to avoid even more round trips) if it's an user-added one or when the client thinks it will be needed when connected to automatically" which is way simpler to implement even though the explanation is longer

Semisol avatar Feb 14 '24 22:02 Semisol

@Semisol Yeah the AUTH on connection could be easier but lost its battle to CLOSED:auth-required message that does has its merits such as putting an end to the doubt regarding if a client received less events it asked for because it had no access rights for some of the requested events or if the relay just didn't have some of the requested events stored.

@monlovesmango has common sense and constructive opinions. I vote for him as interim nostr CEO of this week.

The argument for the private kind range (instead of just A tags or @vitorpamplona's requires_auth_by field) is just so strong! We can't consider reasonable the relay executing a sometimes time/resource demanding query just to check if a client is authorized to access the requested events. @vitorpamplona's flow in response to this argument just isn't what we have spec'ed after like atleast four PRs with months of discussions trying to fix NIP-42 then flaws. CLOSED message is.

The only valid, imo, counter-argument I saw here up until now is: "we'd have to create duplicate kinds for anything we wanted this behavior for".

But it was answered on the openning message of this PR: #1033 is a good enough solution (my opinion atleast) that could even delegate access control to always online bots/DVMs/servers cause the auxiliary event can be owned by them and have its A tags updated in response to a trigger such as a detected payment. Great for payed content.

arthurfranca avatar Feb 15 '24 12:02 arthurfranca

The argument for the private kind range (instead of just A tags or @vitorpamplona's requires_auth_by field) is just so strong!

That's overselling. There is an argument here but it is not really that strong. The fact that you have to specify tags to turn separate kinds in this new range into the other ranges (regular, replaceable, parametrized replaceable, ephemeral) or create auxiliary events is a major complication point that the tag design simply doesn't have.

vitorpamplona avatar Feb 15 '24 14:02 vitorpamplona

The relay should be running unauthenticated and only request AUTH when an event that requires auth and matches the filter shows up.

I disagree. this introduces additional roundtrips. just send auth on connection and clients should authenticate immediately, or when they ask for something that they know will need for ACL before sending the REQ for automatically connected ones not authing does not improve privacy as your filters already leak your identity.

A few relays already work with this deferred-until-needed AUTH process. I don't know if they go around the supposed "additional roundtrips" but this is clearly a relay ops decision. It's fine if they request for AUTH upfront as well to simplify their life. But if they are trying to better support anyone coming in, they would defer AUTH for as long as possible/viable.

Keep in mind that users are not keen on authenticating themselves in relays that are not in their list and since NIP-65 requires Clients to connect to several relays to download and broadcast certain events, the auth for those might not be acceptable by the user. This is not a Client's decision to Auth or not. It's the users. There is a lot of UX friction in this.

vitorpamplona avatar Feb 15 '24 14:02 vitorpamplona

The private kind range aligns perfectly well with the CLOSED flow.

But if we ditch the range, in practice allowing any kind to be private, and, for example, a relay receives the { kinds: [1] } filter which may include kind:1 events that are private, it shouldn't really use the CLOSED:auth-required msg. @vitorpamplona is right in this regard. So down with the CLOSED message too.

If people really don't want the range, clients should be able to make a REQ just after authentication. Using #891 OR we define that NIP-42 supporting relays MUST always send AUTH on connection so that clients can check NIP-11 and wait for that, similar to what @Semisol proposes.

arthurfranca avatar Feb 21 '24 18:02 arthurfranca

I mean, when CLOSED message was considered a good solution, there was really just kind:4 as private events. Relay could easily figure out if a filter would include private events or if it was too broad. The private kind range would keep it easy to figure out.

But now if any event can simply add some tag to become private, no matter the kind number, it is a whole different scenario. One that CLOSED message wouldn't fit well imo.

@fiatjaf what do you think as the creator of the CLOSED thing? Do you think the CLOSED:auth-required flow can still work as intended in an scenario where any event can be private no matter the kind number?

arthurfranca avatar Feb 21 '24 19:02 arthurfranca

I want to add some color that CLOSED doesn't necessarily need "private" events. Relays can use CLOSED in any situation where auth is required to run part of the subscription. It doesn't necessarily mean the event must be protected, private, or encrypted.

For instance, the relay operator may have a business model where only paid users can run certain filters, or where the number of allowed filters is small for free users and only paid, authed folks can run large filters, or where you have to have a minimum web of trust score to run more difficult queries in the relay.

CLOSED might have started with privacy in mind, but there are more common use cases for it than privacy.

vitorpamplona avatar Feb 21 '24 19:02 vitorpamplona

@vitorpamplona Agree, it has its uses. It is just that in all your examples the relay is able to decide to use CLOSED or not before running a DB query.

arthurfranca avatar Feb 21 '24 19:02 arthurfranca

I agree with @vitorpamplona. In my mind CLOSED was never just about private events. I've run into multiple situations in which CLOSED was desireable, even without the auth-required prefix, but with that also.

fiatjaf avatar Feb 22 '24 12:02 fiatjaf

@fiatjaf please could you show me how a relay<->client message exchange looks like for the following request? ["REQ", "subid", { "authors": ["<follow_pub>"], "kinds": [1], "limit": 3 }]. Consider the second event would be accessible just after authenticating.

arthurfranca avatar Feb 22 '24 13:02 arthurfranca

imo the main question is less about whether CLOSED should be used for only private events and more about, should relays have to run queries before being able to determine whether a CLOSED response to the subscription is appropriate?

I agree CLOSED shouldn't be restricted to private events. I disagree that relays should need to run queries before being able to determine whether CLOSED response is appropriate.

monlovesmango avatar Feb 22 '24 16:02 monlovesmango

just created #1083 for restricted events. I ended up going with tag based identification of restricted events, as that is more flexible and seems to be what people want, but in a way that relays know up front that the filter is for restricted events.

monlovesmango avatar Feb 25 '24 04:02 monlovesmango

@fiatjaf please could you show me how a relay<->client message exchange looks like for the following request? ["REQ", "subid", { "authors": ["<follow_pub>"], "kinds": [1], "limit": 3 }]. Consider the second event would be accessible just after authenticating.

@arthurfranca I don't think we should try to solve impossible and non-existing problems. The entire point of having different kinds is to have them be used when the purpose of the event is different. Kind 1 events are meant to be always public. There is no scenario in the real world in which what you're describing would happen. And if there are, then we should fix that pathological scenario instead of adding bloat to the protocol to make it work.

fiatjaf avatar Feb 25 '24 10:02 fiatjaf

@fiatjaf but #1029 would allow that to happen, be it with kind 1 events or other kind such as 30023.

This PR and @monlovesmango's #1083 are ways to prevent that from happening when adding event access control feature. That's what I was trying to make you notice.

arthurfranca avatar Feb 25 '24 14:02 arthurfranca