# NIP-59 Timestamp Configuration Implementation Plan ## Overview Add configurable timestamp randomization for NIP-59 gift wraps to improve compatibility with Nostr apps that don't implement timestamp randomization. ## Problem Statement The NIP-59 protocol specifies that timestamps on gift wraps should have randomness to prevent time-analysis attacks. However, some Nostr platforms don't implement this, causing compatibility issues with direct messaging (NIP-17). ## Solution Add a configuration parameter `nip59_timestamp_max_delay_sec` that controls the maximum random delay applied to timestamps: - **Value = 0**: Use current timestamp (no randomization) for maximum compatibility - **Value > 0**: Use random timestamp between now and N seconds ago - **Default = 0**: Maximum compatibility mode (no randomization) ## Implementation Approach: Option B (Direct Parameter Addition) We chose Option B because: 1. Explicit and stateless - value flows through call chain 2. Thread-safe by design 3. No global state needed in nostr_core_lib 4. DMs are sent rarely, so database query per call is acceptable --- ## Detailed Implementation Steps ### Phase 1: Configuration Setup in c-relay #### 1.1 Add Configuration Parameter **File:** `src/default_config_event.h` **Location:** Line 82 (after `trust_proxy_headers`) ```c // NIP-59 Gift Wrap Timestamp Configuration {"nip59_timestamp_max_delay_sec", "0"} // Default: 0 (no randomization for compatibility) ``` **Rationale:** - Default of 0 seconds (no randomization) for maximum compatibility - Placed after proxy settings, before closing brace - Follows existing naming convention #### 1.2 Add Configuration Validation **File:** `src/config.c` **Function:** `validate_config_field()` (around line 923) Add validation case: ```c else if (strcmp(key, "nip59_timestamp_max_delay_sec") == 0) { long value = strtol(value_str, NULL, 10); if (value < 0 || value > 604800) { // Max 7 days snprintf(error_msg, error_size, "nip59_timestamp_max_delay_sec must be between 0 and 604800 (7 days)"); return -1; } } ``` **Rationale:** - 0 = no randomization (compatibility mode) - 604800 = 7 days maximum (reasonable upper bound) - Prevents negative values or excessive delays --- ### Phase 2: Modify nostr_core_lib Functions #### 2.1 Update random_past_timestamp() Function **File:** `nostr_core_lib/nostr_core/nip059.c` **Current Location:** Lines 31-36 **Current Code:** ```c static time_t random_past_timestamp(void) { time_t now = time(NULL); // Random time up to 2 days (172800 seconds) in the past long random_offset = (long)(rand() % 172800); return now - random_offset; } ``` **New Code:** ```c static time_t random_past_timestamp(long max_delay_sec) { time_t now = time(NULL); // If max_delay_sec is 0, return current timestamp (no randomization) if (max_delay_sec == 0) { return now; } // Random time up to max_delay_sec in the past long random_offset = (long)(rand() % max_delay_sec); return now - random_offset; } ``` **Changes:** - Add `long max_delay_sec` parameter - Handle special case: `max_delay_sec == 0` returns current time - Use `max_delay_sec` instead of hardcoded 172800 #### 2.2 Update nostr_nip59_create_seal() Function **File:** `nostr_core_lib/nostr_core/nip059.c` **Current Location:** Lines 144-215 **Function Signature Change:** ```c // OLD: cJSON* nostr_nip59_create_seal(cJSON* rumor, const unsigned char* sender_private_key, const unsigned char* recipient_public_key); // NEW: cJSON* nostr_nip59_create_seal(cJSON* rumor, const unsigned char* sender_private_key, const unsigned char* recipient_public_key, long max_delay_sec); ``` **Code Change at Line 181:** ```c // OLD: time_t seal_time = random_past_timestamp(); // NEW: time_t seal_time = random_past_timestamp(max_delay_sec); ``` #### 2.3 Update nostr_nip59_create_gift_wrap() Function **File:** `nostr_core_lib/nostr_core/nip059.c` **Current Location:** Lines 220-323 **Function Signature Change:** ```c // OLD: cJSON* nostr_nip59_create_gift_wrap(cJSON* seal, const char* recipient_public_key_hex); // NEW: cJSON* nostr_nip59_create_gift_wrap(cJSON* seal, const char* recipient_public_key_hex, long max_delay_sec); ``` **Code Change at Line 275:** ```c // OLD: time_t wrap_time = random_past_timestamp(); // NEW: time_t wrap_time = random_past_timestamp(max_delay_sec); ``` #### 2.4 Update nip059.h Header **File:** `nostr_core_lib/nostr_core/nip059.h` **Locations:** Lines 38-39 and 48 **Update Function Declarations:** ```c // Line 38-39: Update nostr_nip59_create_seal cJSON* nostr_nip59_create_seal(cJSON* rumor, const unsigned char* sender_private_key, const unsigned char* recipient_public_key, long max_delay_sec); // Line 48: Update nostr_nip59_create_gift_wrap cJSON* nostr_nip59_create_gift_wrap(cJSON* seal, const char* recipient_public_key_hex, long max_delay_sec); ``` **Update Documentation Comments:** ```c /** * NIP-59: Create a seal (kind 13) wrapping a rumor * * @param rumor The rumor event to seal (cJSON object) * @param sender_private_key 32-byte sender private key * @param recipient_public_key 32-byte recipient public key (x-only) * @param max_delay_sec Maximum random delay in seconds (0 = no randomization) * @return cJSON object representing the seal event, or NULL on error */ /** * NIP-59: Create a gift wrap (kind 1059) wrapping a seal * * @param seal The seal event to wrap (cJSON object) * @param recipient_public_key_hex Recipient's public key in hex format * @param max_delay_sec Maximum random delay in seconds (0 = no randomization) * @return cJSON object representing the gift wrap event, or NULL on error */ ``` --- ### Phase 3: Update NIP-17 Integration #### 3.1 Update nostr_nip17_send_dm() Function **File:** `nostr_core_lib/nostr_core/nip017.c` **Current Location:** Lines 260-320 **Function Signature Change:** ```c // OLD: int nostr_nip17_send_dm(cJSON* dm_event, const char** recipient_pubkeys, int num_recipients, const unsigned char* sender_private_key, cJSON** gift_wraps_out, int max_gift_wraps); // NEW: int nostr_nip17_send_dm(cJSON* dm_event, const char** recipient_pubkeys, int num_recipients, const unsigned char* sender_private_key, cJSON** gift_wraps_out, int max_gift_wraps, long max_delay_sec); ``` **Code Changes:** At line 281 (seal creation): ```c // OLD: cJSON* seal = nostr_nip59_create_seal(dm_event, sender_private_key, recipient_public_key); // NEW: cJSON* seal = nostr_nip59_create_seal(dm_event, sender_private_key, recipient_public_key, max_delay_sec); ``` At line 287 (gift wrap creation): ```c // OLD: cJSON* gift_wrap = nostr_nip59_create_gift_wrap(seal, recipient_pubkeys[i]); // NEW: cJSON* gift_wrap = nostr_nip59_create_gift_wrap(seal, recipient_pubkeys[i], max_delay_sec); ``` At line 306 (sender seal creation): ```c // OLD: cJSON* sender_seal = nostr_nip59_create_seal(dm_event, sender_private_key, sender_public_key); // NEW: cJSON* sender_seal = nostr_nip59_create_seal(dm_event, sender_private_key, sender_public_key, max_delay_sec); ``` At line 309 (sender gift wrap creation): ```c // OLD: cJSON* sender_gift_wrap = nostr_nip59_create_gift_wrap(sender_seal, sender_pubkey_hex); // NEW: cJSON* sender_gift_wrap = nostr_nip59_create_gift_wrap(sender_seal, sender_pubkey_hex, max_delay_sec); ``` #### 3.2 Update nip017.h Header **File:** `nostr_core_lib/nostr_core/nip017.h` **Location:** Lines 102-107 **Update Function Declaration:** ```c int nostr_nip17_send_dm(cJSON* dm_event, const char** recipient_pubkeys, int num_recipients, const unsigned char* sender_private_key, cJSON** gift_wraps_out, int max_gift_wraps, long max_delay_sec); ``` **Update Documentation Comment (lines 88-100):** ```c /** * NIP-17: Send a direct message to recipients * * This function creates the appropriate rumor, seals it, gift wraps it, * and returns the final gift wrap events ready for publishing. * * @param dm_event The unsigned DM event (kind 14 or 15) * @param recipient_pubkeys Array of recipient public keys (hex strings) * @param num_recipients Number of recipients * @param sender_private_key 32-byte sender private key * @param gift_wraps_out Array to store resulting gift wrap events (caller must free) * @param max_gift_wraps Maximum number of gift wraps to create * @param max_delay_sec Maximum random timestamp delay in seconds (0 = no randomization) * @return Number of gift wrap events created, or -1 on error */ ``` --- ### Phase 4: Update c-relay Call Sites #### 4.1 Update src/api.c **Location:** Line 1319 **Current Code:** ```c int send_result = nostr_nip17_send_dm( dm_response, // dm_event recipient_pubkeys, // recipient_pubkeys 1, // num_recipients relay_privkey, // sender_private_key gift_wraps, // gift_wraps_out 1 // max_gift_wraps ); ``` **New Code:** ```c // Get timestamp delay configuration long max_delay_sec = get_config_int("nip59_timestamp_max_delay_sec", 0); int send_result = nostr_nip17_send_dm( dm_response, // dm_event recipient_pubkeys, // recipient_pubkeys 1, // num_recipients relay_privkey, // sender_private_key gift_wraps, // gift_wraps_out 1, // max_gift_wraps max_delay_sec // max_delay_sec ); ``` #### 4.2 Update src/dm_admin.c **Location:** Line 371 **Current Code:** ```c int send_result = nostr_nip17_send_dm( success_dm, // dm_event sender_pubkey_array, // recipient_pubkeys 1, // num_recipients relay_privkey, // sender_private_key success_gift_wraps, // gift_wraps_out 1 // max_gift_wraps ); ``` **New Code:** ```c // Get timestamp delay configuration long max_delay_sec = get_config_int("nip59_timestamp_max_delay_sec", 0); int send_result = nostr_nip17_send_dm( success_dm, // dm_event sender_pubkey_array, // recipient_pubkeys 1, // num_recipients relay_privkey, // sender_private_key success_gift_wraps, // gift_wraps_out 1, // max_gift_wraps max_delay_sec // max_delay_sec ); ``` **Note:** Both files already include `config.h`, so `get_config_int()` is available. --- ## Testing Plan ### Test Case 1: No Randomization (Compatibility Mode) **Configuration:** `nip59_timestamp_max_delay_sec = 0` **Expected Behavior:** - Gift wrap timestamps should equal current time - Seal timestamps should equal current time - No random delay applied **Test Command:** ```bash # Set config via admin API # Send test DM # Verify timestamps are current (within 1 second of send time) ``` ### Test Case 2: Custom Delay **Configuration:** `nip59_timestamp_max_delay_sec = 1000` **Expected Behavior:** - Gift wrap timestamps should be between now and 1000 seconds ago - Seal timestamps should be between now and 1000 seconds ago - Random delay applied within specified range **Test Command:** ```bash # Set config via admin API # Send test DM # Verify timestamps are in past but within 1000 seconds ``` ### Test Case 3: Default Behavior **Configuration:** `nip59_timestamp_max_delay_sec = 0` (default) **Expected Behavior:** - Gift wrap timestamps should equal current time - Seal timestamps should equal current time - No randomization (maximum compatibility) **Test Command:** ```bash # Use default config # Send test DM # Verify timestamps are current (within 1 second of send time) ``` ### Test Case 4: Configuration Validation **Test Invalid Values:** - Negative value: Should be rejected - Value > 604800: Should be rejected - Valid boundary values (0, 604800): Should be accepted ### Test Case 5: Interoperability **Test with Other Nostr Clients:** - Send DM with `max_delay_sec = 0` to clients that don't randomize - Send DM with `max_delay_sec = 172800` to clients that do randomize - Verify both scenarios work correctly --- ## Documentation Updates ### Update docs/configuration_guide.md Add new section: ```markdown ### NIP-59 Gift Wrap Timestamp Configuration #### nip59_timestamp_max_delay_sec - **Type:** Integer - **Default:** 0 (no randomization) - **Range:** 0 to 604800 (7 days) - **Description:** Controls timestamp randomization for NIP-59 gift wraps The NIP-59 protocol recommends randomizing timestamps on gift wraps to prevent time-analysis attacks. However, some Nostr platforms don't implement this, causing compatibility issues. **Values:** - `0` (default): No randomization - uses current timestamp (maximum compatibility) - `1-604800`: Random timestamp between now and N seconds ago **Use Cases:** - Keep default `0` for maximum compatibility with clients that don't randomize - Set to `172800` for privacy per NIP-59 specification (2 days randomization) - Set to custom value (e.g., `3600`) for 1-hour randomization window **Example:** ```json ["nip59_timestamp_max_delay_sec", "0"] // Default: compatibility mode ["nip59_timestamp_max_delay_sec", "3600"] // 1 hour randomization ["nip59_timestamp_max_delay_sec", "172800"] // 2 days randomization ``` ``` --- ## Implementation Checklist ### nostr_core_lib Changes - [ ] Modify `random_past_timestamp()` to accept `max_delay_sec` parameter - [ ] Update `nostr_nip59_create_seal()` signature and implementation - [ ] Update `nostr_nip59_create_gift_wrap()` signature and implementation - [ ] Update `nip059.h` function declarations and documentation - [ ] Update `nostr_nip17_send_dm()` signature and implementation - [ ] Update `nip017.h` function declaration and documentation ### c-relay Changes - [ ] Add `nip59_timestamp_max_delay_sec` to `default_config_event.h` - [ ] Add validation in `config.c` for new parameter - [ ] Update `src/api.c` call site to pass `max_delay_sec` - [ ] Update `src/dm_admin.c` call site to pass `max_delay_sec` ### Testing - [ ] Test with `max_delay_sec = 0` (no randomization) - [ ] Test with `max_delay_sec = 1000` (custom delay) - [ ] Test with `max_delay_sec = 172800` (default behavior) - [ ] Test configuration validation (invalid values) - [ ] Test interoperability with other Nostr clients ### Documentation - [ ] Update `docs/configuration_guide.md` - [ ] Add this implementation plan to docs - [ ] Update README if needed --- ## Rollback Plan If issues arise: 1. Revert nostr_core_lib changes (git revert in submodule) 2. Revert c-relay changes 3. Configuration parameter will be ignored if not used 4. Default behavior (0) provides maximum compatibility --- ## Notes - The configuration is read on each DM send, allowing runtime changes - No restart required when changing `nip59_timestamp_max_delay_sec` - Thread-safe by design (no global state) - Default value of 0 provides maximum compatibility with other Nostr clients - Can be changed to 172800 or other values for NIP-59 privacy features --- ## References - [NIP-59: Gift Wrap](https://github.com/nostr-protocol/nips/blob/master/59.md) - [NIP-17: Private Direct Messages](https://github.com/nostr-protocol/nips/blob/master/17.md) - [NIP-44: Versioned Encryption](https://github.com/nostr-protocol/nips/blob/master/44.md)