16 KiB
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:
- Explicit and stateless - value flows through call chain
- Thread-safe by design
- No global state needed in nostr_core_lib
- 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)
// 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:
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:
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:
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_secparameter - Handle special case:
max_delay_sec == 0returns current time - Use
max_delay_secinstead 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:
// 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:
// 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:
// 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:
// 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:
// 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:
/**
* 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:
// 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):
// 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):
// 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):
// 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):
// 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:
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):
/**
* 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:
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:
// 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:
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:
// 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:
# 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:
# 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:
# 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 = 0to clients that don't randomize - Send DM with
max_delay_sec = 172800to clients that do randomize - Verify both scenarios work correctly
Documentation Updates
Update docs/configuration_guide.md
Add new section:
### 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)