|
| 1 | +# NIP-XX: Time Capsules |
| 2 | + |
| 3 | +`draft` `optional` |
| 4 | + |
| 5 | +This NIP defines time-locked capsules: encrypted Nostr events that become readable only at/after a target timestamp or when a threshold of designated witnesses publish unlock shares. This enables delayed revelation, threshold cryptography, digital inheritance, and whistleblowing protection. |
| 6 | + |
| 7 | +Time-locked capsules allow content to be: |
| 8 | + |
| 9 | +- Released automatically after a specific timestamp |
| 10 | +- Unlocked when multiple witnesses collaborate |
| 11 | +- Made accessible after long periods for digital inheritance |
| 12 | +- Protected with built-in delays for sensitive material |
| 13 | + |
| 14 | +## Event Kinds |
| 15 | +Permalink: Event Kinds |
| 16 | + |
| 17 | +- `1990`: Time Capsule (regular) |
| 18 | +- `30095`: Time Capsule (parameterized replaceable; keyed by `d` tag) |
| 19 | +- `1991`: Time Capsule Unlock Share |
| 20 | +- `1992`: Time Capsule Share Distribution |
| 21 | + |
| 22 | +## Specification |
| 23 | +Permalink: Specification |
| 24 | + |
| 25 | +### Time Capsule Events (kinds `1990` and `30095`) |
| 26 | +Permalink: Time Capsule Events |
| 27 | + |
| 28 | +A time capsule event contains encrypted content and unlock conditions. |
| 29 | + |
| 30 | +#### Required tags |
| 31 | + |
| 32 | +- `u`: Unlock configuration in format `["u","<mode>","<param1>","<value1>",...]` |
| 33 | +- `p`: Witness pubkeys (one or more) - `["p","<witness_pubkey_hex>"]` |
| 34 | +- `w-commit`: Merkle root commitment - `["w-commit","<hex_merkle_root>"]` |
| 35 | +- `enc`: Encryption method - `["enc","nip44:v2"]` |
| 36 | +- `loc`: Storage location - `["loc","inline"|"https"|"blossom"|"ipfs"]` |
| 37 | + |
| 38 | +#### Optional tags |
| 39 | + |
| 40 | +- `d`: Identifier (required for kind `30095`) - `["d","<capsule-id>"]` |
| 41 | +- `uri`: External content URI (required when `loc != "inline"`) - `["uri","<url>"]` |
| 42 | +- `sha256`: Content integrity hash - `["sha256","<hex_hash>"]` |
| 43 | +- `expiration`: Expiration timestamp per NIP-40 - `["expiration","<unix>"]` |
| 44 | +- `alt`: Human-readable description - `["alt","<description>"]` |
| 45 | + |
| 46 | +#### Content |
| 47 | + |
| 48 | +The `content` field MUST contain a base64-encoded NIP-44 v2 encrypted payload. When `loc` is `"inline"`, the entire encrypted content is in this field. When `loc` is external, this field MAY be empty and the `uri` tag points to the encrypted content. |
| 49 | + |
| 50 | +### Unlock Modes |
| 51 | +Permalink: Unlock Modes |
| 52 | + |
| 53 | +#### Threshold Mode |
| 54 | + |
| 55 | +```plaintext |
| 56 | +["u","threshold","t","<t>","n","<n>","T","<unix_unlock_time>"] |
| 57 | +``` |
| 58 | + |
| 59 | +- **t**-of-**n** witnesses must provide shares at/after timestamp `T` |
| 60 | +- Prevents unilateral early disclosure but not collusion of any `t` witnesses |
| 61 | + |
| 62 | +#### Scheduled Mode |
| 63 | + |
| 64 | +```plaintext |
| 65 | +["u","scheduled","T","<unix_unlock_time>"] |
| 66 | +``` |
| 67 | + |
| 68 | +- Indicates time-based operational release where witnesses or services intend to post shares after `T` |
| 69 | +- This mode is not a cryptographic timelock; a future revision may define a VDF-based trustless mode |
| 70 | + |
| 71 | +Implementations MUST parse unknown `u` modes conservatively and treat them as unsupported. |
| 72 | + |
| 73 | +### Unlock Share Events (kind `1991`) |
| 74 | +Permalink: Unlock Share Events |
| 75 | + |
| 76 | +A witness posts one share after the unlock timestamp (with optional skew tolerance). |
| 77 | + |
| 78 | +#### Required tags |
| 79 | + |
| 80 | +- `e`: Capsule event reference - `["e","<capsule_event_id>"]` |
| 81 | +- `a`: Addressable reference (if capsule is parameterized replaceable) - `["a","30095:<pubkey_hex>:<d>"]` |
| 82 | +- `p`: Witness pubkey - `["p","<witness_pubkey_hex>"]` |
| 83 | +- `T`: Unlock time from capsule - `["T","<unix_timestamp>"]` |
| 84 | + |
| 85 | +#### Content |
| 86 | + |
| 87 | +- Base64 Shamir share for threshold mode |
| 88 | +- MAY be gift-wrapped (per NIP-59) to reduce metadata leakage |
| 89 | +- Clients MUST access the plaintext share after timestamp `T` |
| 90 | + |
| 91 | +### Share Distribution Events (kind `1992`) |
| 92 | +Permalink: Share Distribution Events |
| 93 | + |
| 94 | +Automates delivery of per-witness shares immediately after capsule creation. |
| 95 | + |
| 96 | +#### Required tags |
| 97 | + |
| 98 | +- `e`: Capsule event reference - `["e","<capsule_event_id>"]` |
| 99 | +- `a`: Addressable reference (if capsule is parameterized replaceable) - `["a","30095:<pubkey_hex>:<d>"]` |
| 100 | +- `p`: Recipient witness - `["p","<witness_pubkey_hex>"]` |
| 101 | +- `share-idx`: Share index - `["share-idx","<0..n-1>"]` |
| 102 | +- `enc`: Encryption method - `["enc","nip44:v2"]` |
| 103 | + |
| 104 | +#### Content |
| 105 | + |
| 106 | +NIP-44 v2 ciphertext containing the Shamir share destined for the witness. Only the intended witness can decrypt. |
| 107 | + |
| 108 | +#### Validation Rules |
| 109 | + |
| 110 | +- Event MUST be authored by the same pubkey as the capsule |
| 111 | +- The target `p` MUST appear in the capsule's witness list |
| 112 | +- `share-idx` MUST be within `[0, n-1]` |
| 113 | + |
| 114 | +## Protocol Flow |
| 115 | +Permalink: Protocol Flow |
| 116 | + |
| 117 | +1. **Create Capsule** (kind `1990` or `30095`) |
| 118 | + - Author generates random key `K` and encrypts payload with NIP-44 v2 → `C` |
| 119 | + - Selects witnesses (p tags), sets threshold `t`, witness count `n`, unlock time `T` |
| 120 | + - Computes `w-commit` over ordered witnesses |
| 121 | + - Publishes capsule with `content=C`, unlock config, witness list, commitment, storage location |
| 122 | + |
| 123 | +2. **Distribute Shares** (kind `1992`) *(recommended)* |
| 124 | + - Split `K` using Shamir's Secret Sharing (t, n) |
| 125 | + - For each witness, publish `1992` with NIP-44 encrypted share for that witness |
| 126 | + - Include `share-idx` to maintain ordering |
| 127 | + |
| 128 | +3. **Unlock** (kind `1991`) |
| 129 | + - At/after timestamp `T` (± skew tolerance), witnesses publish `1991` with plaintext shares |
| 130 | + - Clients collect any `t` valid shares, reconstruct `K`, and decrypt `C` |
| 131 | + |
| 132 | +## Relay Behavior |
| 133 | +Permalink: Relay Behavior |
| 134 | + |
| 135 | +### Validation |
| 136 | + |
| 137 | +Relays MUST: |
| 138 | + |
| 139 | +- Ensure required tags exist and are well-formed |
| 140 | +- For `1991`, reject shares where `now < T - skew` (recommended skew = 300 seconds) |
| 141 | +- For `1992`, validate author matches capsule author and recipient witness is in capsule's witness list |
| 142 | + |
| 143 | +### Indexing |
| 144 | + |
| 145 | +Relays SHOULD: |
| 146 | + |
| 147 | +- Index `p` tags (witnesses) and `e` tags (capsule references) for discovery |
| 148 | +- Not rely on custom tag filters beyond NIP-01 |
| 149 | + |
| 150 | +### NIP-11 Capability Advertisement |
| 151 | +Permalink: NIP-11 Capability Advertisement |
| 152 | + |
| 153 | +Relays implementing this NIP SHOULD advertise their support in their NIP-11 document: |
| 154 | + |
| 155 | +```json |
| 156 | +{ |
| 157 | + "supported_nips": [1, 11, ...], |
| 158 | + "software": "...", |
| 159 | + "version": "...", |
| 160 | + "capsules": { |
| 161 | + "v": "1", |
| 162 | + "modes": ["threshold","scheduled"], |
| 163 | + "max_inline_bytes": 131072 |
| 164 | + } |
| 165 | +} |
| 166 | +``` |
| 167 | + |
| 168 | +### Error Handling |
| 169 | + |
| 170 | +Early share rejection SHOULD use clear error messages per NIP-01 (e.g., `["OK", <event_id>, false, "invalid: too early"]`). |
| 171 | + |
| 172 | +## Client Behavior |
| 173 | +Permalink: Client Behavior |
| 174 | + |
| 175 | +- **Creation**: Generate `K`, encrypt payload with NIP-44 v2, produce capsule event, compute `w-commit`, publish |
| 176 | +- **Distribution**: Publish `1992` per witness with NIP-44 encrypted share; store local copy |
| 177 | +- **Monitoring**: Track timestamp `T`, watch for `1991` from witnesses; tolerate skew ±300s |
| 178 | +- **Reconstruction**: Verify witness membership via `w-commit`, collect any `t` valid shares, reconstruct `K`, decrypt content |
| 179 | +- **Integrity**: When `loc != inline`, fetch `uri`, verify `sha256` hash before decryption |
| 180 | +- **Discovery**: Use standard filters, e.g., witnesses look up: |
| 181 | + |
| 182 | +```json |
| 183 | +{ "kinds": [1992], "#p": ["<witness_pubkey_hex>"] } |
| 184 | +``` |
| 185 | + |
| 186 | +## Security Considerations |
| 187 | +Permalink: Security Considerations |
| 188 | + |
| 189 | +- **Witness Collusion**: Threshold prevents unilateral early disclosure but not collusion of any `t` witnesses. Choose diverse witnesses and set `t` accordingly. |
| 190 | +- **Early Disclosure**: Enforce timestamp `T` at relays (reject pre-`T - skew`) and at clients (ignore early shares). |
| 191 | +- **Time Manipulation**: Use trusted time sources where possible; keep small skew windows. |
| 192 | +- **External Storage Integrity**: Include `sha256` for any `uri` content. |
| 193 | +- **Spam/DoS**: Rate-limit `1991/1992` per capsule and per witness. |
| 194 | + |
| 195 | +## Examples |
| 196 | +Permalink: Examples |
| 197 | + |
| 198 | +### Time Capsule (kind 1990, threshold 2/3) |
| 199 | + |
| 200 | +```json |
| 201 | +{ |
| 202 | + "kind": 1990, |
| 203 | + "pubkey": "a2b3c4d5...", |
| 204 | + "created_at": 1735689600, |
| 205 | + "content": "base64_encoded_nip44v2_ciphertext", |
| 206 | + "tags": [ |
| 207 | + ["u","threshold","t","2","n","3","T","1735776000"], |
| 208 | + ["p","f7234bd4..."], |
| 209 | + ["p","a1a2a3a4..."], |
| 210 | + ["p","b1b2b3b4..."], |
| 211 | + ["w-commit","3a5f...c9"], |
| 212 | + ["enc","nip44:v2"], |
| 213 | + ["loc","inline"], |
| 214 | + ["alt","Secret message requiring 2 of 3 witnesses"] |
| 215 | + ] |
| 216 | +} |
| 217 | +``` |
| 218 | + |
| 219 | +### Time Capsule (kind 30095, external storage) |
| 220 | + |
| 221 | +```json |
| 222 | +{ |
| 223 | + "kind": 30095, |
| 224 | + "pubkey": "a2b3c4d5...", |
| 225 | + "created_at": 1735689600, |
| 226 | + "content": "", |
| 227 | + "tags": [ |
| 228 | + ["d","capsule-2025-07"], |
| 229 | + ["u","threshold","t","3","n","5","T","1736000000"], |
| 230 | + ["p","w1..."], |
| 231 | + ["p","w2..."], |
| 232 | + ["p","w3..."], |
| 233 | + ["p","w4..."], |
| 234 | + ["p","w5..."], |
| 235 | + ["w-commit","9c01...ab"], |
| 236 | + ["enc","nip44:v2"], |
| 237 | + ["loc","https"], |
| 238 | + ["uri","https://media.example/caps/abc"], |
| 239 | + ["sha256","c0ffee..."], |
| 240 | + ["alt","External ciphertext with integrity hash"] |
| 241 | + ] |
| 242 | +} |
| 243 | +``` |
| 244 | + |
| 245 | +### Unlock Share (kind 1991) |
| 246 | + |
| 247 | +```json |
| 248 | +{ |
| 249 | + "kind": 1991, |
| 250 | + "pubkey": "a1a2a3a4...", |
| 251 | + "created_at": 1735776100, |
| 252 | + "content": "base64_shamir_share", |
| 253 | + "tags": [ |
| 254 | + ["e","...capsule_event_id..."], |
| 255 | + ["a","30095:a2b3c4d5...:capsule-2025-07"], |
| 256 | + ["p","a1a2a3a4..."], |
| 257 | + ["T","1735776000"] |
| 258 | + ] |
| 259 | +} |
| 260 | +``` |
| 261 | + |
| 262 | +### Share Distribution (kind 1992) |
| 263 | + |
| 264 | +```json |
| 265 | +{ |
| 266 | + "kind": 1992, |
| 267 | + "pubkey": "a2b3c4d5...", |
| 268 | + "created_at": 1735689700, |
| 269 | + "content": "base64_nip44v2_encrypted_share_for_witness", |
| 270 | + "tags": [ |
| 271 | + ["e","...capsule_event_id..."], |
| 272 | + ["a","30095:a2b3c4d5...:capsule-2025-07"], |
| 273 | + ["p","a1a2a3a4..."], |
| 274 | + ["share-idx","1"], |
| 275 | + ["enc","nip44:v2"] |
| 276 | + ] |
| 277 | +} |
| 278 | +``` |
| 279 | + |
| 280 | +## Test Vectors |
| 281 | +Permalink: Test Vectors |
| 282 | + |
| 283 | +### Test Vector A: Threshold 2-of-3 |
| 284 | + |
| 285 | +- Witnesses (ordered pubkeys): `hex_pubkey_A`, `hex_pubkey_B`, `hex_pubkey_C` |
| 286 | +- `w-commit` = MerkleRoot([(0, `hex_pubkey_A`), (1, `hex_pubkey_B`), (2, `hex_pubkey_C`)]) |
| 287 | +- `T` = `1735776000` |
| 288 | +- Shares: `S0,S1,S2`; any two reconstruct `K` |
| 289 | +- Ciphertext: `C = NIP44v2_Encrypt(K, "hello world")` → `content = base64(C)` |
| 290 | + |
| 291 | +Expected flow: |
| 292 | + |
| 293 | +- `1990` event as shown above |
| 294 | +- `1992` to `hex_pubkey_B` with `share-idx=1` (content = NIP-44 encrypted `S1` to `hex_pubkey_B`) |
| 295 | +- `1991` from `hex_pubkey_B` and `hex_pubkey_C` after `T` (plaintext shares) |
| 296 | +- Client reconstructs `K` and decrypts `C` → `"hello world"` |
| 297 | + |
| 298 | +## Rationale |
| 299 | +Permalink: Rationale |
| 300 | + |
| 301 | +- Uses new kinds to avoid overloading existing semantics; unaware nodes ignore unknown kinds |
| 302 | +- Leverages standard `p`/`e` tags for discovery; avoids non-standard tag filtering |
| 303 | +- `w-commit` binds the witness set to prevent tampering |
| 304 | +- Parameterized replaceable variant (`30095`) supports pre-`T` fixes via the `d` tag and `a` addressing |
| 305 | + |
| 306 | +## Backwards Compatibility |
| 307 | +Permalink: Backwards Compatibility |
| 308 | + |
| 309 | +New kinds are ignored by unaware relays/clients. The `alt` tag provides a human-readable hint for unknown kinds. Use of standard `p` and `e` tags preserves discoverability via existing filters. |
| 310 | + |
| 311 | +## Reference Implementation |
| 312 | +Permalink: Reference Implementation |
| 313 | + |
| 314 | +A reference implementation is provided in [Shugur Relay](https://github.com/Shugur-Network/relay) project: |
| 315 | + |
| 316 | +- Relay validation: `internal/relay/nips/nip_time_capsules.go` |
| 317 | +- Test suite: `tests/nips/test_time_capsules_comprehensive.sh` |
0 commit comments