Multi-Author Nostr Events as a Soft Fork
- Multi-author events
- The id
- Keys
- New form
- Old form
- The Q tag
- Rebuilding the new form
- Replaceable events and deletion
- Transition
- Over the wire
- Post-quantum on top
- Why multi-author first
- Open questions
In Post-Quantum Nostr as a Soft Fork
Adding post-quantum signatures to Nostr without a hard fork. Every event carries a PQ-signed new form inside today's event format, so old clients and relays keep working while new ones verify both signatures, until the old form can be pruned.
Post-Quantum Nostr as a Soft Fork
I described packing two events into one: the event we publish today, signed by the npub, carrying a PQ-signed form of itself in its tags.
Looking at it again, that’s a multi-author event with exactly two authors: your npub and your PQ key.
So this is the same soft fork, generalized. It’s not a post-quantum upgrade anymore, it’s multi-author events. Post-quantum becomes one thing you can do with them, and adding a new signature algorithm later is a small change on top.
Multi-author events
One event, several authors, each signing it. It shows up on every author’s profile, and replies, reactions and zaps all point at the same id.
This isn’t a multisig account, where one identity is controlled by several keys. Every author keeps their own identity, and the event belongs to all of them, like a paper with several names on it.
The id
NIP-01 hashes [0, pubkey, created_at, kind, tags, content]. A multi-author event hashes the same array, with every author’s pubkey where the one used to be:
[0, <pubkey 1>, <pubkey 2>, ..., <created_at>, <kind>, <tags>, <content>]
With a single secp256k1 author, that’s byte-identical to NIP-01. Every event that exists today is already a valid multi-author event with one author. Nothing gets converted, and nothing changes for single-author events.
It still parses without ambiguity: pubkeys are strings and created_at is a number, so the author list ends at the first number. A pubkey can’t appear twice.
Every author signs this same id, and the id covers the whole author list, so:
- A signature can’t be moved onto a different set of authors. Nobody can be quietly added or dropped.
- Nobody can squat the id. A copy with different authors hashes differently.
- The order of authors is part of the id.
The event is valid only once every listed author has signed.
Keys
A bare hex pubkey means secp256k1, exactly as today. Any other algorithm gets a short prefix:
45835c36f41d979bc8129830f2f5d92562f5343d6feddd6f30aa79480730f26e secp256k1
mldsa44:<key> ML-DSA-44
Like old Bitcoin addresses next to bc1 ones, the old format keeps working with no prefix. Prefixes come from a small fixed list: every verifier has to support every algorithm on it, so adding one is a spec change, not something a client can invent.
The prefix is part of the pubkey string, so it’s inside the id and every signature commits to it. A bare key can only mean secp256k1, so nothing can be relabeled.
This replaces the 00/01 prefixes from the old design. npubs and event ids stay exactly as they are. Since single-author events have the same id in both forms, event ids don’t need a prefix at all.
New form
In the new form, pubkey and sig become lists:
{
"id": "<sha256 of [0, alice, bob, created_at, kind, tags, content]>",
"pubkeys": ["<alice>", "<bob>"],
"created_at": 1761200000,
"kind": 1,
"tags": [["t", "nostr"]],
"content": "we wrote this together",
"sigs": ["<alice's sig over id>", "<bob's sig over id>"]
}
A single-author event is the same thing with one-element lists, which is just a NIP-01 event.
One author drafts the event and sends it unsigned to the others, in a DM for example. Each signs the id and sends their signature back. Once all are in, any of them can publish.
Old form
Old relays and clients only understand one pubkey and one signature, so a multi-author event is published inside a normal NIP-01 event, signed by its first secp256k1 author:
{
"id": "<NIP-01 hash, including all tags>",
"pubkey": "<alice>",
"created_at": 1761200000,
"kind": 1,
"tags": [
["t", "nostr"],
["Q", "i:<new id>"],
["Q", "p:<alice>", "<alice's sig over new id>"],
["Q", "p:<bob>", "<bob's sig over new id>"]
],
"content": "we wrote this together",
"sig": "<alice's sig over this event's id>"
}
Old clients see a normal note from Alice. New clients rebuild the new form from it and show it on both profiles.
Alice signs twice: once over the new id, like every author, and once over the wrapper. The first one is what lets the new form verify on its own, without the wrapper.
Single-author events don’t need any of this. They’re published exactly as today, so the soft fork costs nothing for events that don’t use it.
The Q tag
This keeps the Q tag from the old design, for the same reason: old relays only index single-letter tags, and only their first value. So key and value are folded together into tag[1].
| Tag in old form | When | Holds |
|---|---|---|
["Q", "i:…"] |
always | the new form’s id |
["Q", "p:<pubkey>", "<sig>"] |
always, one per author, in order | an author and their signature over the new id |
["Q", "t:<key>:<value>", …] |
a single-letter tag differs | that new-form tag; extra values follow as in the original |
["Q:<key>", …] |
a multi-letter tag differs | that new-form tag; not indexed anyway |
["Qcontent", …] |
content differs | the new form’s content |
These keys are reserved in the old form, so no other tag uses them. pqsig is gone: signatures now sit next to their authors, in the Q p: tags.
Tags differ when the new form points at something old clients can’t see. A co-authored event has a different id in each form, so a reply to one carries the wrapper’s id in its normal e tag and the new id in ["Q", "t:e:<new id>"]. Later, PQ pubkeys in p tags and nostr: mentions differ the same way.
Querying works as before:
| On a relay that understands new forms | On an old relay |
|---|---|
ids: [X] |
#Q: ["i:X"] |
authors: [X] |
authors: [X], plus #Q: ["p:X"] for events X co-authored |
#e: [X] |
#Q: ["t:e:X"] when X is a co-authored event’s new id |
On a relay that understands new forms, authors: [X] matches every event X is an author of.
Rebuilding the new form
pubkeysandsigsfrom theQ p:tags, in order.created_atandkindas they are.contentfromQcontentif present, otherwise the old content.- Tags: leave out the reserved tags. Then for each tag key, if the old form has any
Qentry for that key (t:<key>:…orQ:<key>), use those; otherwise keep the old form’s tags of that key. - Order: kept old tags first, in their order, then
Q-derived tags, in their order. The id hashes tags in order, so the authors build the new form in this order to begin with. - Hash it. The result must equal
Q i:, every signature must verify against its pubkey, and the wrapper’spubkeymust be the first secp256k1 key in the list.
Replaceable events and deletion
The rule for both: a multi-author event behaves like one event per author, packed into one.
Replaceable and addressable events belong to every author, like they normally would. Each author has the same slot as today, keyed by their pubkey, kind and d tag, and the newest event in it wins. A co-authored event fills the slot of every author on it.
Deletion: each author deletes it from their own profile with a normal kind 5. From then on, relays don’t return it for that author’s queries, and clients filter it out the same way. Once every author has deleted it, relays have no reason to keep it.
Transition
Nothing changes here from the old design. Relays already store the wrappers, so they can extract the new form of any stored event at any time.
Handshake. A relay that supports this lists it in NIP-11 supported_nips. The client sends ["FORMAT", "multi"], the relay answers ["FORMAT", "multi", true], and from then on that connection’s events and filters use the new form. Relays that don’t know the message ignore it or send a NOTICE, and the connection stays in the old form.
New-form-only clients. Each client decides for itself when to stop wrapping. Until almost everyone has switched, it keeps publishing wrappers so old clients still see co-authored events.
Pruning. The new form is complete and self-verifying. A relay that still wants to serve old clients only keeps what the wrapper doesn’t share with the new form, which is its signature plus the old versions of any tags or content that differed, and rebuilds the wrapper on request. Once nobody asks for the old form, it can drop those too.
Over the wire
Since the new form is negotiated per connection, it doesn’t have to be JSON. Raw bytes would drop the hex and base64 overhead: ids and pubkeys go from 64 characters to 32 bytes, and signatures shrink by a quarter compared to base64, or by half compared to hex. The id is still hashed over the JSON array, rebuilt for verification, so only the transport changes. Whether that’s too far from how Nostr works, I don’t know yet, but it would be useful.
Pubkey indexes. Client and relay can also keep track of every pubkey sent on a connection, in the order it was first sent. After that, either side can send the pubkey’s index instead of the full key. It’s only a transport shortcut: the id and signatures always use the full key.
For a 32-byte secp256k1 key the saving is small. It matters for post-quantum schemes with short signatures and big keys: an author’s key crosses the connection once, and every event after that pays only for the signature.
Post-quantum on top
With this in place, the post-quantum upgrade from the old article is just a co-author. You sign your events together with your PQ key:
"pubkeys": ["<secp256k1 pubkey>", "<prefix>:<PQ key>"]
The old form is the same wrapper, signed by your npub. What changes from the old design:
- It’s one event with one id, not two events packed together. A like or a reply points at one id. There’s no more liking both forms.
- The PQ key needs no special handling. It’s an author like any other.
- The pairing is mutual. Both keys sign an id that names both of them, where before the new form never referenced the old one. It’s still a hint, not a binding: anyone can co-author with anyone.
After Q-day, anyone can produce secp256k1 signatures, so new clients stop counting secp256k1 authors. Your co-authored events count as your PQ key’s. A forger can put your npub on an event next to their own PQ key, but then it’s just an event by their PQ key. They can’t produce your PQ key’s signature. Events with only secp256k1 authors stop being authenticated at all.
As before, nobody has to agree on a date. Every client and relay switches when it wants. And anyone who never co-authored with a PQ key loses their identity along with their npub.
Why not now. The post-quantum signatures standardized today are big. ML-DSA-44 signatures are 2420 bytes, so a reaction would go from about 0.5 KiB to about 2.5 KiB. Schemes with short signatures exist, like UOV (about 96 bytes) and MAYO (about 186 bytes), but they’re still candidates in NIST’s third round. So post-quantum waits for consensus on a short signature scheme, and then only its prefix has to be added.
Short-signature schemes have big keys instead, tens of KiB for UOV. That’s the better trade: a key repeats across all of an author’s events and can be cached, while a signature is paid on every event.
Why multi-author first
- It’s useful today with nothing but secp256k1: co-written posts, collabs, joint announcements, all showing on every author’s profile.
- The hard parts, which are wrapping, unwrapping,
Qqueries and the format handshake, get built and tested while nothing is urgent and nobody’s identity depends on them. - Short post-quantum signatures aren’t settled yet, and multi-author events don’t have to wait for them.
- Once they are, post-quantum is a new prefix and a verifier, not a new protocol.
Open questions
naddrencodes a single pubkey. Any author’snaddrresolves to that author’s slot, but slots can diverge once one author replaces the event, so there’s no single address for the co-authored event itself.- Bytes on the wire: useful, but is moving the new form off JSON too far from Nostr?
Write a comment