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

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