Skip to content

Add NIP-51 Lists support#58

Merged
kwsantiago merged 13 commits intomainfrom
NIP-51
Jan 24, 2026
Merged

Add NIP-51 Lists support#58
kwsantiago merged 13 commits intomainfrom
NIP-51

Conversation

@wksantiago
Copy link
Contributor

@wksantiago wksantiago commented Jan 24, 2026

Implements NIP-51 list management with support for:

  • Standard lists (mute, pin, bookmarks, communities, search relays)
  • Parameterized replaceable lists (follow sets, relay sets, etc.)
  • Public and private (encrypted) list items
  • Full CRUD operations for list items

Summary by CodeRabbit

  • New Features

    • NIP-51 (Lists) added as an optional build-time feature (enabled by default). Create and manage lists with pubkeys, events, hashtags, words, relays, references, per-item privacy, and metadata (d-tag, title, description, image). Convert lists to/from events.
    • Enabling “all NIPs” now includes NIP-51 automatically.
  • Tests

    • Comprehensive NIP-51 test suite added and executed when the feature is enabled.
  • Documentation

    • Features list updated to include NIP-51.

✏️ Tip: You can customize this high-level summary in your review settings.

@coderabbitai
Copy link

coderabbitai bot commented Jan 24, 2026

Warning

Rate limit exceeded

@kwsantiago has exceeded the limit for the number of commits that can be reviewed per hour. Please wait 9 minutes and 46 seconds before requesting another review.

⌛ How to resolve this issue?

After the wait time has elapsed, a review can be triggered using the @coderabbitai review command as a PR comment. Alternatively, push new commits to this PR.

We recommend that you space out your commits to avoid hitting the rate limit.

🚦 How do rate limits work?

CodeRabbit enforces hourly rate limits for each developer per organization.

Our paid plans have higher rate limits than the trial, open-source and free plans. In all cases, we re-allow further reviews after a brief timeout.

Please see our FAQ for further information.

Walkthrough

Adds NIP‑51 (Lists): feature flag and build/test wiring, new public nostr_list API and implementation (dynamic items, metadata, serialize/deserialize to nostr_event), optional NIP‑44 private-item encryption, tests, and feature-query registration.

Changes

Cohort / File(s) Summary
Build & Feature Flags
CMakeLists.txt, include/nostr_features.h.in, tests/CMakeLists.txt
Add NOSTR_FEATURE_NIP51 (exported, default ON); wire src/nip51.c into build and register test_nip51 when enabled.
Public API
include/nostr.h
Add nostr_list_kind, nostr_list_item, nostr_list types and ~18 public functions (create/free, metadata setters, add_* helpers, to/from event, accessors, remove).
Feature Detection
src/features.c
Register NIP‑51 in nostr_feature_nip_supported and append ,nip51 to feature listing.
NIP‑51 Implementation
src/nip51.c
New gated implementation: dynamic list storage, item types, add/remove/query, serialize/deserialize to/from nostr_event, private-item JSON handling and optional NIP‑44 encryption/decryption; NOT_SUPPORTED stubs when feature absent.
Tests
tests/test_nip51.c
New comprehensive unit tests exercising lifecycle, item kinds, metadata, conversions (including parameterized/encrypted flows), capacity growth, and boundary checks.
Docs
README.md
Document NIP‑51 in optional NIPs list.

Sequence Diagram(s)

sequenceDiagram
    participant Client as Client
    participant List as nostr_list
    participant Crypto as NIP44_Encryption
    participant Event as nostr_event

    Client->>List: nostr_list_create(kind)
    Client->>List: set metadata (d_tag,title,description,image)
    Client->>List: add items (pubkey/event/relay/hashtag/word/reference/etc.)
    Client->>List: nostr_list_to_event(keypair)
    alt Private items present and NIP44 available
        List->>Crypto: encrypt private-items JSON with keypair
        Crypto-->>List: encrypted content (base64)
    end
    List->>Event: build event with tags + content
    Event-->>Client: nostr_event*
Loading
sequenceDiagram
    participant Client as Client
    participant Event as nostr_event
    participant Crypto as NIP44_Decryption
    participant List as nostr_list

    Client->>List: nostr_list_from_event(event, keypair)
    List->>Event: extract tags and content
    alt Encrypted content present and NIP44 available
        List->>Crypto: decrypt content with keypair
        Crypto-->>List: decrypted private-items JSON
    end
    List->>List: parse tags + (decrypted) JSON → items
    List-->>Client: reconstructed nostr_list
