Files
c-relay/docs/nip59_timestamp_configuration_plan.md

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:

  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)

// 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_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:

// 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 = 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:

### 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)