Onions
Is it possible to leverage nostr as a privacy preserving communication channel? What if the use cases are limited to high value, low bandwidth?
A quick recap as to why this might be a strategy worth pursing. Nostr uses the same set of cryptography dependencies as bitcoin (e.g. secp256k1, chacha20poly1305). As a general principle, we want to keep the costs of running anything in the bitcoin ecosystem as low as possible. The lower the cost, the more users will run it, which increases the decentralization of the system as a whole. It is cheaper for a bitcoin application to use nostr verse a whole new cryptography scheme to communicate. Nostr has its downsides compared to a purpose built privacy preserving communication channel. But if the amount of information being sent is low, these rough patches can be iron’d out through redundancy.
Here is a hypothetical situation. Let’s say some provider offers a service on nostr to broadcast a bitcoin transaction. This would allow a light client to publish a transaction without having to connect directly to a node, keeping their identity and intention hidden from the node. However, they are revealing that information indirectly to the nostr relay. The relay sees their IP posted a message and that the service known for broadcasting bitcoin transactions read that message. The light client could use a heavy communication channel like TOR to hide their network metadata, but can we hide this just with nostr itself?
I believe there is some value, at least in the short-term, to inject this privacy layer at the “application” level. In this case, nostr. Applications using bitcoin and nostr tend to value user privacy and would be willing to take on the complexity for this functionality. While the user’s privacy would only be protected for specific use cases (e.g. bitcoin transaction broadcasts), these are potentially very valuable scenarios to protect. Ideally the whole network stack is made better, implicitly fixing all apps, but this quick application specific fix feels worth it.
Some form of onion routing paired with NIP-17 direct message encryption (or a subset of its components) could be a potent combo to make it very expensive for third parties to discover any network metadata. And there appears to be a few attempts to bring some sort of onion routing-esque protocol to nostr.
- Patch #430 for NIP-705 Republish Events by Peers and the original issue #411.
- Patch #499 for NIP-103 Onion Routed Direct Messages.
- Comments in patch #686 for NIP-17 Direct Messages.
- A long form note proposal for NIP-37 Event Publication Onion-routing (although it looks like NIP-37 has been taken, so NIP-37-ish?).
All of these proposals have the same topology where a “router” is a nostr client, not a relay. On other existing onion networks like TOR or the lightning network, a client speaks to one node and their message then skips across the network of nodes. One might think the same should happen in nostr, where a client broadcasts to one relay and the relay then becomes the router. But this breaks the strict topological simplicity of nostr, relays only talk to clients. On the plus side, this forces the design to keep the relays completely out of the loop.
The NIP-705 proposal uses the term republish instead of router, but I am not sure that conveys the correct meaning. The same event is not being directly “republished” because it first has to be decrypted by the router service. I am going to stick with the “router” terminology.
<Alice> --> [RELAY-1] --> <Router> --> [RELAY-2] --> <Bob>
Nostr onion route topology.
Both the NIP-705 and NIP-103 proposals use the old encrypted direct message pattern, NIP-04. They also each define a new ephemeral kind (e.g. 20001
) as a way to flag a message for the routers to know they should decrypt and publish. A new proposal should instead make use of the hardened cryptography patterns of NIP-17, which the NIP-37-ish one does.
If using NIP-17, the underlying NIP-44 irons out the cryptography patterns while NIP-59 hides application metadata with the gift wraps. In the topology shown above, Alice just needs to send a gift wrapped message to the Router who’s rumor contains another gift wrapped message for Bob. All RELAY-1 knows is that Alice talked to Router. All RELAY-2 knows is that Router talked to Bob. This isn’t perfect network privacy. There is a chance that the relays are operated by the same party or two colluding parties. But it buys a lot at a low cost, assuming Alice and Bob aren’t sending heavy amounts of data.
What is cool about this simplicity, the nostr way, is that it is easy to extend.
- Alice can route an event through any number of routers, increasing privacy at the cost of performance.
- Routers could post public notes on relays which they service allowing clients to have a fresh set of routers, like in NIP-89 Recommended Application Handlers.
- Ecash tokens can be embedded in each gift wrap layer to incentivise the routers.
layers
Now for some gritty details. What should the onion layers look like?
Could an onion simply be a NIP-59 gift wrapped message? The kernel of a gift wrap, the rumor, is an unsigned nostr event. This is a feature in the context of the direct message use case. Each layer of the gift wrap can be decrypted and pieced together by the receiver, but if any one layer gets leaked there is plausible deniability. But it complicates the routing use case where the router wants to just publish the whole rumor…but it needs a signature. Should the NIP-59 pattern be extended or a new one introduced?
Reusing NIP-59 would “hide” routing requests in with other gift wrap’d messages. This puts more work on a router to inspect gift wraps sent to them, but increases the anonymity set of potential messages. Maybe this isn’t a huge benefit though if a service is obviously a router, so one could guess that gift wrap’d messages to it are routing requests.
In some ways, the three layers of NIP-59 do not make much sense in the routing use case. Since the rumor needs a signature, the second layer seal is no longer useful. Maybe a new onion pattern could still use NIP-44 cryptography, but only use two layers. A wrapper around an encrypted message to route. Three layers however do give a natural home for router specific data. In NIP-59, the middle seal layer is still meant for the one receiver. In the routing case, the outer layer would still serve to hide application metadata, while the middle layer could hold data intended for the router. And the inner layer would still be the message to publish. I am not sure this middle layer is necessary, but it would make it easy to add any more information the router needs to perform its job.
{
"kind": 20444,
"content": nip44_encrypt("{
'relays': [ 'wss://example1.com' ], // target relay to publish event
'event': { 'id': ...., sig: ..., }, // event to route
'proof': <optional-unencoded-cashu-proof>, // ecash incentive
'mint': 'https://mint.com', // ecash incentive
'unit': 'sat' // ecash incentive
}")
"tags": [
[ "p", "<pubkey-of-router>" ]
]
}
The NIP-37-ish proposal gives the most modern structure.
The NIP-37-ish proposal from above goes with a new kind 20444
which uses NIP-44 encryption and just two layers. The encrypted content
however has a bunch of possible fields including the event to route. Given that the encrypted content is only for the router, it probably doesn’t make sense to use nostr event structure over custom JSON. Nostr event structure is optimized for public readability (e.g. relays can index on certain kinds and tags) which does not apply here. Also, if event structure is used, there is a small chance they would themselves be published by mistake. This would leave an easy to read paper trail for the onion route.
With that said, the custom JSON still seems gross to me. Maybe just too open ended? I wonder if the content could instead be encrypted tags. Maybe an event
tag which holds an event and a relay hint? This would allow a single routing event to hold multiple route requests. There could also be an ecash
tag for the ecash incentive data. Taking some inspiration from NIPs which already use NIP-44 encryption, like in NIP-60 Cashu Wallet. Also the ecash NIPs like (again) NIP-60 Cashu Wallet and NIP-61 Nutzaps.
I wonder if it’s better to have an event
tag hold an event and relay pair or if it should only hold the event. If it holds the pair, then posting a single event to multiple relays requires some event duplication. If it holds just the event and instead there is a separate relay
tag, then all events are posted to all relays. Also, it sure would be cool if there was some sort of standard naddr
like encoding for ecash…and cashu’s NUT-00 describes some. If using cashu’s V4 tokens to serialize the proofs with mint data, the ecash token
would something like cashu:cashuB[base64_token_cbor]
.
{
"kind": 20444,
"content": nip44_encrypt("[
[ "event", "<event-as-json-string>" ], // required event to publish
[ "relay", "wss://example1.com" ] // required relay to target
[ "ecash", "<token-uri>" ], // optional ecash incentive with ecash proof and mint info
]")
"tags": [
[ "p", "<pubkey-of-router>" ]
]
}
Tag only layout, can add on extra events and relays…and I guess ecash? Why not.
You could always wrap this routing event in a gift wrap to hide that it is a routing request, but that might not be very effective if it is already known that the pubkey receiving the event is a router.
announcement
If a sender wants to route a message across a handful of routers, well, they have to know those routers. How do they bootstrap that knowledge? The NIP-37-ish proposal addresses this with a new kind 20690
that the routers can post. The relay
tag is where the router is listening for routing requests. The fee
is the ecash required per-request. Senders can collect these announcements and use them to create onion routes across relays.
{
"kind": 20690,
"tags": [
[ "relay", "wss://example1.com" ],
[ "relay", "wss://example2.com" ],
[ "fee", "1", "sat" ]
],
"pubkey": <pubkey-of-router>
}
The NIP-37-ish announcement kind.
This makes sense, but I was hoping a new kind could be avoided in favor of the NIP-89 Recommended Application Handlers system. NIP-89 defines two types 31990
and 31989
. 31990
allows apps to post that they support a kind, while 31989
allows a user to vouch that the app does indeed support the kind. This is a cool little web-of-trust system which sounds good for a user to ask their friends for their favorite onion routers. My first thought was that routers could post 31990
events announcing that they support 20444
onion route request kinds. My hope was that the relay and fee information could be finagled into the 31990
posting, however, it is not clear from the NIP if this is an abuse of the kind. The content
is uses to hold a kind 0 metadata about the app itself, this probably shouldn’t be extended. The tags like web
and ios
are meant to point to the actual handlers of the event, as in, the user needs to go install an app in order to view a new kind. Or maybe better, the user is directed to open an event in a different client.
{
"kind": 31990,
"pubkey": "<application-pubkey>",
"content": "<optional-kind:0-style-metadata>",
"tags": [
["d", <random-id>],
["k", <supported-event-kind>],
["web", "https://..../a/<bech32>", "nevent"],
["web", "https://..../p/<bech32>", "nprofile"],
["web", "https://..../e/<bech32>"],
["ios", ".../<bech32>"]
],
// other fields...
}
The handler (or announcment) event from NIP-89.
I don’t think there is anything stopping new tags to be added to a 31990
kind. Like the relay and fee tags from the NIP-37-ish announcement kind. But perhaps isn’t the right use for the system? Maybe it is best to define a new announcement kind and use NIP-89 to point to those annoucements as a way for users to vouch for a router.
In that case, the NIP-37-ish 20690
is pretty close to ideal. The one thing I am wondering that might be helpful is some sort of “these are the ecash protocols I support” tag. Or is cashu so dominant that it is implicit?
interoperable ecash
A concern I have threaded through these new events is how interoperable are the ecash implementations. It might be a large ask to get all senders and routers on the same protocol or serialization for tokens.
nip
This little journey got me thinking to propose a new NIP for nostr which essentially just takes all the best parts of the previous ones and refines them. I have never done this before. The NIPs repository has a small blurb in the README, but it left me with a few questions.
What lingo should NIPs use?
RFC-like documents have a certain style if you have read a few. Lots of all caps “the protocol SHOULD do this…”. This style comes from RFC 2119 and it looks to be used in NIPs as well.
- MUST / MUST NOT – Absolute requirement.
- SHOULD / SHOULD NOT – Recommended and should only be ignored with good reason.
- MAY – Optional, not required for interoperability between implementations.
What sections do NIPs contain?
Poking around existing NIP definitions, I don’t see a strong convention for required sections. They all start with their NIP number, their title, and then some tags. draft
appears on most from what I can tell, I wonder when something is not considered a draft?
What number is my new NIP?
There is a bit of a kerfuffle in the nostr community at the moment about this. Nostr’s OG creator, fiatjaf, only wants NIPs to have two characters. This is a unique request, but may help nostr keep its simplicity. Since NIP-99 is already defined, along with most of the numbers between 00 and 99, some hexidecimal has been introduced. This is again, weird, cause NIP-11 which would probably be read as “NIP eleven” is now “NIP one one” and actually 17 in decimal.
There are a few open slot numbers. It appears that 41 is open for example, but not sure how many of these alrady have NIPs closing in on them (there is one for 41).
I opened patch #1713 to add NIP-4A Event Onion Routing, we will see how it goes.