Loading

Estimated code review effort

🎯 4 (Complex) | ⏱️ ~50 minutes

Possibly related issues

  • NIP-51: Lists #41: Implements NIP‑51 list API and data model (nostr_list types, add_*, to/from event, private handling), matching this PR's objectives.

Possibly related PRs

Poem

🐰 I hopped through headers, C files, and test,
I knitted lists where tags and secrets rest.
From items to event I bounded each part,
Encrypted niblets tucked inside my heart.
A tiny carrot flag — NIP‑51, my best!

🚥 Pre-merge checks | ✅ 2 | ❌ 1
❌ Failed checks (1 warning)
Check name Status Explanation Resolution
Docstring Coverage ⚠️ Warning Docstring coverage is 0.00% which is insufficient. The required threshold is 80.00%. Write docstrings for the functions missing them to satisfy the coverage threshold.
✅ Passed checks (2 passed)
Check name Status Explanation
Description Check ✅ Passed Check skipped - CodeRabbit’s high-level summary is enabled.
Title check ✅ Passed The title 'Add NIP-51 Lists support' accurately describes the main change—implementing NIP-51 list management functionality with CRUD operations, public/private items, and full API support.

✏️ Tip: You can configure your own custom pre-merge checks in the settings.


Thanks for using CodeRabbit! It's free for OSS, and your support helps us grow. If you like it, consider giving us a shout-out.

❤️ Share

Comment @coderabbitai help to get the list of available commands and usage tips.

@wksantiago wksantiago requested a review from kwsantiago January 24, 2026 13:20
@wksantiago wksantiago self-assigned this Jan 24, 2026
@wksantiago wksantiago linked an issue Jan 24, 2026 that may be closed by this pull request
Copy link

@coderabbitai coderabbitai bot left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Actionable comments posted: 2

🤖 Fix all issues with AI agents
In `@src/nip51.c`:
- Around line 649-664: The current block silently skips decrypting private
content when NOSTR_FEATURE_NIP44 is not defined (even if event->content is
present and keypair is non-NULL); update the logic in the branch that checks
event->content and keypair so that when NOSTR_FEATURE_NIP44 is not available you
either return NOSTR_ERR_NOT_SUPPORTED or set an output flag/error to indicate
private content was skipped (instead of doing nothing). Locate the check around
event->content and keypair in src/nip51.c and modify the behavior used when
NOSTR_FEATURE_NIP44 is undefined (the branch that would call nostr_nip44_decrypt
and parse_private_content) to propagate a clear non-supported status (or toggle
a provided out-parameter) so callers can detect that encrypted content was not
processed.

In `@tests/test_nip51.c`:
- Around line 74-92: The test calls nostr_list_create(&list,
NOSTR_LIST_KIND_MUTE) but does not verify the creation succeeded before using
list; update test_list_add_pubkey to check the creation result and/or that list
is non-NULL (e.g., assert nostr_list_create returned NOSTR_OK and/or
TEST_ASSERT_NOT_NULL(list)) right after the nostr_list_create call, and if the
check fails ensure the test cleans up (skip further calls to
nostr_list_add_pubkey, nostr_list_count, nostr_list_get, nostr_list_free) so the
test fails cleanly instead of dereferencing a NULL list.
♻️ Duplicate comments (16)
tests/test_nip51.c (16)

94-115: Same create-check concern applies here.


117-128: Same create-check concern applies here.


130-148: Same create-check concern applies here.


150-165: Same create-check concern applies here.


167-182: Same create-check concern applies here.


184-199: Same create-check concern applies here.


201-218: Same create-check concern applies here.


220-246: Same create-check concern applies here.


248-280: Same create-check concern applies here.


282-326: Same create-check concern applies here.


328-360: Same create-check concern applies here.


362-394: Same create-check concern applies here.


396-413: Same create-check concern applies here.


415-430: Same create-check concern applies here.


432-446: Same create-check concern applies here.


448-468: Same create-check concern applies here.

🧹 Nitpick comments (5)
include/nostr.h (1)

1394-1455: Document nostr_list thread-safety expectations.

