Add NIP-42 auth support to relay pool with verbose auth diagnostics test and docs updates

This commit is contained in:
2026-03-01 08:49:55 -04:00
parent 4dc753cdcc
commit 23e68bdc83
17 changed files with 988 additions and 13 deletions

View File

@@ -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.
- **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.

View File

@@ -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);

View File

@@ -1 +1 @@
0.4.9
0.4.10

266
debug.log
View File

@@ -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"]

View File

@@ -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", <challenge-string>]
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", <message>]
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", <challenge-string>]
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);

View File

@@ -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) {

View File

@@ -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

View File

@@ -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", <challenge-string>]
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", <message>]
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.

Binary file not shown.

Binary file not shown.

BIN
tests/nip42_pool_test Executable file

Binary file not shown.

309
tests/nip42_pool_test.c Normal file
View File

@@ -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 <stdio.h>
#include <stdlib.h>
#include <string.h>
#include <time.h>
#include <unistd.h>
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;
}

Binary file not shown.

Binary file not shown.

Binary file not shown.

Binary file not shown.

Binary file not shown.