NIP-01 event canonicalization
Every solution that signs a Nostr event computes the same canonical form, the same event id, and the same BIP-340 input. This is what makes “behaves the same when present” actually mean something across five very different signers.
The serialization
Per NIP-01, the canonical pre-image is a UTF-8 JSON array with no whitespace, in this exact order:
[0, pubkey, created_at, kind, tags, content]
0is the literal integer zero — the format version.pubkeyis the x-only public key, lowercase hex (32 bytes = 64 chars).created_atis a Unix timestamp (seconds), integer.kindis an integer event kind.tagsis an array of arrays of strings.contentis a JSON string (escaped per JSON rules).
JSON serialisation rules:
- No additional whitespace anywhere.
- Object keys MUST NOT appear (the structure is a positional array, not an object).
- Tags MUST keep the order the user provided.
contentMUST be valid UTF-8 and is JSON-string-escaped (",\, control chars�–).
The event id
The event id is sha256(serialised_bytes), lowercase hex.
event_id = SHA-256(JSON.stringify(
[0, pubkey, created_at, kind, tags, content],
no-whitespace-form
))
The signature
The signature is BIP-340 Schnorr over secp256k1:
- Message = the 32-byte event id (not the serialised bytes).
- Key = the signer’s secret key whose x-only public key matches
pubkey.
Worked example
Given the request:
{
"kind": 1,
"created_at": 1714838400,
"content": "hello",
"tags": [["t","intro"]]
}
And signer pubkey
a1b2c3d4e5f60718293a4b5c6d7e8f90a1b2c3d4e5f60718293a4b5c6d7e8f90:
The pre-image is:
[0,"a1b2c3d4e5f60718293a4b5c6d7e8f90a1b2c3d4e5f60718293a4b5c6d7e8f90",1714838400,1,[["t","intro"]],"hello"]
(no spaces). The event id is the SHA-256 of those bytes. The signature is BIP-340 over the event id with the matching secret key.
Why every solution agrees
The same canonical form is exercised by shared conformance vectors
in nSealr/specs under vectors/. A signer that diverges — say it
re-orders tags, or pads content, or sorts JSON keys — fails the
shared tests; it cannot ship under the same contract_id.
What this contract does not cover
- Which event kinds are allowed on which route — that lives in
signing-request-v0+ per-route policy. - Where the secret material lives — see stateless vs persistent custody.
- How the user reviewed it — see
approval_digest.
See also
- Signing request v0 — the envelope that wraps the event template before signing.
- QR envelope — how the canonical request travels over QR.
nSealr/specsundervectors/— the deterministic test corpus.