These list types are mutable; a short thread-safety note (similar to NIP-65’s) would prevent misuse in multi-threaded clients.

src/nip51.c (4)

29-51: Control character escaping is lossy and non-standard.

The json_escape_to function maps all control characters (< 0x20) except \n, \r, \t to \n. This loses information - for example, \b (backspace, 0x08) or \f (form feed, 0x0C) would be incorrectly converted to \n. Per JSON spec, these should use \uXXXX encoding or their specific escape sequences.

♻️ Suggested improvement for proper JSON escaping
         default:
             if (c < 0x20) {
-                *dest++ = '\\';
-                *dest++ = 'n';
+                static const char hex[] = "0123456789abcdef";
+                *dest++ = '\\';
+                *dest++ = 'u';
+                *dest++ = '0';
+                *dest++ = '0';
+                *dest++ = hex[c >> 4];
+                *dest++ = hex[c & 0x0f];
             } else {
                 *dest++ = c;
             }

Note: This requires updating json_escaped_len to return 6 instead of 2 for control characters.


204-220: Consider adding hex character validation for pubkey and event_id.

The validation only checks string length (64 characters) but doesn't verify that the characters are valid hexadecimal. This could allow malformed identifiers to be added to lists.

♻️ Suggested hex validation helper
static bool is_valid_hex64(const char* s)
{
    if (!s || strlen(s) != 64) return false;
    for (size_t i = 0; i < 64; i++) {
        char c = s[i];
        if (!((c >= '0' && c <= '9') || 
              (c >= 'a' && c <= 'f') || 
              (c >= 'A' && c <= 'F'))) {
            return false;
        }
    }
    return true;
}

Then use is_valid_hex64(pubkey) instead of strlen(pubkey) != 64.


513-516: Fixed-size buffers may silently truncate long values.

The parsing uses fixed-size buffers (value[1024], relay_hint[512], etc.) which will silently truncate longer content without any error indication. For robustness, consider either dynamically allocating based on actual content length or returning an error when content exceeds buffer limits.


556-565: Unicode escape sequences are replaced with ?, losing data.

The \uXXXX JSON escape handling replaces all Unicode escape sequences with a literal ? character. This loses potentially important data (e.g., emoji, non-ASCII characters). Consider implementing proper UTF-8 conversion for Unicode escape sequences.

@wksantiago
Copy link
Contributor Author

@coderabbitai review

@coderabbitai
Copy link

coderabbitai bot commented Jan 24, 2026

✅ Actions performed

Review triggered.

Note: CodeRabbit is an incremental review system and does not re-review already reviewed commits. This command is applicable only when automatic reviews are paused.

Copy link

@coderabbitai coderabbitai bot left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Actionable comments posted: 1

🤖 Fix all issues with AI agents
In `@src/nip51.c`:
- Around line 40-46: The default branch currently encodes all control chars
(<0x20) as the two-character sequence '\n' which is wrong; change the serializer
(the code that writes to *dest in this switch) to emit a 6-character JSON
Unicode escape "\\u00XX" for control bytes except for the already-handled '\n',
'\r', '\t' cases (ensure the hex is zero-padded to four digits), and update
json_escaped_len to add 6 characters (not 2) for those control bytes so length
accounting matches the new "\\u00XX" output.
🧹 Nitpick comments (2)
src/nip51.c (1)

513-516: Fixed-size buffers may silently truncate long values.

The parser uses fixed-size stack buffers (e.g., value[1024]) that silently truncate longer strings due to the len < max_len check at line 547. Consider either:

  1. Returning an error if content exceeds buffer size
  2. Using dynamic allocation for values

This is a minor concern since NIP-51 list items are typically short identifiers.

tests/test_nip51.c (1)

95-116: Inconsistent error checking for list creation.

While test_list_add_pubkey (lines 77-78) now properly checks the creation result, test_list_add_pubkey_with_hints and several other tests still call nostr_list_create without verifying success. If creation fails, subsequent operations will dereference NULL.

Apply the same pattern used in test_list_add_pubkey:

