diff --git a/POOL_API.md b/POOL_API.md index 7d5de740..8bfef5a5 100644 --- a/POOL_API.md +++ b/POOL_API.md @@ -6,11 +6,12 @@ This document describes the public API for the Nostr Relay Pool implementation i | Function | Description | |----------|-------------| -| [`nostr_relay_pool_create()`](nostr_core/core_relay_pool.c:219) | Create and initialize a new relay pool | -| [`nostr_relay_pool_destroy()`](nostr_core/core_relay_pool.c:304) | Destroy pool and cleanup all resources | -| [`nostr_relay_pool_add_relay()`](nostr_core/core_relay_pool.c:229) | Add a relay URL to the pool | -| [`nostr_relay_pool_remove_relay()`](nostr_core/core_relay_pool.c:273) | Remove a relay URL from the pool | -| [`nostr_relay_pool_subscribe()`](nostr_core/core_relay_pool.c:399) | Create async subscription with callbacks | +| [`nostr_relay_pool_create()`](nostr_core/core_relay_pool.c:594) | Create and initialize a new relay pool | +| [`nostr_relay_pool_destroy()`](nostr_core/core_relay_pool.c:733) | Destroy pool and cleanup all resources | +| [`nostr_relay_pool_add_relay()`](nostr_core/core_relay_pool.c:648) | Add a relay URL to the pool | +| [`nostr_relay_pool_remove_relay()`](nostr_core/core_relay_pool.c:702) | Remove a relay URL from the pool | +| [`nostr_relay_pool_set_auth()`](nostr_core/core_relay_pool.c:616) | Configure pool-wide NIP-42 authentication key and enable flag | +| [`nostr_relay_pool_subscribe()`](nostr_core/core_relay_pool.c:778) | Create async subscription with callbacks | | [`nostr_pool_subscription_close()`](nostr_core/core_relay_pool.c:491) | Close subscription and free resources | | [`nostr_relay_pool_run()`](nostr_core/core_relay_pool.c:1192) | Run event loop for specified timeout | | [`nostr_relay_pool_poll()`](nostr_core/core_relay_pool.c:1232) | Single iteration poll and dispatch | @@ -71,6 +72,26 @@ void cleanup_pool(nostr_relay_pool_t* pool) { ## Relay Management +### Configure Pool Authentication (NIP-42) +**Function:** [`nostr_relay_pool_set_auth()`](nostr_core/core_relay_pool.c:616) +```c +int nostr_relay_pool_set_auth(nostr_relay_pool_t* pool, const unsigned char* private_key, int enable); +``` + +**Description:** +- Sets a pool-wide private key used to sign NIP-42 `AUTH` responses. +- Enables/disables NIP-42 handling for all relays in the pool. +- Propagates auth-enabled state to existing relay connections. + +**Example:** +```c +unsigned char privkey[32]; +nostr_hex_to_bytes("91ba716fa9e7ea2fcbad360cf4f8e0d312f73984da63d90f524ad61a6a1e7dbe", privkey, 32); + +nostr_relay_pool_t* pool = nostr_relay_pool_create(nostr_pool_reconnect_config_default()); +nostr_relay_pool_set_auth(pool, privkey, 1); // enable NIP-42 auto AUTH handling +``` + ### Add Relay **Function:** [`nostr_relay_pool_add_relay()`](nostr_core/core_relay_pool.c:229) ```c @@ -751,5 +772,6 @@ int main() { - **Memory ownership**: The pool duplicates filters and URLs internally. Caller owns returned events and must free them. - **Event deduplication** is applied pool-wide using a circular buffer of 1000 event IDs. - **Ping functionality** is currently disabled in this build. +- **NIP-42 behavior**: With [`nostr_relay_pool_set_auth()`](nostr_core/core_relay_pool.c:616) enabled, the pool will respond to relay `AUTH` challenges automatically using [`nostr_nip42_create_auth_event()`](nostr_core/nip042.c:26) and [`nostr_nip42_create_auth_message()`](nostr_core/nip042.c:84). - **Reconnection** happens on-demand when sending, but active subscriptions are not automatically re-sent after reconnect. -- **Polling model**: You must drive the event loop via [`nostr_relay_pool_run()`](nostr_core/core_relay_pool.c:1192) or [`nostr_relay_pool_poll()`](nostr_core/core_relay_pool.c:1232) to receive events. \ No newline at end of file +- **Polling model**: You must drive the event loop via [`nostr_relay_pool_run()`](nostr_core/core_relay_pool.c:1745) or [`nostr_relay_pool_poll()`](nostr_core/core_relay_pool.c:1785) to receive events. \ No newline at end of file diff --git a/README.md b/README.md index 124851d7..6fc5dbab 100644 --- a/README.md +++ b/README.md @@ -292,6 +292,7 @@ publish_result_t* synchronous_publish_event_with_progress(const char** relay_url nostr_pool_reconnect_config_t* config = nostr_pool_reconnect_config_default(); nostr_relay_pool_t* nostr_relay_pool_create(nostr_pool_reconnect_config_t* config); int nostr_relay_pool_add_relay(nostr_relay_pool_t* pool, const char* relay_url); +int nostr_relay_pool_set_auth(nostr_relay_pool_t* pool, const unsigned char* private_key, int enable); void nostr_relay_pool_destroy(nostr_relay_pool_t* pool); // Subscribe to events (with auto-reconnection) @@ -300,7 +301,12 @@ nostr_pool_subscription_t* nostr_relay_pool_subscribe( void (*on_event)(cJSON* event, const char* relay_url, void* user_data), void (*on_eose)(void* user_data), void* user_data, int close_on_eose); -// Run event loop (handles reconnection automatically) +// Enable NIP-42 auth (auto-responds to relay AUTH challenges) +unsigned char private_key[32]; +nostr_hex_to_bytes("91ba716fa9e7ea2fcbad360cf4f8e0d312f73984da63d90f524ad61a6a1e7dbe", private_key, 32); +nostr_relay_pool_set_auth(pool, private_key, 1); + +// Run event loop (handles reconnection + incoming AUTH/NOTICE/OK messages) int nostr_relay_pool_run(nostr_relay_pool_t* pool, int timeout_ms); int nostr_relay_pool_poll(nostr_relay_pool_t* pool, int timeout_ms); diff --git a/VERSION b/VERSION index 76914ddc..e8423da8 100644 --- a/VERSION +++ b/VERSION @@ -1 +1 @@ -0.4.9 +0.4.10 diff --git a/debug.log b/debug.log index 3bfa39a5..d1b3cad5 100644 --- a/debug.log +++ b/debug.log @@ -1285,3 +1285,269 @@ "since": 1759830747, "limit": 10 }] + +=== NOSTR WebSocket Debug Log Started === +[08:43:20.134] SEND 127.0.0.1:7777: ["EVENT", { + "pubkey": "b38ce15d3d9874ee710dfabb7ff9801b1e0e20aace6e9a1a05fa7482a04387d1", + "created_at": 1772369000, + "kind": 4, + "tags": [["p", "0000000000000000000000000000000000000000000000000000000000000000"]], + "content": "pool-auth-test message #1 at 1772369000", + "id": "3f05bcb1eecceb8a793f3900ebf4a53e53e0aa8a5c54bb50c180031ec980535e", + "sig": "bcb3d84e75cd7db83b4b9014d25e13ea3de120538e44fde04d0bbb5a86fb240d054033a186c26c9fb8ff62257b126792e48bc9adaa57c3e664eb804c9b00ec15" + }] +[08:43:20.235] RECV 127.0.0.1:7777: ["AUTH", "5b063a862be4208984903f034d4af6e4b3d9cf4f3cab73c14eacdd2a69666326"] +[08:43:20.235] SEND 127.0.0.1:7777: ["AUTH",{"pubkey":"b38ce15d3d9874ee710dfabb7ff9801b1e0e20aace6e9a1a05fa7482a04387d1","created_at":1772369000,"kind":22242,"tags":[["relay","ws://127.0.0.1:7777"],["challenge","5b063a862be4208984903f034d4af6e4b3d9cf4f3cab73c14eacdd2a69666326"]],"content":"","id":"26fe2d66f9b2d387b2a1fe561c2a2fc542c749bb27b35952e2becf0987192773","sig":"e8866c1dfc99d05461c633b56f027da9ff8a0748c7cd010c7ac89930de352da276dc0cd946b2ff770c99cfe612b84c878b91dadbc95d13b0cfbde626e1e887ad"}] +[08:43:20.336] RECV 127.0.0.1:7777: ["NOTICE", "NIP-42 authentication successful"] +[08:43:20.737] SEND 127.0.0.1:7777: ["EVENT", { + "pubkey": "b38ce15d3d9874ee710dfabb7ff9801b1e0e20aace6e9a1a05fa7482a04387d1", + "created_at": 1772369000, + "kind": 4, + "tags": [["p", "0000000000000000000000000000000000000000000000000000000000000000"]], + "content": "pool-auth-test message #2 at 1772369000", + "id": "366a67b33dfe33783940c62511c7fb9be2a4e979a6b064136d482e4f7274019e", + "sig": "9d50286783ef9bbce8c2f1fe1996472f1a83d2f8d67c90496284b7816c0f909b6400bc20f48d6a1ce80be9faec47126f06153414fd4951958b45e2ebba025585" + }] +[08:43:20.838] RECV 127.0.0.1:7777: ["OK", "366a67b33dfe33783940c62511c7fb9be2a4e979a6b064136d482e4f7274019e", true, ""] +[08:43:21.239] SEND 127.0.0.1:7777: ["EVENT", { + "pubkey": "b38ce15d3d9874ee710dfabb7ff9801b1e0e20aace6e9a1a05fa7482a04387d1", + "created_at": 1772369001, + "kind": 4, + "tags": [["p", "0000000000000000000000000000000000000000000000000000000000000000"]], + "content": "pool-auth-test message #3 at 1772369001", + "id": "13bde5c47b56820035e41237f488a51493520cd803c1b8e5f174101ab3086d54", + "sig": "067c75f7b52fd45c29c034ac2a1d9be35aa4d1346e73431efe09437f8ec4d5ad720cc671d85ab438f204ea489ba5cd80ca987fb2738028c5ee766bd8b8325903" + }] +[08:43:21.339] RECV 127.0.0.1:7777: ["OK", "13bde5c47b56820035e41237f488a51493520cd803c1b8e5f174101ab3086d54", true, ""] +[08:43:21.941] SEND 127.0.0.1:7777: ["EVENT", { + "pubkey": "b38ce15d3d9874ee710dfabb7ff9801b1e0e20aace6e9a1a05fa7482a04387d1", + "created_at": 1772369001, + "kind": 4, + "tags": [["p", "0000000000000000000000000000000000000000000000000000000000000000"]], + "content": "pool-auth-test message #4 at 1772369001", + "id": "211025bfe6e0e88c61ffeb993e3ad772a18fb861ccc7c605b1b5a11383314459", + "sig": "7b61f914fbb9fcdc5e9160040830f49ac6f765d1fbd71c4710ba1dbe612996ed5bab075a7051fd7e7ad26757caae06d874cedc4a23a6dbb2ec406708737347a5" + }] +[08:43:22.041] RECV 127.0.0.1:7777: ["OK", "211025bfe6e0e88c61ffeb993e3ad772a18fb861ccc7c605b1b5a11383314459", true, ""] +[08:43:22.643] SEND 127.0.0.1:7777: ["EVENT", { + "pubkey": "b38ce15d3d9874ee710dfabb7ff9801b1e0e20aace6e9a1a05fa7482a04387d1", + "created_at": 1772369002, + "kind": 4, + "tags": [["p", "0000000000000000000000000000000000000000000000000000000000000000"]], + "content": "pool-auth-test message #5 at 1772369002", + "id": "89304aec9b662b1855f59e099dfebc70ab0711ca0ef9c1fa4b2ff53dca75a5f2", + "sig": "43f3f0b998cfaaee4be65106df4e039d9992e71cddde8eca18e7747bded89965f75351688ec806aea1b2f2f5cd77abd81d7bd077b3982fae3b378ac02e92fa7b" + }] +[08:43:22.743] RECV 127.0.0.1:7777: ["OK", "89304aec9b662b1855f59e099dfebc70ab0711ca0ef9c1fa4b2ff53dca75a5f2", true, ""] +[08:43:23.345] SEND 127.0.0.1:7777: ["EVENT", { + "pubkey": "b38ce15d3d9874ee710dfabb7ff9801b1e0e20aace6e9a1a05fa7482a04387d1", + "created_at": 1772369003, + "kind": 4, + "tags": [["p", "0000000000000000000000000000000000000000000000000000000000000000"]], + "content": "pool-auth-test message #6 at 1772369003", + "id": "48885a859de2937b3638a5ce55176d8bba5e257bc1989f923499ccecd5333c7c", + "sig": "082d6316270e03f87f27ab79861ebf9a4defad783c298b4922de0df771eefa74df68eda0706d2b02c88cd4b2a0305c63e8315a71ce3be95aa1b46a7c91833950" + }] +[08:43:23.446] RECV 127.0.0.1:7777: ["OK", "48885a859de2937b3638a5ce55176d8bba5e257bc1989f923499ccecd5333c7c", true, ""] +[08:43:23.847] SEND 127.0.0.1:7777: ["EVENT", { + "pubkey": "b38ce15d3d9874ee710dfabb7ff9801b1e0e20aace6e9a1a05fa7482a04387d1", + "created_at": 1772369003, + "kind": 4, + "tags": [["p", "0000000000000000000000000000000000000000000000000000000000000000"]], + "content": "pool-auth-test message #7 at 1772369003", + "id": "788d2bce8d8ad86eb3ce33d2fe6bb48771ab3ac2eba04c1943c034434d309e0a", + "sig": "f2051d78052aa145ad9feed09e7b2121cf92b7bfb8e6f0a64b0a24b0eb5b3a62e4acff5e4fe24451247242e99685f6aa1dac3b5d9619f09d6b5e3ef1ba466f5c" + }] +[08:43:23.948] RECV 127.0.0.1:7777: ["OK", "788d2bce8d8ad86eb3ce33d2fe6bb48771ab3ac2eba04c1943c034434d309e0a", true, ""] +[08:43:24.550] SEND 127.0.0.1:7777: ["EVENT", { + "pubkey": "b38ce15d3d9874ee710dfabb7ff9801b1e0e20aace6e9a1a05fa7482a04387d1", + "created_at": 1772369004, + "kind": 4, + "tags": [["p", "0000000000000000000000000000000000000000000000000000000000000000"]], + "content": "pool-auth-test message #8 at 1772369004", + "id": "e097ab0e55e2255755aca9f4cb1bd29dd443d616b1f53355a4f0fbed19d4a911", + "sig": "e8c70ab86daeb8368db007d4431ae11042a5399083aa1c7f69cf77640f6fbfe25a0b203a737e221f9a2a9e3a86c7b6a549e99999c876197240319d547df4e324" + }] +[08:43:24.650] RECV 127.0.0.1:7777: ["OK", "e097ab0e55e2255755aca9f4cb1bd29dd443d616b1f53355a4f0fbed19d4a911", true, ""] + +=== NOSTR WebSocket Debug Log Started === +[08:48:11.341] SEND 127.0.0.1:7777: ["EVENT", { + "pubkey": "b38ce15d3d9874ee710dfabb7ff9801b1e0e20aace6e9a1a05fa7482a04387d1", + "created_at": 1772369291, + "kind": 4, + "tags": [["p", "0000000000000000000000000000000000000000000000000000000000000000"]], + "content": "pool-auth-test message #1 at 1772369291", + "id": "1c62334a228667b563e6eb857f143501c3917e1c9229fcec5cdeab5667de3ed2", + "sig": "1eaac0663eedc6cb84082e6b3e0631c698f3d355418e9d346cc8a14af0cdac2a36881b449595338bd3c551635b082feba794ffdabbcfb48b42ebdef16767da1e" + }] +[08:48:11.441] RECV 127.0.0.1:7777: ["AUTH", "1a1a55ddc3f661a8b81fcb8069122fbcd0afed5a64511993731ac17a4947129c"] +[08:48:11.442] SEND 127.0.0.1:7777: ["AUTH",{"pubkey":"b38ce15d3d9874ee710dfabb7ff9801b1e0e20aace6e9a1a05fa7482a04387d1","created_at":1772369291,"kind":22242,"tags":[["relay","ws://127.0.0.1:7777"],["challenge","1a1a55ddc3f661a8b81fcb8069122fbcd0afed5a64511993731ac17a4947129c"]],"content":"","id":"1e515c71de2580aa06dddb0ce3cccf45727469ab89aded2592d1ee57ecc44be0","sig":"94023c5f58a149b40a1ae2077a3b65e1f702c443287c4928ec81a042175e82c93e4bfe549099ff60c09cb955323aad8586976384fbe0321fd83ee46d940f3050"}] +[08:48:11.542] RECV 127.0.0.1:7777: ["NOTICE", "NIP-42 authentication successful"] +[08:48:11.943] SEND 127.0.0.1:7777: ["EVENT", { + "pubkey": "b38ce15d3d9874ee710dfabb7ff9801b1e0e20aace6e9a1a05fa7482a04387d1", + "created_at": 1772369291, + "kind": 4, + "tags": [["p", "0000000000000000000000000000000000000000000000000000000000000000"]], + "content": "pool-auth-test message #2 at 1772369291", + "id": "40ab4111852c7d9e2cf3e8e9dfe26bcfc133e5cef3e95df73f80f4df780ec0f9", + "sig": "0da0ff807eef8c6cf589d41384c0acc5e067a46ba3c59c114d45afc69468660103150da25a3635b9da76dd6a7168374bcc4b117617c43dc91852b9254931c38a" + }] +[08:48:12.043] RECV 127.0.0.1:7777: ["OK", "40ab4111852c7d9e2cf3e8e9dfe26bcfc133e5cef3e95df73f80f4df780ec0f9", true, ""] +[08:48:12.445] SEND 127.0.0.1:7777: ["EVENT", { + "pubkey": "b38ce15d3d9874ee710dfabb7ff9801b1e0e20aace6e9a1a05fa7482a04387d1", + "created_at": 1772369292, + "kind": 4, + "tags": [["p", "0000000000000000000000000000000000000000000000000000000000000000"]], + "content": "pool-auth-test message #3 at 1772369292", + "id": "6fb2a3c20d8d396c40f59e50f02253674cbdef481d9229041a7e52f91812da09", + "sig": "b9aafec899b4c1863658b1554b88f7bbbc24753c41e853bc7717cc4b340ea53aab7ae26d1160ad960391ebe0462b8154016afb7e5090e99b67b3ee1f4fcf624b" + }] +[08:48:12.545] RECV 127.0.0.1:7777: ["OK", "6fb2a3c20d8d396c40f59e50f02253674cbdef481d9229041a7e52f91812da09", true, ""] +[08:48:13.147] SEND 127.0.0.1:7777: ["EVENT", { + "pubkey": "b38ce15d3d9874ee710dfabb7ff9801b1e0e20aace6e9a1a05fa7482a04387d1", + "created_at": 1772369293, + "kind": 4, + "tags": [["p", "0000000000000000000000000000000000000000000000000000000000000000"]], + "content": "pool-auth-test message #4 at 1772369293", + "id": "20b9a8807d174c1b6cf2b5c9a1306b3e747b794fb6238372eae6f552003ace57", + "sig": "12198032d5b7f2072c32b260918cd2a0b34495a1efd4ca079406faf58f9b9de0dd5f0ac48d051cc46819059815fa72bb3877cb26bf2afbfd57cd816fdbb091b5" + }] +[08:48:13.247] RECV 127.0.0.1:7777: ["OK", "20b9a8807d174c1b6cf2b5c9a1306b3e747b794fb6238372eae6f552003ace57", true, ""] +[08:48:13.850] SEND 127.0.0.1:7777: ["EVENT", { + "pubkey": "b38ce15d3d9874ee710dfabb7ff9801b1e0e20aace6e9a1a05fa7482a04387d1", + "created_at": 1772369293, + "kind": 4, + "tags": [["p", "0000000000000000000000000000000000000000000000000000000000000000"]], + "content": "pool-auth-test message #5 at 1772369293", + "id": "d2b0c699f190d0f64c66a3fa5bbe9953aebdb33dea800c7b6510d8326f64502a", + "sig": "ed70bbcf717ef2ef84015eac68ec556ded86829989c519aadcc57beb12eab62e5f065a7e566a8a4c779c559f5e7c021a61d0b92d35bea12922566e1d4e898323" + }] +[08:48:13.950] RECV 127.0.0.1:7777: ["OK", "d2b0c699f190d0f64c66a3fa5bbe9953aebdb33dea800c7b6510d8326f64502a", true, ""] +[08:48:14.552] SEND 127.0.0.1:7777: ["EVENT", { + "pubkey": "b38ce15d3d9874ee710dfabb7ff9801b1e0e20aace6e9a1a05fa7482a04387d1", + "created_at": 1772369294, + "kind": 4, + "tags": [["p", "0000000000000000000000000000000000000000000000000000000000000000"]], + "content": "pool-auth-test message #6 at 1772369294", + "id": "8999d101b39cfdd9c23e77e090b00516ca0659590304be2cee87a482ce099945", + "sig": "42a78a8b8ede4dde81e7e011c90593eb228c00ef3d098b752faab7bd767e7106f77ceb0efaeb1dbc4220e4b03ea2e11d0e93c69b1e1b6c932727fa734ea15e14" + }] +[08:48:14.653] RECV 127.0.0.1:7777: ["OK", "8999d101b39cfdd9c23e77e090b00516ca0659590304be2cee87a482ce099945", true, ""] +[08:48:15.054] SEND 127.0.0.1:7777: ["EVENT", { + "pubkey": "b38ce15d3d9874ee710dfabb7ff9801b1e0e20aace6e9a1a05fa7482a04387d1", + "created_at": 1772369295, + "kind": 4, + "tags": [["p", "0000000000000000000000000000000000000000000000000000000000000000"]], + "content": "pool-auth-test message #7 at 1772369295", + "id": "e34be3b488d277d39f2b5ef2c5a651ef06e8f28da3eed87de006edc7db2843e0", + "sig": "f7f6768eb8f7cf91bb4e31305e93044e3fcea2bfcdd462161da6bf8948a02ae3217e38f1bb4f85f6a8be5753f6aa15369dd7b1b428b87903f95b7525f3bb5024" + }] +[08:48:15.154] RECV 127.0.0.1:7777: ["OK", "e34be3b488d277d39f2b5ef2c5a651ef06e8f28da3eed87de006edc7db2843e0", true, ""] +[08:48:15.756] SEND 127.0.0.1:7777: ["EVENT", { + "pubkey": "b38ce15d3d9874ee710dfabb7ff9801b1e0e20aace6e9a1a05fa7482a04387d1", + "created_at": 1772369295, + "kind": 4, + "tags": [["p", "0000000000000000000000000000000000000000000000000000000000000000"]], + "content": "pool-auth-test message #8 at 1772369295", + "id": "90b5052b8bc56c183510b2c64374aa9c8a83580d83a3416d76ff6173fe30ef22", + "sig": "2e6dd1d5eb3c97b6f9ae4936174110c8d4c72c3a89585f9f1a4d863444caabff9a6705d0cac3bc1257c30ec6209ae409a8c65bf91352fe5d355b60f182cfe3f4" + }] +[08:48:15.856] RECV 127.0.0.1:7777: ["OK", "90b5052b8bc56c183510b2c64374aa9c8a83580d83a3416d76ff6173fe30ef22", true, ""] +[08:48:17.761] SEND 127.0.0.1:7777: ["REQ", "pool_1_1772369297", { + "kinds": [22242], + "authors": ["b38ce15d3d9874ee710dfabb7ff9801b1e0e20aace6e9a1a05fa7482a04387d1"], + "limit": 20 + }] +[08:48:17.763] RECV 127.0.0.1:7777: ["EOSE", "pool_1_1772369297"] +[08:48:17.763] SEND 127.0.0.1:7777: ["CLOSE", "pool_1_1772369297"] + +=== NOSTR WebSocket Debug Log Started === +[08:49:30.820] SEND 127.0.0.1:7777: ["EVENT", { + "pubkey": "b38ce15d3d9874ee710dfabb7ff9801b1e0e20aace6e9a1a05fa7482a04387d1", + "created_at": 1772369370, + "kind": 4, + "tags": [["p", "0000000000000000000000000000000000000000000000000000000000000000"]], + "content": "pool-auth-test message #1 at 1772369370", + "id": "32dd2821b9290cd8b7c448ec1aaef8f74a46f18aed04ac1bec6d37c0a04ed257", + "sig": "1e8de3e16d806f4533c930f064f9e190cf310573acd32c7e5541e2cd2999b7ab4d4299f21b114ef093dd382f01ca6849eba2b1614cc3f7b5b33995e3d4998cd4" + }] +[08:49:30.920] RECV 127.0.0.1:7777: ["AUTH", "17192ffaf320375908c6e268d20e82432ffc9bcb2aa49e15c9531be9e4787376"] +[08:49:30.921] SEND 127.0.0.1:7777: ["AUTH",{"pubkey":"b38ce15d3d9874ee710dfabb7ff9801b1e0e20aace6e9a1a05fa7482a04387d1","created_at":1772369370,"kind":22242,"tags":[["relay","ws://127.0.0.1:7777"],["challenge","17192ffaf320375908c6e268d20e82432ffc9bcb2aa49e15c9531be9e4787376"]],"content":"","id":"a3f4e7cfccc9825253b0191dbcf479676f6f0522d80951eccc50ca117f451307","sig":"0d2bf563986bed1bd4eb7bb4f3364f9c84f9fc222be72eed307b41e6f7496331baf850b2a2ca8fb0be5a06d79a74bb2456bd7f86b0c32b5a86ea0565026fc1d8"}] +[08:49:31.021] RECV 127.0.0.1:7777: ["NOTICE", "NIP-42 authentication successful"] +[08:49:31.422] SEND 127.0.0.1:7777: ["EVENT", { + "pubkey": "b38ce15d3d9874ee710dfabb7ff9801b1e0e20aace6e9a1a05fa7482a04387d1", + "created_at": 1772369371, + "kind": 4, + "tags": [["p", "0000000000000000000000000000000000000000000000000000000000000000"]], + "content": "pool-auth-test message #2 at 1772369371", + "id": "3a3b1e1b63aa60bd61b90f93fbb4c130b91a777889a2598c956e05cba5ebe759", + "sig": "975310b4a5bffbfb53ac09f801b292af2ead2824ba9db0aef9d7cfc42f39fba3ba2bff48b34811d350ee70fb7b85c0819f3ff7b0fa39582fd1e1748942376051" + }] +[08:49:31.523] RECV 127.0.0.1:7777: ["OK", "3a3b1e1b63aa60bd61b90f93fbb4c130b91a777889a2598c956e05cba5ebe759", true, ""] +[08:49:31.925] SEND 127.0.0.1:7777: ["EVENT", { + "pubkey": "b38ce15d3d9874ee710dfabb7ff9801b1e0e20aace6e9a1a05fa7482a04387d1", + "created_at": 1772369371, + "kind": 4, + "tags": [["p", "0000000000000000000000000000000000000000000000000000000000000000"]], + "content": "pool-auth-test message #3 at 1772369371", + "id": "63a4e3a45377010261734b2d10c15cde25c2473527f60f4337583a6077ab76b5", + "sig": "f3034607a1bc049c3fd071f4353f76e8541ec00a07b79c3f7f6afaff295fb3f687729843d0e5646cb0696c91b5b0821f2f99410d3828f64fc8bb11f0b247163f" + }] +[08:49:32.025] RECV 127.0.0.1:7777: ["OK", "63a4e3a45377010261734b2d10c15cde25c2473527f60f4337583a6077ab76b5", true, ""] +[08:49:32.627] SEND 127.0.0.1:7777: ["EVENT", { + "pubkey": "b38ce15d3d9874ee710dfabb7ff9801b1e0e20aace6e9a1a05fa7482a04387d1", + "created_at": 1772369372, + "kind": 4, + "tags": [["p", "0000000000000000000000000000000000000000000000000000000000000000"]], + "content": "pool-auth-test message #4 at 1772369372", + "id": "8aed3ffd3986b2b2acc66f6a8bb6569500c0d8901a363ab9b1a107f5e4f79318", + "sig": "83ed90beb88e0cb72bc394ac9c1f9e32e8dbd5a9e58d27051fc50f39b5ae4dc212d5bb64198e0cdf76da99003e2d5556bb94d444990b9cc8b5c9b7c3139f870e" + }] +[08:49:32.727] RECV 127.0.0.1:7777: ["OK", "8aed3ffd3986b2b2acc66f6a8bb6569500c0d8901a363ab9b1a107f5e4f79318", true, ""] +[08:49:33.329] SEND 127.0.0.1:7777: ["EVENT", { + "pubkey": "b38ce15d3d9874ee710dfabb7ff9801b1e0e20aace6e9a1a05fa7482a04387d1", + "created_at": 1772369373, + "kind": 4, + "tags": [["p", "0000000000000000000000000000000000000000000000000000000000000000"]], + "content": "pool-auth-test message #5 at 1772369373", + "id": "e3239d2346517c8e9fa6c5468b175266aceb23468cf704de7cde4eccadf1f35c", + "sig": "d38a36c90844381d76a21432ebb383a54b30f78291c945683fc92f2760a9067e36095dae9fd55166bf07231c0223dd68b5468f98746dab7f6a59f88155eb4fb8" + }] +[08:49:33.429] RECV 127.0.0.1:7777: ["OK", "e3239d2346517c8e9fa6c5468b175266aceb23468cf704de7cde4eccadf1f35c", true, ""] +[08:49:34.031] SEND 127.0.0.1:7777: ["EVENT", { + "pubkey": "b38ce15d3d9874ee710dfabb7ff9801b1e0e20aace6e9a1a05fa7482a04387d1", + "created_at": 1772369374, + "kind": 4, + "tags": [["p", "0000000000000000000000000000000000000000000000000000000000000000"]], + "content": "pool-auth-test message #6 at 1772369374", + "id": "5ec32bec6aead83ab70c3a0fe636787db4e675c50ef3398d694cdc98b4673a0d", + "sig": "e56b428052fee7c02c045a4b990cdd6208891582ab4379b4f71c1ee6f51903ee3321d37afa364daba9e2162b8f3c44a4dc283c72a40c52fb0da7d3067865460d" + }] +[08:49:34.132] RECV 127.0.0.1:7777: ["OK", "5ec32bec6aead83ab70c3a0fe636787db4e675c50ef3398d694cdc98b4673a0d", true, ""] +[08:49:34.533] SEND 127.0.0.1:7777: ["EVENT", { + "pubkey": "b38ce15d3d9874ee710dfabb7ff9801b1e0e20aace6e9a1a05fa7482a04387d1", + "created_at": 1772369374, + "kind": 4, + "tags": [["p", "0000000000000000000000000000000000000000000000000000000000000000"]], + "content": "pool-auth-test message #7 at 1772369374", + "id": "a4272777a8d4701393eed9343dd5a737e02b7594e2fdb46b2575c306ba57813c", + "sig": "42379e05c7e14ab64a4a686f79e293c27897ae776c2e88c0f4010db74783d6a8c4e5df0cfc70cbb333fd2624b5e1d0481efb4ad2a458edaf2ef389ef3ff07b87" + }] +[08:49:34.634] RECV 127.0.0.1:7777: ["OK", "a4272777a8d4701393eed9343dd5a737e02b7594e2fdb46b2575c306ba57813c", true, ""] +[08:49:35.236] SEND 127.0.0.1:7777: ["EVENT", { + "pubkey": "b38ce15d3d9874ee710dfabb7ff9801b1e0e20aace6e9a1a05fa7482a04387d1", + "created_at": 1772369375, + "kind": 4, + "tags": [["p", "0000000000000000000000000000000000000000000000000000000000000000"]], + "content": "pool-auth-test message #8 at 1772369375", + "id": "da022254d3eeec701c68388ab0be032bca21f0cce91f471f7212447fbaa446f6", + "sig": "d01e0a2f0f464029d6b7e251d1c467359ad4026036fdea8c5b67cc1241d56553634bd50349bc72e6e43f064ba9ee26bf29ca1fb95a169ca96c4118c616246b01" + }] +[08:49:35.336] RECV 127.0.0.1:7777: ["OK", "da022254d3eeec701c68388ab0be032bca21f0cce91f471f7212447fbaa446f6", true, ""] +[08:49:37.240] SEND 127.0.0.1:7777: ["REQ", "pool_1_1772369377", { + "kinds": [22242], + "authors": ["b38ce15d3d9874ee710dfabb7ff9801b1e0e20aace6e9a1a05fa7482a04387d1"], + "limit": 20 + }] +[08:49:37.243] RECV 127.0.0.1:7777: ["EOSE", "pool_1_1772369377"] +[08:49:37.243] SEND 127.0.0.1:7777: ["CLOSE", "pool_1_1772369377"] diff --git a/nostr_core/core_relay_pool.c b/nostr_core/core_relay_pool.c index fe48af37..4f64478d 100644 --- a/nostr_core/core_relay_pool.c +++ b/nostr_core/core_relay_pool.c @@ -26,8 +26,8 @@ // cJSON for JSON handling #include "../cjson/cJSON.h" - - +// NIP-42 Authentication +#include "nip042.h" // ============================================================================= // RELAY POOL MANAGEMENT @@ -105,6 +105,12 @@ typedef struct relay_connection { char last_connection_error[512]; // Last error message from relay connection time_t last_connection_error_time; // When the connection error occurred + // NIP-42 Authentication fields + nostr_auth_state_t auth_state; // Current authentication state + char auth_challenge[NOSTR_NIP42_MAX_CHALLENGE_LENGTH]; // Stored challenge + time_t auth_challenge_time; // When challenge was received + int nip42_enabled; // Whether NIP-42 is enabled for this relay + // Statistics nostr_relay_stats_t stats; } relay_connection_t; @@ -165,6 +171,11 @@ struct nostr_relay_pool { // Pool-wide settings int default_timeout_ms; + + // NIP-42 Authentication configuration + int nip42_enabled; + unsigned char private_key[32]; + int has_private_key; }; // Helper function to generate unique subscription IDs @@ -602,6 +613,38 @@ nostr_relay_pool_t* nostr_relay_pool_create_compat(void) { return nostr_relay_pool_create(NULL); } +int nostr_relay_pool_set_auth(nostr_relay_pool_t* pool, + const unsigned char* private_key, + int enable) { + if (!pool) { + return NOSTR_ERROR_INVALID_INPUT; + } + + pool->nip42_enabled = enable ? 1 : 0; + + if (private_key) { + memcpy(pool->private_key, private_key, sizeof(pool->private_key)); + pool->has_private_key = 1; + } else { + memset(pool->private_key, 0, sizeof(pool->private_key)); + pool->has_private_key = 0; + } + + // Propagate to existing relay connections + for (int i = 0; i < pool->relay_count; i++) { + if (pool->relays[i]) { + pool->relays[i]->nip42_enabled = pool->nip42_enabled; + if (!pool->nip42_enabled) { + pool->relays[i]->auth_state = NOSTR_AUTH_STATE_NONE; + memset(pool->relays[i]->auth_challenge, 0, sizeof(pool->relays[i]->auth_challenge)); + pool->relays[i]->auth_challenge_time = 0; + } + } + } + + return NOSTR_SUCCESS; +} + int nostr_relay_pool_add_relay(nostr_relay_pool_t* pool, const char* relay_url) { if (!pool || !relay_url || pool->relay_count >= NOSTR_POOL_MAX_RELAYS) { return NOSTR_ERROR_INVALID_INPUT; @@ -640,6 +683,12 @@ int nostr_relay_pool_add_relay(nostr_relay_pool_t* pool, const char* relay_url) relay->ping_pending = 0; relay->pending_ping_start_ms = 0.0; + // Initialize NIP-42 authentication fields + relay->auth_state = NOSTR_AUTH_STATE_NONE; + memset(relay->auth_challenge, 0, sizeof(relay->auth_challenge)); + relay->auth_challenge_time = 0; + relay->nip42_enabled = pool->nip42_enabled; + // Initialize statistics memset(&relay->stats, 0, sizeof(relay->stats)); relay->stats.connection_uptime_start = time(NULL); @@ -716,6 +765,11 @@ void nostr_relay_pool_destroy(nostr_relay_pool_t* pool) { } } + if (pool->has_private_key) { + memset(pool->private_key, 0, sizeof(pool->private_key)); + pool->has_private_key = 0; + } + free(pool); } @@ -1067,8 +1121,36 @@ static void process_relay_message(nostr_relay_pool_t* pool, relay_connection_t* // Update relay statistics if (success) { relay->stats.events_published_ok++; + if (relay->auth_state == NOSTR_AUTH_STATE_AUTHENTICATING) { + relay->auth_state = NOSTR_AUTH_STATE_AUTHENTICATED; + } } else { relay->stats.events_published_failed++; + + if (error_message && strstr(error_message, "auth-required") != NULL) { + relay->auth_state = NOSTR_AUTH_STATE_REJECTED; + + // If we already have a challenge, immediately attempt re-authentication + if (relay->nip42_enabled && pool->has_private_key && relay->auth_challenge[0] != '\0') { + cJSON* auth_event = nostr_nip42_create_auth_event( + relay->auth_challenge, + relay->url, + pool->private_key, + 0 + ); + if (auth_event) { + char* auth_message = nostr_nip42_create_auth_message(auth_event); + if (auth_message) { + if (nostr_ws_send_text(relay->ws_client, auth_message) >= 0) { + relay->auth_state = NOSTR_AUTH_STATE_AUTHENTICATING; + } + free(auth_message); + } + cJSON_Delete(auth_event); + } + } + } + // Store error message for legacy API if (error_message) { strncpy(relay->last_publish_error, error_message, @@ -1094,6 +1176,51 @@ static void process_relay_message(nostr_relay_pool_t* pool, relay_connection_t* } } } + } else if (strcmp(msg_type, "AUTH") == 0) { + // Handle AUTH challenge message: ["AUTH", ] + if (relay->nip42_enabled && pool->has_private_key && + cJSON_IsArray(parsed) && cJSON_GetArraySize(parsed) >= 2) { + cJSON* challenge_json = cJSON_GetArrayItem(parsed, 1); + if (cJSON_IsString(challenge_json)) { + const char* challenge = cJSON_GetStringValue(challenge_json); + + strncpy(relay->auth_challenge, challenge, sizeof(relay->auth_challenge) - 1); + relay->auth_challenge[sizeof(relay->auth_challenge) - 1] = '\0'; + relay->auth_challenge_time = time(NULL); + relay->auth_state = NOSTR_AUTH_STATE_CHALLENGE_RECEIVED; + + cJSON* auth_event = nostr_nip42_create_auth_event( + challenge, + relay->url, + pool->private_key, + 0 + ); + if (auth_event) { + char* auth_message = nostr_nip42_create_auth_message(auth_event); + if (auth_message) { + if (nostr_ws_send_text(relay->ws_client, auth_message) >= 0) { + relay->auth_state = NOSTR_AUTH_STATE_AUTHENTICATING; + } + free(auth_message); + } + cJSON_Delete(auth_event); + } + } + } + } else if (strcmp(msg_type, "NOTICE") == 0) { + // Handle NOTICE message: ["NOTICE", ] + if (cJSON_IsArray(parsed) && cJSON_GetArraySize(parsed) >= 2) { + cJSON* notice_msg = cJSON_GetArrayItem(parsed, 1); + if (cJSON_IsString(notice_msg)) { + const char* notice = cJSON_GetStringValue(notice_msg); + if (notice && strstr(notice, "auth-required") != NULL) { + relay->auth_state = NOSTR_AUTH_STATE_REJECTED; + strncpy(relay->last_publish_error, notice, sizeof(relay->last_publish_error) - 1); + relay->last_publish_error[sizeof(relay->last_publish_error) - 1] = '\0'; + relay->last_publish_error_time = time(NULL); + } + } + } } else if (strcmp(msg_type, "PONG") == 0) { // Handle PONG response for connection health monitoring handle_pong_response(relay); @@ -1221,6 +1348,37 @@ cJSON** nostr_relay_pool_query_sync( eose_count++; } } + } else if (msg_type && strcmp(msg_type, "AUTH") == 0) { + // Handle AUTH challenge message: ["AUTH", ] + if (relay->nip42_enabled && pool->has_private_key && + cJSON_IsArray(parsed) && cJSON_GetArraySize(parsed) >= 2) { + cJSON* challenge_json = cJSON_GetArrayItem(parsed, 1); + if (cJSON_IsString(challenge_json)) { + const char* challenge = cJSON_GetStringValue(challenge_json); + + strncpy(relay->auth_challenge, challenge, sizeof(relay->auth_challenge) - 1); + relay->auth_challenge[sizeof(relay->auth_challenge) - 1] = '\0'; + relay->auth_challenge_time = time(NULL); + relay->auth_state = NOSTR_AUTH_STATE_CHALLENGE_RECEIVED; + + cJSON* auth_event = nostr_nip42_create_auth_event( + challenge, + relay->url, + pool->private_key, + 0 + ); + if (auth_event) { + char* auth_message = nostr_nip42_create_auth_message(auth_event); + if (auth_message) { + if (nostr_ws_send_text(relay->ws_client, auth_message) >= 0) { + relay->auth_state = NOSTR_AUTH_STATE_AUTHENTICATING; + } + free(auth_message); + } + cJSON_Delete(auth_event); + } + } + } } if (msg_type) free(msg_type); if (parsed) cJSON_Delete(parsed); diff --git a/nostr_core/nip046.c b/nostr_core/nip046.c index deaca9a4..b42aed13 100644 --- a/nostr_core/nip046.c +++ b/nostr_core/nip046.c @@ -34,7 +34,11 @@ static void safe_copy(char* dst, size_t dst_size, const char* src) { dst[0] = '\0'; return; } - snprintf(dst, dst_size, "%s", src); + + size_t src_len = strlen(src); + size_t copy_len = (src_len < (dst_size - 1U)) ? src_len : (dst_size - 1U); + memcpy(dst, src, copy_len); + dst[copy_len] = '\0'; } static int is_hex_64(const char* s) { diff --git a/nostr_core/nostr_core.h b/nostr_core/nostr_core.h index 8640229a..14cf35ac 100644 --- a/nostr_core/nostr_core.h +++ b/nostr_core/nostr_core.h @@ -2,10 +2,10 @@ #define NOSTR_CORE_H // Version information (auto-updated by increment_and_push.sh) -#define VERSION "v0.4.9" +#define VERSION "v0.4.10" #define VERSION_MAJOR 0 #define VERSION_MINOR 4 -#define VERSION_PATCH 9 +#define VERSION_PATCH 10 /* * NOSTR Core Library - Complete API Reference @@ -249,6 +249,7 @@ nostr_relay_pool_t* nostr_relay_pool_create(nostr_pool_reconnect_config_t* confi nostr_pool_reconnect_config_t* nostr_pool_reconnect_config_default(void); int nostr_relay_pool_add_relay(nostr_relay_pool_t* pool, const char* relay_url); int nostr_relay_pool_remove_relay(nostr_relay_pool_t* pool, const char* relay_url); +int nostr_relay_pool_set_auth(nostr_relay_pool_t* pool, const unsigned char* private_key, int enable); void nostr_relay_pool_destroy(nostr_relay_pool_t* pool); // Subscription management diff --git a/plans/nip42_relay_pool_plan.md b/plans/nip42_relay_pool_plan.md new file mode 100644 index 00000000..de61ae70 --- /dev/null +++ b/plans/nip42_relay_pool_plan.md @@ -0,0 +1,209 @@ +# NIP-42 Authentication Support for core_relay_pool.c + +## Problem Statement + +The relay pool ([`core_relay_pool.c`](nostr_core/core_relay_pool.c)) does not handle NIP-42 authentication. When a relay sends an `AUTH` challenge message, the pool silently ignores it, causing: + +- Subscriptions to fail on auth-required relays (events never delivered) +- Publishes to be rejected with `auth-required:` errors +- No visibility into authentication state per relay + +The synchronous relay functions in [`core_relays.c`](nostr_core/core_relays.c) already have full NIP-42 support (lines 58-63, 211-239, 595-624). The NIP-42 primitives in [`nip042.c`](nostr_core/nip042.c) / [`nip042.h`](nostr_core/nip042.h) are production-ready. This plan wires them into the pool. + +## Architecture Overview + +```mermaid +flowchart TD + A[Relay sends AUTH challenge] --> B[process_relay_message parses AUTH] + B --> C{pool has private_key?} + C -->|No| D[Log warning, mark relay auth_state = CHALLENGE_RECEIVED] + C -->|Yes| E[nostr_nip42_create_auth_event] + E --> F[nostr_nip42_create_auth_message] + F --> G[nostr_ws_send_text to relay] + G --> H[Mark relay auth_state = AUTHENTICATING] + H --> I[Relay sends OK for auth event] + I --> J[Mark relay auth_state = AUTHENTICATED] + + K[Relay sends OK with auth-required] --> L[process_relay_message detects prefix] + L --> M{relay already authenticated?} + M -->|No| N[Trigger auth flow if private_key available] + M -->|Yes| O[Surface as publish failure] + + P[Relay sends NOTICE] --> Q{Contains auth-required?} + Q -->|Yes| R[Log auth-required notice] + Q -->|No| S[Ignore or log] +``` + +## Detailed Changes + +### 1. Add NIP-42 include to core_relay_pool.c + +Add `#include "nip042.h"` alongside the existing includes at the top of [`core_relay_pool.c`](nostr_core/core_relay_pool.c:27). + +### 2. Add auth fields to `relay_connection_t` struct + +Modeled after [`core_relays.c:58-63`](nostr_core/core_relays.c:58), add to the [`relay_connection_t`](nostr_core/core_relay_pool.c:76) struct: + +```c +// NIP-42 Authentication fields +nostr_auth_state_t auth_state; +char auth_challenge[NOSTR_NIP42_MAX_CHALLENGE_LENGTH]; +time_t auth_challenge_time; +int nip42_enabled; +``` + +These go after the existing `last_connection_error_time` field (around line 106). + +### 3. Add private key and NIP-42 config to `nostr_relay_pool` struct + +Add to the [`nostr_relay_pool`](nostr_core/core_relay_pool.c:146) struct: + +```c +// NIP-42 Authentication configuration +int nip42_enabled; +unsigned char private_key[32]; +int has_private_key; +``` + +### 4. Add public API: `nostr_relay_pool_set_auth()` + +Add to [`nostr_core.h`](nostr_core/nostr_core.h) after the existing pool management functions (around line 252): + +```c +// NIP-42 Authentication configuration for relay pool +int nostr_relay_pool_set_auth(nostr_relay_pool_t* pool, + const unsigned char* private_key, + int enable); +``` + +Implement in [`core_relay_pool.c`](nostr_core/core_relay_pool.c): +- Copies the 32-byte private key into the pool struct +- Sets `nip42_enabled` flag +- Propagates `nip42_enabled` to all existing relay connections + +### 5. Handle AUTH in `process_relay_message()` + +Add an `AUTH` handler in [`process_relay_message()`](nostr_core/core_relay_pool.c:912) between the existing `OK` and `PONG` handlers (around line 1097). The logic mirrors [`core_relays.c:211-239`](nostr_core/core_relays.c:211): + +```c +} else if (strcmp(msg_type, "AUTH") == 0) { + // Handle AUTH challenge: ["AUTH", ] + if (pool->nip42_enabled && pool->has_private_key && + cJSON_IsArray(parsed) && cJSON_GetArraySize(parsed) >= 2) { + cJSON* challenge_json = cJSON_GetArrayItem(parsed, 1); + if (cJSON_IsString(challenge_json)) { + const char* challenge = cJSON_GetStringValue(challenge_json); + + // Store challenge + strncpy(relay->auth_challenge, challenge, + sizeof(relay->auth_challenge) - 1); + relay->auth_challenge[sizeof(relay->auth_challenge) - 1] = '\0'; + relay->auth_challenge_time = time(NULL); + relay->auth_state = NOSTR_AUTH_STATE_CHALLENGE_RECEIVED; + + // Create and send auth event + cJSON* auth_event = nostr_nip42_create_auth_event( + challenge, relay->url, pool->private_key, 0); + if (auth_event) { + char* auth_message = nostr_nip42_create_auth_message(auth_event); + if (auth_message) { + if (nostr_ws_send_text(relay->ws_client, auth_message) >= 0) { + relay->auth_state = NOSTR_AUTH_STATE_AUTHENTICATING; + } + free(auth_message); + } + cJSON_Delete(auth_event); + } + } + } +} +``` + +### 6. Handle AUTH in `nostr_relay_pool_query_sync()` inline loop + +The [`nostr_relay_pool_query_sync()`](nostr_core/core_relay_pool.c:1107) function has its own inline message parsing loop (lines 1178-1227) that bypasses `process_relay_message()`. Add AUTH handling there too, similar to the pattern above. + +### 7. Handle NOTICE messages + +Add a `NOTICE` handler in [`process_relay_message()`](nostr_core/core_relay_pool.c:912): + +```c +} else if (strcmp(msg_type, "NOTICE") == 0) { + // Handle NOTICE: ["NOTICE", ] + if (cJSON_IsArray(parsed) && cJSON_GetArraySize(parsed) >= 2) { + cJSON* notice_msg = cJSON_GetArrayItem(parsed, 1); + if (cJSON_IsString(notice_msg)) { + const char* notice = cJSON_GetStringValue(notice_msg); + // Detect auth-required notices + if (strstr(notice, "auth-required") != NULL) { + relay->auth_state = NOSTR_AUTH_STATE_NONE; // Reset for re-auth + } + } + } +} +``` + +### 8. Handle OK auth-required rejections + +Enhance the existing [`OK` handler](nostr_core/core_relay_pool.c:1048) to detect `auth-required:` prefix in error messages. When detected: +- If the pool has a private key and the relay hasn't been authenticated yet, trigger the auth flow +- Surface the rejection through the publish callback with the auth-required message + +### 9. Initialize auth fields in `nostr_relay_pool_add_relay()` + +In [`nostr_relay_pool_add_relay()`](nostr_core/core_relay_pool.c:605), initialize the new auth fields: + +```c +// Initialize NIP-42 authentication fields +relay->auth_state = NOSTR_AUTH_STATE_NONE; +memset(relay->auth_challenge, 0, sizeof(relay->auth_challenge)); +relay->auth_challenge_time = 0; +relay->nip42_enabled = pool->nip42_enabled; +``` + +### 10. Secure cleanup in `nostr_relay_pool_destroy()` + +In [`nostr_relay_pool_destroy()`](nostr_core/core_relay_pool.c:684), zero the private key before freeing: + +```c +// Securely zero private key +if (pool->has_private_key) { + memset(pool->private_key, 0, sizeof(pool->private_key)); + pool->has_private_key = 0; +} +``` + +### 11. Test file + +Create `tests/nip42_pool_test.c` that: +- Creates a pool with auth configured +- Verifies `nostr_relay_pool_set_auth()` stores the key +- Simulates AUTH challenge parsing logic (unit-level, no live relay needed) +- Verifies auth event creation with the pool's private key + +### 12. Documentation updates + +- Update [`POOL_API.md`](POOL_API.md) with `nostr_relay_pool_set_auth()` documentation +- Update [`README.md`](README.md) with a relay pool NIP-42 usage example + +## Files Modified + +| File | Change Type | Description | +|------|-------------|-------------| +| [`nostr_core/core_relay_pool.c`](nostr_core/core_relay_pool.c) | Modified | Add auth fields, AUTH/NOTICE handlers, auth config API | +| [`nostr_core/nostr_core.h`](nostr_core/nostr_core.h) | Modified | Add `nostr_relay_pool_set_auth()` declaration | +| [`tests/nip42_pool_test.c`](tests/nip42_pool_test.c) | New | NIP-42 pool authentication tests | +| [`POOL_API.md`](POOL_API.md) | Modified | Document new auth API | +| [`README.md`](README.md) | Modified | Add pool auth usage example | + +## Key Design Decisions + +1. **Private key stored in pool, not per-relay**: A single identity authenticates to all relays in the pool. This matches the common use case and mirrors how `core_relays.c` accepts a single `private_key` parameter. + +2. **Auth state tracked per-relay**: Each relay connection tracks its own `nostr_auth_state_t` since different relays may challenge at different times. + +3. **Automatic re-auth on reconnect**: When a relay reconnects (via the existing reconnection logic), the auth state resets to `NOSTR_AUTH_STATE_NONE`, allowing the relay to re-challenge. + +4. **No blocking on auth**: The AUTH response is sent asynchronously. If a relay requires auth before accepting subscriptions, the relay will re-send events after authentication succeeds. The pool does not block waiting for auth confirmation. + +5. **Reuse existing NIP-42 primitives**: All crypto and message formatting uses [`nostr_nip42_create_auth_event()`](nostr_core/nip042.c:26) and [`nostr_nip42_create_auth_message()`](nostr_core/nip042.c:84) — no new crypto code needed. diff --git a/tests/async_publish_test b/tests/async_publish_test index 296826cd..ea1fb19a 100755 Binary files a/tests/async_publish_test and b/tests/async_publish_test differ diff --git a/tests/backward_compat_test b/tests/backward_compat_test index 6d7d6821..8bbd0bac 100755 Binary files a/tests/backward_compat_test and b/tests/backward_compat_test differ diff --git a/tests/nip42_pool_test b/tests/nip42_pool_test new file mode 100755 index 00000000..860f9744 Binary files /dev/null and b/tests/nip42_pool_test differ diff --git a/tests/nip42_pool_test.c b/tests/nip42_pool_test.c new file mode 100644 index 00000000..e1d1ec11 --- /dev/null +++ b/tests/nip42_pool_test.c @@ -0,0 +1,309 @@ +#define _DEFAULT_SOURCE +#include "../nostr_core/nostr_core.h" +#include "../cjson/cJSON.h" +#include "../nostr_websocket/nostr_websocket_tls.h" +#include +#include +#include +#include +#include + +static int g_callback_count = 0; +static int g_publish_ok = 0; +static int g_publish_fail = 0; +static int g_auth_required_seen = 0; + +static const char* relay_status_str(nostr_pool_relay_status_t status) { + switch (status) { + case NOSTR_POOL_RELAY_DISCONNECTED: return "DISCONNECTED"; + case NOSTR_POOL_RELAY_CONNECTING: return "CONNECTING"; + case NOSTR_POOL_RELAY_CONNECTED: return "CONNECTED"; + case NOSTR_POOL_RELAY_ERROR: return "ERROR"; + default: return "UNKNOWN"; + } +} + +static double now_ms(void) { + struct timespec ts; + if (clock_gettime(CLOCK_MONOTONIC, &ts) != 0) { + return (double)time(NULL) * 1000.0; + } + return ts.tv_sec * 1000.0 + ts.tv_nsec / 1000000.0; +} + +static void publish_callback(const char* relay_url, + const char* event_id, + int success, + const char* message, + void* user_data) { + (void)user_data; + + g_callback_count++; + if (success) { + g_publish_ok++; + } else { + g_publish_fail++; + if (message && strstr(message, "auth-required") != NULL) { + g_auth_required_seen++; + } + } + + printf("[POOL CALLBACK %d] relay=%s event_id=%s success=%d", g_callback_count, + relay_url ? relay_url : "(null)", event_id ? event_id : "(null)", success); + if (message) { + printf(" message=\"%s\"", message); + } + printf("\n"); +} + +static cJSON* create_kind4_event(const unsigned char* private_key, int sequence) { + if (!private_key) { + return NULL; + } + + char content[256]; + snprintf(content, sizeof(content), "pool-auth-test message #%d at %ld", sequence, (long)time(NULL)); + + cJSON* tags = cJSON_CreateArray(); + if (!tags) { + return NULL; + } + + cJSON* p_tag = cJSON_CreateArray(); + if (!p_tag) { + cJSON_Delete(tags); + return NULL; + } + + cJSON_AddItemToArray(p_tag, cJSON_CreateString("p")); + cJSON_AddItemToArray(p_tag, cJSON_CreateString("0000000000000000000000000000000000000000000000000000000000000000")); + cJSON_AddItemToArray(tags, p_tag); + + cJSON* event = nostr_create_and_sign_event(4, content, tags, private_key, 0); + cJSON_Delete(tags); + return event; +} + +static void run_relay_auth_probe(const char* relay_url) { + printf("\n=== Relay AUTH Probe (raw responses) ===\n"); + + nostr_ws_client_t* probe = nostr_ws_connect(relay_url); + if (!probe) { + printf("[AUTH PROBE] connect failed for %s\n", relay_url); + return; + } + + int auth_seen = 0; + int ok_seen = 0; + int notice_seen = 0; + + double start = now_ms(); + while ((now_ms() - start) < 2500.0) { + char buffer[8192]; + int len = nostr_ws_receive(probe, buffer, sizeof(buffer) - 1, 150); + if (len <= 0) { + continue; + } + + buffer[len] = '\0'; + printf("[AUTH PROBE RAW] %s\n", buffer); + + char* msg_type = NULL; + cJSON* parsed = NULL; + if (nostr_parse_relay_message(buffer, &msg_type, &parsed) == 0) { + if (msg_type) { + printf("[AUTH PROBE PARSED] type=%s\n", msg_type); + } + + if (msg_type && strcmp(msg_type, "AUTH") == 0 && cJSON_IsArray(parsed) && cJSON_GetArraySize(parsed) >= 2) { + cJSON* challenge_json = cJSON_GetArrayItem(parsed, 1); + if (cJSON_IsString(challenge_json)) { + printf("[AUTH PROBE] challenge=%s\n", cJSON_GetStringValue(challenge_json)); + } + auth_seen++; + } else if (msg_type && strcmp(msg_type, "OK") == 0) { + ok_seen++; + } else if (msg_type && strcmp(msg_type, "NOTICE") == 0) { + notice_seen++; + } + } + + if (msg_type) free(msg_type); + if (parsed) cJSON_Delete(parsed); + } + + printf("[AUTH PROBE] summary: AUTH=%d OK=%d NOTICE=%d\n", auth_seen, ok_seen, notice_seen); + nostr_ws_close(probe); +} + +int main(void) { + printf("=== NIP-42 Relay Pool Publish Test (kind-4 over 5s) ===\n"); + + if (nostr_init() != NOSTR_SUCCESS) { + printf("FAILED: nostr_init() failed\n"); + return 1; + } + + const char* relay_url = "ws://127.0.0.1:7777"; + const char* private_key_hex = "91ba716fa9e7ea2fcbad360cf4f8e0d312f73984da63d90f524ad61a6a1e7dbe"; + + unsigned char private_key[32]; + memset(private_key, 0, sizeof(private_key)); + if (nostr_hex_to_bytes(private_key_hex, private_key, sizeof(private_key)) != NOSTR_SUCCESS) { + printf("FAILED: unable to decode private key hex\n"); + nostr_cleanup(); + return 1; + } + + unsigned char public_key[32]; + char pubkey_hex[65]; + if (nostr_ec_public_key_from_private_key(private_key, public_key) != NOSTR_SUCCESS) { + printf("FAILED: unable to derive public key\n"); + memset(private_key, 0, sizeof(private_key)); + nostr_cleanup(); + return 1; + } + nostr_bytes_to_hex(public_key, 32, pubkey_hex); + printf("Using pubkey: %s\n", pubkey_hex); + + nostr_relay_pool_t* pool = nostr_relay_pool_create(NULL); + if (!pool) { + printf("FAILED: unable to create relay pool\n"); + nostr_cleanup(); + return 1; + } + + if (nostr_relay_pool_set_auth(pool, private_key, 1) != NOSTR_SUCCESS) { + printf("FAILED: unable to configure relay pool authentication\n"); + nostr_relay_pool_destroy(pool); + nostr_cleanup(); + return 1; + } + + if (nostr_relay_pool_add_relay(pool, relay_url) != NOSTR_SUCCESS) { + printf("FAILED: unable to add relay %s\n", relay_url); + nostr_relay_pool_destroy(pool); + nostr_cleanup(); + return 1; + } + + run_relay_auth_probe(relay_url); + + const char* relays[] = { relay_url }; + int sent_attempts = 0; + + const double test_duration_ms = 5000.0; + const double publish_interval_ms = 650.0; + double start = now_ms(); + double next_publish = start; + int sequence = 1; + + nostr_pool_relay_status_t last_status = NOSTR_POOL_RELAY_DISCONNECTED; + char last_error_snapshot[512] = {0}; + + while ((now_ms() - start) < test_duration_ms) { + nostr_relay_pool_poll(pool, 100); + + nostr_pool_relay_status_t current_status = nostr_relay_pool_get_relay_status(pool, relay_url); + if (current_status != last_status) { + printf("[POOL STATUS] %s -> %s\n", relay_status_str(last_status), relay_status_str(current_status)); + last_status = current_status; + } + + const char* last_err_live = nostr_relay_pool_get_relay_last_publish_error(pool, relay_url); + if (last_err_live && strcmp(last_err_live, last_error_snapshot) != 0) { + strncpy(last_error_snapshot, last_err_live, sizeof(last_error_snapshot) - 1); + last_error_snapshot[sizeof(last_error_snapshot) - 1] = '\0'; + printf("[POOL ERROR] %s\n", last_error_snapshot); + } + + double t = now_ms(); + if (t >= next_publish) { + cJSON* event = create_kind4_event(private_key, sequence++); + if (!event) { + printf("WARN: failed to create signed event, skipping publish\n"); + next_publish += publish_interval_ms; + usleep(100000); + continue; + } + + int sent = nostr_relay_pool_publish_async(pool, relays, 1, event, publish_callback, NULL); + if (sent > 0) { + sent_attempts++; + } + + printf("[PUBLISH] attempt=%d sent=%d\n", sequence - 1, sent); + + cJSON_Delete(event); + next_publish += publish_interval_ms; + } + + usleep(100000); + } + + // Drain callbacks/messages a bit after last publish + double drain_start = now_ms(); + while ((now_ms() - drain_start) < 1500.0) { + nostr_relay_pool_poll(pool, 100); + usleep(100000); + } + + const char* last_err = nostr_relay_pool_get_relay_last_publish_error(pool, relay_url); + + cJSON* auth_filter = cJSON_CreateObject(); + cJSON* auth_kinds = cJSON_CreateArray(); + cJSON* auth_authors = cJSON_CreateArray(); + cJSON_AddItemToArray(auth_kinds, cJSON_CreateNumber(22242)); + cJSON_AddItemToArray(auth_authors, cJSON_CreateString(pubkey_hex)); + cJSON_AddItemToObject(auth_filter, "kinds", auth_kinds); + cJSON_AddItemToObject(auth_filter, "authors", auth_authors); + cJSON_AddItemToObject(auth_filter, "limit", cJSON_CreateNumber(20)); + + int auth_event_count = 0; + cJSON** auth_events = nostr_relay_pool_query_sync(pool, relays, 1, auth_filter, &auth_event_count, 2500); + cJSON_Delete(auth_filter); + + printf("\n=== AUTH Events Seen On Relay (kind 22242) ===\n"); + printf("count=%d\n", auth_event_count); + for (int i = 0; i < auth_event_count; i++) { + cJSON* id = cJSON_GetObjectItem(auth_events[i], "id"); + cJSON* created_at = cJSON_GetObjectItem(auth_events[i], "created_at"); + printf("[AUTH EVENT %d] id=%s created_at=%lld\n", + i + 1, + (id && cJSON_IsString(id)) ? cJSON_GetStringValue(id) : "(no-id)", + (long long)((created_at && cJSON_IsNumber(created_at)) ? cJSON_GetNumberValue(created_at) : 0)); + } + + printf("\n=== Summary ===\n"); + printf("Relay: %s\n", relay_url); + printf("Publish attempts sent: %d\n", sent_attempts); + printf("Callbacks: %d\n", g_callback_count); + printf("Accepted: %d\n", g_publish_ok); + printf("Rejected/Failed: %d\n", g_publish_fail); + printf("auth-required seen in callbacks: %d\n", g_auth_required_seen); + printf("Last relay publish error: %s\n", last_err ? last_err : "(none)"); + + if (auth_events) { + for (int i = 0; i < auth_event_count; i++) { + if (auth_events[i]) { + cJSON_Delete(auth_events[i]); + } + } + free(auth_events); + } + + nostr_relay_pool_destroy(pool); + memset(private_key, 0, sizeof(private_key)); + memset(public_key, 0, sizeof(public_key)); + nostr_cleanup(); + + // Test is considered successful if the loop executed and we attempted publishes. + // Auth behavior is diagnosed by callback details and summary output. + if (sent_attempts <= 0) { + printf("RESULT: FAIL (no publish attempts were sent)\n"); + return 1; + } + + printf("RESULT: PASS (publish attempts sent; inspect auth behavior in logs above)\n"); + return 0; +} diff --git a/tests/nip46_test b/tests/nip46_test index b922c755..940f3c8f 100755 Binary files a/tests/nip46_test and b/tests/nip46_test differ diff --git a/tests/relay_synchronous_test b/tests/relay_synchronous_test index 6f49fac4..218a6bbb 100755 Binary files a/tests/relay_synchronous_test and b/tests/relay_synchronous_test differ diff --git a/tests/simple_async_test b/tests/simple_async_test index 38ce667b..d49e1c6b 100755 Binary files a/tests/simple_async_test and b/tests/simple_async_test differ diff --git a/tests/sync_relay_test b/tests/sync_relay_test index b6e9120c..9d6546cc 100755 Binary files a/tests/sync_relay_test and b/tests/sync_relay_test differ diff --git a/tests/websocket_debug b/tests/websocket_debug index f4763350..a4efd701 100755 Binary files a/tests/websocket_debug and b/tests/websocket_debug differ