draft optional
This NIP defines time capsules: Nostr events whose plaintext becomes readable at/after a target time using a drand time-lock (tlock). Capsules can be broadcast publicly or delivered privately with NIP-59 gift wrapping; encryption for sealing/wrapping uses NIP-44 v2.
Encoding note: All Base64 in this NIP is RFC 4648 padded and MUST NOT contain line breaks.
Hex note: All hex strings in this NIP are lowercase.
- 1041 — Time Capsule.
A public capsule is a signed kind:1041 event. Its content is a Base64 of the binary (non-armored) age v1 ciphertext with exactly one tlock recipient stanza (no other recipient types).
{
"id": "<32-byte lowercase hex sha256 of serialized event>",
"pubkey": "<32-byte lowercase hex pubkey of the author>",
"created_at": "<unix timestamp in seconds>",
"kind": 1041,
"tags": [
["tlock", "<drand_chain_hex64>", "<drand_round_uint>"],
["alt", "<description>"]
],
"content": "<base64(binary age v1 tlock ciphertext)>",
"sig": "<64-byte lowercase hex signature of the event hash>"
}Rules (public 1041):
- Exactly one
tlocktag (see below). contentMUST be Base64 of binary age v1 with a singletlockrecipient stanza and no other recipient stanzas (e.g., noX25519,scrypt). ASCII-armored age is invalid.- clients enforce unlock by verifying drand beacons; relays do not enforce time.
Single, preferred format (normative):
["tlock", "<drand_chain_hex64>", "<drand_round_uint>"]Validation:
drand_chain_hex64matches^[0-9a-f]{64}$(lowercase).drand_round_uintmatches^[1-9][0-9]{0,18}$(positive, 64-bit safe).- The age ciphertext MUST contain exactly one recipient stanza of type
tlockwhose chain and round equal the tag values; any mismatch MUST be rejected.
- The inner
kind:1041MUST NOT containptags. Clients MUST reject any private capsule whose inner 1041 includes aptag. - On the outer
kind:1059(gift wrap), include at least one["p","<recipient-npub>","<relay_url?>"]per recipient for routing.
- Human-readable description for UX.
A private capsule is delivered via the NIP-59 pipeline:
-
Create the rumor (kind:1041, unsigned).
Same schema as public, but do not sign.
contentis Base64(binary age v1tlockciphertext) and thetlocktag is present. Omitp.Rumor MUST NOT include
sig.idMAY be present; if present, clients MUST recompute it after recovery and reject on mismatch. -
Seal (kind:13).
JSON-serialize the rumor and encrypt it to the recipient using NIP-44 v2; put the ciphertext in
.content.tagsMUST be[]. Sign with the author’s real key. -
Gift wrap (kind:1059).
JSON-serialize the seal and encrypt it to the recipient using NIP-44 v2 with a one-time ephemeral key; put the ciphertext in
.content. Add at least one["p","<recipient>","<relay_url?>"](one 1059 per recipient is best practice). Sign with the ephemeral key.Broadcast only to the recipient’s DM relays as advertised by their relay list metadata (per the relevant NIP).
Rumor (kind:1041, unsigned):
{
"id": "<32-byte lowercase hex sha256 of serialized event>",
"pubkey": "<author pubkey hex32>",
"created_at": 1234567890,
"kind": 1041,
"tags": [
["tlock", "<drand_chain_hex64>", "<drand_round_uint>"],
["alt", "<description>"]
],
"content": "<base64(binary age v1 tlock ciphertext)>"
}Seal (kind:13, signed by author; tags = []):
{
"id": "<32-byte lowercase hex sha256 of serialized event>",
"pubkey": "<author pubkey hex32>",
"created_at": 1234567890,
"kind": 13,
"tags": [],
"content": "<NIP-44 v2 ciphertext of JSON(rumor kind:1041)>",
"sig": "<author signature hex64>"
}Gift wrap (kind:1059, signed by ephemeral; includes p):
{
"id": "<32-byte lowercase hex sha256 of serialized event>",
"pubkey": "<ephemeral pubkey hex32>",
"created_at": 1234567890,
"kind": 1059,
"tags": [["p", "<recipient npub>", "<relay_url>"]],
"content": "<NIP-44 v2 ciphertext of JSON(seal kind:13)>",
"sig": "<ephemeral signature hex64>"
}- Verify NIP-01 signature; check exactly one
tlocktag; Base64-decodecontent. - Fetch the drand beacon for
drand_round_uintand verify it against the chain’s BLS public key derived fromdrand_chain_hex64. - Parse the binary age v1 ciphertext; ensure exactly one recipient stanza of type
tlockwhose chain/round match the tag; reject ASCII armor or extra recipient types. - Decrypt with the verified beacon; the result is the plaintext.
- Validate outer 1059 (ephemeral NIP-01 signature); NIP-44 v2 decrypt
.contentwith your key. - Parse inner kind:13;
tagsMUST be empty; verify author signature; NIP-44 v2 decrypt.contentusing the author↔recipient conversation key. - Parse recovered unsigned kind:1041 rumor. Verify
lower(seal.pubkey) == lower(rumor.pubkey)(both 32-byte lowercase hex). Ifrumor.idis present, recompute and reject on mismatch. For display and ordering, userumor.created_at; thecreated_atof the seal and wrap are transport metadata and MUST NOT replace the rumor’s timestamp in UX. - Fetch & verify drand beacon as above; ensure
tlocktag ↔ age stanza chain/round match; then age-decrypt to recover the plaintext.
- Relays MUST NOT attempt to decrypt or enforce unlock times.
- Clients MUST enforce unlock using verified drand beacons, not local clocks.
- Beacon verification: Always verify drand beacons against the chain’s BLS public key (derived from
drand_chain_hex64) before age decryption. Do not trust local time or unsigned beacons; accept the first BLS-verified beacon from any endpoint. - Ciphertext format: Accept only binary age v1
tlockwith exactly one recipient stanza; reject ASCII-armored inputs and stanza multiplicity or other stanza types. - Bounds & DoS: Before allocation, clients SHOULD enforce
tlock_blob ≤ 4096 bytesand SHOULD reject 1041 whose decodedcontentexceeds 64 KiB. Relays MAY drop 1041 exceeding 256 KiB decoded. - Sealing/wrapping crypto: Use NIP-44 v2 (ECDH → HKDF, ChaCha20, HMAC, padded Base64). Validate MAC in constant time before attempting decryption.
- Timestamps & privacy: Randomize seal/wrap
created_atslightly (e.g., jitter/backdate) for metadata privacy; the rumor’screated_atis canonical for UX.
- Relay Shugur Relay
- Client Shugur Time Capsules