♻️ Suggested fix for test_list_add_pubkey_with_hints
 static int test_list_add_pubkey_with_hints(void)
 {
     nostr_list* list = NULL;
-    nostr_list_create(&list, NOSTR_LIST_KIND_FOLLOW_SET);
+    TEST_ASSERT_EQUAL(NOSTR_OK, nostr_list_create(&list, NOSTR_LIST_KIND_FOLLOW_SET));
+    TEST_ASSERT_NOT_NULL(list);
 
     const char* pubkey = "abcdef1234567890abcdef1234567890abcdef1234567890abcdef1234567890";

The same pattern should be applied to these tests: test_list_add_pubkey_invalid, test_list_add_event, test_list_add_hashtag, test_list_add_word, test_list_add_relay, test_list_add_reference, test_list_remove, test_list_to_event_standard, test_list_to_event_parameterized, test_list_from_event, test_list_from_event_parameterized, test_list_set_metadata, test_list_capacity_growth, test_list_get_out_of_bounds.

Copy link

@coderabbitai coderabbitai bot left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Actionable comments posted: 3

🤖 Fix all issues with AI agents
In `@src/nip51.c`:
- Around line 519-522: The fixed-size local buffers tag_type[64], value[1024],
relay_hint[512], and petname[256] can silently truncate long inputs; change the
parser to detect truncation and fail rather than silently corrupt data (or
switch to dynamic allocation). Specifically, when copying/parsing into tag_type,
value, relay_hint, and petname, check the source length against sizeof(...) (or
the snprintf return value) and return an error code if the input is larger than
the buffer, or replace the stack buffers with heap buffers sized to the actual
input length (malloc/free) and propagate ownership to callers; ensure all call
sites handle the new error or freed memory paths.
- Around line 307-389: The buffer size calculation in build_private_content
undercounts when item->petname is present but item->relay_hint is false because
the code emits an extra empty-string placeholder (the ,"") but item_size only
adds json_escaped_len(item->petname) + 4; update the size calculation logic for
that loop (the item_size computation using json_escaped_len and checks for
item->relay_hint/item->petname) to add the extra 4 bytes for the placeholder
when petname exists and relay_hint is absent, and keep the existing overflow
check (buf_size > SIZE_MAX - item_size) intact so malloc gets a correctly sized
buffer.
- Around line 562-571: In the case 'u' escape handling block, instead of
skipping the four hex chars and appending '?', parse the four hex digits from *p
to compute the Unicode code point (use p and the loop that currently advances
over 4 hex digits), then if the value is <= 0xFF write that byte into
dest[len++] (respecting max_len) so \u00XX round-trips correctly; for values >
0xFF either encode the code point as UTF-8 into dest (respecting max_len) or
fall back to '?' if you don't want to implement UTF-8 here, but do not silently
drop valid \u00XX sequences.

Copy link

@coderabbitai coderabbitai bot left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Actionable comments posted: 2

🤖 Fix all issues with AI agents
In `@src/nip51.c`:
- Around line 701-714: The current block calls nostr_nip44_decrypt and only
handles the success case, silently ignoring errors; change it so that when
nostr_nip44_decrypt returns a non-NOSTR_OK error you propagate that error and
jump to cleanup instead of continuing as if OK. Specifically, after calling
nostr_nip44_decrypt (with nostr_keypair_private_key(keypair),
nostr_keypair_public_key(keypair), event->content, &decrypted, &decrypted_len)
check if err != NOSTR_OK and if so goto cleanup (ensuring any allocated
decrypted is freed if set); keep the existing success path that calls
parse_private_content(decrypted, *list) and free(decrypted) and propagate parse
errors to cleanup as before.
- Around line 595-607: The UTF-8 encoding branches in nip51.c silently drop
multi-byte output when there isn’t room (branches handling codepoint <= 0x7FF
and else for 3-byte sequences write fewer bytes only if space is available), so
update those branches to detect insufficient buffer space and trigger the
existing truncation/error path instead of skipping bytes: check remaining space
(using len and max_len) before starting a multi-byte write, and if there isn’t
enough room set the same truncation indicator or return value used elsewhere in
this function (or call the same error/truncation handler) so the caller observes
failure rather than getting silently truncated output; ensure you reference and
update the logic around dest, len, max_len and the codepoint encoding blocks so
behavior is consistent for 1-, 2- and 3-byte sequences.

@wksantiago wksantiago removed the request for review from kwsantiago January 24, 2026 17:48
Copy link

@coderabbitai coderabbitai bot left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Actionable comments posted: 1

🤖 Fix all issues with AI agents
In `@src/nip51.c`:
- Around line 465-468: Check that the keypair is initialized (i.e.,
nostr_keypair_private_key(keypair) and nostr_keypair_public_key(keypair) are
non-NULL) before calling nostr_nip44_encrypt or nostr_nip44_decrypt; if either
returns NULL, fail fast by returning an error (or setting err) and avoid passing
NULL into nostr_nip44_* functions. Add the guard around the encryption call
where encrypted is produced (the nostr_nip44_encrypt call) and the analogous
guard around the decrypt path (the nostr_nip44_decrypt usage referenced later),
using the same keypair variable and ensure any allocated resources are cleaned
up before returning.
🧹 Nitpick comments (1)
src/nip51.c (1)

548-553: Make unknown-field skipping escape‑aware.
If an extra field contains escaped quotes, the current skip loop can desync parsing. Consider skipping with the same escape-aware logic used in the main parse loop.

♻️ Suggested refinement
-            default:
-                while (*p && *p != '"') p++;
-                if (*p == '"') p++;
-                field++;
-                continue;
+            default:
+                while (*p) {
+                    if (*p == '\\' && *(p + 1)) { p += 2; continue; }
+                    if (*p == '"') { p++; break; }
+                    p++;
+                }
+                field++;
+                continue;

@wksantiago
Copy link
Contributor Author

@coderabbitai review

@coderabbitai
Copy link

coderabbitai bot commented Jan 24, 2026

✅ Actions performed

Review triggered.

Note: CodeRabbit is an incremental review system and does not re-review already reviewed commits. This command is applicable only when automatic reviews are paused.

@wksantiago wksantiago requested a review from kwsantiago January 24, 2026 18:21
- Add all 15 missing list kinds to enum (10009-39092)
- Add group tag helper for NIP-29 simple groups
- Add emoji tag helper for NIP-30 custom emoji
- Fix type mismatch in NIP-44 encrypt/decrypt calls
- Add hex validation for pubkey/event_id inputs
- Fix UTF-16 surrogate pair handling
- Simplify code with extracted helper functions
- Add NIP-51 to README features list
Copy link

@coderabbitai coderabbitai bot left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Actionable comments posted: 1

🤖 Fix all issues with AI agents
In `@src/nip51.c`:
- Around line 98-106: In parse_hex4(const char** pp) ensure you don't read past
the NUL by adding a short-length guard that verifies there are at least four
chars available at p before the for-loop (e.g. check p[0..3] are not '\0' or use
strlen(p) < 4) and return -1 if fewer than four remain; keep the existing
hex_digit_value checks and behavior otherwise so malformed \u escapes produce -1
instead of causing OOB reads.
🧹 Nitpick comments (1)
include/nostr.h (1)

1496-1503: Fix the typo in the public enum name before it becomes an API wart.

NOSTR_LIST_KIND_KIND_MUTE_SET at Line 1502 looks accidental. Consider renaming to NOSTR_LIST_KIND_MUTE_SET and keeping a deprecated alias to avoid breaking early adopters.

♻️ Proposed fix (retain compatibility)
-    NOSTR_LIST_KIND_KIND_MUTE_SET = 30007,
+    NOSTR_LIST_KIND_MUTE_SET = 30007,
+    NOSTR_LIST_KIND_KIND_MUTE_SET = NOSTR_LIST_KIND_MUTE_SET, /* deprecated alias */

Copy link

@coderabbitai coderabbitai bot left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Actionable comments posted: 1

🤖 Fix all issues with AI agents
In `@src/nip51.c`:
- Around line 178-196: Each setter (nostr_list_set_d_tag, nostr_list_set_title,
nostr_list_set_description, nostr_list_set_image) currently computes
&list->member before calling set_string_field which is undefined if list is
NULL; add an explicit NULL check at the top of each function (if (list == NULL)
return an appropriate nostr_error_t invalid-argument error, e.g.,
NOSTR_ERROR_INVALID_ARGUMENT) and only then call set_string_field(&list->...,
...); this ensures you never use the -> operator on a NULL pointer.

@kwsantiago kwsantiago merged commit 12f80c1 into main Jan 24, 2026
12 checks passed
@kwsantiago kwsantiago deleted the NIP-51 branch January 24, 2026 22:16
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

NIP-51: Lists

2 participants