Files
didactyl/plans/nip17_gift_wrap_investigation.md

6.3 KiB

NIP-17 Gift Wrap Send Failure — Investigation

Status: Resolved — Fixed in code (2026-04-11)

Date Discovered: 2026-04-11

Symptom

Every outbound NIP-17 DM send from the Anvil agent fails with:

NIP-17 send aborted: gift_wrap creation failed for 1ec454734dcbf6fe...
auto DM fallback to NIP-04 for 1ec454734dcbf6fe... after NIP-17 send failure

The agent successfully falls back to NIP-04 (kind 4), so DMs are still delivered. However, NIP-17 outbound is completely broken.

Inbound NIP-17 works fine — the agent successfully receives and decrypts kind 1059 gift wraps from the admin.

Affected Code Path

Send path (broken):

  1. nostr_handler_send_dm_auto_with_role() in src/nostr_handler.c:3193 — routes to NIP-17 when dm_protocol is nip17 or both
  2. nostr_handler_send_dm_nip17_with_role() in src/nostr_handler.c:4050 — creates chat event, calls nostr_nip17_send_dm()
  3. nostr_nip17_send_dm() in nostr_core_lib/nostr_core/nip017.c:260 — iterates recipients, creates seal + gift wrap
  4. nostr_nip59_create_seal() in nostr_core_lib/nostr_core/nip059.c:150encrypts rumor JSON with NIP-44
  5. nostr_nip59_create_gift_wrap() in nostr_core_lib/nostr_core/nip059.c:226 — wraps seal with random key

Receive path (working):

  1. on_event() receives kind 1059
  2. nostr_nip17_receive_dm() in nostr_core_lib/nostr_core/nip017.c:326 — unwraps gift, unseals rumor
  3. Successfully extracts kind 14 content

Probable Root Cause: Fixed Buffer Overflow in Seal Encryption

In nostr_core_lib/nostr_core/nip059.c:163-165:

char encrypted_content[4096]; // Should be large enough for most events
int encrypt_result = nostr_nip44_encrypt(sender_private_key, recipient_public_key,
                                        rumor_json, encrypted_content, sizeof(encrypted_content));

The seal creation uses a fixed 4096-byte stack buffer for the NIP-44 encrypted output. The rumor JSON includes the full DM message content. When the agent sends long responses (the FIPS mesh conversation had multi-KB markdown tables), the rumor JSON easily exceeds 4096 bytes after NIP-44 encryption overhead (base64 encoding roughly 4/3x expansion + padding + nonce).

Evidence:

  • The agent's DM responses in the log were 2-5KB of markdown content
  • After JSON serialization of the kind 14 rumor (adding pubkey, tags, created_at, kind fields), the total is larger
  • NIP-44 encryption adds: 32-byte nonce + padding + base64 encoding → roughly 1.5-2x expansion
  • A 3KB message → ~4.5KB rumor JSON → ~6-9KB encrypted → overflows 4096 buffer
  • nostr_nip44_encrypt() returns error, nostr_nip59_create_seal() returns NULL
  • nostr_nip17_send_dm() skips the recipient, returns 0 wraps
  • nostr_handler_send_dm_nip17_with_role() logs "gift_wrap creation failed"

Why receive works: The receive path uses nostr_nip44_decrypt() which likely allocates dynamically or has a larger buffer.

Secondary Suspect: NIP-44 Encryption Implementation

The NIP-44 encrypt function itself may have issues with large payloads. Need to check:

  • nostr_nip44_encrypt() in nostr_core_lib/nostr_core/nip044.c
  • Whether it handles the output buffer size parameter correctly
  • Whether it returns an error code that distinguishes "buffer too small" from other failures

Anvil Config Context

The Anvil agent's genesis config (/home/user/anvil/genesis.jsonc) has:

"dm_protocol": "nip04"

But the encrypted user-settings on Nostr (kind 30078) may have overridden this to "both" at runtime, which is why NIP-17 send is being attempted.

Option A: Dynamic allocation (preferred)

Replace the fixed 4096-byte buffer in nostr_nip59_create_seal() with dynamic allocation:

size_t rumor_len = strlen(rumor_json);
size_t encrypted_buf_size = rumor_len * 2 + 256; // generous overhead for NIP-44
char* encrypted_content = malloc(encrypted_buf_size);

Option B: Larger fixed buffer (quick fix)

Increase the buffer to 64KB or 128KB:

char encrypted_content[131072]; // 128KB should handle any reasonable DM

Note: This is a stack allocation, so very large buffers could cause stack overflow.

Also check nostr_nip59_create_gift_wrap()

The gift wrap function at nip059.c:226 may have a similar fixed-buffer issue for encrypting the seal JSON.

Log Evidence

From /home/user/anvil/debug.log (2026-04-11):

[2026-04-11 09:45:14] [WARN ] NIP-17 send aborted: gift_wrap creation failed for 1ec454734dcbf6fe...
[2026-04-11 09:45:14] [WARN ] auto DM fallback to NIP-04 for 1ec454734dcbf6fe... after NIP-17 send failure
[2026-04-11 09:50:16] [WARN ] NIP-17 send aborted: gift_wrap creation failed for 1ec454734dcbf6fe...
[2026-04-11 09:50:16] [WARN ] auto DM fallback to NIP-04 for 1ec454734dcbf6fe... after NIP-17 send failure
[2026-04-11 09:53:42] [WARN ] NIP-17 send aborted: gift_wrap creation failed for 1ec454734dcbf6fe...
[2026-04-11 09:53:42] [WARN ] auto DM fallback to NIP-04 for 1ec454734dcbf6fe... after NIP-17 send failure

This pattern is 100% consistent — every outbound NIP-17 attempt fails, every time.

  • nostr_core_lib/nostr_core/nip059.c — Gift wrap / seal creation (probable bug location)
  • nostr_core_lib/nostr_core/nip044.c — NIP-44 encryption (check buffer handling)
  • nostr_core_lib/nostr_core/nip017.c — NIP-17 DM send/receive orchestration
  • src/nostr_handler.c:4050 — Agent-level NIP-17 send function
  • plans/nip17_messaging.md — Original NIP-17 implementation plan

The plans/nip17_messaging.md document covers the original NIP-17 implementation. This investigation is specifically about the send-side gift wrap failure that emerged in production.

Resolution Summary

Implemented in nostr_core_lib/nostr_core/nip059.c:

  • Replaced fixed encryption buffers in nostr_nip59_create_seal() and nostr_nip59_create_gift_wrap() with dynamically sized heap buffers calculated from NIP-44 padding/base64 overhead.
  • Replaced fixed decrypt buffers in nostr_nip59_unwrap_gift() and nostr_nip59_unseal_rumor() with dynamic buffers sized from ciphertext length.
  • Added missing cleanup path in nostr_nip59_create_seal() for failure after encryption allocation.

Expected outcome: outbound NIP-17 gift wrap creation no longer fails for multi-KB DM payloads due to fixed buffer limits.