Compare commits
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
cdeb569a16 | ||
|
|
2224448d12 | ||
|
|
9bf74e76ce | ||
|
|
d52ac9c106 | ||
|
|
e087a17eb9 | ||
|
|
d7e43328bb | ||
|
|
0af77dffab |
+1
-1
@@ -8,7 +8,7 @@ node_modules/
|
||||
nostr-tools/
|
||||
tiny-AES-c/
|
||||
blossom/
|
||||
|
||||
ndk/
|
||||
|
||||
Trash/debug_tests/
|
||||
node_modules/
|
||||
|
||||
@@ -1,223 +0,0 @@
|
||||
# NOSTR Core Library - Automatic Versioning System
|
||||
|
||||
## Overview
|
||||
|
||||
The NOSTR Core Library now features an automatic version increment system that automatically increments the patch version (e.g., v0.2.0 → v0.2.1) with each build. This ensures consistent versioning and traceability across builds.
|
||||
|
||||
## How It Works
|
||||
|
||||
### Version Format
|
||||
The library uses semantic versioning with the format: `vMAJOR.MINOR.PATCH`
|
||||
|
||||
- **MAJOR**: Incremented for breaking changes (manual)
|
||||
- **MINOR**: Incremented for new features (manual)
|
||||
- **PATCH**: Incremented automatically with each build
|
||||
|
||||
### Automatic Increment Process
|
||||
|
||||
1. **Version Detection**: The build system scans all git tags matching `v*.*.*` pattern
|
||||
2. **Highest Version**: Uses `sort -V` to find the numerically highest version (not chronologically latest)
|
||||
3. **Patch Increment**: Increments the patch number by 1
|
||||
4. **Git Tag Creation**: Creates a new git tag for the incremented version
|
||||
5. **File Generation**: Generates `nostr_core/version.h` and `nostr_core/version.c` with build metadata
|
||||
|
||||
### Generated Files
|
||||
|
||||
The system automatically generates two files during each build:
|
||||
|
||||
#### `nostr_core/version.h`
|
||||
```c
|
||||
#define VERSION_MAJOR 0
|
||||
#define VERSION_MINOR 2
|
||||
#define VERSION_PATCH 1
|
||||
#define VERSION_STRING "0.2.1"
|
||||
#define VERSION_TAG "v0.2.1"
|
||||
|
||||
#define BUILD_DATE "2025-08-09"
|
||||
#define BUILD_TIME "10:42:45"
|
||||
#define BUILD_TIMESTAMP "2025-08-09 10:42:45"
|
||||
|
||||
#define GIT_HASH "ca6b475"
|
||||
#define GIT_BRANCH "master"
|
||||
|
||||
// API functions
|
||||
const char* nostr_core_get_version(void);
|
||||
const char* nostr_core_get_version_full(void);
|
||||
const char* nostr_core_get_build_info(void);
|
||||
```
|
||||
|
||||
#### `nostr_core/version.c`
|
||||
Contains the implementation of the version API functions.
|
||||
|
||||
## Usage
|
||||
|
||||
### Building with Auto-Versioning
|
||||
|
||||
All major build targets automatically increment the version:
|
||||
|
||||
```bash
|
||||
# Build static library (increments version)
|
||||
./build.sh lib
|
||||
|
||||
# Build shared library (increments version)
|
||||
./build.sh shared
|
||||
|
||||
# Build all libraries (increments version)
|
||||
./build.sh all
|
||||
|
||||
# Build examples (increments version)
|
||||
./build.sh examples
|
||||
|
||||
# Install to system (increments version)
|
||||
./build.sh install
|
||||
```
|
||||
|
||||
### Non-Versioned Builds
|
||||
|
||||
Some targets do not increment versions:
|
||||
|
||||
```bash
|
||||
# Clean build artifacts (no version increment)
|
||||
./build.sh clean
|
||||
|
||||
# Run tests (no version increment)
|
||||
./build.sh test
|
||||
```
|
||||
|
||||
### Using Version Information in Code
|
||||
|
||||
```c
|
||||
#include "version.h"
|
||||
|
||||
// Get version string
|
||||
printf("Version: %s\n", nostr_core_get_version());
|
||||
|
||||
// Get full version with timestamp and commit
|
||||
printf("Full: %s\n", nostr_core_get_version_full());
|
||||
|
||||
// Get detailed build information
|
||||
printf("Build: %s\n", nostr_core_get_build_info());
|
||||
|
||||
// Use version macros
|
||||
#if VERSION_MAJOR >= 1
|
||||
// Use new API
|
||||
#else
|
||||
// Use legacy API
|
||||
#endif
|
||||
```
|
||||
|
||||
### Testing Version System
|
||||
|
||||
A version test example is provided:
|
||||
|
||||
```bash
|
||||
# Build and run version test
|
||||
gcc -I. -Inostr_core examples/version_test.c -o examples/version_test ./libnostr_core.a ./secp256k1/.libs/libsecp256k1.a -lm
|
||||
./examples/version_test
|
||||
```
|
||||
|
||||
## Version History Tracking
|
||||
|
||||
### View All Versions
|
||||
```bash
|
||||
# List all version tags
|
||||
git tag --list
|
||||
|
||||
# View version history
|
||||
git log --oneline --decorate --graph
|
||||
```
|
||||
|
||||
### Current Version
|
||||
```bash
|
||||
# Check current version
|
||||
cat VERSION
|
||||
|
||||
# Or check the latest git tag
|
||||
git describe --tags --abbrev=0
|
||||
```
|
||||
|
||||
## Manual Version Management
|
||||
|
||||
### Major/Minor Version Bumps
|
||||
|
||||
For major or minor version changes, manually create the appropriate tag:
|
||||
|
||||
```bash
|
||||
# For a minor version bump (new features)
|
||||
git tag v0.3.0
|
||||
|
||||
# For a major version bump (breaking changes)
|
||||
git tag v1.0.0
|
||||
```
|
||||
|
||||
The next build will automatically increment from the new base version.
|
||||
|
||||
### Resetting Version
|
||||
|
||||
To reset or fix version numbering:
|
||||
|
||||
```bash
|
||||
# Delete incorrect tags (if needed)
|
||||
git tag -d v0.2.1
|
||||
git push origin --delete v0.2.1
|
||||
|
||||
# Create correct base version
|
||||
git tag v0.2.0
|
||||
|
||||
# Next build will create v0.2.1
|
||||
```
|
||||
|
||||
## Integration Notes
|
||||
|
||||
### Makefile Integration
|
||||
- The `version.c` file is automatically included in `LIB_SOURCES`
|
||||
- Version files are compiled and linked with the library
|
||||
- Clean targets remove generated version object files
|
||||
|
||||
### Git Integration
|
||||
- Version files (`version.h`, `version.c`) are excluded from git via `.gitignore`
|
||||
- Only version tags and the `VERSION` file are tracked
|
||||
- Build system works in any git repository with version tags
|
||||
|
||||
### Build System Integration
|
||||
- Version increment is integrated directly into `build.sh`
|
||||
- No separate scripts or external dependencies required
|
||||
- Self-contained and portable across systems
|
||||
|
||||
## Example Output
|
||||
|
||||
When building, you'll see output like:
|
||||
|
||||
```
|
||||
[INFO] Incrementing version...
|
||||
[INFO] Current version: v0.2.0
|
||||
[INFO] New version: v0.2.1
|
||||
[SUCCESS] Created new version tag: v0.2.1
|
||||
[SUCCESS] Generated version.h and version.c
|
||||
[SUCCESS] Updated VERSION file to 0.2.1
|
||||
```
|
||||
|
||||
## Troubleshooting
|
||||
|
||||
### Version Not Incrementing
|
||||
- Ensure you're in a git repository
|
||||
- Check that git tags exist with `git tag --list`
|
||||
- Verify tag format matches `v*.*.*` pattern
|
||||
|
||||
### Tag Already Exists
|
||||
If a tag already exists, the build will continue with the existing version:
|
||||
|
||||
```
|
||||
[WARNING] Tag v0.2.1 already exists - using existing version
|
||||
```
|
||||
|
||||
### Missing Git Information
|
||||
If git is not available, version files will show "unknown" for git hash and branch.
|
||||
|
||||
## Benefits
|
||||
|
||||
1. **Automatic Traceability**: Every build has a unique version
|
||||
2. **Build Metadata**: Includes timestamp, git commit, and branch information
|
||||
3. **API Integration**: Version information accessible via C API
|
||||
4. **Zero Maintenance**: No manual version file editing required
|
||||
5. **Git Integration**: Automatic git tag creation for version history
|
||||
+755
@@ -0,0 +1,755 @@
|
||||
# Relay Pool API Reference
|
||||
|
||||
This document describes the public API for the Nostr Relay Pool implementation in [`core_relay_pool.c`](nostr_core/core_relay_pool.c).
|
||||
|
||||
## Function Summary
|
||||
|
||||
| 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_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 |
|
||||
| [`nostr_relay_pool_query_sync()`](nostr_core/core_relay_pool.c:695) | Synchronous query returning event array |
|
||||
| [`nostr_relay_pool_get_event()`](nostr_core/core_relay_pool.c:825) | Get single most recent event |
|
||||
| [`nostr_relay_pool_publish()`](nostr_core/core_relay_pool.c:866) | Publish event and wait for OK responses |
|
||||
| [`nostr_relay_pool_get_relay_status()`](nostr_core/core_relay_pool.c:944) | Get connection status for a relay |
|
||||
| [`nostr_relay_pool_list_relays()`](nostr_core/core_relay_pool.c:960) | List all relays and their statuses |
|
||||
| [`nostr_relay_pool_get_relay_stats()`](nostr_core/core_relay_pool.c:992) | Get detailed statistics for a relay |
|
||||
| [`nostr_relay_pool_reset_relay_stats()`](nostr_core/core_relay_pool.c:1008) | Reset statistics for a relay |
|
||||
| [`nostr_relay_pool_get_relay_query_latency()`](nostr_core/core_relay_pool.c:1045) | Get average query latency for a relay |
|
||||
|
||||
## Pool Lifecycle
|
||||
|
||||
### Create Pool
|
||||
**Function:** [`nostr_relay_pool_create()`](nostr_core/core_relay_pool.c:219)
|
||||
```c
|
||||
nostr_relay_pool_t* nostr_relay_pool_create(void);
|
||||
```
|
||||
|
||||
**Example:**
|
||||
```c
|
||||
#include "nostr_core.h"
|
||||
|
||||
int main() {
|
||||
// Create a new relay pool
|
||||
nostr_relay_pool_t* pool = nostr_relay_pool_create();
|
||||
if (!pool) {
|
||||
fprintf(stderr, "Failed to create relay pool\n");
|
||||
return -1;
|
||||
}
|
||||
|
||||
// Use the pool...
|
||||
|
||||
// Clean up
|
||||
nostr_relay_pool_destroy(pool);
|
||||
return 0;
|
||||
}
|
||||
```
|
||||
|
||||
### Destroy Pool
|
||||
**Function:** [`nostr_relay_pool_destroy()`](nostr_core/core_relay_pool.c:304)
|
||||
```c
|
||||
void nostr_relay_pool_destroy(nostr_relay_pool_t* pool);
|
||||
```
|
||||
|
||||
**Example:**
|
||||
```c
|
||||
// Properly cleanup a relay pool
|
||||
void cleanup_pool(nostr_relay_pool_t* pool) {
|
||||
if (pool) {
|
||||
// This will close all active subscriptions and relay connections
|
||||
nostr_relay_pool_destroy(pool);
|
||||
pool = NULL;
|
||||
}
|
||||
}
|
||||
```
|
||||
|
||||
## Relay Management
|
||||
|
||||
### Add Relay
|
||||
**Function:** [`nostr_relay_pool_add_relay()`](nostr_core/core_relay_pool.c:229)
|
||||
```c
|
||||
int nostr_relay_pool_add_relay(nostr_relay_pool_t* pool, const char* relay_url);
|
||||
```
|
||||
|
||||
**Example:**
|
||||
```c
|
||||
int setup_relays(nostr_relay_pool_t* pool) {
|
||||
const char* relays[] = {
|
||||
"wss://relay.damus.io",
|
||||
"wss://nos.lol",
|
||||
"wss://relay.nostr.band"
|
||||
};
|
||||
|
||||
for (int i = 0; i < 3; i++) {
|
||||
int result = nostr_relay_pool_add_relay(pool, relays[i]);
|
||||
if (result != NOSTR_SUCCESS) {
|
||||
fprintf(stderr, "Failed to add relay %s: %d\n", relays[i], result);
|
||||
return -1;
|
||||
}
|
||||
printf("Added relay: %s\n", relays[i]);
|
||||
}
|
||||
|
||||
return 0;
|
||||
}
|
||||
```
|
||||
|
||||
### Remove Relay
|
||||
**Function:** [`nostr_relay_pool_remove_relay()`](nostr_core/core_relay_pool.c:273)
|
||||
```c
|
||||
int nostr_relay_pool_remove_relay(nostr_relay_pool_t* pool, const char* relay_url);
|
||||
```
|
||||
|
||||
**Example:**
|
||||
```c
|
||||
int remove_slow_relay(nostr_relay_pool_t* pool) {
|
||||
const char* slow_relay = "wss://slow-relay.example.com";
|
||||
|
||||
int result = nostr_relay_pool_remove_relay(pool, slow_relay);
|
||||
if (result == NOSTR_SUCCESS) {
|
||||
printf("Successfully removed relay: %s\n", slow_relay);
|
||||
} else {
|
||||
printf("Failed to remove relay %s (may not exist)\n", slow_relay);
|
||||
}
|
||||
|
||||
return result;
|
||||
}
|
||||
```
|
||||
|
||||
## Subscriptions (Asynchronous)
|
||||
|
||||
### Subscribe to Events
|
||||
**Function:** [`nostr_relay_pool_subscribe()`](nostr_core/core_relay_pool.c:399)
|
||||
```c
|
||||
nostr_pool_subscription_t* nostr_relay_pool_subscribe(
|
||||
nostr_relay_pool_t* pool,
|
||||
const char** relay_urls,
|
||||
int relay_count,
|
||||
cJSON* filter,
|
||||
void (*on_event)(cJSON* event, const char* relay_url, void* user_data),
|
||||
void (*on_eose)(void* user_data),
|
||||
void* user_data);
|
||||
```
|
||||
|
||||
**Example:**
|
||||
```c
|
||||
#include "cjson/cJSON.h"
|
||||
|
||||
// Event callback - called for each received event
|
||||
void handle_event(cJSON* event, const char* relay_url, void* user_data) {
|
||||
cJSON* content = cJSON_GetObjectItem(event, "content");
|
||||
cJSON* pubkey = cJSON_GetObjectItem(event, "pubkey");
|
||||
|
||||
if (content && pubkey) {
|
||||
printf("Event from %s: %s (by %s)\n",
|
||||
relay_url,
|
||||
cJSON_GetStringValue(content),
|
||||
cJSON_GetStringValue(pubkey));
|
||||
}
|
||||
}
|
||||
|
||||
// EOSE callback - called when all relays finish sending stored events
|
||||
void handle_eose(void* user_data) {
|
||||
printf("All relays finished sending stored events\n");
|
||||
}
|
||||
|
||||
int subscribe_to_notes(nostr_relay_pool_t* pool) {
|
||||
// Create filter for kind 1 (text notes) from last hour
|
||||
cJSON* filter = cJSON_CreateObject();
|
||||
cJSON* kinds = cJSON_CreateArray();
|
||||
cJSON_AddItemToArray(kinds, cJSON_CreateNumber(1));
|
||||
cJSON_AddItemToObject(filter, "kinds", kinds);
|
||||
|
||||
time_t since = time(NULL) - 3600; // Last hour
|
||||
cJSON_AddItemToObject(filter, "since", cJSON_CreateNumber(since));
|
||||
cJSON_AddItemToObject(filter, "limit", cJSON_CreateNumber(50));
|
||||
|
||||
// Subscribe to specific relays
|
||||
const char* relay_urls[] = {
|
||||
"wss://relay.damus.io",
|
||||
"wss://nos.lol"
|
||||
};
|
||||
|
||||
nostr_pool_subscription_t* sub = nostr_relay_pool_subscribe(
|
||||
pool,
|
||||
relay_urls,
|
||||
2,
|
||||
filter,
|
||||
handle_event,
|
||||
handle_eose,
|
||||
NULL // user_data
|
||||
);
|
||||
|
||||
cJSON_Delete(filter); // Pool makes its own copy
|
||||
|
||||
if (!sub) {
|
||||
fprintf(stderr, "Failed to create subscription\n");
|
||||
return -1;
|
||||
}
|
||||
|
||||
// Drive the event loop to receive events
|
||||
printf("Listening for events for 30 seconds...\n");
|
||||
nostr_relay_pool_run(pool, 30000); // 30 seconds
|
||||
|
||||
// Close subscription
|
||||
nostr_pool_subscription_close(sub);
|
||||
return 0;
|
||||
}
|
||||
```
|
||||
|
||||
### Close Subscription
|
||||
**Function:** [`nostr_pool_subscription_close()`](nostr_core/core_relay_pool.c:491)
|
||||
```c
|
||||
int nostr_pool_subscription_close(nostr_pool_subscription_t* subscription);
|
||||
```
|
||||
|
||||
**Example:**
|
||||
```c
|
||||
// Subscription management with cleanup
|
||||
typedef struct {
|
||||
nostr_pool_subscription_t* subscription;
|
||||
int event_count;
|
||||
int should_stop;
|
||||
} subscription_context_t;
|
||||
|
||||
void event_counter(cJSON* event, const char* relay_url, void* user_data) {
|
||||
subscription_context_t* ctx = (subscription_context_t*)user_data;
|
||||
ctx->event_count++;
|
||||
|
||||
printf("Received event #%d from %s\n", ctx->event_count, relay_url);
|
||||
|
||||
// Stop after 10 events
|
||||
if (ctx->event_count >= 10) {
|
||||
ctx->should_stop = 1;
|
||||
}
|
||||
}
|
||||
|
||||
int limited_subscription(nostr_relay_pool_t* pool) {
|
||||
subscription_context_t ctx = {0};
|
||||
|
||||
// Create filter
|
||||
cJSON* filter = cJSON_CreateObject();
|
||||
cJSON* kinds = cJSON_CreateArray();
|
||||
cJSON_AddItemToArray(kinds, cJSON_CreateNumber(1));
|
||||
cJSON_AddItemToObject(filter, "kinds", kinds);
|
||||
|
||||
const char* relay_urls[] = {"wss://relay.damus.io"};
|
||||
|
||||
ctx.subscription = nostr_relay_pool_subscribe(
|
||||
pool, relay_urls, 1, filter, event_counter, NULL, &ctx);
|
||||
|
||||
cJSON_Delete(filter);
|
||||
|
||||
if (!ctx.subscription) {
|
||||
return -1;
|
||||
}
|
||||
|
||||
// Poll until we should stop
|
||||
while (!ctx.should_stop) {
|
||||
int events = nostr_relay_pool_poll(pool, 100);
|
||||
if (events < 0) break;
|
||||
}
|
||||
|
||||
// Clean up
|
||||
int result = nostr_pool_subscription_close(ctx.subscription);
|
||||
printf("Subscription closed with result: %d\n", result);
|
||||
|
||||
return 0;
|
||||
}
|
||||
```
|
||||
|
||||
## Event Loop
|
||||
|
||||
### Run Timed Loop
|
||||
**Function:** [`nostr_relay_pool_run()`](nostr_core/core_relay_pool.c:1192)
|
||||
```c
|
||||
int nostr_relay_pool_run(nostr_relay_pool_t* pool, int timeout_ms);
|
||||
```
|
||||
|
||||
**Example:**
|
||||
```c
|
||||
int run_event_loop(nostr_relay_pool_t* pool) {
|
||||
printf("Starting event loop for 60 seconds...\n");
|
||||
|
||||
// Run for 60 seconds, processing all incoming events
|
||||
int total_events = nostr_relay_pool_run(pool, 60000);
|
||||
|
||||
if (total_events < 0) {
|
||||
fprintf(stderr, "Event loop error\n");
|
||||
return -1;
|
||||
}
|
||||
|
||||
printf("Processed %d events total\n", total_events);
|
||||
return 0;
|
||||
}
|
||||
```
|
||||
|
||||
### Single Poll Iteration
|
||||
**Function:** [`nostr_relay_pool_poll()`](nostr_core/core_relay_pool.c:1232)
|
||||
```c
|
||||
int nostr_relay_pool_poll(nostr_relay_pool_t* pool, int timeout_ms);
|
||||
```
|
||||
|
||||
**Example:**
|
||||
```c
|
||||
// Integration with custom main loop
|
||||
int custom_main_loop(nostr_relay_pool_t* pool) {
|
||||
int running = 1;
|
||||
int total_events = 0;
|
||||
|
||||
while (running) {
|
||||
// Poll for Nostr events (non-blocking with 50ms timeout)
|
||||
int events = nostr_relay_pool_poll(pool, 50);
|
||||
if (events > 0) {
|
||||
total_events += events;
|
||||
printf("Processed %d events this iteration\n", events);
|
||||
}
|
||||
|
||||
// Do other work in your application
|
||||
// handle_ui_events();
|
||||
// process_background_tasks();
|
||||
|
||||
// Check exit condition
|
||||
// running = !should_exit();
|
||||
|
||||
// Simple exit after 100 events for demo
|
||||
if (total_events >= 100) {
|
||||
running = 0;
|
||||
}
|
||||
}
|
||||
|
||||
printf("Main loop finished, processed %d total events\n", total_events);
|
||||
return 0;
|
||||
}
|
||||
```
|
||||
|
||||
## Synchronous Operations
|
||||
|
||||
### Query Multiple Events
|
||||
**Function:** [`nostr_relay_pool_query_sync()`](nostr_core/core_relay_pool.c:695)
|
||||
```c
|
||||
cJSON** nostr_relay_pool_query_sync(
|
||||
nostr_relay_pool_t* pool,
|
||||
const char** relay_urls,
|
||||
int relay_count,
|
||||
cJSON* filter,
|
||||
int* event_count,
|
||||
int timeout_ms);
|
||||
```
|
||||
|
||||
**Example:**
|
||||
```c
|
||||
int query_recent_notes(nostr_relay_pool_t* pool) {
|
||||
// Create filter for recent text notes
|
||||
cJSON* filter = cJSON_CreateObject();
|
||||
cJSON* kinds = cJSON_CreateArray();
|
||||
cJSON_AddItemToArray(kinds, cJSON_CreateNumber(1));
|
||||
cJSON_AddItemToObject(filter, "kinds", kinds);
|
||||
cJSON_AddItemToObject(filter, "limit", cJSON_CreateNumber(20));
|
||||
|
||||
const char* relay_urls[] = {
|
||||
"wss://relay.damus.io",
|
||||
"wss://nos.lol"
|
||||
};
|
||||
|
||||
int event_count = 0;
|
||||
cJSON** events = nostr_relay_pool_query_sync(
|
||||
pool, relay_urls, 2, filter, &event_count, 10000); // 10 second timeout
|
||||
|
||||
cJSON_Delete(filter);
|
||||
|
||||
if (!events) {
|
||||
printf("No events received or query failed\n");
|
||||
return -1;
|
||||
}
|
||||
|
||||
printf("Received %d events:\n", event_count);
|
||||
for (int i = 0; i < event_count; i++) {
|
||||
cJSON* content = cJSON_GetObjectItem(events[i], "content");
|
||||
if (content) {
|
||||
printf(" %d: %s\n", i + 1, cJSON_GetStringValue(content));
|
||||
}
|
||||
|
||||
// Free each event
|
||||
cJSON_Delete(events[i]);
|
||||
}
|
||||
|
||||
// Free the events array
|
||||
free(events);
|
||||
return event_count;
|
||||
}
|
||||
```
|
||||
|
||||
### Get Single Most Recent Event
|
||||
**Function:** [`nostr_relay_pool_get_event()`](nostr_core/core_relay_pool.c:825)
|
||||
```c
|
||||
cJSON* nostr_relay_pool_get_event(
|
||||
nostr_relay_pool_t* pool,
|
||||
const char** relay_urls,
|
||||
int relay_count,
|
||||
cJSON* filter,
|
||||
int timeout_ms);
|
||||
```
|
||||
|
||||
**Example:**
|
||||
```c
|
||||
int get_latest_note_from_pubkey(nostr_relay_pool_t* pool, const char* pubkey_hex) {
|
||||
// Create filter for specific author's notes
|
||||
cJSON* filter = cJSON_CreateObject();
|
||||
cJSON* kinds = cJSON_CreateArray();
|
||||
cJSON_AddItemToArray(kinds, cJSON_CreateNumber(1));
|
||||
cJSON_AddItemToObject(filter, "kinds", kinds);
|
||||
|
||||
cJSON* authors = cJSON_CreateArray();
|
||||
cJSON_AddItemToArray(authors, cJSON_CreateString(pubkey_hex));
|
||||
cJSON_AddItemToObject(filter, "authors", authors);
|
||||
|
||||
cJSON_AddItemToObject(filter, "limit", cJSON_CreateNumber(1));
|
||||
|
||||
const char* relay_urls[] = {"wss://relay.damus.io"};
|
||||
|
||||
cJSON* event = nostr_relay_pool_get_event(
|
||||
pool, relay_urls, 1, filter, 5000); // 5 second timeout
|
||||
|
||||
cJSON_Delete(filter);
|
||||
|
||||
if (!event) {
|
||||
printf("No recent event found for pubkey %s\n", pubkey_hex);
|
||||
return -1;
|
||||
}
|
||||
|
||||
cJSON* content = cJSON_GetObjectItem(event, "content");
|
||||
cJSON* created_at = cJSON_GetObjectItem(event, "created_at");
|
||||
|
||||
if (content && created_at) {
|
||||
printf("Latest note: %s (created at %ld)\n",
|
||||
cJSON_GetStringValue(content),
|
||||
(long)cJSON_GetNumberValue(created_at));
|
||||
}
|
||||
|
||||
cJSON_Delete(event);
|
||||
return 0;
|
||||
}
|
||||
```
|
||||
|
||||
### Publish Event
|
||||
**Function:** [`nostr_relay_pool_publish()`](nostr_core/core_relay_pool.c:866)
|
||||
```c
|
||||
int nostr_relay_pool_publish(
|
||||
nostr_relay_pool_t* pool,
|
||||
const char** relay_urls,
|
||||
int relay_count,
|
||||
cJSON* event);
|
||||
```
|
||||
|
||||
**Example:**
|
||||
```c
|
||||
int publish_text_note(nostr_relay_pool_t* pool, const char* content) {
|
||||
// Create a basic text note event (this is simplified - real implementation
|
||||
// would need proper signing with private key)
|
||||
cJSON* event = cJSON_CreateObject();
|
||||
cJSON_AddItemToObject(event, "kind", cJSON_CreateNumber(1));
|
||||
cJSON_AddItemToObject(event, "content", cJSON_CreateString(content));
|
||||
cJSON_AddItemToObject(event, "created_at", cJSON_CreateNumber(time(NULL)));
|
||||
|
||||
// In real usage, you'd add pubkey, id, sig fields here
|
||||
cJSON_AddItemToObject(event, "pubkey", cJSON_CreateString("your_pubkey_hex"));
|
||||
cJSON_AddItemToObject(event, "id", cJSON_CreateString("event_id_hash"));
|
||||
cJSON_AddItemToObject(event, "sig", cJSON_CreateString("event_signature"));
|
||||
cJSON_AddItemToObject(event, "tags", cJSON_CreateArray());
|
||||
|
||||
const char* relay_urls[] = {
|
||||
"wss://relay.damus.io",
|
||||
"wss://nos.lol",
|
||||
"wss://relay.nostr.band"
|
||||
};
|
||||
|
||||
printf("Publishing note: %s\n", content);
|
||||
|
||||
int success_count = nostr_relay_pool_publish(
|
||||
pool, relay_urls, 3, event);
|
||||
|
||||
cJSON_Delete(event);
|
||||
|
||||
printf("Successfully published to %d out of 3 relays\n", success_count);
|
||||
|
||||
if (success_count == 0) {
|
||||
fprintf(stderr, "Failed to publish to any relay\n");
|
||||
return -1;
|
||||
}
|
||||
|
||||
return success_count;
|
||||
}
|
||||
```
|
||||
|
||||
## Status and Statistics
|
||||
|
||||
### Get Relay Status
|
||||
**Function:** [`nostr_relay_pool_get_relay_status()`](nostr_core/core_relay_pool.c:944)
|
||||
```c
|
||||
nostr_pool_relay_status_t nostr_relay_pool_get_relay_status(
|
||||
nostr_relay_pool_t* pool,
|
||||
const char* relay_url);
|
||||
```
|
||||
|
||||
**Example:**
|
||||
```c
|
||||
void check_relay_status(nostr_relay_pool_t* pool, const char* relay_url) {
|
||||
nostr_pool_relay_status_t status = nostr_relay_pool_get_relay_status(pool, relay_url);
|
||||
|
||||
const char* status_str;
|
||||
switch (status) {
|
||||
case NOSTR_POOL_RELAY_DISCONNECTED:
|
||||
status_str = "DISCONNECTED";
|
||||
break;
|
||||
case NOSTR_POOL_RELAY_CONNECTING:
|
||||
status_str = "CONNECTING";
|
||||
break;
|
||||
case NOSTR_POOL_RELAY_CONNECTED:
|
||||
status_str = "CONNECTED";
|
||||
break;
|
||||
case NOSTR_POOL_RELAY_ERROR:
|
||||
status_str = "ERROR";
|
||||
break;
|
||||
default:
|
||||
status_str = "UNKNOWN";
|
||||
break;
|
||||
}
|
||||
|
||||
printf("Relay %s status: %s\n", relay_url, status_str);
|
||||
}
|
||||
```
|
||||
|
||||
### List All Relays
|
||||
**Function:** [`nostr_relay_pool_list_relays()`](nostr_core/core_relay_pool.c:960)
|
||||
```c
|
||||
int nostr_relay_pool_list_relays(
|
||||
nostr_relay_pool_t* pool,
|
||||
char*** relay_urls,
|
||||
nostr_pool_relay_status_t** statuses);
|
||||
```
|
||||
|
||||
**Example:**
|
||||
```c
|
||||
void print_all_relays(nostr_relay_pool_t* pool) {
|
||||
char** relay_urls = NULL;
|
||||
nostr_pool_relay_status_t* statuses = NULL;
|
||||
|
||||
int count = nostr_relay_pool_list_relays(pool, &relay_urls, &statuses);
|
||||
|
||||
if (count < 0) {
|
||||
printf("Failed to list relays\n");
|
||||
return;
|
||||
}
|
||||
|
||||
if (count == 0) {
|
||||
printf("No relays configured\n");
|
||||
return;
|
||||
}
|
||||
|
||||
printf("Configured relays (%d):\n", count);
|
||||
for (int i = 0; i < count; i++) {
|
||||
const char* status_str = (statuses[i] == NOSTR_POOL_RELAY_CONNECTED) ?
|
||||
"CONNECTED" : "DISCONNECTED";
|
||||
printf(" %s - %s\n", relay_urls[i], status_str);
|
||||
|
||||
// Free the duplicated URL string
|
||||
free(relay_urls[i]);
|
||||
}
|
||||
|
||||
// Free the arrays
|
||||
free(relay_urls);
|
||||
free(statuses);
|
||||
}
|
||||
```
|
||||
|
||||
### Get Relay Statistics
|
||||
**Function:** [`nostr_relay_pool_get_relay_stats()`](nostr_core/core_relay_pool.c:992)
|
||||
```c
|
||||
const nostr_relay_stats_t* nostr_relay_pool_get_relay_stats(
|
||||
nostr_relay_pool_t* pool,
|
||||
const char* relay_url);
|
||||
```
|
||||
|
||||
**Example:**
|
||||
```c
|
||||
void print_relay_stats(nostr_relay_pool_t* pool, const char* relay_url) {
|
||||
const nostr_relay_stats_t* stats = nostr_relay_pool_get_relay_stats(pool, relay_url);
|
||||
|
||||
if (!stats) {
|
||||
printf("No stats available for relay %s\n", relay_url);
|
||||
return;
|
||||
}
|
||||
|
||||
printf("Statistics for %s:\n", relay_url);
|
||||
printf(" Connection attempts: %d\n", stats->connection_attempts);
|
||||
printf(" Connection failures: %d\n", stats->connection_failures);
|
||||
printf(" Events received: %d\n", stats->events_received);
|
||||
printf(" Events published: %d\n", stats->events_published);
|
||||
printf(" Events published OK: %d\n", stats->events_published_ok);
|
||||
printf(" Events published failed: %d\n", stats->events_published_failed);
|
||||
printf(" Query latency avg: %.2f ms\n", stats->query_latency_avg);
|
||||
printf(" Query samples: %d\n", stats->query_samples);
|
||||
printf(" Publish latency avg: %.2f ms\n", stats->publish_latency_avg);
|
||||
printf(" Publish samples: %d\n", stats->publish_samples);
|
||||
|
||||
if (stats->last_event_time > 0) {
|
||||
printf(" Last event: %ld seconds ago\n",
|
||||
time(NULL) - stats->last_event_time);
|
||||
}
|
||||
}
|
||||
```
|
||||
|
||||
### Reset Relay Statistics
|
||||
**Function:** [`nostr_relay_pool_reset_relay_stats()`](nostr_core/core_relay_pool.c:1008)
|
||||
```c
|
||||
int nostr_relay_pool_reset_relay_stats(
|
||||
nostr_relay_pool_t* pool,
|
||||
const char* relay_url);
|
||||
```
|
||||
|
||||
**Example:**
|
||||
```c
|
||||
void reset_stats_for_relay(nostr_relay_pool_t* pool, const char* relay_url) {
|
||||
int result = nostr_relay_pool_reset_relay_stats(pool, relay_url);
|
||||
|
||||
if (result == NOSTR_SUCCESS) {
|
||||
printf("Successfully reset statistics for %s\n", relay_url);
|
||||
} else {
|
||||
printf("Failed to reset statistics for %s\n", relay_url);
|
||||
}
|
||||
}
|
||||
```
|
||||
|
||||
### Get Query Latency
|
||||
**Function:** [`nostr_relay_pool_get_relay_query_latency()`](nostr_core/core_relay_pool.c:1045)
|
||||
```c
|
||||
double nostr_relay_pool_get_relay_query_latency(
|
||||
nostr_relay_pool_t* pool,
|
||||
const char* relay_url);
|
||||
```
|
||||
|
||||
**Example:**
|
||||
```c
|
||||
void check_relay_performance(nostr_relay_pool_t* pool) {
|
||||
const char* relays[] = {
|
||||
"wss://relay.damus.io",
|
||||
"wss://nos.lol",
|
||||
"wss://relay.nostr.band"
|
||||
};
|
||||
|
||||
printf("Relay performance comparison:\n");
|
||||
for (int i = 0; i < 3; i++) {
|
||||
double latency = nostr_relay_pool_get_relay_query_latency(pool, relays[i]);
|
||||
|
||||
if (latency >= 0) {
|
||||
printf(" %s: %.2f ms average query latency\n", relays[i], latency);
|
||||
} else {
|
||||
printf(" %s: No latency data available\n", relays[i]);
|
||||
}
|
||||
}
|
||||
}
|
||||
```
|
||||
|
||||
## Complete Example Application
|
||||
|
||||
```c
|
||||
#include "nostr_core.h"
|
||||
#include "cjson/cJSON.h"
|
||||
#include <stdio.h>
|
||||
#include <stdlib.h>
|
||||
#include <unistd.h>
|
||||
|
||||
// Global context for the example
|
||||
typedef struct {
|
||||
int event_count;
|
||||
int max_events;
|
||||
} app_context_t;
|
||||
|
||||
void on_text_note(cJSON* event, const char* relay_url, void* user_data) {
|
||||
app_context_t* ctx = (app_context_t*)user_data;
|
||||
|
||||
cJSON* content = cJSON_GetObjectItem(event, "content");
|
||||
if (content && cJSON_IsString(content)) {
|
||||
printf("[%s] Note #%d: %s\n",
|
||||
relay_url, ++ctx->event_count, cJSON_GetStringValue(content));
|
||||
}
|
||||
}
|
||||
|
||||
void on_subscription_complete(void* user_data) {
|
||||
printf("All relays finished sending stored events\n");
|
||||
}
|
||||
|
||||
int main() {
|
||||
// Initialize pool
|
||||
nostr_relay_pool_t* pool = nostr_relay_pool_create();
|
||||
if (!pool) {
|
||||
fprintf(stderr, "Failed to create relay pool\n");
|
||||
return 1;
|
||||
}
|
||||
|
||||
// Add relays
|
||||
const char* relays[] = {
|
||||
"wss://relay.damus.io",
|
||||
"wss://nos.lol"
|
||||
};
|
||||
|
||||
for (int i = 0; i < 2; i++) {
|
||||
if (nostr_relay_pool_add_relay(pool, relays[i]) != NOSTR_SUCCESS) {
|
||||
fprintf(stderr, "Failed to add relay: %s\n", relays[i]);
|
||||
}
|
||||
}
|
||||
|
||||
// Create filter for recent text notes
|
||||
cJSON* filter = cJSON_CreateObject();
|
||||
cJSON* kinds = cJSON_CreateArray();
|
||||
cJSON_AddItemToArray(kinds, cJSON_CreateNumber(1));
|
||||
cJSON_AddItemToObject(filter, "kinds", kinds);
|
||||
cJSON_AddItemToObject(filter, "limit", cJSON_CreateNumber(10));
|
||||
|
||||
// Set up context
|
||||
app_context_t ctx = {0, 10};
|
||||
|
||||
// Subscribe
|
||||
nostr_pool_subscription_t* sub = nostr_relay_pool_subscribe(
|
||||
pool, relays, 2, filter, on_text_note, on_subscription_complete, &ctx);
|
||||
|
||||
cJSON_Delete(filter);
|
||||
|
||||
if (!sub) {
|
||||
fprintf(stderr, "Failed to create subscription\n");
|
||||
nostr_relay_pool_destroy(pool);
|
||||
return 1;
|
||||
}
|
||||
|
||||
// Run event loop for 30 seconds
|
||||
printf("Listening for events...\n");
|
||||
nostr_relay_pool_run(pool, 30000);
|
||||
|
||||
// Print final stats
|
||||
for (int i = 0; i < 2; i++) {
|
||||
print_relay_stats(pool, relays[i]);
|
||||
}
|
||||
|
||||
// Cleanup
|
||||
nostr_pool_subscription_close(sub);
|
||||
nostr_relay_pool_destroy(pool);
|
||||
|
||||
printf("Application finished. Received %d events total.\n", ctx.event_count);
|
||||
return 0;
|
||||
}
|
||||
```
|
||||
|
||||
## Notes
|
||||
|
||||
- All functions are **not thread-safe**. Use from a single thread or add external synchronization.
|
||||
- **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.
|
||||
- **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.
|
||||
@@ -302,20 +302,32 @@ publish_result_t* synchronous_publish_event_with_progress(const char** relay_url
|
||||
|
||||
### Relay Pools (Asynchronous)
|
||||
```c
|
||||
// Create and manage relay pool
|
||||
nostr_relay_pool_t* nostr_relay_pool_create(void);
|
||||
// Create and manage relay pool with reconnection
|
||||
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);
|
||||
void nostr_relay_pool_destroy(nostr_relay_pool_t* pool);
|
||||
|
||||
// Subscribe to events
|
||||
// Subscribe to events (with auto-reconnection)
|
||||
nostr_pool_subscription_t* nostr_relay_pool_subscribe(
|
||||
nostr_relay_pool_t* pool, const char** relay_urls, int relay_count, cJSON* filter,
|
||||
void (*on_event)(cJSON* event, const char* relay_url, void* user_data),
|
||||
void (*on_eose)(void* user_data), void* user_data);
|
||||
void (*on_eose)(void* user_data), void* user_data, int close_on_eose);
|
||||
|
||||
// Run event loop
|
||||
// Run event loop (handles reconnection automatically)
|
||||
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);
|
||||
|
||||
// Reconnection configuration
|
||||
typedef struct {
|
||||
int enable_auto_reconnect; // Enable automatic reconnection
|
||||
int max_reconnect_attempts; // Maximum retry attempts
|
||||
int initial_reconnect_delay_ms; // Initial delay between attempts
|
||||
int max_reconnect_delay_ms; // Maximum delay cap
|
||||
int reconnect_backoff_multiplier; // Exponential backoff factor
|
||||
int ping_interval_seconds; // Health check ping interval
|
||||
int pong_timeout_seconds; // Pong response timeout
|
||||
} nostr_pool_reconnect_config_t;
|
||||
```
|
||||
|
||||
### NIP-05 Identifier Verification
|
||||
@@ -370,6 +382,9 @@ The library includes extensive tests:
|
||||
|
||||
# Individual test categories
|
||||
cd tests && make test
|
||||
|
||||
# Interactive relay pool testing
|
||||
cd tests && ./pool_test
|
||||
```
|
||||
|
||||
**Test Categories:**
|
||||
@@ -383,6 +398,7 @@ cd tests && make test
|
||||
- **Relay Communication**: `relay_pool_test`, `sync_test`
|
||||
- **HTTP/WebSocket**: `http_test`, `wss_test`
|
||||
- **Proof of Work**: `test_pow_loop`
|
||||
- **Interactive Pool Testing**: `pool_test` (menu-driven interface with reconnection testing)
|
||||
|
||||
## 🏗️ Integration
|
||||
|
||||
|
||||
@@ -135,6 +135,7 @@ if [ "$HELP" = true ]; then
|
||||
echo " 011 - Relay information document"
|
||||
echo " 013 - Proof of Work"
|
||||
echo " 019 - Bech32 encoding (nsec/npub)"
|
||||
echo " 042 - Authentication of clients to relays"
|
||||
echo " 044 - Encryption (modern)"
|
||||
echo ""
|
||||
echo "Examples:"
|
||||
@@ -184,7 +185,7 @@ print_info "Auto-detecting needed NIPs from your source code..."
|
||||
NEEDED_NIPS=""
|
||||
if [ -n "$FORCE_NIPS" ]; then
|
||||
if [ "$FORCE_NIPS" = "all" ]; then
|
||||
NEEDED_NIPS="001 004 005 006 011 013 019 044"
|
||||
NEEDED_NIPS="001 004 005 006 011 013 019 042 044"
|
||||
print_info "Forced: Building all available NIPs"
|
||||
else
|
||||
# Convert comma-separated list to space-separated with 3-digit format
|
||||
@@ -203,7 +204,7 @@ else
|
||||
# Check for nostr_core.h (includes everything)
|
||||
if grep -q '#include[[:space:]]*["\<]nostr_core\.h["\>]' *.c 2>/dev/null; then
|
||||
print_info "Found #include \"nostr_core.h\" - building all NIPs"
|
||||
NEEDED_NIPS="001 004 005 006 011 013 019 044"
|
||||
NEEDED_NIPS="001 004 005 006 011 013 019 042 044"
|
||||
elif [ -n "$DETECTED" ]; then
|
||||
NEEDED_NIPS="$DETECTED"
|
||||
print_success "Auto-detected NIPs: $(echo $NEEDED_NIPS | tr ' ' ',')"
|
||||
@@ -221,7 +222,7 @@ fi
|
||||
|
||||
# If building tests, include all NIPs to ensure test compatibility
|
||||
if [ "$BUILD_TESTS" = true ] && [ -z "$FORCE_NIPS" ]; then
|
||||
NEEDED_NIPS="001 004 005 006 011 013 019 044"
|
||||
NEEDED_NIPS="001 004 005 006 011 013 019 042 044"
|
||||
print_info "Building tests - including all available NIPs for test compatibility"
|
||||
fi
|
||||
|
||||
@@ -484,13 +485,15 @@ detect_system_curl
|
||||
###########################################################################################
|
||||
|
||||
SOURCES="nostr_core/crypto/nostr_secp256k1.c"
|
||||
SOURCES="$SOURCES nostr_core/crypto/nostr_aes.c"
|
||||
SOURCES="$SOURCES nostr_core/crypto/nostr_aes.c"
|
||||
SOURCES="$SOURCES nostr_core/crypto/nostr_chacha20.c"
|
||||
SOURCES="$SOURCES cjson/cJSON.c"
|
||||
SOURCES="$SOURCES nostr_core/utils.c"
|
||||
SOURCES="$SOURCES nostr_core/nostr_common.c"
|
||||
SOURCES="$SOURCES nostr_core/core_relays.c"
|
||||
SOURCES="$SOURCES nostr_core/core_relay_pool.c"
|
||||
SOURCES="$SOURCES nostr_websocket/nostr_websocket_openssl.c"
|
||||
SOURCES="$SOURCES nostr_core/request_validator.c"
|
||||
|
||||
NIP_DESCRIPTIONS=""
|
||||
|
||||
@@ -506,6 +509,7 @@ for nip in $NEEDED_NIPS; do
|
||||
011) NIP_DESCRIPTIONS="$NIP_DESCRIPTIONS NIP-011(Relay-Info)" ;;
|
||||
013) NIP_DESCRIPTIONS="$NIP_DESCRIPTIONS NIP-013(PoW)" ;;
|
||||
019) NIP_DESCRIPTIONS="$NIP_DESCRIPTIONS NIP-019(Bech32)" ;;
|
||||
042) NIP_DESCRIPTIONS="$NIP_DESCRIPTIONS NIP-042(Auth)" ;;
|
||||
044) NIP_DESCRIPTIONS="$NIP_DESCRIPTIONS NIP-044(Encrypt)" ;;
|
||||
esac
|
||||
else
|
||||
|
||||
-394
@@ -1,394 +0,0 @@
|
||||
#!/bin/bash
|
||||
|
||||
# NOSTR Core Library Build Script
|
||||
# Provides convenient build targets for the standalone library
|
||||
# Automatically increments patch version with each build
|
||||
|
||||
set -e # Exit on any error
|
||||
|
||||
# Colors for output
|
||||
RED='\033[0;31m'
|
||||
GREEN='\033[0;32m'
|
||||
YELLOW='\033[1;33m'
|
||||
BLUE='\033[0;34m'
|
||||
NC='\033[0m' # No Color
|
||||
|
||||
# Function to print colored output
|
||||
print_status() {
|
||||
echo -e "${BLUE}[INFO]${NC} $1"
|
||||
}
|
||||
|
||||
print_success() {
|
||||
echo -e "${GREEN}[SUCCESS]${NC} $1"
|
||||
}
|
||||
|
||||
print_warning() {
|
||||
echo -e "${YELLOW}[WARNING]${NC} $1"
|
||||
}
|
||||
|
||||
print_error() {
|
||||
echo -e "${RED}[ERROR]${NC} $1"
|
||||
}
|
||||
|
||||
# Function to automatically increment version
|
||||
increment_version() {
|
||||
print_status "Incrementing version..."
|
||||
|
||||
# Check if we're in a git repository
|
||||
if ! git rev-parse --git-dir > /dev/null 2>&1; then
|
||||
print_warning "Not in a git repository - skipping version increment"
|
||||
return 0
|
||||
fi
|
||||
|
||||
# Get the highest version tag (not necessarily the most recent chronologically)
|
||||
LATEST_TAG=$(git tag -l 'v*.*.*' | sort -V | tail -n 1 || echo "v0.1.0")
|
||||
if [[ -z "$LATEST_TAG" ]]; then
|
||||
LATEST_TAG="v0.1.0"
|
||||
fi
|
||||
|
||||
# Extract version components (remove 'v' prefix if present)
|
||||
VERSION=${LATEST_TAG#v}
|
||||
|
||||
# Parse major.minor.patch
|
||||
if [[ $VERSION =~ ^([0-9]+)\.([0-9]+)\.([0-9]+)$ ]]; then
|
||||
MAJOR=${BASH_REMATCH[1]}
|
||||
MINOR=${BASH_REMATCH[2]}
|
||||
PATCH=${BASH_REMATCH[3]}
|
||||
else
|
||||
print_error "Invalid version format in tag: $LATEST_TAG"
|
||||
print_error "Expected format: v0.1.0"
|
||||
return 1
|
||||
fi
|
||||
|
||||
# Increment patch version
|
||||
NEW_PATCH=$((PATCH + 1))
|
||||
NEW_VERSION="v${MAJOR}.${MINOR}.${NEW_PATCH}"
|
||||
|
||||
print_status "Current version: $LATEST_TAG"
|
||||
print_status "New version: $NEW_VERSION"
|
||||
|
||||
# Create new git tag
|
||||
if git tag "$NEW_VERSION" 2>/dev/null; then
|
||||
print_success "Created new version tag: $NEW_VERSION"
|
||||
else
|
||||
print_warning "Tag $NEW_VERSION already exists - using existing version"
|
||||
NEW_VERSION=$LATEST_TAG
|
||||
fi
|
||||
|
||||
# Update VERSION file for compatibility
|
||||
echo "${NEW_VERSION#v}" > VERSION
|
||||
print_success "Updated VERSION file to ${NEW_VERSION#v}"
|
||||
}
|
||||
|
||||
# Function to perform git operations after successful build
|
||||
perform_git_operations() {
|
||||
local commit_message="$1"
|
||||
|
||||
if [[ -z "$commit_message" ]]; then
|
||||
return 0 # No commit message provided, skip git operations
|
||||
fi
|
||||
|
||||
print_status "Performing git operations..."
|
||||
|
||||
# Check if we're in a git repository
|
||||
if ! git rev-parse --git-dir > /dev/null 2>&1; then
|
||||
print_warning "Not in a git repository - skipping git operations"
|
||||
return 0
|
||||
fi
|
||||
|
||||
# Check if there are changes to commit
|
||||
if git diff --quiet && git diff --cached --quiet; then
|
||||
print_warning "No changes to commit"
|
||||
return 0
|
||||
fi
|
||||
|
||||
# Add all changes
|
||||
print_status "Adding changes to git..."
|
||||
if ! git add .; then
|
||||
print_error "Failed to add changes to git"
|
||||
return 1
|
||||
fi
|
||||
|
||||
# Commit changes
|
||||
print_status "Committing changes with message: '$commit_message'"
|
||||
if ! git commit -m "$commit_message"; then
|
||||
print_error "Failed to commit changes"
|
||||
return 1
|
||||
fi
|
||||
|
||||
# Push changes
|
||||
print_status "Pushing changes to remote repository..."
|
||||
if ! git push; then
|
||||
print_error "Failed to push changes to remote repository"
|
||||
print_warning "Changes have been committed locally but not pushed"
|
||||
return 1
|
||||
fi
|
||||
|
||||
print_success "Git operations completed successfully!"
|
||||
return 0
|
||||
}
|
||||
|
||||
# Function to show usage
|
||||
show_usage() {
|
||||
echo "NOSTR Core Library Build Script"
|
||||
echo "==============================="
|
||||
echo ""
|
||||
echo "Usage: $0 [target] [-m \"commit message\"]"
|
||||
echo ""
|
||||
echo "Available targets:"
|
||||
echo " clean - Clean all build artifacts"
|
||||
echo " lib - Build static libraries for both x64 and ARM64 (default)"
|
||||
echo " x64 - Build x64 static library only"
|
||||
echo " arm64 - Build ARM64 static library only"
|
||||
echo " all - Build both architectures and examples"
|
||||
echo " examples - Build example programs"
|
||||
echo " test - Run tests"
|
||||
echo " install - Install library to system"
|
||||
echo " uninstall - Remove library from system"
|
||||
echo " help - Show this help message"
|
||||
echo ""
|
||||
echo "Options:"
|
||||
echo " -m \"message\" - Git commit message (triggers automatic git add, commit, push after successful build)"
|
||||
echo ""
|
||||
echo "Examples:"
|
||||
echo " $0 lib -m \"Add new proof-of-work parameters\""
|
||||
echo " $0 x64 -m \"Fix OpenSSL minimal build configuration\""
|
||||
echo " $0 lib # Build without git operations"
|
||||
echo ""
|
||||
echo "Library outputs (both self-contained with secp256k1):"
|
||||
echo " libnostr_core.a - x86_64 static library"
|
||||
echo " libnostr_core_arm64.a - ARM64 static library"
|
||||
echo " examples/* - Example programs"
|
||||
echo ""
|
||||
echo "Both libraries include secp256k1 objects internally."
|
||||
echo "Users only need to link with the library + -lm."
|
||||
}
|
||||
|
||||
# Parse command line arguments
|
||||
TARGET=""
|
||||
COMMIT_MESSAGE=""
|
||||
|
||||
# Parse arguments
|
||||
while [[ $# -gt 0 ]]; do
|
||||
case $1 in
|
||||
-m)
|
||||
COMMIT_MESSAGE="$2"
|
||||
shift 2
|
||||
;;
|
||||
-*)
|
||||
print_error "Unknown option: $1"
|
||||
show_usage
|
||||
exit 1
|
||||
;;
|
||||
*)
|
||||
if [[ -z "$TARGET" ]]; then
|
||||
TARGET="$1"
|
||||
else
|
||||
print_error "Multiple targets specified: $TARGET and $1"
|
||||
show_usage
|
||||
exit 1
|
||||
fi
|
||||
shift
|
||||
;;
|
||||
esac
|
||||
done
|
||||
|
||||
# Set default target if none specified
|
||||
TARGET=${TARGET:-lib}
|
||||
|
||||
case "$TARGET" in
|
||||
clean)
|
||||
print_status "Cleaning build artifacts..."
|
||||
make clean
|
||||
print_success "Clean completed"
|
||||
;;
|
||||
|
||||
lib|library)
|
||||
increment_version
|
||||
print_status "Building both x64 and ARM64 static libraries..."
|
||||
make clean
|
||||
make
|
||||
|
||||
# Check both libraries were built
|
||||
SUCCESS=0
|
||||
if [ -f "libnostr_core.a" ]; then
|
||||
SIZE_X64=$(stat -c%s "libnostr_core.a")
|
||||
print_success "x64 static library built successfully (${SIZE_X64} bytes)"
|
||||
SUCCESS=$((SUCCESS + 1))
|
||||
else
|
||||
print_error "Failed to build x64 static library"
|
||||
fi
|
||||
|
||||
if [ -f "libnostr_core_arm64.a" ]; then
|
||||
SIZE_ARM64=$(stat -c%s "libnostr_core_arm64.a")
|
||||
print_success "ARM64 static library built successfully (${SIZE_ARM64} bytes)"
|
||||
SUCCESS=$((SUCCESS + 1))
|
||||
else
|
||||
print_error "Failed to build ARM64 static library"
|
||||
fi
|
||||
|
||||
if [ $SUCCESS -eq 2 ]; then
|
||||
print_success "Both architectures built successfully!"
|
||||
ls -la libnostr_core*.a
|
||||
perform_git_operations "$COMMIT_MESSAGE"
|
||||
else
|
||||
print_error "Failed to build all libraries"
|
||||
exit 1
|
||||
fi
|
||||
;;
|
||||
|
||||
x64|x64-only)
|
||||
increment_version
|
||||
print_status "Building x64 static library only..."
|
||||
make clean
|
||||
make x64
|
||||
if [ -f "libnostr_core.a" ]; then
|
||||
SIZE=$(stat -c%s "libnostr_core.a")
|
||||
print_success "x64 static library built successfully (${SIZE} bytes)"
|
||||
ls -la libnostr_core.a
|
||||
perform_git_operations "$COMMIT_MESSAGE"
|
||||
else
|
||||
print_error "Failed to build x64 static library"
|
||||
exit 1
|
||||
fi
|
||||
;;
|
||||
|
||||
arm64|arm64-only)
|
||||
increment_version
|
||||
print_status "Building ARM64 static library only..."
|
||||
make clean
|
||||
make arm64
|
||||
if [ -f "libnostr_core_arm64.a" ]; then
|
||||
SIZE=$(stat -c%s "libnostr_core_arm64.a")
|
||||
print_success "ARM64 static library built successfully (${SIZE} bytes)"
|
||||
ls -la libnostr_core_arm64.a
|
||||
perform_git_operations "$COMMIT_MESSAGE"
|
||||
else
|
||||
print_error "Failed to build ARM64 static library"
|
||||
exit 1
|
||||
fi
|
||||
;;
|
||||
|
||||
shared)
|
||||
increment_version
|
||||
print_status "Building shared library..."
|
||||
make clean
|
||||
make libnostr_core.so
|
||||
if [ -f "libnostr_core.so" ]; then
|
||||
SIZE=$(stat -c%s "libnostr_core.so")
|
||||
print_success "Shared library built successfully (${SIZE} bytes)"
|
||||
ls -la libnostr_core.so
|
||||
perform_git_operations "$COMMIT_MESSAGE"
|
||||
else
|
||||
print_error "Failed to build shared library"
|
||||
exit 1
|
||||
fi
|
||||
;;
|
||||
|
||||
all)
|
||||
increment_version
|
||||
print_status "Building all libraries and examples..."
|
||||
make clean
|
||||
make all
|
||||
|
||||
# Check both libraries and examples were built
|
||||
SUCCESS=0
|
||||
if [ -f "libnostr_core.a" ]; then
|
||||
SIZE_X64=$(stat -c%s "libnostr_core.a")
|
||||
print_success "x64 static library built successfully (${SIZE_X64} bytes)"
|
||||
SUCCESS=$((SUCCESS + 1))
|
||||
else
|
||||
print_error "Failed to build x64 static library"
|
||||
fi
|
||||
|
||||
if [ -f "libnostr_core_arm64.a" ]; then
|
||||
SIZE_ARM64=$(stat -c%s "libnostr_core_arm64.a")
|
||||
print_success "ARM64 static library built successfully (${SIZE_ARM64} bytes)"
|
||||
SUCCESS=$((SUCCESS + 1))
|
||||
else
|
||||
print_error "Failed to build ARM64 static library"
|
||||
fi
|
||||
|
||||
if [ $SUCCESS -eq 2 ]; then
|
||||
print_success "All libraries and examples built successfully!"
|
||||
ls -la libnostr_core*.a
|
||||
ls -la examples/
|
||||
perform_git_operations "$COMMIT_MESSAGE"
|
||||
else
|
||||
print_error "Failed to build all components"
|
||||
exit 1
|
||||
fi
|
||||
;;
|
||||
|
||||
examples)
|
||||
increment_version
|
||||
print_status "Building both libraries and examples..."
|
||||
make clean
|
||||
make
|
||||
make examples
|
||||
|
||||
# Verify libraries were built
|
||||
if [ -f "libnostr_core.a" ] && [ -f "libnostr_core_arm64.a" ]; then
|
||||
print_success "Both libraries and examples built successfully"
|
||||
ls -la libnostr_core*.a
|
||||
ls -la examples/
|
||||
perform_git_operations "$COMMIT_MESSAGE"
|
||||
else
|
||||
print_error "Failed to build libraries for examples"
|
||||
exit 1
|
||||
fi
|
||||
;;
|
||||
|
||||
test)
|
||||
print_status "Running tests..."
|
||||
make clean
|
||||
make
|
||||
if make test-crypto 2>/dev/null; then
|
||||
print_success "All tests passed"
|
||||
else
|
||||
print_warning "Running simple test instead..."
|
||||
make test
|
||||
print_success "Basic test completed"
|
||||
fi
|
||||
;;
|
||||
|
||||
tests)
|
||||
print_status "Running tests..."
|
||||
make clean
|
||||
make
|
||||
if make test-crypto 2>/dev/null; then
|
||||
print_success "All tests passed"
|
||||
else
|
||||
print_warning "Running simple test instead..."
|
||||
make test
|
||||
print_success "Basic test completed"
|
||||
fi
|
||||
;;
|
||||
|
||||
install)
|
||||
increment_version
|
||||
print_status "Installing library to system..."
|
||||
make clean
|
||||
make all
|
||||
sudo make install
|
||||
print_success "Library installed to /usr/local"
|
||||
perform_git_operations "$COMMIT_MESSAGE"
|
||||
;;
|
||||
|
||||
uninstall)
|
||||
print_status "Uninstalling library from system..."
|
||||
sudo make uninstall
|
||||
print_success "Library uninstalled"
|
||||
;;
|
||||
|
||||
help|--help|-h)
|
||||
show_usage
|
||||
;;
|
||||
|
||||
*)
|
||||
print_error "Unknown target: $TARGET"
|
||||
echo ""
|
||||
show_usage
|
||||
exit 1
|
||||
;;
|
||||
esac
|
||||
Executable
+150
@@ -0,0 +1,150 @@
|
||||
#!/bin/bash
|
||||
|
||||
# increment_and_push.sh - Version increment and git automation script
|
||||
# Usage: ./increment_and_push.sh "meaningful git comment"
|
||||
|
||||
set -e # Exit on error
|
||||
|
||||
# Color constants
|
||||
RED='\033[31m'
|
||||
GREEN='\033[32m'
|
||||
YELLOW='\033[33m'
|
||||
BLUE='\033[34m'
|
||||
BOLD='\033[1m'
|
||||
RESET='\033[0m'
|
||||
|
||||
# Function to print output with colors
|
||||
print_info() {
|
||||
if [ "$USE_COLORS" = true ]; then
|
||||
echo -e "${BLUE}[INFO]${RESET} $1"
|
||||
else
|
||||
echo "[INFO] $1"
|
||||
fi
|
||||
}
|
||||
|
||||
print_success() {
|
||||
if [ "$USE_COLORS" = true ]; then
|
||||
echo -e "${GREEN}${BOLD}[SUCCESS]${RESET} $1"
|
||||
else
|
||||
echo "[SUCCESS] $1"
|
||||
fi
|
||||
}
|
||||
|
||||
print_warning() {
|
||||
if [ "$USE_COLORS" = true ]; then
|
||||
echo -e "${YELLOW}[WARNING]${RESET} $1"
|
||||
else
|
||||
echo "[WARNING] $1"
|
||||
fi
|
||||
}
|
||||
|
||||
print_error() {
|
||||
if [ "$USE_COLORS" = true ]; then
|
||||
echo -e "${RED}${BOLD}[ERROR]${RESET} $1"
|
||||
else
|
||||
echo "[ERROR] $1"
|
||||
fi
|
||||
}
|
||||
|
||||
# Check if we're in the correct directory
|
||||
CURRENT_DIR=$(basename "$(pwd)")
|
||||
if [ "$CURRENT_DIR" != "nostr_core_lib" ]; then
|
||||
print_error "Script must be run from the nostr_core_lib directory"
|
||||
echo ""
|
||||
echo "Current directory: $CURRENT_DIR"
|
||||
echo "Expected directory: nostr_core_lib"
|
||||
echo ""
|
||||
echo "Please change to the nostr_core_lib directory first."
|
||||
echo ""
|
||||
exit 1
|
||||
fi
|
||||
|
||||
# Check if git repository exists
|
||||
if ! git rev-parse --git-dir > /dev/null 2>&1; then
|
||||
print_error "Not a git repository. Please initialize git first."
|
||||
exit 1
|
||||
fi
|
||||
|
||||
# Check if we have a commit message
|
||||
if [ $# -eq 0 ]; then
|
||||
print_error "Usage: $0 \"meaningful git comment\""
|
||||
echo ""
|
||||
echo "Example: $0 \"Add enhanced subscription functionality\""
|
||||
echo ""
|
||||
exit 1
|
||||
fi
|
||||
|
||||
COMMIT_MESSAGE="$1"
|
||||
|
||||
# Check if nostr_core.h exists
|
||||
if [ ! -f "nostr_core/nostr_core.h" ]; then
|
||||
print_error "nostr_core/nostr_core.h not found"
|
||||
exit 1
|
||||
fi
|
||||
|
||||
print_info "Starting version increment and push process..."
|
||||
|
||||
# Extract current version from nostr_core.h
|
||||
CURRENT_VERSION=$(grep '#define VERSION ' nostr_core/nostr_core.h | cut -d'"' -f2)
|
||||
if [ -z "$CURRENT_VERSION" ]; then
|
||||
print_error "Could not find VERSION define in nostr_core.h"
|
||||
exit 1
|
||||
fi
|
||||
|
||||
# Extract version components
|
||||
VERSION_MAJOR=$(grep '#define VERSION_MAJOR ' nostr_core/nostr_core.h | awk '{print $3}')
|
||||
VERSION_MINOR=$(grep '#define VERSION_MINOR ' nostr_core/nostr_core.h | awk '{print $3}')
|
||||
VERSION_PATCH=$(grep '#define VERSION_PATCH ' nostr_core/nostr_core.h | awk '{print $3}')
|
||||
|
||||
if [ -z "$VERSION_MAJOR" ] || [ -z "$VERSION_MINOR" ] || [ -z "$VERSION_PATCH" ]; then
|
||||
print_error "Could not extract version components from nostr_core.h"
|
||||
exit 1
|
||||
fi
|
||||
|
||||
print_info "Current version: $CURRENT_VERSION (Major: $VERSION_MAJOR, Minor: $VERSION_MINOR, Patch: $VERSION_PATCH)"
|
||||
|
||||
# Increment patch version
|
||||
NEW_PATCH=$((VERSION_PATCH + 1))
|
||||
NEW_VERSION="v$VERSION_MAJOR.$VERSION_MINOR.$NEW_PATCH"
|
||||
|
||||
print_info "New version will be: $NEW_VERSION"
|
||||
|
||||
# Update version in nostr_core.h
|
||||
sed -i "s/#define VERSION .*/#define VERSION \"$NEW_VERSION\"/" nostr_core/nostr_core.h
|
||||
sed -i "s/#define VERSION_PATCH .*/#define VERSION_PATCH $NEW_PATCH/" nostr_core/nostr_core.h
|
||||
|
||||
print_success "Updated version in nostr_core.h"
|
||||
|
||||
# Check if VERSION file exists and update it
|
||||
if [ -f "VERSION" ]; then
|
||||
echo "$VERSION_MAJOR.$VERSION_MINOR.$NEW_PATCH" > VERSION
|
||||
print_success "Updated VERSION file"
|
||||
fi
|
||||
|
||||
# Check git status
|
||||
if ! git diff --quiet; then
|
||||
print_info "Adding changes to git..."
|
||||
git add .
|
||||
|
||||
print_info "Committing changes..."
|
||||
git commit -m "$COMMIT_MESSAGE"
|
||||
|
||||
print_success "Changes committed"
|
||||
else
|
||||
print_warning "No changes to commit"
|
||||
fi
|
||||
|
||||
# Create and push git tag
|
||||
print_info "Creating git tag: $NEW_VERSION"
|
||||
git tag "$NEW_VERSION"
|
||||
|
||||
print_info "Pushing commits and tags..."
|
||||
git push origin main
|
||||
git push origin "$NEW_VERSION"
|
||||
|
||||
print_success "Version $NEW_VERSION successfully released!"
|
||||
print_info "Git commit: $COMMIT_MESSAGE"
|
||||
print_info "Tag: $NEW_VERSION"
|
||||
|
||||
echo ""
|
||||
echo "🎉 Release complete! Version $NEW_VERSION is now live."
|
||||
+458
-232
@@ -65,22 +65,26 @@ typedef struct relay_connection {
|
||||
char* url;
|
||||
nostr_ws_client_t* ws_client;
|
||||
nostr_pool_relay_status_t status;
|
||||
|
||||
|
||||
// Connection management
|
||||
time_t last_ping;
|
||||
time_t connect_time;
|
||||
nostr_relay_pool_t* pool; // Back reference to pool for config access
|
||||
|
||||
// Reconnection management
|
||||
int reconnect_attempts;
|
||||
|
||||
// Ping management for latency measurement
|
||||
time_t last_reconnect_attempt;
|
||||
time_t next_reconnect_time;
|
||||
|
||||
// Connection health monitoring (ping/pong)
|
||||
time_t last_ping_sent;
|
||||
time_t next_ping_time; // last_ping_sent + NOSTR_POOL_PING_INTERVAL
|
||||
time_t last_pong_received;
|
||||
int ping_pending;
|
||||
double pending_ping_start_ms; // High-resolution timestamp for ping measurement
|
||||
int ping_pending; // Flag to track if ping response is expected
|
||||
|
||||
|
||||
// Multi-subscription latency tracking (REQ->first EVENT/EOSE)
|
||||
subscription_timing_t pending_subscriptions[NOSTR_POOL_MAX_PENDING_SUBSCRIPTIONS];
|
||||
int pending_subscription_count;
|
||||
|
||||
|
||||
// Statistics
|
||||
nostr_relay_stats_t stats;
|
||||
} relay_connection_t;
|
||||
@@ -88,34 +92,53 @@ typedef struct relay_connection {
|
||||
struct nostr_pool_subscription {
|
||||
char subscription_id[NOSTR_POOL_SUBSCRIPTION_ID_SIZE];
|
||||
cJSON* filter;
|
||||
|
||||
|
||||
// Relay-specific subscription tracking
|
||||
char** relay_urls;
|
||||
int relay_count;
|
||||
int* eose_received; // Track EOSE from each relay
|
||||
|
||||
|
||||
// Callbacks
|
||||
void (*on_event)(cJSON* event, const char* relay_url, void* user_data);
|
||||
void (*on_eose)(void* user_data);
|
||||
void (*on_eose)(cJSON** events, int event_count, void* user_data);
|
||||
void* user_data;
|
||||
|
||||
|
||||
int closed;
|
||||
int close_on_eose; // Auto-close subscription when all relays send EOSE
|
||||
nostr_relay_pool_t* pool; // Back reference to pool
|
||||
|
||||
// New subscription control parameters
|
||||
int enable_deduplication; // Per-subscription deduplication control
|
||||
nostr_pool_eose_result_mode_t result_mode; // EOSE result selection mode
|
||||
int relay_timeout_seconds; // Timeout for individual relay operations
|
||||
int eose_timeout_seconds; // Timeout for waiting for EOSE completion
|
||||
time_t subscription_start_time; // When subscription was created
|
||||
|
||||
// Event collection for EOSE result modes
|
||||
cJSON** collected_events;
|
||||
int collected_event_count;
|
||||
int collected_events_capacity;
|
||||
|
||||
// Per-relay timeout tracking
|
||||
time_t* relay_last_activity; // Last activity time per relay
|
||||
};
|
||||
|
||||
struct nostr_relay_pool {
|
||||
relay_connection_t* relays[NOSTR_POOL_MAX_RELAYS];
|
||||
int relay_count;
|
||||
|
||||
|
||||
// Reconnection configuration
|
||||
nostr_pool_reconnect_config_t reconnect_config;
|
||||
|
||||
// Event deduplication - simple hash table with linear probing
|
||||
char seen_event_ids[NOSTR_POOL_MAX_SEEN_EVENTS][65]; // 64 hex chars + null terminator
|
||||
int seen_count;
|
||||
int seen_next_index;
|
||||
|
||||
|
||||
// Active subscriptions
|
||||
nostr_pool_subscription_t* subscriptions[NOSTR_POOL_MAX_SUBSCRIPTIONS];
|
||||
int subscription_count;
|
||||
|
||||
|
||||
// Pool-wide settings
|
||||
int default_timeout_ms;
|
||||
};
|
||||
@@ -163,33 +186,211 @@ static int add_subscription_timing(relay_connection_t* relay, const char* subscr
|
||||
// Helper function to find and remove subscription timing
|
||||
static double remove_subscription_timing(relay_connection_t* relay, const char* subscription_id) {
|
||||
if (!relay || !subscription_id) return -1.0;
|
||||
|
||||
|
||||
for (int i = 0; i < relay->pending_subscription_count; i++) {
|
||||
if (relay->pending_subscriptions[i].active &&
|
||||
if (relay->pending_subscriptions[i].active &&
|
||||
strcmp(relay->pending_subscriptions[i].subscription_id, subscription_id) == 0) {
|
||||
|
||||
|
||||
// Calculate latency
|
||||
double current_time_ms = get_current_time_ms();
|
||||
double latency_ms = current_time_ms - relay->pending_subscriptions[i].start_time_ms;
|
||||
|
||||
|
||||
// Mark as inactive and remove by shifting remaining entries
|
||||
relay->pending_subscriptions[i].active = 0;
|
||||
for (int j = i; j < relay->pending_subscription_count - 1; j++) {
|
||||
relay->pending_subscriptions[j] = relay->pending_subscriptions[j + 1];
|
||||
}
|
||||
relay->pending_subscription_count--;
|
||||
|
||||
|
||||
return latency_ms;
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
return -1.0; // Not found
|
||||
}
|
||||
|
||||
// Helper function to check if event ID has been seen
|
||||
// Helper function to ensure relay connection
|
||||
static int ensure_relay_connection(relay_connection_t* relay) {
|
||||
if (!relay) {
|
||||
return -1;
|
||||
}
|
||||
|
||||
if (relay->ws_client && nostr_ws_get_state(relay->ws_client) == NOSTR_WS_CONNECTED) {
|
||||
relay->status = NOSTR_POOL_RELAY_CONNECTED;
|
||||
return 0; // Already connected
|
||||
}
|
||||
|
||||
// Close existing connection if any
|
||||
if (relay->ws_client) {
|
||||
nostr_ws_close(relay->ws_client);
|
||||
relay->ws_client = NULL;
|
||||
}
|
||||
|
||||
// Attempt connection
|
||||
relay->status = NOSTR_POOL_RELAY_CONNECTING;
|
||||
relay->stats.connection_attempts++;
|
||||
|
||||
relay->ws_client = nostr_ws_connect(relay->url);
|
||||
|
||||
if (!relay->ws_client) {
|
||||
relay->status = NOSTR_POOL_RELAY_ERROR;
|
||||
relay->reconnect_attempts++;
|
||||
relay->stats.connection_failures++;
|
||||
return -1;
|
||||
}
|
||||
|
||||
nostr_ws_state_t state = nostr_ws_get_state(relay->ws_client);
|
||||
|
||||
if (state == NOSTR_WS_CONNECTED) {
|
||||
relay->status = NOSTR_POOL_RELAY_CONNECTED;
|
||||
relay->connect_time = time(NULL);
|
||||
relay->reconnect_attempts = 0;
|
||||
|
||||
// Initialize ping/pong monitoring on new connection
|
||||
relay->last_ping_sent = time(NULL);
|
||||
relay->last_pong_received = time(NULL);
|
||||
relay->ping_pending = 0;
|
||||
|
||||
return 0;
|
||||
} else {
|
||||
relay->status = NOSTR_POOL_RELAY_ERROR;
|
||||
relay->reconnect_attempts++;
|
||||
relay->stats.connection_failures++;
|
||||
|
||||
// Close the failed connection
|
||||
nostr_ws_close(relay->ws_client);
|
||||
relay->ws_client = NULL;
|
||||
|
||||
return -1;
|
||||
}
|
||||
}
|
||||
|
||||
// Reconnection helper functions
|
||||
static int should_attempt_reconnect(relay_connection_t* relay) {
|
||||
if (!relay->pool->reconnect_config.enable_auto_reconnect) return 0;
|
||||
if (relay->status == NOSTR_POOL_RELAY_CONNECTED) return 0;
|
||||
if (relay->reconnect_attempts >= relay->pool->reconnect_config.max_reconnect_attempts) return 0;
|
||||
|
||||
time_t now = time(NULL);
|
||||
return (now >= relay->next_reconnect_time);
|
||||
}
|
||||
|
||||
static int calculate_reconnect_delay(relay_connection_t* relay) {
|
||||
int delay = relay->pool->reconnect_config.initial_reconnect_delay_ms;
|
||||
|
||||
// Apply exponential backoff
|
||||
for (int i = 1; i < relay->reconnect_attempts; i++) {
|
||||
delay *= relay->pool->reconnect_config.reconnect_backoff_multiplier;
|
||||
if (delay > relay->pool->reconnect_config.max_reconnect_delay_ms) {
|
||||
delay = relay->pool->reconnect_config.max_reconnect_delay_ms;
|
||||
break;
|
||||
}
|
||||
}
|
||||
|
||||
return delay;
|
||||
}
|
||||
|
||||
static void restore_subscriptions_on_reconnect(relay_connection_t* relay) {
|
||||
// Find subscriptions that should be active on this relay
|
||||
for (int i = 0; i < relay->pool->subscription_count; i++) {
|
||||
nostr_pool_subscription_t* sub = relay->pool->subscriptions[i];
|
||||
if (!sub->closed) {
|
||||
// Check if this subscription should be on this relay
|
||||
for (int j = 0; j < sub->relay_count; j++) {
|
||||
if (strcmp(sub->relay_urls[j], relay->url) == 0) {
|
||||
// Re-send the subscription
|
||||
if (nostr_relay_send_req(relay->ws_client, sub->subscription_id, sub->filter) >= 0) {
|
||||
// Add timing for latency measurement
|
||||
add_subscription_timing(relay, sub->subscription_id);
|
||||
}
|
||||
break;
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
static void attempt_reconnect(relay_connection_t* relay) {
|
||||
relay->status = NOSTR_POOL_RELAY_CONNECTING;
|
||||
relay->last_reconnect_attempt = time(NULL);
|
||||
relay->reconnect_attempts++;
|
||||
|
||||
if (ensure_relay_connection(relay) == 0) {
|
||||
// Success! Reset reconnection state
|
||||
relay->reconnect_attempts = 0;
|
||||
relay->next_reconnect_time = 0;
|
||||
|
||||
// Restore subscriptions on reconnect
|
||||
restore_subscriptions_on_reconnect(relay);
|
||||
} else {
|
||||
// Failed - schedule next attempt with backoff
|
||||
int delay_ms = calculate_reconnect_delay(relay);
|
||||
relay->next_reconnect_time = time(NULL) + (delay_ms / 1000);
|
||||
}
|
||||
}
|
||||
|
||||
// Connection health monitoring (ping/pong)
|
||||
static void check_connection_health(relay_connection_t* relay) {
|
||||
time_t now = time(NULL);
|
||||
|
||||
// Send ping if interval elapsed and ping is enabled
|
||||
if (relay->pool->reconnect_config.ping_interval_seconds > 0 &&
|
||||
now - relay->last_ping_sent >= relay->pool->reconnect_config.ping_interval_seconds &&
|
||||
!relay->ping_pending) {
|
||||
|
||||
if (nostr_ws_ping(relay->ws_client) == 0) {
|
||||
relay->last_ping_sent = now;
|
||||
relay->ping_pending = 1;
|
||||
// Store high-resolution start time for latency measurement
|
||||
relay->pending_ping_start_ms = get_current_time_ms();
|
||||
}
|
||||
}
|
||||
|
||||
// Check for pong timeout
|
||||
if (relay->ping_pending &&
|
||||
now - relay->last_ping_sent > relay->pool->reconnect_config.pong_timeout_seconds) {
|
||||
|
||||
// No pong received - connection is dead
|
||||
relay->status = NOSTR_POOL_RELAY_DISCONNECTED;
|
||||
relay->ping_pending = 0;
|
||||
}
|
||||
}
|
||||
|
||||
static void handle_pong_response(relay_connection_t* relay) {
|
||||
relay->last_pong_received = time(NULL);
|
||||
|
||||
if (relay->ping_pending) {
|
||||
// Calculate ping latency
|
||||
double current_time_ms = get_current_time_ms();
|
||||
double ping_latency = current_time_ms - relay->pending_ping_start_ms;
|
||||
|
||||
// Update ping statistics
|
||||
if (relay->stats.ping_samples == 0) {
|
||||
relay->stats.ping_latency_avg = ping_latency;
|
||||
relay->stats.ping_latency_min = ping_latency;
|
||||
relay->stats.ping_latency_max = ping_latency;
|
||||
} else {
|
||||
relay->stats.ping_latency_avg =
|
||||
(relay->stats.ping_latency_avg * relay->stats.ping_samples + ping_latency) /
|
||||
(relay->stats.ping_samples + 1);
|
||||
|
||||
if (ping_latency < relay->stats.ping_latency_min) {
|
||||
relay->stats.ping_latency_min = ping_latency;
|
||||
}
|
||||
if (ping_latency > relay->stats.ping_latency_max) {
|
||||
relay->stats.ping_latency_max = ping_latency;
|
||||
}
|
||||
}
|
||||
|
||||
relay->stats.ping_latency_current = ping_latency;
|
||||
relay->stats.ping_samples++;
|
||||
relay->ping_pending = 0;
|
||||
}
|
||||
}
|
||||
|
||||
static int is_event_seen(nostr_relay_pool_t* pool, const char* event_id) {
|
||||
if (!pool || !event_id) return 0;
|
||||
|
||||
|
||||
for (int i = 0; i < pool->seen_count; i++) {
|
||||
if (strcmp(pool->seen_event_ids[i], event_id) == 0) {
|
||||
return 1;
|
||||
@@ -201,28 +402,49 @@ static int is_event_seen(nostr_relay_pool_t* pool, const char* event_id) {
|
||||
// Helper function to mark event as seen
|
||||
static void mark_event_seen(nostr_relay_pool_t* pool, const char* event_id) {
|
||||
if (!pool || !event_id) return;
|
||||
|
||||
|
||||
// Don't add duplicates
|
||||
if (is_event_seen(pool, event_id)) return;
|
||||
|
||||
|
||||
// Use circular buffer for seen events
|
||||
strncpy(pool->seen_event_ids[pool->seen_next_index], event_id, 64);
|
||||
pool->seen_event_ids[pool->seen_next_index][64] = '\0';
|
||||
|
||||
|
||||
pool->seen_next_index = (pool->seen_next_index + 1) % NOSTR_POOL_MAX_SEEN_EVENTS;
|
||||
if (pool->seen_count < NOSTR_POOL_MAX_SEEN_EVENTS) {
|
||||
pool->seen_count++;
|
||||
}
|
||||
}
|
||||
|
||||
// Default configuration helper
|
||||
nostr_pool_reconnect_config_t* nostr_pool_reconnect_config_default(void) {
|
||||
static nostr_pool_reconnect_config_t config = {
|
||||
.enable_auto_reconnect = 1,
|
||||
.max_reconnect_attempts = 10,
|
||||
.initial_reconnect_delay_ms = 1000,
|
||||
.max_reconnect_delay_ms = 30000,
|
||||
.reconnect_backoff_multiplier = 2,
|
||||
.ping_interval_seconds = 30,
|
||||
.pong_timeout_seconds = 10
|
||||
};
|
||||
return &config;
|
||||
}
|
||||
|
||||
// Pool management functions
|
||||
nostr_relay_pool_t* nostr_relay_pool_create(void) {
|
||||
nostr_relay_pool_t* nostr_relay_pool_create(nostr_pool_reconnect_config_t* config) {
|
||||
if (!config) {
|
||||
config = nostr_pool_reconnect_config_default();
|
||||
}
|
||||
|
||||
nostr_relay_pool_t* pool = calloc(1, sizeof(nostr_relay_pool_t));
|
||||
if (!pool) {
|
||||
return NULL;
|
||||
}
|
||||
|
||||
|
||||
// Copy configuration
|
||||
pool->reconnect_config = *config;
|
||||
pool->default_timeout_ms = NOSTR_POOL_DEFAULT_TIMEOUT;
|
||||
|
||||
return pool;
|
||||
}
|
||||
|
||||
@@ -250,15 +472,19 @@ int nostr_relay_pool_add_relay(nostr_relay_pool_t* pool, const char* relay_url)
|
||||
|
||||
relay->status = NOSTR_POOL_RELAY_DISCONNECTED;
|
||||
relay->ws_client = NULL;
|
||||
relay->last_ping = 0;
|
||||
relay->connect_time = 0;
|
||||
relay->pool = pool; // Set back reference
|
||||
|
||||
// Initialize reconnection state
|
||||
relay->reconnect_attempts = 0;
|
||||
|
||||
// Initialize ping management
|
||||
relay->last_reconnect_attempt = 0;
|
||||
relay->next_reconnect_time = 0;
|
||||
|
||||
// Initialize ping/pong monitoring
|
||||
relay->last_ping_sent = 0;
|
||||
relay->next_ping_time = 0;
|
||||
relay->pending_ping_start_ms = 0.0;
|
||||
relay->last_pong_received = 0;
|
||||
relay->ping_pending = 0;
|
||||
relay->pending_ping_start_ms = 0.0;
|
||||
|
||||
// Initialize statistics
|
||||
memset(&relay->stats, 0, sizeof(relay->stats));
|
||||
@@ -325,85 +551,21 @@ void nostr_relay_pool_destroy(nostr_relay_pool_t* pool) {
|
||||
free(pool);
|
||||
}
|
||||
|
||||
// Helper function to ensure relay connection
|
||||
static int ensure_relay_connection(relay_connection_t* relay) {
|
||||
if (!relay) {
|
||||
return -1;
|
||||
}
|
||||
|
||||
|
||||
if (relay->ws_client && nostr_ws_get_state(relay->ws_client) == NOSTR_WS_CONNECTED) {
|
||||
relay->status = NOSTR_POOL_RELAY_CONNECTED;
|
||||
return 0; // Already connected
|
||||
}
|
||||
|
||||
// Close existing connection if any
|
||||
if (relay->ws_client) {
|
||||
nostr_ws_close(relay->ws_client);
|
||||
relay->ws_client = NULL;
|
||||
}
|
||||
|
||||
// Attempt connection
|
||||
|
||||
relay->status = NOSTR_POOL_RELAY_CONNECTING;
|
||||
relay->stats.connection_attempts++;
|
||||
|
||||
relay->ws_client = nostr_ws_connect(relay->url);
|
||||
|
||||
if (!relay->ws_client) {
|
||||
relay->status = NOSTR_POOL_RELAY_ERROR;
|
||||
relay->reconnect_attempts++;
|
||||
relay->stats.connection_failures++;
|
||||
return -1;
|
||||
}
|
||||
|
||||
nostr_ws_state_t state = nostr_ws_get_state(relay->ws_client);
|
||||
|
||||
|
||||
if (state == NOSTR_WS_CONNECTED) {
|
||||
|
||||
relay->status = NOSTR_POOL_RELAY_CONNECTED;
|
||||
relay->connect_time = time(NULL);
|
||||
relay->reconnect_attempts = 0;
|
||||
|
||||
// PING FUNCTIONALITY DISABLED - Initial ping on connection establishment
|
||||
/* COMMENTED OUT - PING FUNCTIONALITY DISABLED
|
||||
// Trigger immediate ping on new connection
|
||||
time_t current_time = time(NULL);
|
||||
relay->pending_ping_start_ms = get_current_time_ms();
|
||||
relay->ping_pending = 1;
|
||||
relay->last_ping_sent = current_time;
|
||||
relay->next_ping_time = current_time + NOSTR_POOL_PING_INTERVAL;
|
||||
|
||||
if (nostr_ws_send_ping(relay->ws_client, "ping", 4) < 0) {
|
||||
relay->ping_pending = 0;
|
||||
}
|
||||
*/
|
||||
|
||||
return 0;
|
||||
} else {
|
||||
|
||||
relay->status = NOSTR_POOL_RELAY_ERROR;
|
||||
relay->reconnect_attempts++;
|
||||
relay->stats.connection_failures++;
|
||||
|
||||
// Close the failed connection
|
||||
nostr_ws_close(relay->ws_client);
|
||||
relay->ws_client = NULL;
|
||||
|
||||
return -1;
|
||||
}
|
||||
}
|
||||
|
||||
// Subscription management
|
||||
nostr_pool_subscription_t* nostr_relay_pool_subscribe(
|
||||
nostr_relay_pool_t* pool,
|
||||
const char** relay_urls,
|
||||
const char** relay_urls,
|
||||
int relay_count,
|
||||
cJSON* filter,
|
||||
void (*on_event)(cJSON* event, const char* relay_url, void* user_data),
|
||||
void (*on_eose)(void* user_data),
|
||||
void* user_data) {
|
||||
void (*on_eose)(cJSON** events, int event_count, void* user_data),
|
||||
void* user_data,
|
||||
int close_on_eose,
|
||||
int enable_deduplication,
|
||||
nostr_pool_eose_result_mode_t result_mode,
|
||||
int relay_timeout_seconds,
|
||||
int eose_timeout_seconds) {
|
||||
|
||||
if (!pool || !relay_urls || relay_count <= 0 || !filter ||
|
||||
pool->subscription_count >= NOSTR_POOL_MAX_SUBSCRIPTIONS) {
|
||||
@@ -458,7 +620,57 @@ nostr_pool_subscription_t* nostr_relay_pool_subscribe(
|
||||
sub->on_eose = on_eose;
|
||||
sub->user_data = user_data;
|
||||
sub->closed = 0;
|
||||
sub->close_on_eose = close_on_eose;
|
||||
sub->pool = pool;
|
||||
|
||||
// Set new subscription control parameters
|
||||
sub->enable_deduplication = enable_deduplication;
|
||||
sub->result_mode = result_mode;
|
||||
sub->relay_timeout_seconds = relay_timeout_seconds;
|
||||
sub->eose_timeout_seconds = eose_timeout_seconds;
|
||||
sub->subscription_start_time = time(NULL);
|
||||
|
||||
// Initialize event collection arrays (only for EOSE result modes)
|
||||
if (result_mode != NOSTR_POOL_EOSE_FIRST) {
|
||||
sub->collected_events_capacity = 10; // Initial capacity
|
||||
sub->collected_events = calloc(sub->collected_events_capacity, sizeof(cJSON*));
|
||||
if (!sub->collected_events) {
|
||||
// Cleanup on failure
|
||||
cJSON_Delete(sub->filter);
|
||||
for (int j = 0; j < relay_count; j++) {
|
||||
free(sub->relay_urls[j]);
|
||||
}
|
||||
free(sub->relay_urls);
|
||||
free(sub->eose_received);
|
||||
free(sub);
|
||||
return NULL;
|
||||
}
|
||||
sub->collected_event_count = 0;
|
||||
} else {
|
||||
sub->collected_events = NULL;
|
||||
sub->collected_event_count = 0;
|
||||
sub->collected_events_capacity = 0;
|
||||
}
|
||||
|
||||
// Initialize per-relay activity tracking
|
||||
sub->relay_last_activity = calloc(relay_count, sizeof(time_t));
|
||||
if (!sub->relay_last_activity) {
|
||||
// Cleanup on failure
|
||||
if (sub->collected_events) free(sub->collected_events);
|
||||
cJSON_Delete(sub->filter);
|
||||
for (int j = 0; j < relay_count; j++) {
|
||||
free(sub->relay_urls[j]);
|
||||
}
|
||||
free(sub->relay_urls);
|
||||
free(sub->eose_received);
|
||||
free(sub);
|
||||
return NULL;
|
||||
}
|
||||
// Initialize all relay activity times to current time
|
||||
time_t now = time(NULL);
|
||||
for (int i = 0; i < relay_count; i++) {
|
||||
sub->relay_last_activity[i] = now;
|
||||
}
|
||||
|
||||
// Add to pool
|
||||
pool->subscriptions[pool->subscription_count++] = sub;
|
||||
@@ -556,43 +768,56 @@ static void process_relay_message(nostr_relay_pool_t* pool, relay_connection_t*
|
||||
if (event_id_json && cJSON_IsString(event_id_json)) {
|
||||
const char* event_id = cJSON_GetStringValue(event_id_json);
|
||||
|
||||
// Check for duplicate
|
||||
if (!is_event_seen(pool, event_id)) {
|
||||
mark_event_seen(pool, event_id);
|
||||
relay->stats.events_received++;
|
||||
|
||||
// Measure query latency (first event response)
|
||||
double latency_ms = remove_subscription_timing(relay, subscription_id);
|
||||
if (latency_ms > 0.0) {
|
||||
// Update query latency statistics
|
||||
if (relay->stats.query_samples == 0) {
|
||||
relay->stats.query_latency_avg = latency_ms;
|
||||
relay->stats.query_latency_min = latency_ms;
|
||||
relay->stats.query_latency_max = latency_ms;
|
||||
} else {
|
||||
relay->stats.query_latency_avg =
|
||||
(relay->stats.query_latency_avg * relay->stats.query_samples + latency_ms) /
|
||||
(relay->stats.query_samples + 1);
|
||||
|
||||
if (latency_ms < relay->stats.query_latency_min) {
|
||||
relay->stats.query_latency_min = latency_ms;
|
||||
}
|
||||
if (latency_ms > relay->stats.query_latency_max) {
|
||||
relay->stats.query_latency_max = latency_ms;
|
||||
}
|
||||
}
|
||||
relay->stats.query_samples++;
|
||||
// Find subscription first
|
||||
nostr_pool_subscription_t* sub = NULL;
|
||||
for (int i = 0; i < pool->subscription_count; i++) {
|
||||
if (pool->subscriptions[i] && !pool->subscriptions[i]->closed &&
|
||||
strcmp(pool->subscriptions[i]->subscription_id, subscription_id) == 0) {
|
||||
sub = pool->subscriptions[i];
|
||||
break;
|
||||
}
|
||||
|
||||
// Find subscription and call callback
|
||||
for (int i = 0; i < pool->subscription_count; i++) {
|
||||
nostr_pool_subscription_t* sub = pool->subscriptions[i];
|
||||
if (sub && !sub->closed &&
|
||||
strcmp(sub->subscription_id, subscription_id) == 0) {
|
||||
if (sub->on_event) {
|
||||
sub->on_event(event, relay->url, sub->user_data);
|
||||
}
|
||||
|
||||
if (sub) {
|
||||
// Check for duplicate (per-subscription deduplication)
|
||||
int is_duplicate = 0;
|
||||
if (sub->enable_deduplication) {
|
||||
if (is_event_seen(pool, event_id)) {
|
||||
is_duplicate = 1;
|
||||
} else {
|
||||
mark_event_seen(pool, event_id);
|
||||
}
|
||||
}
|
||||
|
||||
if (!is_duplicate) {
|
||||
relay->stats.events_received++;
|
||||
|
||||
// Measure query latency (first event response)
|
||||
double latency_ms = remove_subscription_timing(relay, subscription_id);
|
||||
if (latency_ms > 0.0) {
|
||||
// Update query latency statistics
|
||||
if (relay->stats.query_samples == 0) {
|
||||
relay->stats.query_latency_avg = latency_ms;
|
||||
relay->stats.query_latency_min = latency_ms;
|
||||
relay->stats.query_latency_max = latency_ms;
|
||||
} else {
|
||||
relay->stats.query_latency_avg =
|
||||
(relay->stats.query_latency_avg * relay->stats.query_samples + latency_ms) /
|
||||
(relay->stats.query_samples + 1);
|
||||
|
||||
if (latency_ms < relay->stats.query_latency_min) {
|
||||
relay->stats.query_latency_min = latency_ms;
|
||||
}
|
||||
if (latency_ms > relay->stats.query_latency_max) {
|
||||
relay->stats.query_latency_max = latency_ms;
|
||||
}
|
||||
}
|
||||
break;
|
||||
relay->stats.query_samples++;
|
||||
}
|
||||
|
||||
// Call event callback
|
||||
if (sub->on_event) {
|
||||
sub->on_event(event, relay->url, sub->user_data);
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -630,8 +855,22 @@ static void process_relay_message(nostr_relay_pool_t* pool, relay_connection_t*
|
||||
}
|
||||
}
|
||||
|
||||
if (all_eose && sub->on_eose) {
|
||||
sub->on_eose(sub->user_data);
|
||||
if (all_eose) {
|
||||
if (sub->on_eose) {
|
||||
// Pass collected events based on result mode
|
||||
if (sub->result_mode == NOSTR_POOL_EOSE_FIRST) {
|
||||
// FIRST mode: no events collected, pass NULL/0
|
||||
sub->on_eose(NULL, 0, sub->user_data);
|
||||
} else {
|
||||
// FULL_SET or MOST_RECENT: pass collected events
|
||||
sub->on_eose(sub->collected_events, sub->collected_event_count, sub->user_data);
|
||||
}
|
||||
}
|
||||
|
||||
// Auto-close subscription if close_on_eose is enabled
|
||||
if (sub->close_on_eose && !sub->closed) {
|
||||
nostr_pool_subscription_close(sub);
|
||||
}
|
||||
}
|
||||
break;
|
||||
}
|
||||
@@ -652,39 +891,8 @@ static void process_relay_message(nostr_relay_pool_t* pool, relay_connection_t*
|
||||
}
|
||||
}
|
||||
} else if (strcmp(msg_type, "PONG") == 0) {
|
||||
// PING FUNCTIONALITY DISABLED - Handle PONG response
|
||||
/* COMMENTED OUT - PING FUNCTIONALITY DISABLED
|
||||
if (relay->ping_pending) {
|
||||
double current_time_ms = get_current_time_ms();
|
||||
double ping_latency = current_time_ms - relay->pending_ping_start_ms;
|
||||
|
||||
// Update ping statistics
|
||||
if (relay->stats.ping_samples == 0) {
|
||||
relay->stats.ping_latency_avg = ping_latency;
|
||||
relay->stats.ping_latency_min = ping_latency;
|
||||
relay->stats.ping_latency_max = ping_latency;
|
||||
} else {
|
||||
relay->stats.ping_latency_avg =
|
||||
(relay->stats.ping_latency_avg * relay->stats.ping_samples + ping_latency) /
|
||||
(relay->stats.ping_samples + 1);
|
||||
|
||||
if (ping_latency < relay->stats.ping_latency_min) {
|
||||
relay->stats.ping_latency_min = ping_latency;
|
||||
}
|
||||
if (ping_latency > relay->stats.ping_latency_max) {
|
||||
relay->stats.ping_latency_max = ping_latency;
|
||||
}
|
||||
}
|
||||
|
||||
relay->stats.ping_latency_current = ping_latency;
|
||||
relay->stats.ping_samples++;
|
||||
relay->ping_pending = 0;
|
||||
|
||||
#ifdef NOSTR_DEBUG_ENABLED
|
||||
printf("🏓 DEBUG: PONG from %s - latency: %.2f ms\n", relay->url, ping_latency);
|
||||
#endif
|
||||
}
|
||||
*/
|
||||
// Handle PONG response for connection health monitoring
|
||||
handle_pong_response(relay);
|
||||
}
|
||||
|
||||
if (msg_type) free(msg_type);
|
||||
@@ -757,11 +965,17 @@ cJSON** nostr_relay_pool_query_sync(
|
||||
int len = nostr_ws_receive(relay->ws_client, buffer, sizeof(buffer) - 1, 100);
|
||||
if (len > 0) {
|
||||
buffer[len] = '\0';
|
||||
|
||||
char* msg_type = NULL;
|
||||
cJSON* parsed = NULL;
|
||||
if (nostr_parse_relay_message(buffer, &msg_type, &parsed) == 0) {
|
||||
if (msg_type && strcmp(msg_type, "EVENT") == 0) {
|
||||
|
||||
// Check if this is a pong message (WebSocket library prefixes pong messages)
|
||||
if (strncmp(buffer, "__PONG__", 8) == 0) {
|
||||
// Handle pong response for connection health monitoring
|
||||
handle_pong_response(relay);
|
||||
} else {
|
||||
// Process as regular NOSTR message
|
||||
char* msg_type = NULL;
|
||||
cJSON* parsed = NULL;
|
||||
if (nostr_parse_relay_message(buffer, &msg_type, &parsed) == 0) {
|
||||
if (msg_type && strcmp(msg_type, "EVENT") == 0) {
|
||||
// Handle EVENT message
|
||||
if (cJSON_IsArray(parsed) && cJSON_GetArraySize(parsed) >= 3) {
|
||||
cJSON* sub_id_json = cJSON_GetArrayItem(parsed, 1);
|
||||
@@ -806,6 +1020,7 @@ cJSON** nostr_relay_pool_query_sync(
|
||||
}
|
||||
if (msg_type) free(msg_type);
|
||||
if (parsed) cJSON_Delete(parsed);
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -1059,76 +1274,78 @@ double nostr_relay_pool_get_relay_query_latency(
|
||||
}
|
||||
|
||||
int nostr_relay_pool_ping_relay(
|
||||
nostr_relay_pool_t* pool,
|
||||
nostr_relay_pool_t* pool,
|
||||
const char* relay_url) {
|
||||
|
||||
|
||||
// PING FUNCTIONALITY DISABLED
|
||||
/* COMMENTED OUT - PING FUNCTIONALITY DISABLED
|
||||
if (!pool || !relay_url) {
|
||||
return NOSTR_ERROR_INVALID_INPUT;
|
||||
}
|
||||
|
||||
|
||||
relay_connection_t* relay = find_relay_by_url(pool, relay_url);
|
||||
if (!relay || !relay->ws_client) {
|
||||
return NOSTR_ERROR_INVALID_INPUT;
|
||||
}
|
||||
|
||||
|
||||
if (ensure_relay_connection(relay) != 0) {
|
||||
return NOSTR_ERROR_NETWORK_FAILED;
|
||||
}
|
||||
|
||||
|
||||
time_t current_time = time(NULL);
|
||||
relay->pending_ping_start_ms = get_current_time_ms();
|
||||
relay->ping_pending = 1;
|
||||
relay->last_ping_sent = current_time;
|
||||
relay->next_ping_time = current_time + NOSTR_POOL_PING_INTERVAL;
|
||||
|
||||
|
||||
if (nostr_ws_send_ping(relay->ws_client, "ping", 4) < 0) {
|
||||
relay->ping_pending = 0;
|
||||
return NOSTR_ERROR_NETWORK_FAILED;
|
||||
}
|
||||
|
||||
|
||||
return NOSTR_SUCCESS;
|
||||
*/
|
||||
|
||||
|
||||
(void)pool; // Suppress unused parameter warning
|
||||
(void)relay_url; // Suppress unused parameter warning
|
||||
return NOSTR_ERROR_INVALID_INPUT; // Function disabled
|
||||
}
|
||||
|
||||
int nostr_relay_pool_ping_relay_sync(
|
||||
nostr_relay_pool_t* pool,
|
||||
const char* relay_url,
|
||||
nostr_relay_pool_t* pool,
|
||||
const char* relay_url,
|
||||
int timeout_seconds) {
|
||||
|
||||
|
||||
// PING FUNCTIONALITY DISABLED
|
||||
/* COMMENTED OUT - PING FUNCTIONALITY DISABLED
|
||||
if (!pool || !relay_url) {
|
||||
return NOSTR_ERROR_INVALID_INPUT;
|
||||
}
|
||||
|
||||
|
||||
relay_connection_t* relay = find_relay_by_url(pool, relay_url);
|
||||
if (!relay || !relay->ws_client) {
|
||||
return NOSTR_ERROR_INVALID_INPUT;
|
||||
}
|
||||
|
||||
|
||||
if (ensure_relay_connection(relay) != 0) {
|
||||
return NOSTR_ERROR_NETWORK_FAILED;
|
||||
}
|
||||
|
||||
|
||||
if (timeout_seconds <= 0) {
|
||||
timeout_seconds = 5;
|
||||
}
|
||||
|
||||
|
||||
time_t current_time = time(NULL);
|
||||
relay->pending_ping_start_ms = get_current_time_ms();
|
||||
relay->ping_pending = 1;
|
||||
relay->last_ping_sent = current_time;
|
||||
relay->next_ping_time = current_time + NOSTR_POOL_PING_INTERVAL;
|
||||
|
||||
|
||||
if (nostr_ws_send_ping(relay->ws_client, "ping", 4) < 0) {
|
||||
relay->ping_pending = 0;
|
||||
return NOSTR_ERROR_NETWORK_FAILED;
|
||||
}
|
||||
|
||||
|
||||
// Wait for PONG response
|
||||
time_t wait_start = time(NULL);
|
||||
while (time(NULL) - wait_start < timeout_seconds && relay->ping_pending) {
|
||||
@@ -1136,7 +1353,7 @@ int nostr_relay_pool_ping_relay_sync(
|
||||
int len = nostr_ws_receive(relay->ws_client, buffer, sizeof(buffer) - 1, 1000);
|
||||
if (len > 0) {
|
||||
buffer[len] = '\0';
|
||||
|
||||
|
||||
char* msg_type = NULL;
|
||||
cJSON* parsed = NULL;
|
||||
if (nostr_parse_relay_message(buffer, &msg_type, &parsed) == 0) {
|
||||
@@ -1146,17 +1363,17 @@ int nostr_relay_pool_ping_relay_sync(
|
||||
if (relay->ping_pending) {
|
||||
double current_time_ms = get_current_time_ms();
|
||||
double ping_latency = current_time_ms - relay->pending_ping_start_ms;
|
||||
|
||||
|
||||
// Update ping statistics
|
||||
if (relay->stats.ping_samples == 0) {
|
||||
relay->stats.ping_latency_avg = ping_latency;
|
||||
relay->stats.ping_latency_min = ping_latency;
|
||||
relay->stats.ping_latency_max = ping_latency;
|
||||
} else {
|
||||
relay->stats.ping_latency_avg =
|
||||
(relay->stats.ping_latency_avg * relay->stats.ping_samples + ping_latency) /
|
||||
relay->stats.ping_latency_avg =
|
||||
(relay->stats.ping_latency_avg * relay->stats.ping_samples + ping_latency) /
|
||||
(relay->stats.ping_samples + 1);
|
||||
|
||||
|
||||
if (ping_latency < relay->stats.ping_latency_min) {
|
||||
relay->stats.ping_latency_min = ping_latency;
|
||||
}
|
||||
@@ -1164,11 +1381,11 @@ int nostr_relay_pool_ping_relay_sync(
|
||||
relay->stats.ping_latency_max = ping_latency;
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
relay->stats.ping_latency_current = ping_latency;
|
||||
relay->stats.ping_samples++;
|
||||
relay->ping_pending = 0;
|
||||
|
||||
|
||||
if (msg_type) free(msg_type);
|
||||
if (parsed) cJSON_Delete(parsed);
|
||||
return NOSTR_SUCCESS;
|
||||
@@ -1179,12 +1396,15 @@ int nostr_relay_pool_ping_relay_sync(
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
// Timeout
|
||||
relay->ping_pending = 0;
|
||||
return NOSTR_ERROR_NETWORK_FAILED;
|
||||
*/
|
||||
|
||||
|
||||
(void)pool; // Suppress unused parameter warning
|
||||
(void)relay_url; // Suppress unused parameter warning
|
||||
(void)timeout_seconds; // Suppress unused parameter warning
|
||||
return NOSTR_ERROR_INVALID_INPUT; // Function disabled
|
||||
}
|
||||
|
||||
@@ -1233,52 +1453,58 @@ int nostr_relay_pool_poll(nostr_relay_pool_t* pool, int timeout_ms) {
|
||||
if (!pool) {
|
||||
return -1;
|
||||
}
|
||||
|
||||
|
||||
int events_processed = 0;
|
||||
|
||||
|
||||
for (int i = 0; i < pool->relay_count; i++) {
|
||||
relay_connection_t* relay = pool->relays[i];
|
||||
if (!relay || !relay->ws_client) {
|
||||
if (!relay) {
|
||||
continue;
|
||||
}
|
||||
|
||||
|
||||
// Check if reconnection is needed
|
||||
if (should_attempt_reconnect(relay)) {
|
||||
attempt_reconnect(relay);
|
||||
}
|
||||
|
||||
// Skip if no WebSocket client
|
||||
if (!relay->ws_client) {
|
||||
continue;
|
||||
}
|
||||
|
||||
// Check connection state
|
||||
nostr_ws_state_t state = nostr_ws_get_state(relay->ws_client);
|
||||
|
||||
|
||||
if (state != NOSTR_WS_CONNECTED) {
|
||||
relay->status = (state == NOSTR_WS_ERROR) ? NOSTR_POOL_RELAY_ERROR : NOSTR_POOL_RELAY_DISCONNECTED;
|
||||
continue;
|
||||
}
|
||||
|
||||
|
||||
relay->status = NOSTR_POOL_RELAY_CONNECTED;
|
||||
|
||||
// PING FUNCTIONALITY DISABLED - Automatic ping management
|
||||
/* COMMENTED OUT - PING FUNCTIONALITY DISABLED
|
||||
// Check if we need to send a ping to keep the connection alive
|
||||
if (current_time >= relay->next_ping_time && !relay->ping_pending) {
|
||||
relay->pending_ping_start_ms = get_current_time_ms();
|
||||
relay->ping_pending = 1;
|
||||
relay->last_ping_sent = current_time;
|
||||
relay->next_ping_time = current_time + NOSTR_POOL_PING_INTERVAL;
|
||||
|
||||
if (nostr_ws_send_ping(relay->ws_client, "ping", 4) < 0) {
|
||||
relay->ping_pending = 0;
|
||||
}
|
||||
}
|
||||
*/
|
||||
|
||||
|
||||
// Connection health monitoring (ping/pong)
|
||||
check_connection_health(relay);
|
||||
|
||||
// Process incoming messages
|
||||
char buffer[8192];
|
||||
int timeout_per_relay = timeout_ms / pool->relay_count;
|
||||
|
||||
|
||||
int len = nostr_ws_receive(relay->ws_client, buffer, sizeof(buffer) - 1, timeout_per_relay);
|
||||
|
||||
|
||||
if (len > 0) {
|
||||
buffer[len] = '\0';
|
||||
process_relay_message(pool, relay, buffer);
|
||||
|
||||
// Check if this is a pong message (WebSocket library prefixes pong messages)
|
||||
if (strncmp(buffer, "__PONG__", 8) == 0) {
|
||||
// Handle pong response for connection health monitoring
|
||||
handle_pong_response(relay);
|
||||
} else {
|
||||
// Process as regular NOSTR message
|
||||
process_relay_message(pool, relay, buffer);
|
||||
}
|
||||
events_processed++;
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
return events_processed;
|
||||
}
|
||||
|
||||
+117
-30
@@ -13,7 +13,7 @@
|
||||
#define _GNU_SOURCE
|
||||
#define _POSIX_C_SOURCE 200809L
|
||||
|
||||
#include "nostr_common.h"
|
||||
#include "nostr_core.h"
|
||||
#include <stdio.h>
|
||||
#include <stdlib.h>
|
||||
#include <string.h>
|
||||
@@ -26,6 +26,9 @@
|
||||
// cJSON for JSON handling
|
||||
#include "../cjson/cJSON.h"
|
||||
|
||||
// NIP-42 Authentication
|
||||
#include "nip042.h"
|
||||
|
||||
// =============================================================================
|
||||
// TYPE DEFINITIONS FOR SYNCHRONOUS RELAY QUERIES
|
||||
// =============================================================================
|
||||
@@ -51,6 +54,12 @@ typedef struct {
|
||||
cJSON** events; // Array of events from this relay
|
||||
int events_capacity; // Allocated capacity
|
||||
char subscription_id[32]; // Unique subscription ID
|
||||
|
||||
// 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
|
||||
} relay_connection_t;
|
||||
|
||||
|
||||
@@ -65,7 +74,9 @@ cJSON** synchronous_query_relays_with_progress(
|
||||
int* result_count,
|
||||
int relay_timeout_seconds,
|
||||
relay_progress_callback_t callback,
|
||||
void* user_data) {
|
||||
void* user_data,
|
||||
int nip42_enabled,
|
||||
const unsigned char* private_key) {
|
||||
|
||||
if (!relay_urls || relay_count <= 0 || !filter || !result_count) {
|
||||
if (result_count) *result_count = 0;
|
||||
@@ -95,11 +106,17 @@ cJSON** synchronous_query_relays_with_progress(
|
||||
relays[i].last_activity = start_time;
|
||||
relays[i].events_capacity = 10;
|
||||
relays[i].events = malloc(relays[i].events_capacity * sizeof(cJSON*));
|
||||
|
||||
|
||||
// Initialize NIP-42 authentication fields
|
||||
relays[i].auth_state = NOSTR_AUTH_STATE_NONE;
|
||||
memset(relays[i].auth_challenge, 0, sizeof(relays[i].auth_challenge));
|
||||
relays[i].auth_challenge_time = 0;
|
||||
relays[i].nip42_enabled = nip42_enabled;
|
||||
|
||||
// Generate unique subscription ID
|
||||
snprintf(relays[i].subscription_id, sizeof(relays[i].subscription_id),
|
||||
snprintf(relays[i].subscription_id, sizeof(relays[i].subscription_id),
|
||||
"sync_%d_%ld", i, start_time);
|
||||
|
||||
|
||||
if (callback) {
|
||||
callback(relays[i].url, "connecting", NULL, 0, relay_count, 0, user_data);
|
||||
}
|
||||
@@ -191,19 +208,50 @@ cJSON** synchronous_query_relays_with_progress(
|
||||
cJSON* parsed = NULL;
|
||||
if (nostr_parse_relay_message(buffer, &msg_type, &parsed) == 0) {
|
||||
|
||||
if (msg_type && strcmp(msg_type, "EVENT") == 0) {
|
||||
if (msg_type && strcmp(msg_type, "AUTH") == 0) {
|
||||
// Handle AUTH challenge message: ["AUTH", <challenge-string>]
|
||||
if (relay->nip42_enabled && 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 and attempt authentication
|
||||
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 authentication event
|
||||
cJSON* auth_event = nostr_nip42_create_auth_event(challenge, relay->url, 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->client, auth_message) >= 0) {
|
||||
relay->auth_state = NOSTR_AUTH_STATE_AUTHENTICATING;
|
||||
if (callback) {
|
||||
callback(relay->url, "authenticating", NULL, 0, relay_count, completed_relays, user_data);
|
||||
}
|
||||
}
|
||||
free(auth_message);
|
||||
}
|
||||
cJSON_Delete(auth_event);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
} else if (msg_type && strcmp(msg_type, "EVENT") == 0) {
|
||||
// Handle EVENT message
|
||||
if (cJSON_IsArray(parsed) && cJSON_GetArraySize(parsed) >= 3) {
|
||||
cJSON* sub_id_json = cJSON_GetArrayItem(parsed, 1);
|
||||
cJSON* event = cJSON_GetArrayItem(parsed, 2);
|
||||
|
||||
|
||||
if (cJSON_IsString(sub_id_json) && event &&
|
||||
strcmp(cJSON_GetStringValue(sub_id_json), relay->subscription_id) == 0) {
|
||||
|
||||
|
||||
cJSON* event_id_json = cJSON_GetObjectItem(event, "id");
|
||||
if (event_id_json && cJSON_IsString(event_id_json)) {
|
||||
const char* event_id = cJSON_GetStringValue(event_id_json);
|
||||
|
||||
|
||||
// Check for duplicate
|
||||
int is_duplicate = 0;
|
||||
for (int j = 0; j < seen_count; j++) {
|
||||
@@ -212,31 +260,31 @@ cJSON** synchronous_query_relays_with_progress(
|
||||
break;
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
if (!is_duplicate && seen_count < 1000) {
|
||||
// New event - add to seen list
|
||||
strncpy(seen_event_ids[seen_count], event_id, 64);
|
||||
seen_event_ids[seen_count][64] = '\0';
|
||||
seen_count++;
|
||||
total_unique_events++;
|
||||
|
||||
|
||||
// Store event in relay's array
|
||||
if (relay->events_received >= relay->events_capacity) {
|
||||
relay->events_capacity *= 2;
|
||||
relay->events = realloc(relay->events,
|
||||
relay->events = realloc(relay->events,
|
||||
relay->events_capacity * sizeof(cJSON*));
|
||||
}
|
||||
|
||||
|
||||
relay->events[relay->events_received] = cJSON_Duplicate(event, 1);
|
||||
relay->events_received++;
|
||||
relay->state = RELAY_STATE_ACTIVE;
|
||||
|
||||
|
||||
if (callback) {
|
||||
callback(relay->url, "event_found", event_id,
|
||||
relay->events_received, relay_count,
|
||||
callback(relay->url, "event_found", event_id,
|
||||
relay->events_received, relay_count,
|
||||
completed_relays, user_data);
|
||||
}
|
||||
|
||||
|
||||
// For FIRST_RESULT mode, return immediately
|
||||
if (mode == RELAY_QUERY_FIRST_RESULT) {
|
||||
result_array = malloc(sizeof(cJSON*));
|
||||
@@ -244,7 +292,7 @@ cJSON** synchronous_query_relays_with_progress(
|
||||
result_array[0] = cJSON_Duplicate(event, 1);
|
||||
*result_count = 1;
|
||||
if (callback) {
|
||||
callback(NULL, "first_result", event_id,
|
||||
callback(NULL, "first_result", event_id,
|
||||
1, relay_count, completed_relays, user_data);
|
||||
}
|
||||
}
|
||||
@@ -254,7 +302,7 @@ cJSON** synchronous_query_relays_with_progress(
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
} else if (msg_type && strcmp(msg_type, "EOSE") == 0) {
|
||||
// Handle End of Stored Events
|
||||
cJSON* sub_id_json = cJSON_GetArrayItem(parsed, 1);
|
||||
@@ -400,7 +448,9 @@ publish_result_t* synchronous_publish_event_with_progress(
|
||||
int* success_count,
|
||||
int relay_timeout_seconds,
|
||||
publish_progress_callback_t callback,
|
||||
void* user_data) {
|
||||
void* user_data,
|
||||
int nip42_enabled,
|
||||
const unsigned char* private_key) {
|
||||
|
||||
if (!relay_urls || relay_count <= 0 || !event || !success_count) {
|
||||
if (success_count) *success_count = 0;
|
||||
@@ -443,7 +493,13 @@ publish_result_t* synchronous_publish_event_with_progress(
|
||||
relays[i].state = RELAY_STATE_CONNECTING;
|
||||
relays[i].last_activity = start_time;
|
||||
results[i] = PUBLISH_ERROR; // Default to error
|
||||
|
||||
|
||||
// Initialize NIP-42 authentication fields
|
||||
relays[i].auth_state = NOSTR_AUTH_STATE_NONE;
|
||||
memset(relays[i].auth_challenge, 0, sizeof(relays[i].auth_challenge));
|
||||
relays[i].auth_challenge_time = 0;
|
||||
relays[i].nip42_enabled = nip42_enabled;
|
||||
|
||||
if (callback) {
|
||||
callback(relays[i].url, "connecting", NULL, 0, relay_count, 0, user_data);
|
||||
}
|
||||
@@ -535,34 +591,65 @@ publish_result_t* synchronous_publish_event_with_progress(
|
||||
char* msg_type = NULL;
|
||||
cJSON* parsed = NULL;
|
||||
if (nostr_parse_relay_message(buffer, &msg_type, &parsed) == 0) {
|
||||
|
||||
if (msg_type && strcmp(msg_type, "OK") == 0) {
|
||||
|
||||
if (msg_type && strcmp(msg_type, "AUTH") == 0) {
|
||||
// Handle AUTH challenge message: ["AUTH", <challenge-string>]
|
||||
if (relay->nip42_enabled && 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 and attempt authentication
|
||||
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 authentication event
|
||||
cJSON* auth_event = nostr_nip42_create_auth_event(challenge, relay->url, 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->client, auth_message) >= 0) {
|
||||
relay->auth_state = NOSTR_AUTH_STATE_AUTHENTICATING;
|
||||
if (callback) {
|
||||
callback(relay->url, "authenticating", NULL, 0, relay_count, completed_relays, user_data);
|
||||
}
|
||||
}
|
||||
free(auth_message);
|
||||
}
|
||||
cJSON_Delete(auth_event);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
} else if (msg_type && strcmp(msg_type, "OK") == 0) {
|
||||
// Handle OK message: ["OK", <event_id>, <true|false>, <message>]
|
||||
if (cJSON_IsArray(parsed) && cJSON_GetArraySize(parsed) >= 3) {
|
||||
cJSON* ok_event_id = cJSON_GetArrayItem(parsed, 1);
|
||||
cJSON* accepted = cJSON_GetArrayItem(parsed, 2);
|
||||
cJSON* message = cJSON_GetArrayItem(parsed, 3);
|
||||
|
||||
|
||||
// Verify this OK is for our event
|
||||
if (ok_event_id && cJSON_IsString(ok_event_id) && event_id &&
|
||||
strcmp(cJSON_GetStringValue(ok_event_id), event_id) == 0) {
|
||||
|
||||
|
||||
relay->state = RELAY_STATE_EOSE_RECEIVED; // Reuse for "completed"
|
||||
active_relays--;
|
||||
completed_relays++;
|
||||
|
||||
|
||||
const char* ok_message = "";
|
||||
if (message && cJSON_IsString(message)) {
|
||||
ok_message = cJSON_GetStringValue(message);
|
||||
}
|
||||
|
||||
|
||||
if (accepted && cJSON_IsBool(accepted) && cJSON_IsTrue(accepted)) {
|
||||
// Event accepted
|
||||
results[i] = PUBLISH_SUCCESS;
|
||||
(*success_count)++;
|
||||
|
||||
if (callback) {
|
||||
callback(relay->url, "accepted", ok_message,
|
||||
callback(relay->url, "accepted", ok_message,
|
||||
*success_count, relay_count, completed_relays, user_data);
|
||||
}
|
||||
} else {
|
||||
@@ -570,11 +657,11 @@ publish_result_t* synchronous_publish_event_with_progress(
|
||||
results[i] = PUBLISH_REJECTED;
|
||||
|
||||
if (callback) {
|
||||
callback(relay->url, "rejected", ok_message,
|
||||
callback(relay->url, "rejected", ok_message,
|
||||
*success_count, relay_count, completed_relays, user_data);
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
// Close connection
|
||||
nostr_ws_close(relay->client);
|
||||
relay->client = NULL;
|
||||
|
||||
@@ -277,3 +277,332 @@ int nostr_add_proof_of_work(cJSON* event, const unsigned char* private_key,
|
||||
// If we reach here, we've exceeded max attempts
|
||||
return NOSTR_ERROR_CRYPTO_FAILED;
|
||||
}
|
||||
|
||||
/**
|
||||
* Calculate PoW difficulty (leading zero bits) for an event ID
|
||||
*
|
||||
* @param event_id_hex Hexadecimal event ID string (64 characters)
|
||||
* @return Number of leading zero bits, or negative error code on failure
|
||||
*/
|
||||
int nostr_calculate_pow_difficulty(const char* event_id_hex) {
|
||||
if (!event_id_hex) {
|
||||
return NOSTR_ERROR_INVALID_INPUT;
|
||||
}
|
||||
|
||||
// Validate hex string length (should be 64 characters for SHA-256)
|
||||
size_t hex_len = strlen(event_id_hex);
|
||||
if (hex_len != 64) {
|
||||
return NOSTR_ERROR_NIP13_CALCULATION;
|
||||
}
|
||||
|
||||
// Convert hex to bytes
|
||||
unsigned char hash[32];
|
||||
if (nostr_hex_to_bytes(event_id_hex, hash, 32) != NOSTR_SUCCESS) {
|
||||
return NOSTR_ERROR_NIP13_CALCULATION;
|
||||
}
|
||||
|
||||
// Use existing NIP-13 reference implementation
|
||||
return count_leading_zero_bits(hash);
|
||||
}
|
||||
|
||||
/**
|
||||
* Extract nonce information from event tags
|
||||
*
|
||||
* @param event Complete event JSON object
|
||||
* @param nonce_out Output pointer for nonce value (can be NULL)
|
||||
* @param target_difficulty_out Output pointer for target difficulty (can be NULL)
|
||||
* @return NOSTR_SUCCESS if nonce tag found, NOSTR_ERROR_NIP13_NO_NONCE_TAG if not found, other error codes on failure
|
||||
*/
|
||||
int nostr_extract_nonce_info(cJSON* event, uint64_t* nonce_out, int* target_difficulty_out) {
|
||||
if (!event) {
|
||||
return NOSTR_ERROR_INVALID_INPUT;
|
||||
}
|
||||
|
||||
// Initialize output values
|
||||
if (nonce_out) *nonce_out = 0;
|
||||
if (target_difficulty_out) *target_difficulty_out = -1;
|
||||
|
||||
// Get tags array
|
||||
cJSON* tags = cJSON_GetObjectItem(event, "tags");
|
||||
if (!tags || !cJSON_IsArray(tags)) {
|
||||
return NOSTR_ERROR_NIP13_NO_NONCE_TAG;
|
||||
}
|
||||
|
||||
// Search for nonce tag
|
||||
cJSON* tag = NULL;
|
||||
cJSON_ArrayForEach(tag, tags) {
|
||||
if (!cJSON_IsArray(tag) || cJSON_GetArraySize(tag) < 2) {
|
||||
continue;
|
||||
}
|
||||
|
||||
// Check if this is a nonce tag
|
||||
cJSON* tag_type = cJSON_GetArrayItem(tag, 0);
|
||||
if (!tag_type || !cJSON_IsString(tag_type)) {
|
||||
continue;
|
||||
}
|
||||
|
||||
const char* tag_name = cJSON_GetStringValue(tag_type);
|
||||
if (!tag_name || strcmp(tag_name, "nonce") != 0) {
|
||||
continue;
|
||||
}
|
||||
|
||||
// Extract nonce value (second element)
|
||||
cJSON* nonce_item = cJSON_GetArrayItem(tag, 1);
|
||||
if (!nonce_item || !cJSON_IsString(nonce_item)) {
|
||||
return NOSTR_ERROR_NIP13_INVALID_NONCE_TAG;
|
||||
}
|
||||
|
||||
const char* nonce_str = cJSON_GetStringValue(nonce_item);
|
||||
if (!nonce_str) {
|
||||
return NOSTR_ERROR_NIP13_INVALID_NONCE_TAG;
|
||||
}
|
||||
|
||||
// Parse nonce value
|
||||
char* endptr;
|
||||
uint64_t nonce_val = strtoull(nonce_str, &endptr, 10);
|
||||
if (*endptr != '\0') {
|
||||
return NOSTR_ERROR_NIP13_INVALID_NONCE_TAG;
|
||||
}
|
||||
|
||||
if (nonce_out) *nonce_out = nonce_val;
|
||||
|
||||
// Extract target difficulty (third element, optional)
|
||||
if (cJSON_GetArraySize(tag) >= 3) {
|
||||
cJSON* target_item = cJSON_GetArrayItem(tag, 2);
|
||||
if (target_item && cJSON_IsString(target_item)) {
|
||||
const char* target_str = cJSON_GetStringValue(target_item);
|
||||
if (target_str) {
|
||||
char* endptr2;
|
||||
long target_val = strtol(target_str, &endptr2, 10);
|
||||
if (*endptr2 == '\0' && target_val >= 0) {
|
||||
if (target_difficulty_out) *target_difficulty_out = (int)target_val;
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
return NOSTR_SUCCESS;
|
||||
}
|
||||
|
||||
// No nonce tag found
|
||||
return NOSTR_ERROR_NIP13_NO_NONCE_TAG;
|
||||
}
|
||||
|
||||
/**
|
||||
* Validate just the nonce tag format (without PoW calculation)
|
||||
*
|
||||
* @param nonce_tag_array JSON array representing a nonce tag
|
||||
* @return NOSTR_SUCCESS if valid format, error code otherwise
|
||||
*/
|
||||
int nostr_validate_nonce_tag(cJSON* nonce_tag_array) {
|
||||
if (!nonce_tag_array || !cJSON_IsArray(nonce_tag_array)) {
|
||||
return NOSTR_ERROR_NIP13_INVALID_NONCE_TAG;
|
||||
}
|
||||
|
||||
int array_size = cJSON_GetArraySize(nonce_tag_array);
|
||||
if (array_size < 2) {
|
||||
return NOSTR_ERROR_NIP13_INVALID_NONCE_TAG;
|
||||
}
|
||||
|
||||
// First element should be "nonce"
|
||||
cJSON* tag_type = cJSON_GetArrayItem(nonce_tag_array, 0);
|
||||
if (!tag_type || !cJSON_IsString(tag_type)) {
|
||||
return NOSTR_ERROR_NIP13_INVALID_NONCE_TAG;
|
||||
}
|
||||
|
||||
const char* tag_name = cJSON_GetStringValue(tag_type);
|
||||
if (!tag_name || strcmp(tag_name, "nonce") != 0) {
|
||||
return NOSTR_ERROR_NIP13_INVALID_NONCE_TAG;
|
||||
}
|
||||
|
||||
// Second element should be a valid nonce (numeric string)
|
||||
cJSON* nonce_item = cJSON_GetArrayItem(nonce_tag_array, 1);
|
||||
if (!nonce_item || !cJSON_IsString(nonce_item)) {
|
||||
return NOSTR_ERROR_NIP13_INVALID_NONCE_TAG;
|
||||
}
|
||||
|
||||
const char* nonce_str = cJSON_GetStringValue(nonce_item);
|
||||
if (!nonce_str) {
|
||||
return NOSTR_ERROR_NIP13_INVALID_NONCE_TAG;
|
||||
}
|
||||
|
||||
// Validate nonce is a valid number
|
||||
char* endptr;
|
||||
strtoull(nonce_str, &endptr, 10);
|
||||
if (*endptr != '\0') {
|
||||
return NOSTR_ERROR_NIP13_INVALID_NONCE_TAG;
|
||||
}
|
||||
|
||||
// Third element (target difficulty) is optional, but if present should be valid
|
||||
if (array_size >= 3) {
|
||||
cJSON* target_item = cJSON_GetArrayItem(nonce_tag_array, 2);
|
||||
if (target_item && cJSON_IsString(target_item)) {
|
||||
const char* target_str = cJSON_GetStringValue(target_item);
|
||||
if (target_str) {
|
||||
char* endptr2;
|
||||
strtol(target_str, &endptr2, 10);
|
||||
if (*endptr2 != '\0') {
|
||||
return NOSTR_ERROR_NIP13_INVALID_NONCE_TAG;
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
return NOSTR_SUCCESS;
|
||||
}
|
||||
|
||||
/**
|
||||
* Validate Proof of Work for an event according to NIP-13
|
||||
*
|
||||
* @param event Complete event JSON object to validate
|
||||
* @param min_difficulty Minimum difficulty required by the relay (0 = no requirement)
|
||||
* @param validation_flags Bitflags for validation options
|
||||
* @param result_info Optional output struct for detailed results (can be NULL)
|
||||
* @return NOSTR_SUCCESS if PoW is valid and meets requirements, error code otherwise
|
||||
*/
|
||||
int nostr_validate_pow(cJSON* event, int min_difficulty, int validation_flags,
|
||||
nostr_pow_result_t* result_info) {
|
||||
if (!event) {
|
||||
return NOSTR_ERROR_INVALID_INPUT;
|
||||
}
|
||||
|
||||
// Initialize result info if provided
|
||||
if (result_info) {
|
||||
result_info->actual_difficulty = 0;
|
||||
result_info->committed_target = -1;
|
||||
result_info->nonce_value = 0;
|
||||
result_info->has_nonce_tag = 0;
|
||||
result_info->error_detail[0] = '\0';
|
||||
}
|
||||
|
||||
// Get event ID for PoW calculation
|
||||
cJSON* id_item = cJSON_GetObjectItem(event, "id");
|
||||
if (!id_item || !cJSON_IsString(id_item)) {
|
||||
if (result_info) {
|
||||
snprintf(result_info->error_detail, sizeof(result_info->error_detail),
|
||||
"Missing or invalid event ID");
|
||||
}
|
||||
return NOSTR_ERROR_EVENT_INVALID_ID;
|
||||
}
|
||||
|
||||
const char* event_id = cJSON_GetStringValue(id_item);
|
||||
if (!event_id) {
|
||||
if (result_info) {
|
||||
snprintf(result_info->error_detail, sizeof(result_info->error_detail),
|
||||
"Event ID is not a string");
|
||||
}
|
||||
return NOSTR_ERROR_EVENT_INVALID_ID;
|
||||
}
|
||||
|
||||
// Calculate actual PoW difficulty
|
||||
int actual_difficulty = nostr_calculate_pow_difficulty(event_id);
|
||||
if (actual_difficulty < 0) {
|
||||
if (result_info) {
|
||||
snprintf(result_info->error_detail, sizeof(result_info->error_detail),
|
||||
"Failed to calculate PoW difficulty");
|
||||
}
|
||||
return actual_difficulty; // Return the specific error from calculation
|
||||
}
|
||||
|
||||
if (result_info) {
|
||||
result_info->actual_difficulty = actual_difficulty;
|
||||
}
|
||||
|
||||
// Check if minimum difficulty requirement is met
|
||||
if (min_difficulty > 0 && actual_difficulty < min_difficulty) {
|
||||
if (result_info) {
|
||||
snprintf(result_info->error_detail, sizeof(result_info->error_detail),
|
||||
"Insufficient difficulty: %d < %d", actual_difficulty, min_difficulty);
|
||||
}
|
||||
return NOSTR_ERROR_NIP13_INSUFFICIENT;
|
||||
}
|
||||
|
||||
// Extract nonce information if validation flags require it
|
||||
uint64_t nonce_value = 0;
|
||||
int committed_target = -1;
|
||||
int nonce_extract_result = nostr_extract_nonce_info(event, &nonce_value, &committed_target);
|
||||
|
||||
if (result_info) {
|
||||
result_info->nonce_value = nonce_value;
|
||||
result_info->committed_target = committed_target;
|
||||
result_info->has_nonce_tag = (nonce_extract_result == NOSTR_SUCCESS) ? 1 : 0;
|
||||
}
|
||||
|
||||
// Validate nonce tag presence if required
|
||||
if (validation_flags & NOSTR_POW_VALIDATE_NONCE_TAG) {
|
||||
if (nonce_extract_result == NOSTR_ERROR_NIP13_NO_NONCE_TAG) {
|
||||
if (result_info) {
|
||||
snprintf(result_info->error_detail, sizeof(result_info->error_detail),
|
||||
"Missing required nonce tag");
|
||||
}
|
||||
return NOSTR_ERROR_NIP13_NO_NONCE_TAG;
|
||||
} else if (nonce_extract_result != NOSTR_SUCCESS) {
|
||||
if (result_info) {
|
||||
snprintf(result_info->error_detail, sizeof(result_info->error_detail),
|
||||
"Invalid nonce tag format");
|
||||
}
|
||||
return nonce_extract_result;
|
||||
}
|
||||
|
||||
// If strict format validation is enabled, validate the nonce tag structure
|
||||
if (validation_flags & NOSTR_POW_STRICT_FORMAT) {
|
||||
cJSON* tags = cJSON_GetObjectItem(event, "tags");
|
||||
if (tags && cJSON_IsArray(tags)) {
|
||||
cJSON* tag = NULL;
|
||||
cJSON_ArrayForEach(tag, tags) {
|
||||
if (cJSON_IsArray(tag) && cJSON_GetArraySize(tag) >= 2) {
|
||||
cJSON* tag_type = cJSON_GetArrayItem(tag, 0);
|
||||
if (tag_type && cJSON_IsString(tag_type)) {
|
||||
const char* tag_name = cJSON_GetStringValue(tag_type);
|
||||
if (tag_name && strcmp(tag_name, "nonce") == 0) {
|
||||
int validation_result = nostr_validate_nonce_tag(tag);
|
||||
if (validation_result != NOSTR_SUCCESS) {
|
||||
if (result_info) {
|
||||
snprintf(result_info->error_detail, sizeof(result_info->error_detail),
|
||||
"Nonce tag failed strict format validation");
|
||||
}
|
||||
return validation_result;
|
||||
}
|
||||
break;
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// Validate committed target difficulty if required
|
||||
if (validation_flags & NOSTR_POW_VALIDATE_TARGET_COMMIT) {
|
||||
if (nonce_extract_result == NOSTR_SUCCESS && committed_target == -1) {
|
||||
if (result_info) {
|
||||
snprintf(result_info->error_detail, sizeof(result_info->error_detail),
|
||||
"Missing committed target difficulty in nonce tag");
|
||||
}
|
||||
return NOSTR_ERROR_NIP13_INVALID_NONCE_TAG;
|
||||
}
|
||||
|
||||
// Check for target/difficulty mismatch if rejecting lower targets
|
||||
if (validation_flags & NOSTR_POW_REJECT_LOWER_TARGET) {
|
||||
// According to NIP-13: "if you require 40 bits to reply to your thread and see
|
||||
// a committed target of 30, you can safely reject it even if the note has 40 bits difficulty"
|
||||
// This means we reject if committed_target < min_difficulty, not actual_difficulty
|
||||
if (committed_target != -1 && min_difficulty > 0 && committed_target < min_difficulty) {
|
||||
if (result_info) {
|
||||
snprintf(result_info->error_detail, sizeof(result_info->error_detail),
|
||||
"Committed target (%d) is lower than required minimum (%d)",
|
||||
committed_target, min_difficulty);
|
||||
}
|
||||
return NOSTR_ERROR_NIP13_TARGET_MISMATCH;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// All validations passed
|
||||
if (result_info) {
|
||||
snprintf(result_info->error_detail, sizeof(result_info->error_detail),
|
||||
"PoW validation successful");
|
||||
}
|
||||
|
||||
return NOSTR_SUCCESS;
|
||||
}
|
||||
|
||||
+31
-1
@@ -8,11 +8,41 @@
|
||||
#include "nip001.h"
|
||||
#include <stdint.h>
|
||||
|
||||
// PoW validation flags
|
||||
#define NOSTR_POW_VALIDATE_NONCE_TAG 0x01 // Require and validate nonce tag format
|
||||
#define NOSTR_POW_VALIDATE_TARGET_COMMIT 0x02 // Validate committed target difficulty
|
||||
#define NOSTR_POW_REJECT_LOWER_TARGET 0x04 // Reject if committed target < actual difficulty
|
||||
#define NOSTR_POW_STRICT_FORMAT 0x08 // Strict nonce tag format validation
|
||||
|
||||
// Convenience combinations
|
||||
#define NOSTR_POW_VALIDATE_BASIC (NOSTR_POW_VALIDATE_NONCE_TAG)
|
||||
#define NOSTR_POW_VALIDATE_FULL (NOSTR_POW_VALIDATE_NONCE_TAG | NOSTR_POW_VALIDATE_TARGET_COMMIT)
|
||||
#define NOSTR_POW_VALIDATE_ANTI_SPAM (NOSTR_POW_VALIDATE_FULL | NOSTR_POW_REJECT_LOWER_TARGET)
|
||||
|
||||
// Result information structure (optional output)
|
||||
typedef struct {
|
||||
int actual_difficulty; // Calculated difficulty (leading zero bits)
|
||||
int committed_target; // Target difficulty from nonce tag (-1 if none)
|
||||
uint64_t nonce_value; // Nonce value from tag (0 if none)
|
||||
int has_nonce_tag; // 1 if nonce tag present, 0 otherwise
|
||||
char error_detail[256]; // Detailed error description
|
||||
} nostr_pow_result_t;
|
||||
|
||||
// Function declarations
|
||||
int nostr_add_proof_of_work(cJSON* event, const unsigned char* private_key,
|
||||
int nostr_add_proof_of_work(cJSON* event, const unsigned char* private_key,
|
||||
int target_difficulty, int max_attempts,
|
||||
int progress_report_interval, int timestamp_update_interval,
|
||||
void (*progress_callback)(int current_difficulty, uint64_t nonce, void* user_data),
|
||||
void* user_data);
|
||||
|
||||
// PoW validation functions
|
||||
int nostr_validate_pow(cJSON* event, int min_difficulty, int validation_flags,
|
||||
nostr_pow_result_t* result_info);
|
||||
|
||||
int nostr_calculate_pow_difficulty(const char* event_id_hex);
|
||||
|
||||
int nostr_extract_nonce_info(cJSON* event, uint64_t* nonce_out, int* target_difficulty_out);
|
||||
|
||||
int nostr_validate_nonce_tag(cJSON* nonce_tag_array);
|
||||
|
||||
#endif // NIP013_H
|
||||
|
||||
@@ -0,0 +1,628 @@
|
||||
/*
|
||||
* NOSTR Core Library - NIP-042: Authentication of clients to relays
|
||||
*
|
||||
* Implements client authentication through signed ephemeral events
|
||||
*/
|
||||
|
||||
#include "nip042.h"
|
||||
#include "nip001.h"
|
||||
#include "utils.h"
|
||||
#include "../cjson/cJSON.h"
|
||||
#include <stdio.h>
|
||||
#include <stdlib.h>
|
||||
#include <string.h>
|
||||
#include <time.h>
|
||||
|
||||
// Forward declarations for crypto functions
|
||||
int nostr_secp256k1_get_random_bytes(unsigned char* buf, size_t len);
|
||||
|
||||
// =============================================================================
|
||||
// CLIENT-SIDE FUNCTIONS
|
||||
// =============================================================================
|
||||
|
||||
/**
|
||||
* Create NIP-42 authentication event (kind 22242)
|
||||
*/
|
||||
cJSON* nostr_nip42_create_auth_event(const char* challenge,
|
||||
const char* relay_url,
|
||||
const unsigned char* private_key,
|
||||
time_t timestamp) {
|
||||
if (!challenge || !relay_url || !private_key) {
|
||||
return NULL;
|
||||
}
|
||||
|
||||
// Validate challenge format
|
||||
size_t challenge_len = strlen(challenge);
|
||||
if (challenge_len < NOSTR_NIP42_MIN_CHALLENGE_LENGTH ||
|
||||
challenge_len >= NOSTR_NIP42_MAX_CHALLENGE_LENGTH) {
|
||||
return NULL;
|
||||
}
|
||||
|
||||
// Create tags array with relay and challenge
|
||||
cJSON* tags = cJSON_CreateArray();
|
||||
if (!tags) {
|
||||
return NULL;
|
||||
}
|
||||
|
||||
// Add relay tag
|
||||
cJSON* relay_tag = cJSON_CreateArray();
|
||||
if (!relay_tag) {
|
||||
cJSON_Delete(tags);
|
||||
return NULL;
|
||||
}
|
||||
cJSON_AddItemToArray(relay_tag, cJSON_CreateString("relay"));
|
||||
cJSON_AddItemToArray(relay_tag, cJSON_CreateString(relay_url));
|
||||
cJSON_AddItemToArray(tags, relay_tag);
|
||||
|
||||
// Add challenge tag
|
||||
cJSON* challenge_tag = cJSON_CreateArray();
|
||||
if (!challenge_tag) {
|
||||
cJSON_Delete(tags);
|
||||
return NULL;
|
||||
}
|
||||
cJSON_AddItemToArray(challenge_tag, cJSON_CreateString("challenge"));
|
||||
cJSON_AddItemToArray(challenge_tag, cJSON_CreateString(challenge));
|
||||
cJSON_AddItemToArray(tags, challenge_tag);
|
||||
|
||||
// Create authentication event using existing function
|
||||
// Note: Empty content as per NIP-42 specification
|
||||
cJSON* auth_event = nostr_create_and_sign_event(
|
||||
NOSTR_NIP42_AUTH_EVENT_KIND,
|
||||
"", // Empty content
|
||||
tags,
|
||||
private_key,
|
||||
timestamp
|
||||
);
|
||||
|
||||
cJSON_Delete(tags);
|
||||
return auth_event;
|
||||
}
|
||||
|
||||
/**
|
||||
* Create AUTH message JSON for relay communication
|
||||
*/
|
||||
char* nostr_nip42_create_auth_message(cJSON* auth_event) {
|
||||
if (!auth_event) {
|
||||
return NULL;
|
||||
}
|
||||
|
||||
// Create AUTH message array: ["AUTH", <event-json>]
|
||||
cJSON* message_array = cJSON_CreateArray();
|
||||
if (!message_array) {
|
||||
return NULL;
|
||||
}
|
||||
|
||||
cJSON_AddItemToArray(message_array, cJSON_CreateString("AUTH"));
|
||||
cJSON_AddItemToArray(message_array, cJSON_Duplicate(auth_event, 1));
|
||||
|
||||
char* message_string = cJSON_PrintUnformatted(message_array);
|
||||
cJSON_Delete(message_array);
|
||||
|
||||
return message_string;
|
||||
}
|
||||
|
||||
/**
|
||||
* Validate challenge string format and freshness
|
||||
*/
|
||||
int nostr_nip42_validate_challenge(const char* challenge,
|
||||
time_t received_at,
|
||||
int time_tolerance) {
|
||||
if (!challenge) {
|
||||
return NOSTR_ERROR_INVALID_INPUT;
|
||||
}
|
||||
|
||||
size_t challenge_len = strlen(challenge);
|
||||
|
||||
// Check challenge length
|
||||
if (challenge_len < NOSTR_NIP42_MIN_CHALLENGE_LENGTH) {
|
||||
return NOSTR_ERROR_NIP42_CHALLENGE_TOO_SHORT;
|
||||
}
|
||||
if (challenge_len >= NOSTR_NIP42_MAX_CHALLENGE_LENGTH) {
|
||||
return NOSTR_ERROR_NIP42_CHALLENGE_TOO_LONG;
|
||||
}
|
||||
|
||||
// Check time validity if provided
|
||||
if (received_at > 0) {
|
||||
time_t now = time(NULL);
|
||||
int tolerance = (time_tolerance > 0) ? time_tolerance : NOSTR_NIP42_DEFAULT_TIME_TOLERANCE;
|
||||
|
||||
if (now - received_at > tolerance) {
|
||||
return NOSTR_ERROR_NIP42_CHALLENGE_EXPIRED;
|
||||
}
|
||||
}
|
||||
|
||||
return NOSTR_SUCCESS;
|
||||
}
|
||||
|
||||
/**
|
||||
* Parse AUTH challenge message from relay
|
||||
*/
|
||||
int nostr_nip42_parse_auth_challenge(const char* message,
|
||||
char* challenge_out,
|
||||
size_t challenge_size) {
|
||||
if (!message || !challenge_out || challenge_size == 0) {
|
||||
return NOSTR_ERROR_INVALID_INPUT;
|
||||
}
|
||||
|
||||
cJSON* json = cJSON_Parse(message);
|
||||
if (!json || !cJSON_IsArray(json)) {
|
||||
if (json) cJSON_Delete(json);
|
||||
return NOSTR_ERROR_NIP42_INVALID_MESSAGE_FORMAT;
|
||||
}
|
||||
|
||||
// Check array has exactly 2 elements
|
||||
if (cJSON_GetArraySize(json) != 2) {
|
||||
cJSON_Delete(json);
|
||||
return NOSTR_ERROR_NIP42_INVALID_MESSAGE_FORMAT;
|
||||
}
|
||||
|
||||
// Check first element is "AUTH"
|
||||
cJSON* message_type = cJSON_GetArrayItem(json, 0);
|
||||
if (!message_type || !cJSON_IsString(message_type) ||
|
||||
strcmp(cJSON_GetStringValue(message_type), "AUTH") != 0) {
|
||||
cJSON_Delete(json);
|
||||
return NOSTR_ERROR_NIP42_INVALID_MESSAGE_FORMAT;
|
||||
}
|
||||
|
||||
// Get challenge string
|
||||
cJSON* challenge_item = cJSON_GetArrayItem(json, 1);
|
||||
if (!challenge_item || !cJSON_IsString(challenge_item)) {
|
||||
cJSON_Delete(json);
|
||||
return NOSTR_ERROR_NIP42_INVALID_MESSAGE_FORMAT;
|
||||
}
|
||||
|
||||
const char* challenge_str = cJSON_GetStringValue(challenge_item);
|
||||
if (!challenge_str || strlen(challenge_str) >= challenge_size) {
|
||||
cJSON_Delete(json);
|
||||
return NOSTR_ERROR_NIP42_INVALID_CHALLENGE;
|
||||
}
|
||||
|
||||
strcpy(challenge_out, challenge_str);
|
||||
cJSON_Delete(json);
|
||||
|
||||
return NOSTR_SUCCESS;
|
||||
}
|
||||
|
||||
// =============================================================================
|
||||
// SERVER-SIDE FUNCTIONS
|
||||
// =============================================================================
|
||||
|
||||
/**
|
||||
* Generate cryptographically secure challenge string
|
||||
*/
|
||||
int nostr_nip42_generate_challenge(char* challenge_out, size_t length) {
|
||||
if (!challenge_out || length < NOSTR_NIP42_MIN_CHALLENGE_LENGTH ||
|
||||
length > NOSTR_NIP42_MAX_CHALLENGE_LENGTH / 2) {
|
||||
return NOSTR_ERROR_INVALID_INPUT;
|
||||
}
|
||||
|
||||
// Generate random bytes
|
||||
unsigned char random_bytes[NOSTR_NIP42_MAX_CHALLENGE_LENGTH / 2];
|
||||
if (nostr_secp256k1_get_random_bytes(random_bytes, length) != 1) {
|
||||
return NOSTR_ERROR_CRYPTO_FAILED;
|
||||
}
|
||||
|
||||
// Convert to hex string (reusing existing function)
|
||||
nostr_bytes_to_hex(random_bytes, length, challenge_out);
|
||||
|
||||
return NOSTR_SUCCESS;
|
||||
}
|
||||
|
||||
/**
|
||||
* Verify NIP-42 authentication event
|
||||
*/
|
||||
int nostr_nip42_verify_auth_event(cJSON* auth_event,
|
||||
const char* expected_challenge,
|
||||
const char* relay_url,
|
||||
int time_tolerance) {
|
||||
if (!auth_event || !expected_challenge || !relay_url) {
|
||||
return NOSTR_ERROR_INVALID_INPUT;
|
||||
}
|
||||
|
||||
// First validate basic event structure using existing function
|
||||
int structure_result = nostr_validate_event_structure(auth_event);
|
||||
if (structure_result != NOSTR_SUCCESS) {
|
||||
return structure_result;
|
||||
}
|
||||
|
||||
// Validate NIP-42 specific structure
|
||||
int nip42_structure_result = nostr_nip42_validate_auth_event_structure(
|
||||
auth_event, relay_url, expected_challenge, time_tolerance);
|
||||
if (nip42_structure_result != NOSTR_SUCCESS) {
|
||||
return nip42_structure_result;
|
||||
}
|
||||
|
||||
// Finally verify cryptographic signature using existing function
|
||||
return nostr_verify_event_signature(auth_event);
|
||||
}
|
||||
|
||||
/**
|
||||
* Parse AUTH message from client
|
||||
*/
|
||||
int nostr_nip42_parse_auth_message(const char* message, cJSON** auth_event_out) {
|
||||
if (!message || !auth_event_out) {
|
||||
return NOSTR_ERROR_INVALID_INPUT;
|
||||
}
|
||||
|
||||
cJSON* json = cJSON_Parse(message);
|
||||
if (!json || !cJSON_IsArray(json)) {
|
||||
if (json) cJSON_Delete(json);
|
||||
return NOSTR_ERROR_NIP42_INVALID_MESSAGE_FORMAT;
|
||||
}
|
||||
|
||||
// Check array has exactly 2 elements
|
||||
if (cJSON_GetArraySize(json) != 2) {
|
||||
cJSON_Delete(json);
|
||||
return NOSTR_ERROR_NIP42_INVALID_MESSAGE_FORMAT;
|
||||
}
|
||||
|
||||
// Check first element is "AUTH"
|
||||
cJSON* message_type = cJSON_GetArrayItem(json, 0);
|
||||
if (!message_type || !cJSON_IsString(message_type) ||
|
||||
strcmp(cJSON_GetStringValue(message_type), "AUTH") != 0) {
|
||||
cJSON_Delete(json);
|
||||
return NOSTR_ERROR_NIP42_INVALID_MESSAGE_FORMAT;
|
||||
}
|
||||
|
||||
// Get event object
|
||||
cJSON* event_item = cJSON_GetArrayItem(json, 1);
|
||||
if (!event_item || !cJSON_IsObject(event_item)) {
|
||||
cJSON_Delete(json);
|
||||
return NOSTR_ERROR_NIP42_INVALID_MESSAGE_FORMAT;
|
||||
}
|
||||
|
||||
// Duplicate the event for the caller
|
||||
*auth_event_out = cJSON_Duplicate(event_item, 1);
|
||||
cJSON_Delete(json);
|
||||
|
||||
if (!*auth_event_out) {
|
||||
return NOSTR_ERROR_MEMORY_FAILED;
|
||||
}
|
||||
|
||||
return NOSTR_SUCCESS;
|
||||
}
|
||||
|
||||
/**
|
||||
* Create "auth-required" error response
|
||||
*/
|
||||
char* nostr_nip42_create_auth_required_message(const char* subscription_id,
|
||||
const char* event_id,
|
||||
const char* reason) {
|
||||
const char* default_reason = "authentication required";
|
||||
const char* message_reason = reason ? reason : default_reason;
|
||||
|
||||
cJSON* response = cJSON_CreateArray();
|
||||
if (!response) {
|
||||
return NULL;
|
||||
}
|
||||
|
||||
if (subscription_id) {
|
||||
// CLOSED message for subscriptions
|
||||
cJSON_AddItemToArray(response, cJSON_CreateString("CLOSED"));
|
||||
cJSON_AddItemToArray(response, cJSON_CreateString(subscription_id));
|
||||
|
||||
char prefix_message[512];
|
||||
snprintf(prefix_message, sizeof(prefix_message), "auth-required: %s", message_reason);
|
||||
cJSON_AddItemToArray(response, cJSON_CreateString(prefix_message));
|
||||
} else if (event_id) {
|
||||
// OK message for events
|
||||
cJSON_AddItemToArray(response, cJSON_CreateString("OK"));
|
||||
cJSON_AddItemToArray(response, cJSON_CreateString(event_id));
|
||||
cJSON_AddItemToArray(response, cJSON_CreateBool(0)); // false
|
||||
|
||||
char prefix_message[512];
|
||||
snprintf(prefix_message, sizeof(prefix_message), "auth-required: %s", message_reason);
|
||||
cJSON_AddItemToArray(response, cJSON_CreateString(prefix_message));
|
||||
} else {
|
||||
cJSON_Delete(response);
|
||||
return NULL;
|
||||
}
|
||||
|
||||
char* message_string = cJSON_PrintUnformatted(response);
|
||||
cJSON_Delete(response);
|
||||
|
||||
return message_string;
|
||||
}
|
||||
|
||||
/**
|
||||
* Create "restricted" error response
|
||||
*/
|
||||
char* nostr_nip42_create_restricted_message(const char* subscription_id,
|
||||
const char* event_id,
|
||||
const char* reason) {
|
||||
const char* default_reason = "access restricted";
|
||||
const char* message_reason = reason ? reason : default_reason;
|
||||
|
||||
cJSON* response = cJSON_CreateArray();
|
||||
if (!response) {
|
||||
return NULL;
|
||||
}
|
||||
|
||||
if (subscription_id) {
|
||||
// CLOSED message for subscriptions
|
||||
cJSON_AddItemToArray(response, cJSON_CreateString("CLOSED"));
|
||||
cJSON_AddItemToArray(response, cJSON_CreateString(subscription_id));
|
||||
|
||||
char prefix_message[512];
|
||||
snprintf(prefix_message, sizeof(prefix_message), "restricted: %s", message_reason);
|
||||
cJSON_AddItemToArray(response, cJSON_CreateString(prefix_message));
|
||||
} else if (event_id) {
|
||||
// OK message for events
|
||||
cJSON_AddItemToArray(response, cJSON_CreateString("OK"));
|
||||
cJSON_AddItemToArray(response, cJSON_CreateString(event_id));
|
||||
cJSON_AddItemToArray(response, cJSON_CreateBool(0)); // false
|
||||
|
||||
char prefix_message[512];
|
||||
snprintf(prefix_message, sizeof(prefix_message), "restricted: %s", message_reason);
|
||||
cJSON_AddItemToArray(response, cJSON_CreateString(prefix_message));
|
||||
} else {
|
||||
cJSON_Delete(response);
|
||||
return NULL;
|
||||
}
|
||||
|
||||
char* message_string = cJSON_PrintUnformatted(response);
|
||||
cJSON_Delete(response);
|
||||
|
||||
return message_string;
|
||||
}
|
||||
|
||||
// =============================================================================
|
||||
// URL NORMALIZATION FUNCTIONS
|
||||
// =============================================================================
|
||||
|
||||
/**
|
||||
* Normalize relay URL for comparison
|
||||
*/
|
||||
char* nostr_nip42_normalize_url(const char* url) {
|
||||
if (!url) {
|
||||
return NULL;
|
||||
}
|
||||
|
||||
size_t url_len = strlen(url);
|
||||
char* normalized = malloc(url_len + 1);
|
||||
if (!normalized) {
|
||||
return NULL;
|
||||
}
|
||||
|
||||
strcpy(normalized, url);
|
||||
|
||||
// Remove trailing slash
|
||||
if (url_len > 1 && normalized[url_len - 1] == '/') {
|
||||
normalized[url_len - 1] = '\0';
|
||||
}
|
||||
|
||||
// Convert to lowercase for domain comparison
|
||||
for (size_t i = 0; normalized[i]; i++) {
|
||||
if (normalized[i] >= 'A' && normalized[i] <= 'Z') {
|
||||
normalized[i] = normalized[i] + ('a' - 'A');
|
||||
}
|
||||
}
|
||||
|
||||
return normalized;
|
||||
}
|
||||
|
||||
/**
|
||||
* Check if two relay URLs match after normalization
|
||||
*/
|
||||
int nostr_nip42_urls_match(const char* url1, const char* url2) {
|
||||
if (!url1 || !url2) {
|
||||
return -1;
|
||||
}
|
||||
|
||||
char* norm1 = nostr_nip42_normalize_url(url1);
|
||||
char* norm2 = nostr_nip42_normalize_url(url2);
|
||||
|
||||
if (!norm1 || !norm2) {
|
||||
free(norm1);
|
||||
free(norm2);
|
||||
return -1;
|
||||
}
|
||||
|
||||
int result = (strcmp(norm1, norm2) == 0) ? 1 : 0;
|
||||
|
||||
free(norm1);
|
||||
free(norm2);
|
||||
|
||||
return result;
|
||||
}
|
||||
|
||||
// =============================================================================
|
||||
// UTILITY FUNCTIONS
|
||||
// =============================================================================
|
||||
|
||||
/**
|
||||
* Get string description of authentication state
|
||||
*/
|
||||
const char* nostr_nip42_auth_state_str(nostr_auth_state_t state) {
|
||||
switch (state) {
|
||||
case NOSTR_AUTH_STATE_NONE:
|
||||
return "none";
|
||||
case NOSTR_AUTH_STATE_CHALLENGE_RECEIVED:
|
||||
return "challenge_received";
|
||||
case NOSTR_AUTH_STATE_AUTHENTICATING:
|
||||
return "authenticating";
|
||||
case NOSTR_AUTH_STATE_AUTHENTICATED:
|
||||
return "authenticated";
|
||||
case NOSTR_AUTH_STATE_REJECTED:
|
||||
return "rejected";
|
||||
default:
|
||||
return "unknown";
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Initialize authentication context structure
|
||||
*/
|
||||
int nostr_nip42_init_auth_context(nostr_auth_context_t* ctx,
|
||||
const char* relay_url,
|
||||
const char* challenge,
|
||||
int time_tolerance) {
|
||||
if (!ctx || !relay_url || !challenge) {
|
||||
return NOSTR_ERROR_INVALID_INPUT;
|
||||
}
|
||||
|
||||
memset(ctx, 0, sizeof(nostr_auth_context_t));
|
||||
|
||||
ctx->relay_url = malloc(strlen(relay_url) + 1);
|
||||
if (!ctx->relay_url) {
|
||||
return NOSTR_ERROR_MEMORY_FAILED;
|
||||
}
|
||||
strcpy(ctx->relay_url, relay_url);
|
||||
|
||||
ctx->challenge = malloc(strlen(challenge) + 1);
|
||||
if (!ctx->challenge) {
|
||||
free(ctx->relay_url);
|
||||
ctx->relay_url = NULL;
|
||||
return NOSTR_ERROR_MEMORY_FAILED;
|
||||
}
|
||||
strcpy(ctx->challenge, challenge);
|
||||
|
||||
ctx->timestamp = time(NULL);
|
||||
ctx->time_tolerance = (time_tolerance > 0) ? time_tolerance : NOSTR_NIP42_DEFAULT_TIME_TOLERANCE;
|
||||
|
||||
return NOSTR_SUCCESS;
|
||||
}
|
||||
|
||||
/**
|
||||
* Free authentication context structure
|
||||
*/
|
||||
void nostr_nip42_free_auth_context(nostr_auth_context_t* ctx) {
|
||||
if (!ctx) {
|
||||
return;
|
||||
}
|
||||
|
||||
free(ctx->relay_url);
|
||||
free(ctx->challenge);
|
||||
free(ctx->pubkey_hex);
|
||||
|
||||
memset(ctx, 0, sizeof(nostr_auth_context_t));
|
||||
}
|
||||
|
||||
/**
|
||||
* Validate authentication event structure (without signature verification)
|
||||
*/
|
||||
int nostr_nip42_validate_auth_event_structure(cJSON* auth_event,
|
||||
const char* relay_url,
|
||||
const char* challenge,
|
||||
int time_tolerance) {
|
||||
if (!auth_event || !relay_url || !challenge) {
|
||||
return NOSTR_ERROR_INVALID_INPUT;
|
||||
}
|
||||
|
||||
// Check event kind is 22242
|
||||
cJSON* kind_item = cJSON_GetObjectItem(auth_event, "kind");
|
||||
if (!kind_item || !cJSON_IsNumber(kind_item) ||
|
||||
(int)cJSON_GetNumberValue(kind_item) != NOSTR_NIP42_AUTH_EVENT_KIND) {
|
||||
return NOSTR_ERROR_NIP42_AUTH_EVENT_INVALID;
|
||||
}
|
||||
|
||||
// Check timestamp is within tolerance
|
||||
cJSON* created_at_item = cJSON_GetObjectItem(auth_event, "created_at");
|
||||
if (!created_at_item || !cJSON_IsNumber(created_at_item)) {
|
||||
return NOSTR_ERROR_EVENT_INVALID_CREATED_AT;
|
||||
}
|
||||
|
||||
time_t event_time = (time_t)cJSON_GetNumberValue(created_at_item);
|
||||
time_t now = time(NULL);
|
||||
int tolerance = (time_tolerance > 0) ? time_tolerance : NOSTR_NIP42_DEFAULT_TIME_TOLERANCE;
|
||||
|
||||
if (abs((int)(now - event_time)) > tolerance) {
|
||||
return NOSTR_ERROR_NIP42_TIME_TOLERANCE;
|
||||
}
|
||||
|
||||
// Check tags contain required relay and challenge
|
||||
cJSON* tags_item = cJSON_GetObjectItem(auth_event, "tags");
|
||||
if (!tags_item || !cJSON_IsArray(tags_item)) {
|
||||
return NOSTR_ERROR_EVENT_INVALID_TAGS;
|
||||
}
|
||||
|
||||
int found_relay = 0, found_challenge = 0;
|
||||
|
||||
cJSON* tag_item;
|
||||
cJSON_ArrayForEach(tag_item, tags_item) {
|
||||
if (!cJSON_IsArray(tag_item) || cJSON_GetArraySize(tag_item) < 2) {
|
||||
continue;
|
||||
}
|
||||
|
||||
cJSON* tag_name = cJSON_GetArrayItem(tag_item, 0);
|
||||
cJSON* tag_value = cJSON_GetArrayItem(tag_item, 1);
|
||||
|
||||
if (!cJSON_IsString(tag_name) || !cJSON_IsString(tag_value)) {
|
||||
continue;
|
||||
}
|
||||
|
||||
const char* name = cJSON_GetStringValue(tag_name);
|
||||
const char* value = cJSON_GetStringValue(tag_value);
|
||||
|
||||
if (strcmp(name, "relay") == 0) {
|
||||
if (nostr_nip42_urls_match(value, relay_url) == 1) {
|
||||
found_relay = 1;
|
||||
}
|
||||
} else if (strcmp(name, "challenge") == 0) {
|
||||
if (strcmp(value, challenge) == 0) {
|
||||
found_challenge = 1;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
if (!found_relay) {
|
||||
return NOSTR_ERROR_NIP42_URL_MISMATCH;
|
||||
}
|
||||
|
||||
if (!found_challenge) {
|
||||
return NOSTR_ERROR_NIP42_INVALID_CHALLENGE;
|
||||
}
|
||||
|
||||
return NOSTR_SUCCESS;
|
||||
}
|
||||
|
||||
// =============================================================================
|
||||
// WEBSOCKET CLIENT INTEGRATION STUB FUNCTIONS
|
||||
// =============================================================================
|
||||
// Note: These will need to be implemented when WebSocket client structure is available
|
||||
|
||||
int nostr_ws_authenticate(struct nostr_ws_client* client,
|
||||
const unsigned char* private_key,
|
||||
int time_tolerance) {
|
||||
// TODO: Implement when WebSocket client structure is available
|
||||
(void)client;
|
||||
(void)private_key;
|
||||
(void)time_tolerance;
|
||||
return NOSTR_ERROR_NETWORK_FAILED; // Placeholder
|
||||
}
|
||||
|
||||
nostr_auth_state_t nostr_ws_get_auth_state(struct nostr_ws_client* client) {
|
||||
// TODO: Implement when WebSocket client structure is available
|
||||
(void)client;
|
||||
return NOSTR_AUTH_STATE_NONE; // Placeholder
|
||||
}
|
||||
|
||||
int nostr_ws_has_valid_challenge(struct nostr_ws_client* client) {
|
||||
// TODO: Implement when WebSocket client structure is available
|
||||
(void)client;
|
||||
return 0; // Placeholder
|
||||
}
|
||||
|
||||
int nostr_ws_get_challenge(struct nostr_ws_client* client,
|
||||
char* challenge_out,
|
||||
size_t challenge_size) {
|
||||
// TODO: Implement when WebSocket client structure is available
|
||||
(void)client;
|
||||
(void)challenge_out;
|
||||
(void)challenge_size;
|
||||
return NOSTR_ERROR_NETWORK_FAILED; // Placeholder
|
||||
}
|
||||
|
||||
int nostr_ws_store_challenge(struct nostr_ws_client* client,
|
||||
const char* challenge) {
|
||||
// TODO: Implement when WebSocket client structure is available
|
||||
(void)client;
|
||||
(void)challenge;
|
||||
return NOSTR_ERROR_NETWORK_FAILED; // Placeholder
|
||||
}
|
||||
|
||||
int nostr_ws_clear_auth_state(struct nostr_ws_client* client) {
|
||||
// TODO: Implement when WebSocket client structure is available
|
||||
(void)client;
|
||||
return NOSTR_ERROR_NETWORK_FAILED; // Placeholder
|
||||
}
|
||||
@@ -0,0 +1,281 @@
|
||||
/*
|
||||
* NOSTR Core Library - NIP-042: Authentication of clients to relays
|
||||
*
|
||||
* Implements client authentication through signed ephemeral events
|
||||
*/
|
||||
|
||||
#ifndef NIP042_H
|
||||
#define NIP042_H
|
||||
|
||||
#include <stddef.h>
|
||||
#include <stdint.h>
|
||||
#include <time.h>
|
||||
#include "../cjson/cJSON.h"
|
||||
#include "nostr_common.h"
|
||||
|
||||
#ifdef __cplusplus
|
||||
extern "C" {
|
||||
#endif
|
||||
|
||||
// =============================================================================
|
||||
// NIP-42 CONSTANTS AND DEFINITIONS
|
||||
// =============================================================================
|
||||
|
||||
#define NOSTR_NIP42_AUTH_EVENT_KIND 22242
|
||||
#define NOSTR_NIP42_DEFAULT_CHALLENGE_LENGTH 32
|
||||
#define NOSTR_NIP42_DEFAULT_TIME_TOLERANCE 600 // 10 minutes in seconds
|
||||
#define NOSTR_NIP42_MAX_CHALLENGE_LENGTH 256
|
||||
#define NOSTR_NIP42_MIN_CHALLENGE_LENGTH 16
|
||||
|
||||
// Authentication states for WebSocket client integration
|
||||
typedef enum {
|
||||
NOSTR_AUTH_STATE_NONE = 0, // No authentication attempted
|
||||
NOSTR_AUTH_STATE_CHALLENGE_RECEIVED = 1, // Challenge received from relay
|
||||
NOSTR_AUTH_STATE_AUTHENTICATING = 2, // AUTH event sent, waiting for OK
|
||||
NOSTR_AUTH_STATE_AUTHENTICATED = 3, // Successfully authenticated
|
||||
NOSTR_AUTH_STATE_REJECTED = 4 // Authentication rejected
|
||||
} nostr_auth_state_t;
|
||||
|
||||
// Challenge storage structure
|
||||
typedef struct {
|
||||
char challenge[NOSTR_NIP42_MAX_CHALLENGE_LENGTH];
|
||||
time_t received_at;
|
||||
int is_valid;
|
||||
} nostr_auth_challenge_t;
|
||||
|
||||
// Authentication context for relay verification
|
||||
typedef struct {
|
||||
char* relay_url;
|
||||
char* challenge;
|
||||
time_t timestamp;
|
||||
int time_tolerance;
|
||||
char* pubkey_hex;
|
||||
} nostr_auth_context_t;
|
||||
|
||||
// =============================================================================
|
||||
// CLIENT-SIDE FUNCTIONS (for nostr clients)
|
||||
// =============================================================================
|
||||
|
||||
/**
|
||||
* Create NIP-42 authentication event (kind 22242)
|
||||
* @param challenge Challenge string received from relay
|
||||
* @param relay_url Relay URL (normalized)
|
||||
* @param private_key 32-byte private key for signing
|
||||
* @param timestamp Event timestamp (0 for current time)
|
||||
* @return cJSON event object or NULL on error
|
||||
*/
|
||||
cJSON* nostr_nip42_create_auth_event(const char* challenge,
|
||||
const char* relay_url,
|
||||
const unsigned char* private_key,
|
||||
time_t timestamp);
|
||||
|
||||
/**
|
||||
* Create AUTH message JSON for relay communication
|
||||
* @param auth_event Authentication event (kind 22242)
|
||||
* @return JSON string for AUTH message or NULL on error (caller must free)
|
||||
*/
|
||||
char* nostr_nip42_create_auth_message(cJSON* auth_event);
|
||||
|
||||
/**
|
||||
* Validate challenge string format and freshness
|
||||
* @param challenge Challenge string to validate
|
||||
* @param received_at Time when challenge was received (0 for no time check)
|
||||
* @param time_tolerance Maximum age in seconds (0 for default)
|
||||
* @return NOSTR_SUCCESS or error code
|
||||
*/
|
||||
int nostr_nip42_validate_challenge(const char* challenge,
|
||||
time_t received_at,
|
||||
int time_tolerance);
|
||||
|
||||
/**
|
||||
* Parse AUTH challenge message from relay
|
||||
* @param message Raw message from relay
|
||||
* @param challenge_out Output buffer for challenge string
|
||||
* @param challenge_size Size of challenge buffer
|
||||
* @return NOSTR_SUCCESS or error code
|
||||
*/
|
||||
int nostr_nip42_parse_auth_challenge(const char* message,
|
||||
char* challenge_out,
|
||||
size_t challenge_size);
|
||||
|
||||
// =============================================================================
|
||||
// SERVER-SIDE FUNCTIONS (for relay implementations)
|
||||
// =============================================================================
|
||||
|
||||
/**
|
||||
* Generate cryptographically secure challenge string
|
||||
* @param challenge_out Output buffer for challenge (must be at least length*2+1)
|
||||
* @param length Desired challenge length in bytes (16-128)
|
||||
* @return NOSTR_SUCCESS or error code
|
||||
*/
|
||||
int nostr_nip42_generate_challenge(char* challenge_out, size_t length);
|
||||
|
||||
/**
|
||||
* Verify NIP-42 authentication event
|
||||
* @param auth_event Authentication event to verify
|
||||
* @param expected_challenge Challenge that was sent to client
|
||||
* @param relay_url Expected relay URL
|
||||
* @param time_tolerance Maximum timestamp deviation in seconds
|
||||
* @return NOSTR_SUCCESS or error code
|
||||
*/
|
||||
int nostr_nip42_verify_auth_event(cJSON* auth_event,
|
||||
const char* expected_challenge,
|
||||
const char* relay_url,
|
||||
int time_tolerance);
|
||||
|
||||
/**
|
||||
* Parse AUTH message from client
|
||||
* @param message Raw AUTH message from client
|
||||
* @param auth_event_out Output pointer to parsed event (caller must free)
|
||||
* @return NOSTR_SUCCESS or error code
|
||||
*/
|
||||
int nostr_nip42_parse_auth_message(const char* message, cJSON** auth_event_out);
|
||||
|
||||
/**
|
||||
* Create "auth-required" error response
|
||||
* @param subscription_id Subscription ID (for CLOSED) or NULL (for OK)
|
||||
* @param event_id Event ID (for OK) or NULL (for CLOSED)
|
||||
* @param reason Human-readable reason
|
||||
* @return JSON string for response or NULL on error (caller must free)
|
||||
*/
|
||||
char* nostr_nip42_create_auth_required_message(const char* subscription_id,
|
||||
const char* event_id,
|
||||
const char* reason);
|
||||
|
||||
/**
|
||||
* Create "restricted" error response
|
||||
* @param subscription_id Subscription ID (for CLOSED) or NULL (for OK)
|
||||
* @param event_id Event ID (for OK) or NULL (for CLOSED)
|
||||
* @param reason Human-readable reason
|
||||
* @return JSON string for response or NULL on error (caller must free)
|
||||
*/
|
||||
char* nostr_nip42_create_restricted_message(const char* subscription_id,
|
||||
const char* event_id,
|
||||
const char* reason);
|
||||
|
||||
// =============================================================================
|
||||
// URL NORMALIZATION FUNCTIONS
|
||||
// =============================================================================
|
||||
|
||||
/**
|
||||
* Normalize relay URL for comparison (removes trailing slashes, etc.)
|
||||
* @param url Original URL
|
||||
* @return Normalized URL string or NULL on error (caller must free)
|
||||
*/
|
||||
char* nostr_nip42_normalize_url(const char* url);
|
||||
|
||||
/**
|
||||
* Check if two relay URLs match after normalization
|
||||
* @param url1 First URL
|
||||
* @param url2 Second URL
|
||||
* @return 1 if URLs match, 0 if they don't, -1 on error
|
||||
*/
|
||||
int nostr_nip42_urls_match(const char* url1, const char* url2);
|
||||
|
||||
// =============================================================================
|
||||
// UTILITY FUNCTIONS
|
||||
// =============================================================================
|
||||
|
||||
/**
|
||||
* Get string description of authentication state
|
||||
* @param state Authentication state
|
||||
* @return Human-readable string
|
||||
*/
|
||||
const char* nostr_nip42_auth_state_str(nostr_auth_state_t state);
|
||||
|
||||
/**
|
||||
* Initialize authentication context structure
|
||||
* @param ctx Context to initialize
|
||||
* @param relay_url Relay URL
|
||||
* @param challenge Challenge string
|
||||
* @param time_tolerance Time tolerance in seconds
|
||||
* @return NOSTR_SUCCESS or error code
|
||||
*/
|
||||
int nostr_nip42_init_auth_context(nostr_auth_context_t* ctx,
|
||||
const char* relay_url,
|
||||
const char* challenge,
|
||||
int time_tolerance);
|
||||
|
||||
/**
|
||||
* Free authentication context structure
|
||||
* @param ctx Context to free
|
||||
*/
|
||||
void nostr_nip42_free_auth_context(nostr_auth_context_t* ctx);
|
||||
|
||||
/**
|
||||
* Validate authentication event structure (without signature verification)
|
||||
* @param auth_event Event to validate
|
||||
* @param relay_url Expected relay URL
|
||||
* @param challenge Expected challenge
|
||||
* @param time_tolerance Maximum timestamp deviation in seconds
|
||||
* @return NOSTR_SUCCESS or error code
|
||||
*/
|
||||
int nostr_nip42_validate_auth_event_structure(cJSON* auth_event,
|
||||
const char* relay_url,
|
||||
const char* challenge,
|
||||
int time_tolerance);
|
||||
|
||||
// =============================================================================
|
||||
// WEBSOCKET CLIENT INTEGRATION
|
||||
// =============================================================================
|
||||
|
||||
// Forward declaration for WebSocket client
|
||||
struct nostr_ws_client;
|
||||
|
||||
/**
|
||||
* Authenticate WebSocket client with relay
|
||||
* @param client WebSocket client handle
|
||||
* @param private_key 32-byte private key for authentication
|
||||
* @param time_tolerance Maximum timestamp deviation in seconds (0 for default)
|
||||
* @return NOSTR_SUCCESS or error code
|
||||
*/
|
||||
int nostr_ws_authenticate(struct nostr_ws_client* client,
|
||||
const unsigned char* private_key,
|
||||
int time_tolerance);
|
||||
|
||||
/**
|
||||
* Get current authentication state of WebSocket client
|
||||
* @param client WebSocket client handle
|
||||
* @return Current authentication state
|
||||
*/
|
||||
nostr_auth_state_t nostr_ws_get_auth_state(struct nostr_ws_client* client);
|
||||
|
||||
/**
|
||||
* Check if WebSocket client has stored valid challenge
|
||||
* @param client WebSocket client handle
|
||||
* @return 1 if valid challenge exists, 0 otherwise
|
||||
*/
|
||||
int nostr_ws_has_valid_challenge(struct nostr_ws_client* client);
|
||||
|
||||
/**
|
||||
* Get stored challenge from WebSocket client
|
||||
* @param client WebSocket client handle
|
||||
* @param challenge_out Output buffer for challenge
|
||||
* @param challenge_size Size of output buffer
|
||||
* @return NOSTR_SUCCESS or error code
|
||||
*/
|
||||
int nostr_ws_get_challenge(struct nostr_ws_client* client,
|
||||
char* challenge_out,
|
||||
size_t challenge_size);
|
||||
|
||||
/**
|
||||
* Store challenge in WebSocket client (internal function)
|
||||
* @param client WebSocket client handle
|
||||
* @param challenge Challenge string to store
|
||||
* @return NOSTR_SUCCESS or error code
|
||||
*/
|
||||
int nostr_ws_store_challenge(struct nostr_ws_client* client,
|
||||
const char* challenge);
|
||||
|
||||
/**
|
||||
* Clear authentication state in WebSocket client
|
||||
* @param client WebSocket client handle
|
||||
* @return NOSTR_SUCCESS or error code
|
||||
*/
|
||||
int nostr_ws_clear_auth_state(struct nostr_ws_client* client);
|
||||
|
||||
#ifdef __cplusplus
|
||||
}
|
||||
#endif
|
||||
|
||||
#endif // NIP042_H
|
||||
@@ -38,6 +38,20 @@ const char* nostr_strerror(int error_code) {
|
||||
case NOSTR_ERROR_EVENT_INVALID_KIND: return "Event has invalid kind";
|
||||
case NOSTR_ERROR_EVENT_INVALID_TAGS: return "Event has invalid tags";
|
||||
case NOSTR_ERROR_EVENT_INVALID_CONTENT: return "Event has invalid content";
|
||||
case NOSTR_ERROR_NIP13_INSUFFICIENT: return "NIP-13: Insufficient PoW difficulty";
|
||||
case NOSTR_ERROR_NIP13_NO_NONCE_TAG: return "NIP-13: Missing nonce tag";
|
||||
case NOSTR_ERROR_NIP13_INVALID_NONCE_TAG: return "NIP-13: Invalid nonce tag format";
|
||||
case NOSTR_ERROR_NIP13_TARGET_MISMATCH: return "NIP-13: Target difficulty mismatch";
|
||||
case NOSTR_ERROR_NIP13_CALCULATION: return "NIP-13: PoW calculation error";
|
||||
case NOSTR_ERROR_NIP42_INVALID_CHALLENGE: return "NIP-42: Invalid challenge";
|
||||
case NOSTR_ERROR_NIP42_CHALLENGE_EXPIRED: return "NIP-42: Challenge expired";
|
||||
case NOSTR_ERROR_NIP42_AUTH_EVENT_INVALID: return "NIP-42: Authentication event invalid";
|
||||
case NOSTR_ERROR_NIP42_URL_MISMATCH: return "NIP-42: Relay URL mismatch";
|
||||
case NOSTR_ERROR_NIP42_TIME_TOLERANCE: return "NIP-42: Timestamp outside tolerance";
|
||||
case NOSTR_ERROR_NIP42_NOT_AUTHENTICATED: return "NIP-42: Client not authenticated";
|
||||
case NOSTR_ERROR_NIP42_INVALID_MESSAGE_FORMAT: return "NIP-42: Invalid message format";
|
||||
case NOSTR_ERROR_NIP42_CHALLENGE_TOO_SHORT: return "NIP-42: Challenge too short";
|
||||
case NOSTR_ERROR_NIP42_CHALLENGE_TOO_LONG: return "NIP-42: Challenge too long";
|
||||
default: return "Unknown error";
|
||||
}
|
||||
}
|
||||
|
||||
+26
-56
@@ -36,6 +36,31 @@
|
||||
#define NOSTR_ERROR_EVENT_INVALID_TAGS -36
|
||||
#define NOSTR_ERROR_EVENT_INVALID_CONTENT -37
|
||||
|
||||
// Authentication Rules System Error Codes
|
||||
#define NOSTR_ERROR_AUTH_RULES_DISABLED -50
|
||||
#define NOSTR_ERROR_AUTH_RULES_DENIED -51
|
||||
#define NOSTR_ERROR_AUTH_RULES_DB_FAILED -52
|
||||
#define NOSTR_ERROR_AUTH_RULES_INVALID_RULE -53
|
||||
#define NOSTR_ERROR_AUTH_RULES_CACHE_FAILED -54
|
||||
#define NOSTR_ERROR_AUTH_RULES_BACKEND_NOT_FOUND -55
|
||||
|
||||
// NIP-13 PoW-specific error codes
|
||||
#define NOSTR_ERROR_NIP13_INSUFFICIENT -100
|
||||
#define NOSTR_ERROR_NIP13_NO_NONCE_TAG -101
|
||||
#define NOSTR_ERROR_NIP13_INVALID_NONCE_TAG -102
|
||||
#define NOSTR_ERROR_NIP13_TARGET_MISMATCH -103
|
||||
#define NOSTR_ERROR_NIP13_CALCULATION -104
|
||||
|
||||
// NIP-42 Authentication-specific error codes
|
||||
#define NOSTR_ERROR_NIP42_INVALID_CHALLENGE -200
|
||||
#define NOSTR_ERROR_NIP42_CHALLENGE_EXPIRED -201
|
||||
#define NOSTR_ERROR_NIP42_AUTH_EVENT_INVALID -202
|
||||
#define NOSTR_ERROR_NIP42_URL_MISMATCH -203
|
||||
#define NOSTR_ERROR_NIP42_TIME_TOLERANCE -204
|
||||
#define NOSTR_ERROR_NIP42_NOT_AUTHENTICATED -205
|
||||
#define NOSTR_ERROR_NIP42_INVALID_MESSAGE_FORMAT -206
|
||||
#define NOSTR_ERROR_NIP42_CHALLENGE_TOO_SHORT -207
|
||||
#define NOSTR_ERROR_NIP42_CHALLENGE_TOO_LONG -208
|
||||
|
||||
// Constants
|
||||
#define NOSTR_PRIVATE_KEY_SIZE 32
|
||||
@@ -56,66 +81,11 @@
|
||||
// Forward declaration for cJSON (to avoid requiring cJSON.h in header)
|
||||
struct cJSON;
|
||||
|
||||
// Relay query modes
|
||||
typedef enum {
|
||||
RELAY_QUERY_FIRST_RESULT, // Return as soon as first event is received
|
||||
RELAY_QUERY_MOST_RECENT, // Return the most recent event from all relays
|
||||
RELAY_QUERY_ALL_RESULTS // Return all unique events from all relays
|
||||
} relay_query_mode_t;
|
||||
|
||||
// Publish result types
|
||||
typedef enum {
|
||||
PUBLISH_SUCCESS, // Event was accepted by relay
|
||||
PUBLISH_REJECTED, // Event was rejected by relay
|
||||
PUBLISH_TIMEOUT, // No response within timeout
|
||||
PUBLISH_ERROR // Connection or other error
|
||||
} publish_result_t;
|
||||
|
||||
// Progress callback function types
|
||||
typedef void (*relay_progress_callback_t)(
|
||||
const char* relay_url,
|
||||
const char* status,
|
||||
const char* event_id,
|
||||
int events_received,
|
||||
int total_relays,
|
||||
int completed_relays,
|
||||
void* user_data);
|
||||
|
||||
typedef void (*publish_progress_callback_t)(
|
||||
const char* relay_url,
|
||||
const char* status,
|
||||
const char* message,
|
||||
int success_count,
|
||||
int total_relays,
|
||||
int completed_relays,
|
||||
void* user_data);
|
||||
|
||||
// Function declarations
|
||||
const char* nostr_strerror(int error_code);
|
||||
|
||||
// Library initialization functions
|
||||
// Library initialization functions
|
||||
int nostr_init(void);
|
||||
void nostr_cleanup(void);
|
||||
|
||||
// Relay query functions
|
||||
struct cJSON** synchronous_query_relays_with_progress(
|
||||
const char** relay_urls,
|
||||
int relay_count,
|
||||
struct cJSON* filter,
|
||||
relay_query_mode_t mode,
|
||||
int* result_count,
|
||||
int relay_timeout_seconds,
|
||||
relay_progress_callback_t callback,
|
||||
void* user_data);
|
||||
|
||||
// Relay publish functions
|
||||
publish_result_t* synchronous_publish_event_with_progress(
|
||||
const char** relay_urls,
|
||||
int relay_count,
|
||||
struct cJSON* event,
|
||||
int* success_count,
|
||||
int relay_timeout_seconds,
|
||||
publish_progress_callback_t callback,
|
||||
void* user_data);
|
||||
|
||||
#endif // NOSTR_COMMON_H
|
||||
|
||||
+229
-3
@@ -1,6 +1,12 @@
|
||||
#ifndef NOSTR_CORE_H
|
||||
#define NOSTR_CORE_H
|
||||
|
||||
// Version information (auto-updated by increment_and_push.sh)
|
||||
#define VERSION "v0.4.3"
|
||||
#define VERSION_MAJOR 0
|
||||
#define VERSION_MINOR 4
|
||||
#define VERSION_PATCH 3
|
||||
|
||||
/*
|
||||
* NOSTR Core Library - Complete API Reference
|
||||
*
|
||||
@@ -42,7 +48,14 @@
|
||||
* - nostr_nip44_encrypt() -> Encrypt with ChaCha20 + HMAC
|
||||
* - nostr_nip44_encrypt_with_nonce() -> Encrypt with specific nonce (testing)
|
||||
* - nostr_nip44_decrypt() -> Decrypt ChaCha20 + HMAC messages
|
||||
*
|
||||
*
|
||||
* NIP-42 AUTHENTICATION:
|
||||
* - nostr_nip42_create_auth_event() -> Create authentication event (kind 22242)
|
||||
* - nostr_nip42_verify_auth_event() -> Verify authentication event (relay-side)
|
||||
* - nostr_nip42_generate_challenge() -> Generate challenge string (relay-side)
|
||||
* - nostr_ws_authenticate() -> Authenticate WebSocket client
|
||||
* - nostr_ws_get_auth_state() -> Get client authentication state
|
||||
*
|
||||
* BIP39 MNEMONICS:
|
||||
* - nostr_bip39_mnemonic_from_bytes() -> Generate mnemonic from entropy
|
||||
* - nostr_bip39_mnemonic_validate() -> Validate mnemonic phrase
|
||||
@@ -65,7 +78,25 @@
|
||||
* - nostr_hex_to_bytes() -> Convert hex string to bytes
|
||||
* - base64_encode() -> Base64 encoding
|
||||
* - base64_decode() -> Base64 decoding
|
||||
*
|
||||
*
|
||||
* REQUEST VALIDATION & AUTHENTICATION:
|
||||
* - nostr_validate_request() -> Unified request validation (events + auth rules)
|
||||
* - nostr_request_validator_init() -> Initialize authentication system
|
||||
* - nostr_auth_check_upload() -> Validate file upload requests
|
||||
* - nostr_auth_check_delete() -> Validate file delete requests
|
||||
* - nostr_auth_check_publish() -> Validate event publish requests
|
||||
* - nostr_auth_rule_add() -> Add authentication rule
|
||||
* - nostr_auth_rule_remove() -> Remove authentication rule
|
||||
*
|
||||
* RELAY OPERATIONS:
|
||||
* - synchronous_query_relays_with_progress() -> One-off query from multiple relays
|
||||
* - synchronous_publish_event_with_progress() -> One-off publish to multiple relays
|
||||
* *
|
||||
* RELAY POOL OPERATIONS:
|
||||
* - nostr_relay_pool_create() -> Create relay pool for persistent connections
|
||||
* - nostr_relay_pool_subscribe() -> Subscribe to events with callbacks
|
||||
* - nostr_relay_pool_run() -> Run event loop for receiving events
|
||||
|
||||
* SYSTEM FUNCTIONS:
|
||||
* - nostr_crypto_init() -> Initialize crypto subsystem
|
||||
* - nostr_crypto_cleanup() -> Cleanup crypto subsystem
|
||||
@@ -96,7 +127,11 @@
|
||||
* nostr_bip32_key_from_seed(seed, 64, &master_key);
|
||||
* uint32_t path[] = {44, 1237, 0, 0, 0}; // m/44'/1237'/0'/0/0
|
||||
* nostr_bip32_derive_path(&master_key, path, 5, &derived_key);
|
||||
*
|
||||
*
|
||||
* Client Authentication (NIP-42):
|
||||
* cJSON* auth_event = nostr_nip42_create_auth_event(challenge, relay_url, private_key, 0);
|
||||
* nostr_ws_authenticate(client, private_key, 600); // Auto-authenticate WebSocket
|
||||
*
|
||||
* ============================================================================
|
||||
*/
|
||||
|
||||
@@ -116,8 +151,199 @@ extern "C" {
|
||||
#include "nip011.h" // Relay information document
|
||||
#include "nip013.h" // Proof of Work
|
||||
#include "nip019.h" // Bech32 encoding (nsec/npub)
|
||||
#include "nip042.h" // Authentication of clients to relays
|
||||
#include "nip044.h" // Encryption (modern)
|
||||
|
||||
// Authentication and request validation system
|
||||
#include "request_validator.h" // Request validation and authentication rules
|
||||
|
||||
// Relay pool types and functions
|
||||
typedef enum {
|
||||
NOSTR_POOL_RELAY_DISCONNECTED = 0,
|
||||
NOSTR_POOL_RELAY_CONNECTING = 1,
|
||||
NOSTR_POOL_RELAY_CONNECTED = 2,
|
||||
NOSTR_POOL_RELAY_ERROR = -1
|
||||
} nostr_pool_relay_status_t;
|
||||
|
||||
// EOSE result mode for subscriptions
|
||||
typedef enum {
|
||||
NOSTR_POOL_EOSE_FULL_SET, // Wait for all relays, return all events
|
||||
NOSTR_POOL_EOSE_MOST_RECENT, // Wait for all relays, return most recent event
|
||||
NOSTR_POOL_EOSE_FIRST // Return results on first EOSE (fastest response)
|
||||
} nostr_pool_eose_result_mode_t;
|
||||
|
||||
typedef struct {
|
||||
int connection_attempts;
|
||||
int connection_failures;
|
||||
int events_received;
|
||||
int events_published;
|
||||
int events_published_ok;
|
||||
int events_published_failed;
|
||||
time_t last_event_time;
|
||||
time_t connection_uptime_start;
|
||||
double ping_latency_avg;
|
||||
double ping_latency_min;
|
||||
double ping_latency_max;
|
||||
double ping_latency_current;
|
||||
int ping_samples;
|
||||
double query_latency_avg;
|
||||
double query_latency_min;
|
||||
double query_latency_max;
|
||||
int query_samples;
|
||||
double publish_latency_avg;
|
||||
int publish_samples;
|
||||
} nostr_relay_stats_t;
|
||||
|
||||
typedef struct nostr_relay_pool nostr_relay_pool_t;
|
||||
typedef struct nostr_pool_subscription nostr_pool_subscription_t;
|
||||
|
||||
// Reconnection configuration
|
||||
typedef struct {
|
||||
int enable_auto_reconnect; // 1 = enable, 0 = disable
|
||||
int max_reconnect_attempts; // Max attempts per relay
|
||||
int initial_reconnect_delay_ms; // Initial delay between attempts
|
||||
int max_reconnect_delay_ms; // Max delay (cap exponential backoff)
|
||||
int reconnect_backoff_multiplier; // Delay multiplier
|
||||
int ping_interval_seconds; // How often to ping (0 = disable)
|
||||
int pong_timeout_seconds; // How long to wait for pong before reconnecting
|
||||
} nostr_pool_reconnect_config_t;
|
||||
|
||||
// Relay pool management functions
|
||||
nostr_relay_pool_t* nostr_relay_pool_create(nostr_pool_reconnect_config_t* config);
|
||||
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);
|
||||
void nostr_relay_pool_destroy(nostr_relay_pool_t* pool);
|
||||
|
||||
// Subscription management
|
||||
nostr_pool_subscription_t* nostr_relay_pool_subscribe(
|
||||
nostr_relay_pool_t* pool,
|
||||
const char** relay_urls,
|
||||
int relay_count,
|
||||
cJSON* filter,
|
||||
void (*on_event)(cJSON* event, const char* relay_url, void* user_data),
|
||||
void (*on_eose)(cJSON** events, int event_count, void* user_data),
|
||||
void* user_data,
|
||||
int close_on_eose,
|
||||
int enable_deduplication,
|
||||
nostr_pool_eose_result_mode_t result_mode,
|
||||
int relay_timeout_seconds,
|
||||
int eose_timeout_seconds);
|
||||
int nostr_pool_subscription_close(nostr_pool_subscription_t* subscription);
|
||||
|
||||
// Backward compatibility wrapper
|
||||
nostr_pool_subscription_t* nostr_relay_pool_subscribe_compat(
|
||||
nostr_relay_pool_t* pool,
|
||||
const char** relay_urls,
|
||||
int relay_count,
|
||||
cJSON* filter,
|
||||
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);
|
||||
|
||||
// Event loop functions
|
||||
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);
|
||||
|
||||
// Synchronous query/publish functions
|
||||
cJSON** nostr_relay_pool_query_sync(
|
||||
nostr_relay_pool_t* pool,
|
||||
const char** relay_urls,
|
||||
int relay_count,
|
||||
cJSON* filter,
|
||||
int* event_count,
|
||||
int timeout_ms);
|
||||
cJSON* nostr_relay_pool_get_event(
|
||||
nostr_relay_pool_t* pool,
|
||||
const char** relay_urls,
|
||||
int relay_count,
|
||||
cJSON* filter,
|
||||
int timeout_ms);
|
||||
int nostr_relay_pool_publish(
|
||||
nostr_relay_pool_t* pool,
|
||||
const char** relay_urls,
|
||||
int relay_count,
|
||||
cJSON* event);
|
||||
|
||||
// Status and statistics functions
|
||||
nostr_pool_relay_status_t nostr_relay_pool_get_relay_status(
|
||||
nostr_relay_pool_t* pool,
|
||||
const char* relay_url);
|
||||
int nostr_relay_pool_list_relays(
|
||||
nostr_relay_pool_t* pool,
|
||||
char*** relay_urls,
|
||||
nostr_pool_relay_status_t** statuses);
|
||||
const nostr_relay_stats_t* nostr_relay_pool_get_relay_stats(
|
||||
nostr_relay_pool_t* pool,
|
||||
const char* relay_url);
|
||||
int nostr_relay_pool_reset_relay_stats(
|
||||
nostr_relay_pool_t* pool,
|
||||
const char* relay_url);
|
||||
double nostr_relay_pool_get_relay_query_latency(
|
||||
nostr_relay_pool_t* pool,
|
||||
const char* relay_url);
|
||||
double nostr_relay_pool_get_relay_ping_latency(
|
||||
nostr_relay_pool_t* pool,
|
||||
const char* relay_url);
|
||||
|
||||
// Synchronous relay operations (one-off queries/publishes)
|
||||
typedef enum {
|
||||
RELAY_QUERY_FIRST_RESULT, // Return as soon as first event is received
|
||||
RELAY_QUERY_MOST_RECENT, // Return the most recent event from all relays
|
||||
RELAY_QUERY_ALL_RESULTS // Return all unique events from all relays
|
||||
} relay_query_mode_t;
|
||||
|
||||
typedef enum {
|
||||
PUBLISH_SUCCESS, // Event was accepted by relay
|
||||
PUBLISH_REJECTED, // Event was rejected by relay
|
||||
PUBLISH_TIMEOUT, // No response within timeout
|
||||
PUBLISH_ERROR // Connection or other error
|
||||
} publish_result_t;
|
||||
|
||||
typedef void (*relay_progress_callback_t)(
|
||||
const char* relay_url,
|
||||
const char* status,
|
||||
const char* event_id,
|
||||
int events_received,
|
||||
int total_relays,
|
||||
int completed_relays,
|
||||
void* user_data);
|
||||
|
||||
typedef void (*publish_progress_callback_t)(
|
||||
const char* relay_url,
|
||||
const char* status,
|
||||
const char* message,
|
||||
int success_count,
|
||||
int total_relays,
|
||||
int completed_relays,
|
||||
void* user_data);
|
||||
|
||||
// Synchronous relay query functions
|
||||
struct cJSON** synchronous_query_relays_with_progress(
|
||||
const char** relay_urls,
|
||||
int relay_count,
|
||||
struct cJSON* filter,
|
||||
relay_query_mode_t mode,
|
||||
int* result_count,
|
||||
int relay_timeout_seconds,
|
||||
relay_progress_callback_t callback,
|
||||
void* user_data,
|
||||
int nip42_enabled,
|
||||
const unsigned char* private_key);
|
||||
|
||||
// Synchronous relay publish functions
|
||||
publish_result_t* synchronous_publish_event_with_progress(
|
||||
const char** relay_urls,
|
||||
int relay_count,
|
||||
struct cJSON* event,
|
||||
int* success_count,
|
||||
int relay_timeout_seconds,
|
||||
publish_progress_callback_t callback,
|
||||
void* user_data,
|
||||
int nip42_enabled,
|
||||
const unsigned char* private_key);
|
||||
|
||||
// Relay communication functions are defined in nostr_common.h
|
||||
// WebSocket functions are defined in nostr_common.h
|
||||
|
||||
|
||||
File diff suppressed because it is too large
Load Diff
@@ -0,0 +1,274 @@
|
||||
/*
|
||||
* NOSTR Core Library - Request Validator
|
||||
*
|
||||
* Unified authentication and authorization system for NOSTR applications.
|
||||
* Provides rule-based validation for requests with pluggable database backends.
|
||||
*
|
||||
* This module combines basic NOSTR event validation with sophisticated
|
||||
* authentication rules to provide a single entry point for request validation
|
||||
* across different NOSTR applications (ginxsom, c-relay, etc.).
|
||||
*/
|
||||
|
||||
#ifndef NOSTR_REQUEST_VALIDATOR_H
|
||||
#define NOSTR_REQUEST_VALIDATOR_H
|
||||
|
||||
#include "nostr_common.h"
|
||||
#include <time.h>
|
||||
#include <sqlite3.h>
|
||||
|
||||
#ifdef __cplusplus
|
||||
extern "C" {
|
||||
#endif
|
||||
|
||||
// Forward declaration for cJSON
|
||||
struct cJSON;
|
||||
|
||||
// Authentication rule types
|
||||
typedef enum {
|
||||
NOSTR_AUTH_RULE_PUBKEY_WHITELIST,
|
||||
NOSTR_AUTH_RULE_PUBKEY_BLACKLIST,
|
||||
NOSTR_AUTH_RULE_HASH_BLACKLIST,
|
||||
NOSTR_AUTH_RULE_MIME_WHITELIST,
|
||||
NOSTR_AUTH_RULE_MIME_BLACKLIST,
|
||||
NOSTR_AUTH_RULE_SIZE_LIMIT,
|
||||
NOSTR_AUTH_RULE_RATE_LIMIT,
|
||||
NOSTR_AUTH_RULE_CUSTOM
|
||||
} nostr_auth_rule_type_t;
|
||||
|
||||
// Authentication request context
|
||||
typedef struct {
|
||||
const char* operation; // Operation type ("upload", "delete", "list", "publish")
|
||||
const char* auth_header; // Raw authorization header (optional)
|
||||
struct cJSON* event; // NOSTR event for validation (optional)
|
||||
|
||||
// Resource context (for file/blob operations)
|
||||
const char* resource_hash; // Resource hash (SHA-256, optional)
|
||||
const char* mime_type; // MIME type (optional)
|
||||
long file_size; // File size (optional)
|
||||
|
||||
// Client context
|
||||
const char* client_ip; // Client IP for rate limiting (optional)
|
||||
void* app_context; // Application-specific context (optional)
|
||||
} nostr_request_t;
|
||||
|
||||
// Authentication result
|
||||
typedef struct {
|
||||
int valid; // 0 = invalid/denied, 1 = valid/allowed
|
||||
int error_code; // NOSTR_SUCCESS or specific error code
|
||||
char reason[256]; // Human-readable reason for denial
|
||||
char pubkey[65]; // Extracted pubkey from validated event (if available)
|
||||
int rule_id; // Rule ID that made the decision (0 if no rule)
|
||||
int priority; // Priority of the rule that matched
|
||||
time_t cached_until; // Cache expiration time
|
||||
} nostr_request_result_t;
|
||||
|
||||
// Authentication rule definition
|
||||
typedef struct {
|
||||
int rule_id; // Unique rule identifier
|
||||
nostr_auth_rule_type_t type; // Rule type
|
||||
char operation[32]; // Target operation ("*", "upload", "delete", "publish", etc.)
|
||||
char target[256]; // Rule target (pubkey, hash, mime pattern, etc.)
|
||||
char value[256]; // Rule value (size limit, rate limit, custom data)
|
||||
int priority; // Rule priority (lower number = higher priority)
|
||||
int enabled; // 1 = enabled, 0 = disabled
|
||||
time_t expires_at; // Expiration timestamp (0 = never expires)
|
||||
char description[512]; // Human-readable description
|
||||
time_t created_at; // Creation timestamp
|
||||
} nostr_auth_rule_t;
|
||||
|
||||
// Database backend interface (pluggable)
|
||||
typedef struct nostr_auth_db_interface {
|
||||
const char* name; // Backend name ("sqlite", "redis", etc.)
|
||||
|
||||
// Database lifecycle
|
||||
int (*init)(const char* db_path, const char* app_name);
|
||||
void (*cleanup)(void);
|
||||
|
||||
// Configuration management
|
||||
int (*get_config)(const char* key, char* value, size_t value_size);
|
||||
int (*set_config)(const char* key, const char* value);
|
||||
|
||||
// Rule querying and management
|
||||
int (*query_rules)(const nostr_request_t* request, nostr_auth_rule_t** rules, int* count);
|
||||
int (*rule_add)(const nostr_auth_rule_t* rule);
|
||||
int (*rule_remove)(int rule_id);
|
||||
int (*rule_update)(const nostr_auth_rule_t* rule);
|
||||
int (*rule_list)(const char* operation, nostr_auth_rule_t** rules, int* count);
|
||||
|
||||
// Caching operations
|
||||
int (*cache_get)(const char* cache_key, nostr_request_result_t* result);
|
||||
int (*cache_set)(const char* cache_key, const nostr_request_result_t* result, int ttl);
|
||||
int (*cache_clear)(void);
|
||||
} nostr_auth_db_interface_t;
|
||||
|
||||
//=============================================================================
|
||||
// CORE API FUNCTIONS
|
||||
//=============================================================================
|
||||
|
||||
/**
|
||||
* Initialize the request validator system with application database
|
||||
*
|
||||
* @param app_db_path Path to application's SQLite database
|
||||
* @param app_name Application name for logging/identification
|
||||
* @return NOSTR_SUCCESS on success, error code on failure
|
||||
*/
|
||||
int nostr_request_validator_init(const char* app_db_path, const char* app_name);
|
||||
|
||||
/**
|
||||
* Initialize with shared database (future use)
|
||||
*
|
||||
* @param shared_db_path Path to shared authentication database
|
||||
* @param app_name Application name for logging/identification
|
||||
* @return NOSTR_SUCCESS on success, error code on failure
|
||||
*/
|
||||
int nostr_request_validator_init_shared(const char* shared_db_path, const char* app_name);
|
||||
|
||||
/**
|
||||
* Main request validation function - validates both NOSTR events and authentication rules
|
||||
*
|
||||
* @param request Request context with operation, auth header, and resource details
|
||||
* @param result Result structure with validation outcome and details
|
||||
* @return NOSTR_SUCCESS on successful validation processing, error code on system failure
|
||||
*/
|
||||
int nostr_validate_request(const nostr_request_t* request, nostr_request_result_t* result);
|
||||
|
||||
/**
|
||||
* Check if authentication rules system is enabled
|
||||
*
|
||||
* @return 1 if enabled, 0 if disabled
|
||||
*/
|
||||
int nostr_auth_rules_enabled(void);
|
||||
|
||||
/**
|
||||
* Cleanup request validator resources
|
||||
*/
|
||||
void nostr_request_validator_cleanup(void);
|
||||
|
||||
//=============================================================================
|
||||
// CONVENIENCE FUNCTIONS
|
||||
//=============================================================================
|
||||
|
||||
/**
|
||||
* Convenience function for upload validation (ginxsom integration)
|
||||
*
|
||||
* @param pubkey Uploader public key (optional, extracted from auth if NULL)
|
||||
* @param auth_header Authorization header with NOSTR event
|
||||
* @param hash File hash (SHA-256)
|
||||
* @param mime_type File MIME type
|
||||
* @param file_size File size in bytes
|
||||
* @return NOSTR_SUCCESS if allowed, error code if denied
|
||||
*/
|
||||
int nostr_auth_check_upload(const char* pubkey, const char* auth_header,
|
||||
const char* hash, const char* mime_type, long file_size);
|
||||
|
||||
/**
|
||||
* Convenience function for delete validation (ginxsom integration)
|
||||
*
|
||||
* @param pubkey Requester public key
|
||||
* @param auth_header Authorization header with NOSTR event
|
||||
* @param hash File hash to delete
|
||||
* @return NOSTR_SUCCESS if allowed, error code if denied
|
||||
*/
|
||||
int nostr_auth_check_delete(const char* pubkey, const char* auth_header, const char* hash);
|
||||
|
||||
/**
|
||||
* Convenience function for publish validation (c-relay integration)
|
||||
*
|
||||
* @param pubkey Publisher public key
|
||||
* @param event NOSTR event to publish
|
||||
* @return NOSTR_SUCCESS if allowed, error code if denied
|
||||
*/
|
||||
int nostr_auth_check_publish(const char* pubkey, struct cJSON* event);
|
||||
|
||||
//=============================================================================
|
||||
// RULE MANAGEMENT FUNCTIONS
|
||||
//=============================================================================
|
||||
|
||||
/**
|
||||
* Add a new authentication rule
|
||||
*
|
||||
* @param rule Rule definition to add
|
||||
* @return NOSTR_SUCCESS on success, error code on failure
|
||||
*/
|
||||
int nostr_auth_rule_add(const nostr_auth_rule_t* rule);
|
||||
|
||||
/**
|
||||
* Remove an authentication rule by ID
|
||||
*
|
||||
* @param rule_id Rule ID to remove
|
||||
* @return NOSTR_SUCCESS on success, error code on failure
|
||||
*/
|
||||
int nostr_auth_rule_remove(int rule_id);
|
||||
|
||||
/**
|
||||
* Update an existing authentication rule
|
||||
*
|
||||
* @param rule Updated rule definition
|
||||
* @return NOSTR_SUCCESS on success, error code on failure
|
||||
*/
|
||||
int nostr_auth_rule_update(const nostr_auth_rule_t* rule);
|
||||
|
||||
/**
|
||||
* List authentication rules for a specific operation
|
||||
*
|
||||
* @param operation Target operation ("*" for all operations)
|
||||
* @param rules Pointer to receive allocated array of rules
|
||||
* @param count Pointer to receive number of rules returned
|
||||
* @return NOSTR_SUCCESS on success, error code on failure
|
||||
*/
|
||||
int nostr_auth_rule_list(const char* operation, nostr_auth_rule_t** rules, int* count);
|
||||
|
||||
/**
|
||||
* Free rule array allocated by nostr_auth_rule_list
|
||||
*
|
||||
* @param rules Rule array to free
|
||||
* @param count Number of rules in array
|
||||
*/
|
||||
void nostr_auth_rules_free(nostr_auth_rule_t* rules, int count);
|
||||
|
||||
//=============================================================================
|
||||
// DATABASE BACKEND MANAGEMENT
|
||||
//=============================================================================
|
||||
|
||||
/**
|
||||
* Register a database backend implementation
|
||||
*
|
||||
* @param backend Backend interface implementation
|
||||
* @return NOSTR_SUCCESS on success, error code on failure
|
||||
*/
|
||||
int nostr_auth_register_db_backend(const nostr_auth_db_interface_t* backend);
|
||||
|
||||
/**
|
||||
* Set active database backend by name
|
||||
*
|
||||
* @param backend_name Name of backend to activate
|
||||
* @return NOSTR_SUCCESS on success, error code on failure
|
||||
*/
|
||||
int nostr_auth_set_db_backend(const char* backend_name);
|
||||
|
||||
//=============================================================================
|
||||
// CACHE MANAGEMENT
|
||||
//=============================================================================
|
||||
|
||||
/**
|
||||
* Clear authentication decision cache
|
||||
*
|
||||
* @return NOSTR_SUCCESS on success, error code on failure
|
||||
*/
|
||||
int nostr_auth_cache_clear(void);
|
||||
|
||||
/**
|
||||
* Get cache statistics
|
||||
*
|
||||
* @param hit_count Pointer to receive cache hit count
|
||||
* @param miss_count Pointer to receive cache miss count
|
||||
* @param entries Pointer to receive current number of cache entries
|
||||
* @return NOSTR_SUCCESS on success, error code on failure
|
||||
*/
|
||||
int nostr_auth_cache_stats(int* hit_count, int* miss_count, int* entries);
|
||||
|
||||
#ifdef __cplusplus
|
||||
}
|
||||
#endif
|
||||
|
||||
#endif /* NOSTR_REQUEST_VALIDATOR_H */
|
||||
@@ -0,0 +1,10 @@
|
||||
{
|
||||
"folders": [
|
||||
{
|
||||
"path": "."
|
||||
}
|
||||
],
|
||||
"settings": {
|
||||
"git.ignoreLimitWarning": true
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,491 @@
|
||||
/*
|
||||
* NIP-13 Proof of Work Test Suite
|
||||
* Tests PoW generation, difficulty calculation, nonce extraction, and validation
|
||||
* Following TESTS POLICY: Shows expected vs actual values, prints entire JSON events
|
||||
*/
|
||||
|
||||
#define _GNU_SOURCE // For strdup on Linux
|
||||
#include <stdio.h>
|
||||
#include <stdlib.h>
|
||||
#include <string.h>
|
||||
#include <time.h>
|
||||
#include <unistd.h>
|
||||
#include "../nostr_core/nip013.h"
|
||||
#include "../nostr_core/nip001.h"
|
||||
#include "../nostr_core/nostr_common.h"
|
||||
#include "../nostr_core/utils.h"
|
||||
#include "../cjson/cJSON.h"
|
||||
|
||||
// Ensure strdup is declared
|
||||
#ifndef strdup
|
||||
extern char *strdup(const char *s);
|
||||
#endif
|
||||
|
||||
// Test counter for tracking progress
|
||||
static int test_count = 0;
|
||||
static int passed_tests = 0;
|
||||
|
||||
void print_test_header(const char* test_name) {
|
||||
test_count++;
|
||||
printf("\n=== TEST %d: %s ===\n", test_count, test_name);
|
||||
}
|
||||
|
||||
void print_test_result(int passed, const char* test_name) {
|
||||
if (passed) {
|
||||
passed_tests++;
|
||||
printf("✅ PASS: %s\n", test_name);
|
||||
} else {
|
||||
printf("❌ FAIL: %s\n", test_name);
|
||||
}
|
||||
}
|
||||
|
||||
// Test 1: Difficulty calculation with NIP-13 example
|
||||
int test_difficulty_calculation_reference(void) {
|
||||
print_test_header("Difficulty Calculation - NIP-13 Reference");
|
||||
|
||||
// Event ID from NIP-13 spec: 000006d8c378af1779d2feebc7603a125d99eca0ccf1085959b307f64e5dd358
|
||||
// This should have ~20 leading zero bits as stated in the spec
|
||||
const char* event_id = "000006d8c378af1779d2feebc7603a125d99eca0ccf1085959b307f64e5dd358";
|
||||
|
||||
printf("Testing event ID: %s\n", event_id);
|
||||
printf("Expected: ~20 leading zero bits\n");
|
||||
|
||||
int difficulty = nostr_calculate_pow_difficulty(event_id);
|
||||
printf("Actual: %d leading zero bits\n", difficulty);
|
||||
|
||||
if (difficulty < 0) {
|
||||
printf("❌ Error calculating difficulty: %d (%s)\n", difficulty, nostr_strerror(difficulty));
|
||||
return 0;
|
||||
}
|
||||
|
||||
// The NIP-13 spec example should have around 20 bits
|
||||
if (difficulty >= 19 && difficulty <= 21) {
|
||||
printf("✅ Difficulty calculation matches expected range (19-21 bits)\n");
|
||||
return 1;
|
||||
} else {
|
||||
printf("❌ Difficulty %d is outside expected range (19-21 bits)\n", difficulty);
|
||||
return 0;
|
||||
}
|
||||
}
|
||||
|
||||
// Test 2: Extract nonce info from NIP-13 reference event
|
||||
int test_nonce_extraction_reference(void) {
|
||||
print_test_header("Nonce Extraction - NIP-13 Reference Event");
|
||||
|
||||
// Complete event from NIP-13 spec
|
||||
const char* event_json = "{"
|
||||
"\"id\":\"000006d8c378af1779d2feebc7603a125d99eca0ccf1085959b307f64e5dd358\","
|
||||
"\"pubkey\":\"a48380f4cfcc1ad5378294fcac36439770f9c878dd880ffa94bb74ea54a6f243\","
|
||||
"\"created_at\":1651794653,"
|
||||
"\"kind\":1,"
|
||||
"\"tags\":[[\"nonce\",\"776797\",\"20\"]],"
|
||||
"\"content\":\"It's just me mining my own business\","
|
||||
"\"sig\":\"284622fc0a3f4f1303455d5175f7ba962a3300d136085b9566801bc2e0699de0c7e31e44c81fb40ad9049173742e904713c3594a1da0fc5d2382a25c11aba977\""
|
||||
"}";
|
||||
|
||||
printf("Input Event JSON:\n%s\n\n", event_json);
|
||||
|
||||
cJSON* event = cJSON_Parse(event_json);
|
||||
if (!event) {
|
||||
printf("❌ JSON Parse Error\n");
|
||||
return 0;
|
||||
}
|
||||
|
||||
uint64_t nonce_value = 0;
|
||||
int target_difficulty = -1;
|
||||
int result = nostr_extract_nonce_info(event, &nonce_value, &target_difficulty);
|
||||
|
||||
printf("Extraction result: %d (%s)\n", result, nostr_strerror(result));
|
||||
printf("Expected nonce: 776797\n");
|
||||
printf("Actual nonce: %llu\n", (unsigned long long)nonce_value);
|
||||
printf("Expected target: 20\n");
|
||||
printf("Actual target: %d\n", target_difficulty);
|
||||
|
||||
cJSON_Delete(event);
|
||||
|
||||
if (result != NOSTR_SUCCESS) {
|
||||
printf("❌ Failed to extract nonce info\n");
|
||||
return 0;
|
||||
}
|
||||
|
||||
if (nonce_value == 776797 && target_difficulty == 20) {
|
||||
printf("✅ Nonce extraction successful\n");
|
||||
return 1;
|
||||
} else {
|
||||
printf("❌ Extracted values don't match expected\n");
|
||||
return 0;
|
||||
}
|
||||
}
|
||||
|
||||
// Test 3: PoW validation with reference event
|
||||
int test_pow_validation_reference(void) {
|
||||
print_test_header("PoW Validation - NIP-13 Reference Event");
|
||||
|
||||
const char* event_json = "{"
|
||||
"\"id\":\"000006d8c378af1779d2feebc7603a125d99eca0ccf1085959b307f64e5dd358\","
|
||||
"\"pubkey\":\"a48380f4cfcc1ad5378294fcac36439770f9c878dd880ffa94bb74ea54a6f243\","
|
||||
"\"created_at\":1651794653,"
|
||||
"\"kind\":1,"
|
||||
"\"tags\":[[\"nonce\",\"776797\",\"20\"]],"
|
||||
"\"content\":\"It's just me mining my own business\","
|
||||
"\"sig\":\"284622fc0a3f4f1303455d5175f7ba962a3300d136085b9566801bc2e0699de0c7e31e44c81fb40ad9049173742e904713c3594a1da0fc5d2382a25c11aba977\""
|
||||
"}";
|
||||
|
||||
printf("Input Event JSON:\n%s\n\n", event_json);
|
||||
|
||||
cJSON* event = cJSON_Parse(event_json);
|
||||
if (!event) {
|
||||
printf("❌ JSON Parse Error\n");
|
||||
return 0;
|
||||
}
|
||||
|
||||
// Test basic validation (no minimum difficulty)
|
||||
nostr_pow_result_t result_info;
|
||||
int validation_result = nostr_validate_pow(event, 0, NOSTR_POW_VALIDATE_BASIC, &result_info);
|
||||
|
||||
printf("Validation result: %d (%s)\n", validation_result, nostr_strerror(validation_result));
|
||||
printf("Actual difficulty: %d\n", result_info.actual_difficulty);
|
||||
printf("Committed target: %d\n", result_info.committed_target);
|
||||
printf("Nonce value: %llu\n", (unsigned long long)result_info.nonce_value);
|
||||
printf("Has nonce tag: %d\n", result_info.has_nonce_tag);
|
||||
printf("Error detail: %s\n", result_info.error_detail);
|
||||
|
||||
cJSON_Delete(event);
|
||||
return (validation_result == NOSTR_SUCCESS && result_info.has_nonce_tag == 1);
|
||||
}
|
||||
|
||||
// Test 4: Generate PoW with our library and validate it
|
||||
int test_pow_generation_and_validation(void) {
|
||||
print_test_header("PoW Generation and Validation - Our Library");
|
||||
|
||||
// Create a test event for mining
|
||||
const char* private_key_hex = "91ba716fa9e7ea2fcbad360cf4f8e0d312f73984da63d90f524ad61a6a1e7dbe";
|
||||
unsigned char private_key[32];
|
||||
nostr_hex_to_bytes(private_key_hex, private_key, 32);
|
||||
|
||||
cJSON* tags = cJSON_CreateArray();
|
||||
|
||||
printf("Creating base event...\n");
|
||||
cJSON* event = nostr_create_and_sign_event(1, "Testing PoW generation", tags, private_key, time(NULL));
|
||||
|
||||
if (!event) {
|
||||
printf("❌ Event creation failed\n");
|
||||
cJSON_Delete(tags);
|
||||
return 0;
|
||||
}
|
||||
|
||||
char* original_event_str = cJSON_Print(event);
|
||||
printf("Original event (no PoW):\n%s\n\n", original_event_str);
|
||||
free(original_event_str);
|
||||
|
||||
// Add PoW with difficulty 8 (reasonable for testing)
|
||||
printf("Adding PoW with difficulty 8...\n");
|
||||
int pow_result = nostr_add_proof_of_work(event, private_key, 8, 100000, 10000, 10000, NULL, NULL);
|
||||
|
||||
if (pow_result != NOSTR_SUCCESS) {
|
||||
printf("❌ PoW generation failed: %d (%s)\n", pow_result, nostr_strerror(pow_result));
|
||||
cJSON_Delete(event);
|
||||
return 0;
|
||||
}
|
||||
|
||||
char* pow_event_str = cJSON_Print(event);
|
||||
printf("Event with PoW:\n%s\n\n", pow_event_str);
|
||||
free(pow_event_str);
|
||||
|
||||
// Now validate the PoW
|
||||
printf("Validating generated PoW...\n");
|
||||
nostr_pow_result_t result_info;
|
||||
int validation_result = nostr_validate_pow(event, 8, NOSTR_POW_VALIDATE_FULL, &result_info);
|
||||
|
||||
printf("Validation result: %d (%s)\n", validation_result, nostr_strerror(validation_result));
|
||||
printf("Actual difficulty: %d\n", result_info.actual_difficulty);
|
||||
printf("Committed target: %d\n", result_info.committed_target);
|
||||
printf("Has nonce tag: %d\n", result_info.has_nonce_tag);
|
||||
printf("Error detail: %s\n", result_info.error_detail);
|
||||
|
||||
cJSON_Delete(event);
|
||||
|
||||
if (validation_result == NOSTR_SUCCESS && result_info.actual_difficulty >= 8) {
|
||||
printf("✅ PoW generation and validation successful\n");
|
||||
return 1;
|
||||
} else {
|
||||
printf("❌ PoW validation failed\n");
|
||||
return 0;
|
||||
}
|
||||
}
|
||||
|
||||
// Test 5: Test various validation flag combinations
|
||||
int test_validation_flags(void) {
|
||||
print_test_header("Validation Flags - Various Combinations");
|
||||
|
||||
// Use NIP-13 reference event
|
||||
const char* event_json = "{"
|
||||
"\"id\":\"000006d8c378af1779d2feebc7603a125d99eca0ccf1085959b307f64e5dd358\","
|
||||
"\"pubkey\":\"a48380f4cfcc1ad5378294fcac36439770f9c878dd880ffa94bb74ea54a6f243\","
|
||||
"\"created_at\":1651794653,"
|
||||
"\"kind\":1,"
|
||||
"\"tags\":[[\"nonce\",\"776797\",\"20\"]],"
|
||||
"\"content\":\"It's just me mining my own business\","
|
||||
"\"sig\":\"284622fc0a3f4f1303455d5175f7ba962a3300d136085b9566801bc2e0699de0c7e31e44c81fb40ad9049173742e904713c3594a1da0fc5d2382a25c11aba977\""
|
||||
"}";
|
||||
|
||||
cJSON* event = cJSON_Parse(event_json);
|
||||
if (!event) {
|
||||
printf("❌ JSON Parse Error\n");
|
||||
return 0;
|
||||
}
|
||||
|
||||
int all_passed = 1;
|
||||
nostr_pow_result_t result_info;
|
||||
|
||||
// Test 1: Basic validation
|
||||
printf("\nSubtest 1: NOSTR_POW_VALIDATE_BASIC\n");
|
||||
int result = nostr_validate_pow(event, 0, NOSTR_POW_VALIDATE_BASIC, &result_info);
|
||||
printf("Result: %d (%s)\n", result, nostr_strerror(result));
|
||||
if (result != NOSTR_SUCCESS) all_passed = 0;
|
||||
|
||||
// Test 2: Full validation
|
||||
printf("\nSubtest 2: NOSTR_POW_VALIDATE_FULL\n");
|
||||
result = nostr_validate_pow(event, 0, NOSTR_POW_VALIDATE_FULL, &result_info);
|
||||
printf("Result: %d (%s)\n", result, nostr_strerror(result));
|
||||
if (result != NOSTR_SUCCESS) all_passed = 0;
|
||||
|
||||
// Test 3: Anti-spam validation (should pass since committed target matches actual)
|
||||
printf("\nSubtest 3: NOSTR_POW_VALIDATE_ANTI_SPAM\n");
|
||||
result = nostr_validate_pow(event, 0, NOSTR_POW_VALIDATE_ANTI_SPAM, &result_info);
|
||||
printf("Result: %d (%s)\n", result, nostr_strerror(result));
|
||||
if (result != NOSTR_SUCCESS) all_passed = 0;
|
||||
|
||||
// Test 4: Minimum difficulty requirement (15 bits - should pass)
|
||||
printf("\nSubtest 4: Minimum difficulty 15 bits\n");
|
||||
result = nostr_validate_pow(event, 15, NOSTR_POW_VALIDATE_BASIC, &result_info);
|
||||
printf("Result: %d (%s)\n", result, nostr_strerror(result));
|
||||
if (result != NOSTR_SUCCESS) all_passed = 0;
|
||||
|
||||
// Test 5: Too high minimum difficulty (30 bits - should fail)
|
||||
printf("\nSubtest 5: Minimum difficulty 30 bits (should fail)\n");
|
||||
result = nostr_validate_pow(event, 30, NOSTR_POW_VALIDATE_BASIC, &result_info);
|
||||
printf("Result: %d (%s)\n", result, nostr_strerror(result));
|
||||
if (result == NOSTR_SUCCESS) {
|
||||
printf("❌ Should have failed but didn't\n");
|
||||
all_passed = 0;
|
||||
} else if (result == NOSTR_ERROR_NIP13_INSUFFICIENT) {
|
||||
printf("✅ Correctly failed with insufficient difficulty\n");
|
||||
} else {
|
||||
printf("❌ Failed with unexpected error\n");
|
||||
all_passed = 0;
|
||||
}
|
||||
|
||||
cJSON_Delete(event);
|
||||
return all_passed;
|
||||
}
|
||||
|
||||
// Test 6: Error conditions and edge cases
|
||||
int test_error_conditions(void) {
|
||||
print_test_header("Error Conditions and Edge Cases");
|
||||
|
||||
int all_passed = 1;
|
||||
|
||||
// Test 1: NULL event
|
||||
printf("\nSubtest 1: NULL event\n");
|
||||
int result = nostr_validate_pow(NULL, 0, NOSTR_POW_VALIDATE_BASIC, NULL);
|
||||
printf("Expected: NOSTR_ERROR_INVALID_INPUT (-1)\n");
|
||||
printf("Actual: %d (%s)\n", result, nostr_strerror(result));
|
||||
if (result != NOSTR_ERROR_INVALID_INPUT) all_passed = 0;
|
||||
|
||||
// Test 2: Event without ID
|
||||
printf("\nSubtest 2: Event without ID\n");
|
||||
const char* no_id_json = "{\"kind\":1,\"content\":\"test\",\"tags\":[]}";
|
||||
cJSON* no_id_event = cJSON_Parse(no_id_json);
|
||||
result = nostr_validate_pow(no_id_event, 0, NOSTR_POW_VALIDATE_BASIC, NULL);
|
||||
printf("Expected: NOSTR_ERROR_EVENT_INVALID_ID (-31)\n");
|
||||
printf("Actual: %d (%s)\n", result, nostr_strerror(result));
|
||||
if (result != NOSTR_ERROR_EVENT_INVALID_ID) all_passed = 0;
|
||||
cJSON_Delete(no_id_event);
|
||||
|
||||
// Test 3: Event without nonce tag when required
|
||||
printf("\nSubtest 3: Event without nonce tag when required\n");
|
||||
const char* no_nonce_json = "{"
|
||||
"\"id\":\"f1e582c90f071c0110cc5bcac2dcc6d8c32250e3cc26fcbe93470d918f2ffaf0\","
|
||||
"\"kind\":1,\"content\":\"test\",\"tags\":[]"
|
||||
"}";
|
||||
cJSON* no_nonce_event = cJSON_Parse(no_nonce_json);
|
||||
result = nostr_validate_pow(no_nonce_event, 0, NOSTR_POW_VALIDATE_NONCE_TAG, NULL);
|
||||
printf("Expected: NOSTR_ERROR_NIP13_NO_NONCE_TAG (-101)\n");
|
||||
printf("Actual: %d (%s)\n", result, nostr_strerror(result));
|
||||
if (result != NOSTR_ERROR_NIP13_NO_NONCE_TAG) all_passed = 0;
|
||||
cJSON_Delete(no_nonce_event);
|
||||
|
||||
// Test 4: Invalid hex ID
|
||||
printf("\nSubtest 4: Invalid hex ID\n");
|
||||
result = nostr_calculate_pow_difficulty("invalid_hex_string");
|
||||
printf("Expected: NOSTR_ERROR_NIP13_CALCULATION (-104)\n");
|
||||
printf("Actual: %d (%s)\n", result, nostr_strerror(result));
|
||||
if (result != NOSTR_ERROR_NIP13_CALCULATION) all_passed = 0;
|
||||
|
||||
// Test 5: Wrong length hex ID
|
||||
printf("\nSubtest 5: Wrong length hex ID\n");
|
||||
result = nostr_calculate_pow_difficulty("f1e582c90f071c0110cc5bcac2dcc6d8c32250e3cc26fcbe93470d918f2ffa"); // 63 chars instead of 64
|
||||
printf("Expected: NOSTR_ERROR_NIP13_CALCULATION (-104)\n");
|
||||
printf("Actual: %d (%s)\n", result, nostr_strerror(result));
|
||||
if (result != NOSTR_ERROR_NIP13_CALCULATION) all_passed = 0;
|
||||
|
||||
return all_passed;
|
||||
}
|
||||
|
||||
// Test 7: Integration with nak-generated PoW events
|
||||
int test_nak_integration(void) {
|
||||
print_test_header("Integration with nak-generated PoW events");
|
||||
|
||||
// Generate a test event with nak and PoW difficulty 8
|
||||
printf("Generating PoW event with nak (difficulty 8)...\n");
|
||||
|
||||
// Create a temporary key for this test
|
||||
char temp_key[] = "/tmp/nip13_test_key_XXXXXX";
|
||||
int fd = mkstemp(temp_key);
|
||||
if (fd == -1) {
|
||||
printf("❌ Failed to create temporary key file\n");
|
||||
return 0;
|
||||
}
|
||||
|
||||
// Write test key to file (same as used in other tests)
|
||||
const char* test_key = "91ba716fa9e7ea2fcbad360cf4f8e0d312f73984da63d90f524ad61a6a1e7dbe";
|
||||
ssize_t bytes_written = write(fd, test_key, strlen(test_key));
|
||||
close(fd);
|
||||
|
||||
if (bytes_written != (ssize_t)strlen(test_key)) {
|
||||
printf("❌ Failed to write test key to temporary file\n");
|
||||
unlink(temp_key);
|
||||
return 0;
|
||||
}
|
||||
|
||||
// Generate event with PoW using nak
|
||||
char cmd[1024];
|
||||
snprintf(cmd, sizeof(cmd), "nak event --sec %s -c \"PoW test from nak\" --pow 8 --ts %ld",
|
||||
test_key, (long)time(NULL));
|
||||
|
||||
printf("Executing: %s\n", cmd);
|
||||
FILE* pipe = popen(cmd, "r");
|
||||
if (!pipe) {
|
||||
printf("❌ Failed to execute nak command\n");
|
||||
unlink(temp_key);
|
||||
return 0;
|
||||
}
|
||||
|
||||
char event_json[4096] = {0};
|
||||
if (!fgets(event_json, sizeof(event_json), pipe)) {
|
||||
printf("❌ Failed to read nak output\n");
|
||||
pclose(pipe);
|
||||
unlink(temp_key);
|
||||
return 0;
|
||||
}
|
||||
pclose(pipe);
|
||||
unlink(temp_key);
|
||||
|
||||
// Remove trailing newline
|
||||
event_json[strcspn(event_json, "\n")] = 0;
|
||||
|
||||
printf("nak-generated event:\n%s\n\n", event_json);
|
||||
|
||||
// Parse and validate the event
|
||||
cJSON* event = cJSON_Parse(event_json);
|
||||
if (!event) {
|
||||
printf("❌ Failed to parse nak-generated JSON\n");
|
||||
return 0;
|
||||
}
|
||||
|
||||
// Validate the PoW
|
||||
nostr_pow_result_t result_info;
|
||||
int validation_result = nostr_validate_pow(event, 8, NOSTR_POW_VALIDATE_FULL, &result_info);
|
||||
|
||||
printf("Validation result: %d (%s)\n", validation_result, nostr_strerror(validation_result));
|
||||
printf("Actual difficulty: %d\n", result_info.actual_difficulty);
|
||||
printf("Committed target: %d\n", result_info.committed_target);
|
||||
printf("Has nonce tag: %d\n", result_info.has_nonce_tag);
|
||||
printf("Error detail: %s\n", result_info.error_detail);
|
||||
|
||||
cJSON_Delete(event);
|
||||
|
||||
if (validation_result == NOSTR_SUCCESS && result_info.actual_difficulty >= 8) {
|
||||
printf("✅ nak-generated PoW event validates successfully\n");
|
||||
return 1;
|
||||
} else {
|
||||
printf("❌ nak-generated PoW event validation failed\n");
|
||||
return 0;
|
||||
}
|
||||
}
|
||||
|
||||
int main(void) {
|
||||
printf("=== NIP-13 Proof of Work Test Suite ===\n");
|
||||
printf("Following TESTS POLICY: Shows expected vs actual values, prints entire JSON events\n");
|
||||
printf("Tests both PoW generation and validation functionality\n");
|
||||
|
||||
// Initialize crypto library
|
||||
if (nostr_init() != NOSTR_SUCCESS) {
|
||||
printf("❌ Failed to initialize nostr library\n");
|
||||
return 1;
|
||||
}
|
||||
|
||||
int all_passed = 1;
|
||||
int test_result;
|
||||
|
||||
// Test 1: Basic difficulty calculation
|
||||
test_result = test_difficulty_calculation_reference();
|
||||
print_test_result(test_result, "Difficulty Calculation - NIP-13 Reference");
|
||||
if (!test_result) all_passed = 0;
|
||||
|
||||
// Test 2: Nonce extraction
|
||||
test_result = test_nonce_extraction_reference();
|
||||
print_test_result(test_result, "Nonce Extraction - NIP-13 Reference Event");
|
||||
if (!test_result) all_passed = 0;
|
||||
|
||||
// Test 3: Basic PoW validation
|
||||
test_result = test_pow_validation_reference();
|
||||
print_test_result(test_result, "PoW Validation - NIP-13 Reference Event");
|
||||
if (!test_result) all_passed = 0;
|
||||
|
||||
// Test 4: PoW generation and validation
|
||||
test_result = test_pow_generation_and_validation();
|
||||
print_test_result(test_result, "PoW Generation and Validation - Our Library");
|
||||
if (!test_result) all_passed = 0;
|
||||
|
||||
// Test 5: Validation flags
|
||||
test_result = test_validation_flags();
|
||||
print_test_result(test_result, "Validation Flags - Various Combinations");
|
||||
if (!test_result) all_passed = 0;
|
||||
|
||||
// Test 6: Error conditions
|
||||
test_result = test_error_conditions();
|
||||
print_test_result(test_result, "Error Conditions and Edge Cases");
|
||||
if (!test_result) all_passed = 0;
|
||||
|
||||
// Test 7: nak integration (optional - may fail if nak not available)
|
||||
printf("\n=== Optional nak Integration Test ===\n");
|
||||
test_result = test_nak_integration();
|
||||
if (test_result) {
|
||||
print_test_result(test_result, "Integration with nak-generated PoW events");
|
||||
} else {
|
||||
printf("⚠️ SKIP: Integration with nak-generated PoW events (nak may not be available)\n");
|
||||
// Don't fail the entire test suite for this optional test
|
||||
}
|
||||
|
||||
// Summary
|
||||
printf("\n=== TEST SUMMARY ===\n");
|
||||
printf("Total tests: %d\n", test_count);
|
||||
printf("Passed: %d\n", passed_tests);
|
||||
printf("Failed: %d\n", test_count - passed_tests);
|
||||
|
||||
if (all_passed) {
|
||||
printf("🎉 ALL TESTS PASSED! NIP-13 PoW implementation is working correctly.\n");
|
||||
printf("✅ PoW generation works\n");
|
||||
printf("✅ PoW difficulty calculation works\n");
|
||||
printf("✅ Nonce extraction works\n");
|
||||
printf("✅ PoW validation works\n");
|
||||
printf("✅ Error handling works\n");
|
||||
} else {
|
||||
printf("❌ SOME TESTS FAILED. Please review the output above.\n");
|
||||
}
|
||||
|
||||
nostr_cleanup();
|
||||
return all_passed ? 0 : 1;
|
||||
}
|
||||
@@ -0,0 +1,690 @@
|
||||
/*
|
||||
* NIP-42 Authentication of Clients to Relays Test Suite
|
||||
* Tests auth challenge generation, event creation, validation, and message parsing
|
||||
* Following TESTS POLICY: Shows expected vs actual values, prints entire JSON events
|
||||
*/
|
||||
|
||||
#define _GNU_SOURCE // For strdup on Linux
|
||||
#include <stdio.h>
|
||||
#include <stdlib.h>
|
||||
#include <string.h>
|
||||
#include <time.h>
|
||||
#include <unistd.h>
|
||||
#include "../nostr_core/nip042.h"
|
||||
#include "../nostr_core/nip001.h"
|
||||
#include "../nostr_core/nostr_common.h"
|
||||
#include "../nostr_core/utils.h"
|
||||
#include "../cjson/cJSON.h"
|
||||
|
||||
// Ensure strdup is declared
|
||||
#ifndef strdup
|
||||
extern char *strdup(const char *s);
|
||||
#endif
|
||||
|
||||
// Test counter for tracking progress
|
||||
static int test_count = 0;
|
||||
static int passed_tests = 0;
|
||||
|
||||
void print_test_header(const char* test_name) {
|
||||
test_count++;
|
||||
printf("\n=== TEST %d: %s ===\n", test_count, test_name);
|
||||
}
|
||||
|
||||
void print_test_result(int passed, const char* test_name) {
|
||||
if (passed) {
|
||||
passed_tests++;
|
||||
printf("✅ PASS: %s\n", test_name);
|
||||
} else {
|
||||
printf("❌ FAIL: %s\n", test_name);
|
||||
}
|
||||
}
|
||||
|
||||
void print_json_comparison(const char* label, cJSON* expected, cJSON* actual) {
|
||||
char* expected_str = cJSON_Print(expected);
|
||||
char* actual_str;
|
||||
|
||||
if (actual) {
|
||||
actual_str = cJSON_Print(actual);
|
||||
} else {
|
||||
actual_str = strdup("NULL");
|
||||
}
|
||||
|
||||
printf("%s Expected JSON:\n%s\n", label, expected_str ? expected_str : "NULL");
|
||||
printf("%s Actual JSON:\n%s\n", label, actual_str ? actual_str : "NULL");
|
||||
|
||||
if (expected_str) free(expected_str);
|
||||
if (actual_str) free(actual_str);
|
||||
}
|
||||
|
||||
// Test 1: Challenge generation
|
||||
int test_challenge_generation(void) {
|
||||
print_test_header("Challenge Generation");
|
||||
|
||||
nostr_auth_challenge_t challenge1, challenge2;
|
||||
|
||||
printf("Generating first challenge...\n");
|
||||
int result1 = nostr_nip42_generate_challenge(challenge1.challenge, NOSTR_NIP42_DEFAULT_CHALLENGE_LENGTH);
|
||||
printf("Result: %d (%s)\n", result1, nostr_strerror(result1));
|
||||
|
||||
if (result1 != NOSTR_SUCCESS) {
|
||||
printf("❌ Failed to generate first challenge\n");
|
||||
return 0;
|
||||
}
|
||||
|
||||
printf("First challenge: %s\n", challenge1.challenge);
|
||||
printf("Challenge length: %lu\n", strlen(challenge1.challenge));
|
||||
printf("Expected length: %d\n", NOSTR_NIP42_DEFAULT_CHALLENGE_LENGTH * 2);
|
||||
|
||||
if (strlen(challenge1.challenge) != NOSTR_NIP42_DEFAULT_CHALLENGE_LENGTH * 2) {
|
||||
printf("❌ Challenge length incorrect\n");
|
||||
return 0;
|
||||
}
|
||||
|
||||
printf("Generating second challenge...\n");
|
||||
int result2 = nostr_nip42_generate_challenge(challenge2.challenge, NOSTR_NIP42_DEFAULT_CHALLENGE_LENGTH);
|
||||
printf("Result: %d (%s)\n", result2, nostr_strerror(result2));
|
||||
|
||||
if (result2 != NOSTR_SUCCESS) {
|
||||
printf("❌ Failed to generate second challenge\n");
|
||||
return 0;
|
||||
}
|
||||
|
||||
printf("Second challenge: %s\n", challenge2.challenge);
|
||||
|
||||
// Challenges should be different (extremely high probability)
|
||||
if (strcmp(challenge1.challenge, challenge2.challenge) == 0) {
|
||||
printf("❌ Two challenges are identical (highly unlikely)\n");
|
||||
return 0;
|
||||
}
|
||||
|
||||
printf("✅ Challenges are different (good entropy)\n");
|
||||
return 1;
|
||||
}
|
||||
|
||||
// Test 2: AUTH message creation (server-side challenge)
|
||||
int test_auth_message_creation(void) {
|
||||
print_test_header("AUTH Message Creation - Server Challenge");
|
||||
|
||||
nostr_auth_challenge_t challenge;
|
||||
int gen_result = nostr_nip42_generate_challenge(challenge.challenge, NOSTR_NIP42_DEFAULT_CHALLENGE_LENGTH);
|
||||
|
||||
if (gen_result != NOSTR_SUCCESS) {
|
||||
printf("❌ Failed to generate challenge for message test\n");
|
||||
return 0;
|
||||
}
|
||||
|
||||
printf("Generated challenge: %s\n", challenge.challenge);
|
||||
|
||||
// Create AUTH challenge message: ["AUTH", "challenge_string"]
|
||||
cJSON* message_array = cJSON_CreateArray();
|
||||
if (!message_array) {
|
||||
printf("❌ Failed to create message array\n");
|
||||
return 0;
|
||||
}
|
||||
|
||||
cJSON_AddItemToArray(message_array, cJSON_CreateString("AUTH"));
|
||||
cJSON_AddItemToArray(message_array, cJSON_CreateString(challenge.challenge));
|
||||
|
||||
char* auth_message = cJSON_PrintUnformatted(message_array);
|
||||
cJSON_Delete(message_array);
|
||||
|
||||
if (!auth_message) {
|
||||
printf("❌ Failed to create AUTH message\n");
|
||||
return 0;
|
||||
}
|
||||
|
||||
int result = NOSTR_SUCCESS;
|
||||
|
||||
printf("Message creation result: %d (%s)\n", result, nostr_strerror(result));
|
||||
printf("AUTH message: %s\n", auth_message);
|
||||
|
||||
// Parse the message to verify format
|
||||
cJSON* parsed = cJSON_Parse(auth_message);
|
||||
if (!parsed) {
|
||||
printf("❌ AUTH message is not valid JSON\n");
|
||||
return 0;
|
||||
}
|
||||
|
||||
// Check if it's an array with 2 elements
|
||||
if (!cJSON_IsArray(parsed) || cJSON_GetArraySize(parsed) != 2) {
|
||||
printf("❌ AUTH message is not a 2-element array\n");
|
||||
cJSON_Delete(parsed);
|
||||
return 0;
|
||||
}
|
||||
|
||||
// Check first element is "AUTH"
|
||||
cJSON* first = cJSON_GetArrayItem(parsed, 0);
|
||||
if (!cJSON_IsString(first) || strcmp(cJSON_GetStringValue(first), "AUTH") != 0) {
|
||||
printf("❌ First element is not 'AUTH'\n");
|
||||
cJSON_Delete(parsed);
|
||||
return 0;
|
||||
}
|
||||
|
||||
// Check second element is our challenge string
|
||||
cJSON* second = cJSON_GetArrayItem(parsed, 1);
|
||||
if (!cJSON_IsString(second) || strcmp(cJSON_GetStringValue(second), challenge.challenge) != 0) {
|
||||
printf("❌ Second element is not our challenge string\n");
|
||||
printf("Expected: %s\n", challenge.challenge);
|
||||
printf("Actual: %s\n", cJSON_GetStringValue(second));
|
||||
cJSON_Delete(parsed);
|
||||
free(auth_message);
|
||||
return 0;
|
||||
}
|
||||
|
||||
printf("✅ AUTH challenge message format is correct\n");
|
||||
cJSON_Delete(parsed);
|
||||
free(auth_message);
|
||||
return 1;
|
||||
}
|
||||
|
||||
// Test 3: Authentication event creation (client-side)
|
||||
int test_auth_event_creation(void) {
|
||||
print_test_header("Authentication Event Creation - Client Side");
|
||||
|
||||
const char* private_key_hex = "91ba716fa9e7ea2fcbad360cf4f8e0d312f73984da63d90f524ad61a6a1e7dbe";
|
||||
const char* relay_url = "wss://relay.example.com";
|
||||
const char* challenge_string = "test_challenge_12345678901234567890123456789012";
|
||||
|
||||
unsigned char private_key[32];
|
||||
nostr_hex_to_bytes(private_key_hex, private_key, 32);
|
||||
|
||||
printf("Private key (hex): %s\n", private_key_hex);
|
||||
printf("Relay URL: %s\n", relay_url);
|
||||
printf("Challenge: %s\n", challenge_string);
|
||||
|
||||
cJSON* auth_event = nostr_nip42_create_auth_event(challenge_string, relay_url, private_key, 0);
|
||||
|
||||
if (!auth_event) {
|
||||
printf("❌ Failed to create authentication event\n");
|
||||
return 0;
|
||||
}
|
||||
|
||||
char* event_str = cJSON_Print(auth_event);
|
||||
printf("Created Auth Event JSON:\n%s\n", event_str);
|
||||
free(event_str);
|
||||
|
||||
// Validate the event structure
|
||||
int structure_result = nostr_validate_event_structure(auth_event);
|
||||
printf("Structure validation result: %d (%s)\n", structure_result, nostr_strerror(structure_result));
|
||||
|
||||
if (structure_result != NOSTR_SUCCESS) {
|
||||
printf("❌ Auth event failed structure validation\n");
|
||||
cJSON_Delete(auth_event);
|
||||
return 0;
|
||||
}
|
||||
|
||||
// Validate the event signature
|
||||
int crypto_result = nostr_verify_event_signature(auth_event);
|
||||
printf("Signature validation result: %d (%s)\n", crypto_result, nostr_strerror(crypto_result));
|
||||
|
||||
if (crypto_result != NOSTR_SUCCESS) {
|
||||
printf("❌ Auth event failed signature validation\n");
|
||||
cJSON_Delete(auth_event);
|
||||
return 0;
|
||||
}
|
||||
|
||||
// Check kind is 22242
|
||||
cJSON* kind = cJSON_GetObjectItem(auth_event, "kind");
|
||||
if (!cJSON_IsNumber(kind) || cJSON_GetNumberValue(kind) != 22242) {
|
||||
printf("❌ Auth event kind is not 22242\n");
|
||||
cJSON_Delete(auth_event);
|
||||
return 0;
|
||||
}
|
||||
|
||||
// Check for relay tag
|
||||
cJSON* tags = cJSON_GetObjectItem(auth_event, "tags");
|
||||
int found_relay = 0, found_challenge = 0;
|
||||
|
||||
if (cJSON_IsArray(tags)) {
|
||||
cJSON* tag = NULL;
|
||||
cJSON_ArrayForEach(tag, tags) {
|
||||
if (cJSON_IsArray(tag) && cJSON_GetArraySize(tag) >= 2) {
|
||||
cJSON* tag_name = cJSON_GetArrayItem(tag, 0);
|
||||
cJSON* tag_value = cJSON_GetArrayItem(tag, 1);
|
||||
|
||||
if (cJSON_IsString(tag_name) && cJSON_IsString(tag_value)) {
|
||||
if (strcmp(cJSON_GetStringValue(tag_name), "relay") == 0) {
|
||||
found_relay = 1;
|
||||
if (strcmp(cJSON_GetStringValue(tag_value), relay_url) != 0) {
|
||||
printf("❌ Relay tag value incorrect\n");
|
||||
printf("Expected: %s\n", relay_url);
|
||||
printf("Actual: %s\n", cJSON_GetStringValue(tag_value));
|
||||
cJSON_Delete(auth_event);
|
||||
return 0;
|
||||
}
|
||||
} else if (strcmp(cJSON_GetStringValue(tag_name), "challenge") == 0) {
|
||||
found_challenge = 1;
|
||||
if (strcmp(cJSON_GetStringValue(tag_value), challenge_string) != 0) {
|
||||
printf("❌ Challenge tag value incorrect\n");
|
||||
printf("Expected: %s\n", challenge_string);
|
||||
printf("Actual: %s\n", cJSON_GetStringValue(tag_value));
|
||||
cJSON_Delete(auth_event);
|
||||
return 0;
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
if (!found_relay) {
|
||||
printf("❌ Missing relay tag\n");
|
||||
cJSON_Delete(auth_event);
|
||||
return 0;
|
||||
}
|
||||
|
||||
if (!found_challenge) {
|
||||
printf("❌ Missing challenge tag\n");
|
||||
cJSON_Delete(auth_event);
|
||||
return 0;
|
||||
}
|
||||
|
||||
printf("✅ Authentication event created successfully with correct tags\n");
|
||||
cJSON_Delete(auth_event);
|
||||
return 1;
|
||||
}
|
||||
|
||||
// Test 4: Authentication event validation (server-side)
|
||||
int test_auth_event_validation(void) {
|
||||
print_test_header("Authentication Event Validation - Server Side");
|
||||
|
||||
// Create a valid auth event first
|
||||
const char* private_key_hex = "91ba716fa9e7ea2fcbad360cf4f8e0d312f73984da63d90f524ad61a6a1e7dbe";
|
||||
const char* relay_url = "wss://relay.example.com";
|
||||
const char* challenge_string = "validation_challenge_1234567890123456789012";
|
||||
|
||||
unsigned char private_key[32];
|
||||
nostr_hex_to_bytes(private_key_hex, private_key, 32);
|
||||
|
||||
cJSON* auth_event = nostr_nip42_create_auth_event(challenge_string, relay_url, private_key, 0);
|
||||
|
||||
if (!auth_event) {
|
||||
printf("❌ Failed to create auth event for validation test\n");
|
||||
return 0;
|
||||
}
|
||||
|
||||
char* event_str = cJSON_Print(auth_event);
|
||||
printf("Auth Event to validate:\n%s\n", event_str);
|
||||
free(event_str);
|
||||
|
||||
// Test successful validation
|
||||
printf("Testing successful validation...\n");
|
||||
int result = nostr_nip42_verify_auth_event(auth_event, challenge_string, relay_url, 0);
|
||||
printf("Validation result: %d (%s)\n", result, nostr_strerror(result));
|
||||
|
||||
if (result != NOSTR_SUCCESS) {
|
||||
printf("❌ Valid auth event failed validation\n");
|
||||
cJSON_Delete(auth_event);
|
||||
return 0;
|
||||
}
|
||||
printf("✅ Valid auth event passed validation\n");
|
||||
|
||||
// Test wrong relay URL
|
||||
printf("\nTesting wrong relay URL...\n");
|
||||
result = nostr_nip42_verify_auth_event(auth_event, challenge_string, "wss://wrong.relay.com", 0);
|
||||
printf("Expected: NOSTR_ERROR_NIP42_URL_MISMATCH (-206)\n");
|
||||
printf("Actual: %d (%s)\n", result, nostr_strerror(result));
|
||||
|
||||
if (result != NOSTR_ERROR_NIP42_URL_MISMATCH) {
|
||||
printf("❌ Wrong relay validation didn't fail correctly\n");
|
||||
cJSON_Delete(auth_event);
|
||||
return 0;
|
||||
}
|
||||
printf("✅ Wrong relay URL correctly rejected\n");
|
||||
|
||||
// Test wrong challenge
|
||||
printf("\nTesting wrong challenge...\n");
|
||||
result = nostr_nip42_verify_auth_event(auth_event, "wrong_challenge_string_here", relay_url, 0);
|
||||
printf("Expected: NOSTR_ERROR_NIP42_INVALID_CHALLENGE (-203)\n");
|
||||
printf("Actual: %d (%s)\n", result, nostr_strerror(result));
|
||||
|
||||
if (result != NOSTR_ERROR_NIP42_INVALID_CHALLENGE) {
|
||||
printf("❌ Wrong challenge validation didn't fail correctly\n");
|
||||
cJSON_Delete(auth_event);
|
||||
return 0;
|
||||
}
|
||||
printf("✅ Wrong challenge correctly rejected\n");
|
||||
|
||||
cJSON_Delete(auth_event);
|
||||
return 1;
|
||||
}
|
||||
|
||||
// Test 5: AUTH message parsing (client-side)
|
||||
int test_auth_message_parsing(void) {
|
||||
print_test_header("AUTH Message Parsing - Client Side");
|
||||
|
||||
// Test parsing challenge message
|
||||
const char* challenge_msg = "[\"AUTH\", \"test_challenge_from_server_123456789012\"]";
|
||||
printf("Parsing AUTH challenge message: %s\n", challenge_msg);
|
||||
|
||||
char extracted_challenge[NOSTR_NIP42_MAX_CHALLENGE_LENGTH];
|
||||
int result = nostr_nip42_parse_auth_challenge(challenge_msg, extracted_challenge, sizeof(extracted_challenge));
|
||||
|
||||
printf("Parse result: %d (%s)\n", result, nostr_strerror(result));
|
||||
printf("Expected challenge: test_challenge_from_server_123456789012\n");
|
||||
printf("Extracted challenge: %s\n", extracted_challenge);
|
||||
|
||||
if (result != NOSTR_SUCCESS) {
|
||||
printf("❌ Failed to parse valid AUTH message\n");
|
||||
return 0;
|
||||
}
|
||||
|
||||
if (strcmp(extracted_challenge, "test_challenge_from_server_123456789012") != 0) {
|
||||
printf("❌ Extracted challenge doesn't match expected\n");
|
||||
return 0;
|
||||
}
|
||||
printf("✅ AUTH challenge message parsed correctly\n");
|
||||
|
||||
// Test invalid message format
|
||||
printf("\nTesting invalid message format...\n");
|
||||
const char* invalid_msg = "[\"WRONG\", \"challenge\"]";
|
||||
result = nostr_nip42_parse_auth_challenge(invalid_msg, extracted_challenge, sizeof(extracted_challenge));
|
||||
|
||||
printf("Parse result: %d (%s)\n", result, nostr_strerror(result));
|
||||
printf("Expected: NOSTR_ERROR_NIP42_INVALID_MESSAGE_FORMAT (-205)\n");
|
||||
|
||||
if (result != NOSTR_ERROR_NIP42_INVALID_MESSAGE_FORMAT) {
|
||||
printf("❌ Invalid message format should have failed\n");
|
||||
return 0;
|
||||
}
|
||||
printf("✅ Invalid message format correctly rejected\n");
|
||||
|
||||
return 1;
|
||||
}
|
||||
|
||||
// Test 6: AUTH response message creation (client-side)
|
||||
int test_auth_response_creation(void) {
|
||||
print_test_header("AUTH Response Message Creation - Client Side");
|
||||
|
||||
// Create an auth event first
|
||||
const char* private_key_hex = "91ba716fa9e7ea2fcbad360cf4f8e0d312f73984da63d90f524ad61a6a1e7dbe";
|
||||
const char* relay_url = "wss://relay.example.com";
|
||||
const char* challenge_string = "response_test_challenge_1234567890123456";
|
||||
|
||||
unsigned char private_key[32];
|
||||
nostr_hex_to_bytes(private_key_hex, private_key, 32);
|
||||
|
||||
cJSON* auth_event = nostr_nip42_create_auth_event(challenge_string, relay_url, private_key, 0);
|
||||
|
||||
if (!auth_event) {
|
||||
printf("❌ Failed to create auth event for response test\n");
|
||||
return 0;
|
||||
}
|
||||
|
||||
char* auth_response = nostr_nip42_create_auth_message(auth_event);
|
||||
int result = auth_response ? NOSTR_SUCCESS : NOSTR_ERROR_MEMORY_FAILED;
|
||||
|
||||
printf("Response creation result: %d (%s)\n", result, nostr_strerror(result));
|
||||
printf("AUTH response message: %s\n", auth_response ? auth_response : "NULL");
|
||||
|
||||
if (result != NOSTR_SUCCESS) {
|
||||
printf("❌ Failed to create AUTH response message\n");
|
||||
cJSON_Delete(auth_event);
|
||||
return 0;
|
||||
}
|
||||
|
||||
// Parse and validate the response format
|
||||
cJSON* parsed = cJSON_Parse(auth_response);
|
||||
if (!parsed) {
|
||||
printf("❌ AUTH response is not valid JSON\n");
|
||||
cJSON_Delete(auth_event);
|
||||
return 0;
|
||||
}
|
||||
|
||||
// Should be ["AUTH", <event-json>]
|
||||
if (!cJSON_IsArray(parsed) || cJSON_GetArraySize(parsed) != 2) {
|
||||
printf("❌ AUTH response is not a 2-element array\n");
|
||||
cJSON_Delete(parsed);
|
||||
cJSON_Delete(auth_event);
|
||||
return 0;
|
||||
}
|
||||
|
||||
cJSON* first = cJSON_GetArrayItem(parsed, 0);
|
||||
if (!cJSON_IsString(first) || strcmp(cJSON_GetStringValue(first), "AUTH") != 0) {
|
||||
printf("❌ First element is not 'AUTH'\n");
|
||||
cJSON_Delete(parsed);
|
||||
cJSON_Delete(auth_event);
|
||||
return 0;
|
||||
}
|
||||
|
||||
cJSON* second = cJSON_GetArrayItem(parsed, 1);
|
||||
if (!cJSON_IsObject(second)) {
|
||||
printf("❌ Second element is not an object (event)\n");
|
||||
cJSON_Delete(parsed);
|
||||
cJSON_Delete(auth_event);
|
||||
return 0;
|
||||
}
|
||||
|
||||
// Check if the event in the response matches our created event
|
||||
cJSON* response_kind = cJSON_GetObjectItem(second, "kind");
|
||||
if (!cJSON_IsNumber(response_kind) || cJSON_GetNumberValue(response_kind) != 22242) {
|
||||
printf("❌ Response event kind is not 22242\n");
|
||||
cJSON_Delete(parsed);
|
||||
cJSON_Delete(auth_event);
|
||||
return 0;
|
||||
}
|
||||
|
||||
printf("✅ AUTH response message format is correct\n");
|
||||
cJSON_Delete(parsed);
|
||||
cJSON_Delete(auth_event);
|
||||
free(auth_response);
|
||||
return 1;
|
||||
}
|
||||
|
||||
// Test 7: Error conditions and edge cases
|
||||
int test_error_conditions(void) {
|
||||
print_test_header("Error Conditions and Edge Cases");
|
||||
|
||||
int all_passed = 1;
|
||||
|
||||
// Test 1: NULL parameters
|
||||
printf("\nSubtest 1: NULL parameters\n");
|
||||
int result = nostr_nip42_generate_challenge(NULL, 32);
|
||||
printf("Expected: NOSTR_ERROR_INVALID_INPUT (-1)\n");
|
||||
printf("Actual: %d (%s)\n", result, nostr_strerror(result));
|
||||
if (result != NOSTR_ERROR_INVALID_INPUT) all_passed = 0;
|
||||
|
||||
// Test 2: Invalid challenge length
|
||||
printf("\nSubtest 2: Invalid challenge in validation\n");
|
||||
const char* private_key_hex = "91ba716fa9e7ea2fcbad360cf4f8e0d312f73984da63d90f524ad61a6a1e7dbe";
|
||||
const char* relay_url = "wss://relay.example.com";
|
||||
const char* valid_challenge = "valid_challenge_1234567890123456789012345";
|
||||
|
||||
unsigned char private_key[32];
|
||||
nostr_hex_to_bytes(private_key_hex, private_key, 32);
|
||||
|
||||
cJSON* auth_event = nostr_nip42_create_auth_event(valid_challenge, relay_url, private_key, 0);
|
||||
if (auth_event) {
|
||||
result = nostr_nip42_verify_auth_event(auth_event, "short", relay_url, 0); // Too short
|
||||
printf("Expected: NOSTR_ERROR_NIP42_INVALID_CHALLENGE (-203)\n");
|
||||
printf("Actual: %d (%s)\n", result, nostr_strerror(result));
|
||||
if (result != NOSTR_ERROR_NIP42_INVALID_CHALLENGE) all_passed = 0;
|
||||
cJSON_Delete(auth_event);
|
||||
} else {
|
||||
printf("❌ Failed to create auth event for validation test\n");
|
||||
all_passed = 0;
|
||||
}
|
||||
|
||||
// Test 3: Invalid JSON parsing
|
||||
printf("\nSubtest 3: Invalid JSON in message parsing\n");
|
||||
char challenge_buffer[NOSTR_NIP42_MAX_CHALLENGE_LENGTH];
|
||||
result = nostr_nip42_parse_auth_challenge("invalid json", challenge_buffer, sizeof(challenge_buffer));
|
||||
printf("Expected: NOSTR_ERROR_NIP42_INVALID_MESSAGE_FORMAT (-205)\n");
|
||||
printf("Actual: %d (%s)\n", result, nostr_strerror(result));
|
||||
if (result != NOSTR_ERROR_NIP42_INVALID_MESSAGE_FORMAT) all_passed = 0;
|
||||
|
||||
// Test 4: Wrong array size
|
||||
printf("\nSubtest 4: Wrong array size in message parsing\n");
|
||||
result = nostr_nip42_parse_auth_challenge("[\"AUTH\"]", challenge_buffer, sizeof(challenge_buffer)); // Only 1 element
|
||||
printf("Expected: NOSTR_ERROR_NIP42_INVALID_MESSAGE_FORMAT (-205)\n");
|
||||
printf("Actual: %d (%s)\n", result, nostr_strerror(result));
|
||||
if (result != NOSTR_ERROR_NIP42_INVALID_MESSAGE_FORMAT) all_passed = 0;
|
||||
|
||||
return all_passed;
|
||||
}
|
||||
|
||||
// Test 8: Full authentication flow simulation
|
||||
int test_full_auth_flow(void) {
|
||||
print_test_header("Full Authentication Flow Simulation");
|
||||
|
||||
const char* private_key_hex = "91ba716fa9e7ea2fcbad360cf4f8e0d312f73984da63d90f524ad61a6a1e7dbe";
|
||||
const char* relay_url = "wss://test-relay.nostr.com";
|
||||
|
||||
unsigned char private_key[32];
|
||||
nostr_hex_to_bytes(private_key_hex, private_key, 32);
|
||||
|
||||
printf("=== STEP 1: Server generates challenge ===\n");
|
||||
nostr_auth_challenge_t challenge;
|
||||
int result = nostr_nip42_generate_challenge(challenge.challenge, NOSTR_NIP42_DEFAULT_CHALLENGE_LENGTH);
|
||||
if (result != NOSTR_SUCCESS) {
|
||||
printf("❌ Failed to generate challenge\n");
|
||||
return 0;
|
||||
}
|
||||
printf("Generated challenge: %s\n", challenge.challenge);
|
||||
|
||||
printf("\n=== STEP 2: Server sends AUTH message ===\n");
|
||||
cJSON* message_array = cJSON_CreateArray();
|
||||
cJSON_AddItemToArray(message_array, cJSON_CreateString("AUTH"));
|
||||
cJSON_AddItemToArray(message_array, cJSON_CreateString(challenge.challenge));
|
||||
char* auth_message = cJSON_PrintUnformatted(message_array);
|
||||
cJSON_Delete(message_array);
|
||||
|
||||
if (!auth_message) {
|
||||
printf("❌ Failed to create AUTH message\n");
|
||||
return 0;
|
||||
}
|
||||
printf("Server sends: %s\n", auth_message);
|
||||
|
||||
printf("\n=== STEP 3: Client parses AUTH message ===\n");
|
||||
char parsed_challenge[NOSTR_NIP42_MAX_CHALLENGE_LENGTH];
|
||||
result = nostr_nip42_parse_auth_challenge(auth_message, parsed_challenge, sizeof(parsed_challenge));
|
||||
if (result != NOSTR_SUCCESS) {
|
||||
printf("❌ Failed to parse AUTH message\n");
|
||||
free(auth_message);
|
||||
return 0;
|
||||
}
|
||||
printf("Client extracted challenge: %s\n", parsed_challenge);
|
||||
|
||||
if (strcmp(challenge.challenge, parsed_challenge) != 0) {
|
||||
printf("❌ Parsed challenge doesn't match original\n");
|
||||
free(auth_message);
|
||||
return 0;
|
||||
}
|
||||
|
||||
printf("\n=== STEP 4: Client creates auth event ===\n");
|
||||
cJSON* auth_event = nostr_nip42_create_auth_event(parsed_challenge, relay_url, private_key, 0);
|
||||
if (!auth_event) {
|
||||
printf("❌ Failed to create auth event\n");
|
||||
return 0;
|
||||
}
|
||||
|
||||
char* event_str = cJSON_Print(auth_event);
|
||||
printf("Client created auth event:\n%s\n", event_str);
|
||||
free(event_str);
|
||||
|
||||
printf("\n=== STEP 5: Client sends AUTH response ===\n");
|
||||
char* auth_response = nostr_nip42_create_auth_message(auth_event);
|
||||
if (!auth_response) {
|
||||
printf("❌ Failed to create AUTH response\n");
|
||||
cJSON_Delete(auth_event);
|
||||
free(auth_message);
|
||||
return 0;
|
||||
}
|
||||
printf("Client sends: %s\n", auth_response);
|
||||
|
||||
printf("\n=== STEP 6: Server validates auth event ===\n");
|
||||
result = nostr_nip42_verify_auth_event(auth_event, challenge.challenge, relay_url, 0);
|
||||
printf("Server validation result: %d (%s)\n", result, nostr_strerror(result));
|
||||
|
||||
if (result != NOSTR_SUCCESS) {
|
||||
printf("❌ Server validation failed\n");
|
||||
cJSON_Delete(auth_event);
|
||||
free(auth_message);
|
||||
free(auth_response);
|
||||
return 0;
|
||||
}
|
||||
|
||||
printf("✅ FULL AUTHENTICATION FLOW COMPLETED SUCCESSFULLY!\n");
|
||||
printf("✅ Challenge generated -> Message sent -> Challenge parsed -> Event created -> Response sent -> Event validated\n");
|
||||
|
||||
cJSON_Delete(auth_event);
|
||||
free(auth_message);
|
||||
free(auth_response);
|
||||
return 1;
|
||||
}
|
||||
|
||||
int main(void) {
|
||||
printf("=== NIP-42 Authentication of Clients to Relays Test Suite ===\n");
|
||||
printf("Following TESTS POLICY: Shows expected vs actual values, prints entire JSON events\n");
|
||||
printf("Tests both client-side and server-side authentication functionality\n");
|
||||
|
||||
// Initialize crypto library
|
||||
if (nostr_init() != NOSTR_SUCCESS) {
|
||||
printf("❌ Failed to initialize nostr library\n");
|
||||
return 1;
|
||||
}
|
||||
|
||||
int all_passed = 1;
|
||||
int test_result;
|
||||
|
||||
// Test 1: Challenge generation
|
||||
test_result = test_challenge_generation();
|
||||
print_test_result(test_result, "Challenge Generation");
|
||||
if (!test_result) all_passed = 0;
|
||||
|
||||
// Test 2: AUTH message creation
|
||||
test_result = test_auth_message_creation();
|
||||
print_test_result(test_result, "AUTH Message Creation - Server Challenge");
|
||||
if (!test_result) all_passed = 0;
|
||||
|
||||
// Test 3: Auth event creation
|
||||
test_result = test_auth_event_creation();
|
||||
print_test_result(test_result, "Authentication Event Creation - Client Side");
|
||||
if (!test_result) all_passed = 0;
|
||||
|
||||
// Test 4: Auth event validation
|
||||
test_result = test_auth_event_validation();
|
||||
print_test_result(test_result, "Authentication Event Validation - Server Side");
|
||||
if (!test_result) all_passed = 0;
|
||||
|
||||
// Test 5: AUTH message parsing
|
||||
test_result = test_auth_message_parsing();
|
||||
print_test_result(test_result, "AUTH Message Parsing - Client Side");
|
||||
if (!test_result) all_passed = 0;
|
||||
|
||||
// Test 6: AUTH response creation
|
||||
test_result = test_auth_response_creation();
|
||||
print_test_result(test_result, "AUTH Response Message Creation - Client Side");
|
||||
if (!test_result) all_passed = 0;
|
||||
|
||||
// Test 7: Error conditions
|
||||
test_result = test_error_conditions();
|
||||
print_test_result(test_result, "Error Conditions and Edge Cases");
|
||||
if (!test_result) all_passed = 0;
|
||||
|
||||
// Test 8: Full authentication flow
|
||||
test_result = test_full_auth_flow();
|
||||
print_test_result(test_result, "Full Authentication Flow Simulation");
|
||||
if (!test_result) all_passed = 0;
|
||||
|
||||
// Summary
|
||||
printf("\n=== TEST SUMMARY ===\n");
|
||||
printf("Total tests: %d\n", test_count);
|
||||
printf("Passed: %d\n", passed_tests);
|
||||
printf("Failed: %d\n", test_count - passed_tests);
|
||||
|
||||
if (all_passed) {
|
||||
printf("🎉 ALL TESTS PASSED! NIP-42 Authentication implementation is working correctly.\n");
|
||||
printf("✅ Challenge generation works\n");
|
||||
printf("✅ AUTH message creation/parsing works\n");
|
||||
printf("✅ Authentication event creation works\n");
|
||||
printf("✅ Authentication event validation works\n");
|
||||
printf("✅ Full authentication flow works\n");
|
||||
printf("✅ Error handling works\n");
|
||||
} else {
|
||||
printf("❌ SOME TESTS FAILED. Please review the output above.\n");
|
||||
}
|
||||
|
||||
nostr_cleanup();
|
||||
return all_passed ? 0 : 1;
|
||||
}
|
||||
@@ -0,0 +1,692 @@
|
||||
/*
|
||||
* Interactive Relay Pool Test Program
|
||||
*
|
||||
* Interactive command-line interface for testing nostr_relay_pool functionality.
|
||||
* All output is logged to pool.log while the menu runs in the terminal.
|
||||
*
|
||||
* Usage: ./pool_test
|
||||
*/
|
||||
|
||||
#define _POSIX_C_SOURCE 200809L
|
||||
#include <stdio.h>
|
||||
#include <stdlib.h>
|
||||
#include <string.h>
|
||||
#include <signal.h>
|
||||
#include <time.h>
|
||||
#include <unistd.h>
|
||||
#include <pthread.h>
|
||||
#include <fcntl.h>
|
||||
#include <sys/stat.h>
|
||||
#include "../nostr_core/nostr_core.h"
|
||||
#include "../cjson/cJSON.h"
|
||||
|
||||
// Global variables
|
||||
volatile sig_atomic_t running = 1;
|
||||
nostr_relay_pool_t* pool = NULL;
|
||||
nostr_pool_subscription_t** subscriptions = NULL;
|
||||
int subscription_count = 0;
|
||||
int subscription_capacity = 0;
|
||||
pthread_t poll_thread;
|
||||
int log_fd = -1;
|
||||
|
||||
// Signal handler for clean shutdown
|
||||
void signal_handler(int signum) {
|
||||
(void)signum;
|
||||
running = 0;
|
||||
}
|
||||
|
||||
// Event callback - called when an event is received
|
||||
void on_event(cJSON* event, const char* relay_url, void* user_data) {
|
||||
(void)user_data;
|
||||
|
||||
// Extract basic event information
|
||||
cJSON* id = cJSON_GetObjectItem(event, "id");
|
||||
cJSON* pubkey = cJSON_GetObjectItem(event, "pubkey");
|
||||
cJSON* created_at = cJSON_GetObjectItem(event, "created_at");
|
||||
cJSON* kind = cJSON_GetObjectItem(event, "kind");
|
||||
cJSON* content = cJSON_GetObjectItem(event, "content");
|
||||
|
||||
time_t now = time(NULL);
|
||||
char timestamp[26];
|
||||
ctime_r(&now, timestamp);
|
||||
timestamp[24] = '\0'; // Remove newline
|
||||
|
||||
dprintf(log_fd, "[%s] 📨 EVENT from %s\n", timestamp, relay_url);
|
||||
dprintf(log_fd, "├── ID: %.12s...\n", id && cJSON_IsString(id) ? cJSON_GetStringValue(id) : "unknown");
|
||||
dprintf(log_fd, "├── Pubkey: %.12s...\n", pubkey && cJSON_IsString(pubkey) ? cJSON_GetStringValue(pubkey) : "unknown");
|
||||
dprintf(log_fd, "├── Kind: %d\n", kind && cJSON_IsNumber(kind) ? (int)cJSON_GetNumberValue(kind) : -1);
|
||||
dprintf(log_fd, "├── Created: %lld\n", created_at && cJSON_IsNumber(created_at) ? (long long)cJSON_GetNumberValue(created_at) : 0);
|
||||
|
||||
// Truncate content if too long
|
||||
if (content && cJSON_IsString(content)) {
|
||||
const char* content_str = cJSON_GetStringValue(content);
|
||||
size_t content_len = strlen(content_str);
|
||||
if (content_len > 100) {
|
||||
dprintf(log_fd, "└── Content: %.97s...\n", content_str);
|
||||
} else {
|
||||
dprintf(log_fd, "└── Content: %s\n", content_str);
|
||||
}
|
||||
} else {
|
||||
dprintf(log_fd, "└── Content: (empty)\n");
|
||||
}
|
||||
dprintf(log_fd, "\n");
|
||||
}
|
||||
|
||||
// EOSE callback - called when End of Stored Events is received
|
||||
void on_eose(cJSON** events, int event_count, void* user_data) {
|
||||
(void)user_data;
|
||||
time_t now = time(NULL);
|
||||
char timestamp[26];
|
||||
ctime_r(&now, timestamp);
|
||||
timestamp[24] = '\0';
|
||||
dprintf(log_fd, "[%s] 📋 EOSE received - %d events collected\n", timestamp, event_count);
|
||||
|
||||
// Log collected events if any
|
||||
for (int i = 0; i < event_count; i++) {
|
||||
cJSON* id = cJSON_GetObjectItem(events[i], "id");
|
||||
if (id && cJSON_IsString(id)) {
|
||||
dprintf(log_fd, " Event %d: %.12s...\n", i + 1, cJSON_GetStringValue(id));
|
||||
}
|
||||
}
|
||||
dprintf(log_fd, "\n");
|
||||
}
|
||||
|
||||
// Background polling thread
|
||||
void* poll_thread_func(void* arg) {
|
||||
(void)arg;
|
||||
|
||||
while (running) {
|
||||
if (pool) {
|
||||
nostr_relay_pool_poll(pool, 100);
|
||||
}
|
||||
struct timespec ts = {0, 10000000}; // 10ms
|
||||
nanosleep(&ts, NULL);
|
||||
}
|
||||
|
||||
return NULL;
|
||||
}
|
||||
|
||||
// Print menu
|
||||
void print_menu() {
|
||||
printf("\n=== NOSTR Relay Pool Test Menu ===\n");
|
||||
printf("1. Start Pool (wss://relay.laantungir.net)\n");
|
||||
printf("2. Stop Pool\n");
|
||||
printf("3. Add relay to pool\n");
|
||||
printf("4. Remove relay from pool\n");
|
||||
printf("5. Add subscription\n");
|
||||
printf("6. Remove subscription\n");
|
||||
printf("7. Show pool status\n");
|
||||
printf("8. Test reconnection (simulate disconnect)\n");
|
||||
printf("9. Exit\n");
|
||||
printf("Choice: ");
|
||||
}
|
||||
|
||||
// Get user input with default
|
||||
char* get_input(const char* prompt, const char* default_value) {
|
||||
static char buffer[1024];
|
||||
printf("%s", prompt);
|
||||
if (default_value) {
|
||||
printf(" [%s]", default_value);
|
||||
}
|
||||
printf(": ");
|
||||
|
||||
if (!fgets(buffer, sizeof(buffer), stdin)) {
|
||||
return NULL;
|
||||
}
|
||||
|
||||
// Remove newline
|
||||
size_t len = strlen(buffer);
|
||||
if (len > 0 && buffer[len-1] == '\n') {
|
||||
buffer[len-1] = '\0';
|
||||
}
|
||||
|
||||
// Return default if empty
|
||||
if (strlen(buffer) == 0 && default_value) {
|
||||
return strdup(default_value);
|
||||
}
|
||||
|
||||
return strdup(buffer);
|
||||
}
|
||||
|
||||
// Parse comma-separated list into cJSON array
|
||||
cJSON* parse_comma_list(const char* input, int is_number) {
|
||||
if (!input || strlen(input) == 0) {
|
||||
return NULL;
|
||||
}
|
||||
|
||||
cJSON* array = cJSON_CreateArray();
|
||||
if (!array) return NULL;
|
||||
|
||||
char* input_copy = strdup(input);
|
||||
char* token = strtok(input_copy, ",");
|
||||
|
||||
while (token) {
|
||||
// Trim whitespace
|
||||
while (*token == ' ') token++;
|
||||
char* end = token + strlen(token) - 1;
|
||||
while (end > token && *end == ' ') *end-- = '\0';
|
||||
|
||||
if (is_number) {
|
||||
int num = atoi(token);
|
||||
cJSON_AddItemToArray(array, cJSON_CreateNumber(num));
|
||||
} else {
|
||||
cJSON_AddItemToArray(array, cJSON_CreateString(token));
|
||||
}
|
||||
|
||||
token = strtok(NULL, ",");
|
||||
}
|
||||
|
||||
free(input_copy);
|
||||
return array;
|
||||
}
|
||||
|
||||
// Add subscription interactively
|
||||
void add_subscription() {
|
||||
if (!pool) {
|
||||
printf("❌ Pool not started\n");
|
||||
return;
|
||||
}
|
||||
|
||||
printf("\n--- Add Subscription ---\n");
|
||||
printf("Enter filter values (press Enter for no value):\n");
|
||||
|
||||
cJSON* filter = cJSON_CreateObject();
|
||||
|
||||
// ids
|
||||
char* ids_input = get_input("ids (comma-separated event ids)", NULL);
|
||||
if (ids_input && strlen(ids_input) > 0) {
|
||||
cJSON* ids = parse_comma_list(ids_input, 0);
|
||||
if (ids) cJSON_AddItemToObject(filter, "ids", ids);
|
||||
}
|
||||
free(ids_input);
|
||||
|
||||
// authors
|
||||
char* authors_input = get_input("authors (comma-separated pubkeys)", NULL);
|
||||
if (authors_input && strlen(authors_input) > 0) {
|
||||
cJSON* authors = parse_comma_list(authors_input, 0);
|
||||
if (authors) cJSON_AddItemToObject(filter, "authors", authors);
|
||||
}
|
||||
free(authors_input);
|
||||
|
||||
// kinds
|
||||
char* kinds_input = get_input("kinds (comma-separated numbers)", NULL);
|
||||
if (kinds_input && strlen(kinds_input) > 0) {
|
||||
cJSON* kinds = parse_comma_list(kinds_input, 1);
|
||||
if (kinds) cJSON_AddItemToObject(filter, "kinds", kinds);
|
||||
}
|
||||
free(kinds_input);
|
||||
|
||||
// #e tag
|
||||
char* e_input = get_input("#e (comma-separated event ids)", NULL);
|
||||
if (e_input && strlen(e_input) > 0) {
|
||||
cJSON* e_array = parse_comma_list(e_input, 0);
|
||||
if (e_array) cJSON_AddItemToObject(filter, "#e", e_array);
|
||||
}
|
||||
free(e_input);
|
||||
|
||||
// #p tag
|
||||
char* p_input = get_input("#p (comma-separated pubkeys)", NULL);
|
||||
if (p_input && strlen(p_input) > 0) {
|
||||
cJSON* p_array = parse_comma_list(p_input, 0);
|
||||
if (p_array) cJSON_AddItemToObject(filter, "#p", p_array);
|
||||
}
|
||||
free(p_input);
|
||||
|
||||
// since
|
||||
char* since_input = get_input("since (unix timestamp or 'n' for now)", NULL);
|
||||
if (since_input && strlen(since_input) > 0) {
|
||||
if (strcmp(since_input, "n") == 0) {
|
||||
// Use current timestamp
|
||||
time_t now = time(NULL);
|
||||
cJSON_AddItemToObject(filter, "since", cJSON_CreateNumber((int)now));
|
||||
printf("Using current timestamp: %ld\n", now);
|
||||
} else {
|
||||
int since = atoi(since_input);
|
||||
if (since > 0) cJSON_AddItemToObject(filter, "since", cJSON_CreateNumber(since));
|
||||
}
|
||||
}
|
||||
free(since_input);
|
||||
|
||||
// until
|
||||
char* until_input = get_input("until (unix timestamp)", NULL);
|
||||
if (until_input && strlen(until_input) > 0) {
|
||||
int until = atoi(until_input);
|
||||
if (until > 0) cJSON_AddItemToObject(filter, "until", cJSON_CreateNumber(until));
|
||||
}
|
||||
free(until_input);
|
||||
|
||||
// limit
|
||||
char* limit_input = get_input("limit (max events)", "10");
|
||||
if (limit_input && strlen(limit_input) > 0) {
|
||||
int limit = atoi(limit_input);
|
||||
if (limit > 0) cJSON_AddItemToObject(filter, "limit", cJSON_CreateNumber(limit));
|
||||
}
|
||||
free(limit_input);
|
||||
|
||||
// Get relay URLs from pool
|
||||
char** relay_urls = NULL;
|
||||
nostr_pool_relay_status_t* statuses = NULL;
|
||||
int relay_count = nostr_relay_pool_list_relays(pool, &relay_urls, &statuses);
|
||||
|
||||
if (relay_count <= 0) {
|
||||
printf("❌ No relays in pool\n");
|
||||
cJSON_Delete(filter);
|
||||
return;
|
||||
}
|
||||
|
||||
// Ask about close_on_eose behavior
|
||||
char* close_input = get_input("Close subscription on EOSE? (y/n)", "n");
|
||||
int close_on_eose = (close_input && strcmp(close_input, "y") == 0) ? 1 : 0;
|
||||
free(close_input);
|
||||
|
||||
// Create subscription with new parameters
|
||||
nostr_pool_subscription_t* sub = nostr_relay_pool_subscribe(
|
||||
pool,
|
||||
(const char**)relay_urls,
|
||||
relay_count,
|
||||
filter,
|
||||
on_event,
|
||||
on_eose,
|
||||
NULL,
|
||||
close_on_eose,
|
||||
1, // enable_deduplication
|
||||
NOSTR_POOL_EOSE_FULL_SET, // result_mode
|
||||
30, // relay_timeout_seconds
|
||||
60 // eose_timeout_seconds
|
||||
);
|
||||
|
||||
// Free relay URLs
|
||||
for (int i = 0; i < relay_count; i++) {
|
||||
free(relay_urls[i]);
|
||||
}
|
||||
free(relay_urls);
|
||||
free(statuses);
|
||||
|
||||
if (!sub) {
|
||||
printf("❌ Failed to create subscription\n");
|
||||
cJSON_Delete(filter);
|
||||
return;
|
||||
}
|
||||
|
||||
// Store subscription
|
||||
if (subscription_count >= subscription_capacity) {
|
||||
subscription_capacity = subscription_capacity == 0 ? 10 : subscription_capacity * 2;
|
||||
subscriptions = realloc(subscriptions, subscription_capacity * sizeof(nostr_pool_subscription_t*));
|
||||
}
|
||||
subscriptions[subscription_count++] = sub;
|
||||
|
||||
printf("✅ Subscription created (ID: %d)\n", subscription_count);
|
||||
|
||||
// Log the filter
|
||||
char* filter_json = cJSON_Print(filter);
|
||||
time_t now = time(NULL);
|
||||
char timestamp[26];
|
||||
ctime_r(&now, timestamp);
|
||||
timestamp[24] = '\0';
|
||||
dprintf(log_fd, "[%s] 🔍 New subscription created (ID: %d)\n", timestamp, subscription_count);
|
||||
dprintf(log_fd, "Filter: %s\n\n", filter_json);
|
||||
free(filter_json);
|
||||
}
|
||||
|
||||
// Remove subscription
|
||||
void remove_subscription() {
|
||||
if (subscription_count == 0) {
|
||||
printf("❌ No subscriptions to remove\n");
|
||||
return;
|
||||
}
|
||||
|
||||
printf("\n--- Remove Subscription ---\n");
|
||||
printf("Available subscriptions:\n");
|
||||
for (int i = 0; i < subscription_count; i++) {
|
||||
printf("%d. Subscription %d\n", i + 1, i + 1);
|
||||
}
|
||||
|
||||
char* choice_input = get_input("Enter subscription number to remove", NULL);
|
||||
if (!choice_input || strlen(choice_input) == 0) {
|
||||
free(choice_input);
|
||||
return;
|
||||
}
|
||||
|
||||
int choice = atoi(choice_input) - 1;
|
||||
free(choice_input);
|
||||
|
||||
if (choice < 0 || choice >= subscription_count) {
|
||||
printf("❌ Invalid subscription number\n");
|
||||
return;
|
||||
}
|
||||
|
||||
nostr_pool_subscription_close(subscriptions[choice]);
|
||||
|
||||
// Shift remaining subscriptions
|
||||
for (int i = choice; i < subscription_count - 1; i++) {
|
||||
subscriptions[i] = subscriptions[i + 1];
|
||||
}
|
||||
subscription_count--;
|
||||
|
||||
printf("✅ Subscription removed\n");
|
||||
|
||||
time_t now = time(NULL);
|
||||
char timestamp[26];
|
||||
ctime_r(&now, timestamp);
|
||||
timestamp[24] = '\0';
|
||||
dprintf(log_fd, "[%s] 🗑️ Subscription removed (was ID: %d)\n\n", timestamp, choice + 1);
|
||||
}
|
||||
|
||||
// Show pool status
|
||||
void show_pool_status() {
|
||||
if (!pool) {
|
||||
printf("❌ Pool not started\n");
|
||||
return;
|
||||
}
|
||||
|
||||
// Give polling thread time to establish connections
|
||||
printf("⏳ Waiting for connections to establish...\n");
|
||||
sleep(3);
|
||||
|
||||
char** relay_urls = NULL;
|
||||
nostr_pool_relay_status_t* statuses = NULL;
|
||||
int relay_count = nostr_relay_pool_list_relays(pool, &relay_urls, &statuses);
|
||||
|
||||
printf("\n📊 POOL STATUS\n");
|
||||
printf("Relays: %d\n", relay_count);
|
||||
printf("Subscriptions: %d\n", subscription_count);
|
||||
|
||||
if (relay_count > 0) {
|
||||
printf("\nRelay Details:\n");
|
||||
for (int i = 0; i < relay_count; i++) {
|
||||
const char* status_str;
|
||||
switch (statuses[i]) {
|
||||
case NOSTR_POOL_RELAY_CONNECTED: status_str = "🟢 CONNECTED"; break;
|
||||
case NOSTR_POOL_RELAY_CONNECTING: status_str = "🟡 CONNECTING"; break;
|
||||
case NOSTR_POOL_RELAY_DISCONNECTED: status_str = "⚪ DISCONNECTED"; break;
|
||||
case NOSTR_POOL_RELAY_ERROR: status_str = "🔴 ERROR"; break;
|
||||
default: status_str = "❓ UNKNOWN"; break;
|
||||
}
|
||||
|
||||
printf("├── %s: %s\n", relay_urls[i], status_str);
|
||||
|
||||
const nostr_relay_stats_t* stats = nostr_relay_pool_get_relay_stats(pool, relay_urls[i]);
|
||||
if (stats) {
|
||||
printf("│ ├── Events received: %d\n", stats->events_received);
|
||||
printf("│ ├── Connection attempts: %d\n", stats->connection_attempts);
|
||||
printf("│ ├── Connection failures: %d\n", stats->connection_failures);
|
||||
printf("│ ├── Ping latency: %.2f ms\n", stats->ping_latency_current);
|
||||
printf("│ └── Query latency: %.2f ms\n", stats->query_latency_avg);
|
||||
}
|
||||
|
||||
free(relay_urls[i]);
|
||||
}
|
||||
free(relay_urls);
|
||||
free(statuses);
|
||||
}
|
||||
printf("\n");
|
||||
}
|
||||
|
||||
int main() {
|
||||
// Setup logging to file
|
||||
log_fd = open("pool.log", O_WRONLY | O_CREAT | O_TRUNC, 0644);
|
||||
if (log_fd == -1) {
|
||||
fprintf(stderr, "❌ Failed to open pool.log for writing\n");
|
||||
return 1;
|
||||
}
|
||||
|
||||
// Initialize NOSTR library
|
||||
if (nostr_init() != NOSTR_SUCCESS) {
|
||||
fprintf(stderr, "❌ Failed to initialize NOSTR library\n");
|
||||
close(log_fd);
|
||||
return 1;
|
||||
}
|
||||
|
||||
// Setup signal handler
|
||||
signal(SIGINT, signal_handler);
|
||||
signal(SIGTERM, signal_handler);
|
||||
|
||||
// Start polling thread
|
||||
if (pthread_create(&poll_thread, NULL, poll_thread_func, NULL) != 0) {
|
||||
fprintf(stderr, "❌ Failed to create polling thread\n");
|
||||
nostr_cleanup();
|
||||
close(log_fd);
|
||||
return 1;
|
||||
}
|
||||
|
||||
printf("🔗 NOSTR Relay Pool Interactive Test\n");
|
||||
printf("=====================================\n");
|
||||
printf("All event output is logged to pool.log\n");
|
||||
printf("Press Ctrl+C to exit\n\n");
|
||||
|
||||
time_t now = time(NULL);
|
||||
char timestamp[26];
|
||||
ctime_r(&now, timestamp);
|
||||
timestamp[24] = '\0';
|
||||
dprintf(log_fd, "[%s] 🚀 Pool test started\n\n", timestamp);
|
||||
|
||||
// Main menu loop
|
||||
while (running) {
|
||||
print_menu();
|
||||
|
||||
char choice;
|
||||
if (scanf("%c", &choice) != 1) {
|
||||
break;
|
||||
}
|
||||
|
||||
// Consume newline
|
||||
int c;
|
||||
while ((c = getchar()) != '\n' && c != EOF);
|
||||
|
||||
switch (choice) {
|
||||
case '1': { // Start Pool
|
||||
if (pool) {
|
||||
printf("❌ Pool already started\n");
|
||||
break;
|
||||
}
|
||||
|
||||
// Create pool with custom reconnection configuration for faster testing
|
||||
nostr_pool_reconnect_config_t* config = nostr_pool_reconnect_config_default();
|
||||
config->ping_interval_seconds = 5; // Ping every 5 seconds for testing
|
||||
pool = nostr_relay_pool_create(config);
|
||||
if (!pool) {
|
||||
printf("❌ Failed to create pool\n");
|
||||
break;
|
||||
}
|
||||
|
||||
if (nostr_relay_pool_add_relay(pool, "wss://relay.laantungir.net") != NOSTR_SUCCESS) {
|
||||
printf("❌ Failed to add default relay\n");
|
||||
nostr_relay_pool_destroy(pool);
|
||||
pool = NULL;
|
||||
break;
|
||||
}
|
||||
|
||||
printf("✅ Pool started with wss://relay.laantungir.net\n");
|
||||
|
||||
now = time(NULL);
|
||||
ctime_r(&now, timestamp);
|
||||
timestamp[24] = '\0';
|
||||
dprintf(log_fd, "[%s] 🏊 Pool started with default relay\n\n", timestamp);
|
||||
break;
|
||||
}
|
||||
|
||||
case '2': { // Stop Pool
|
||||
if (!pool) {
|
||||
printf("❌ Pool not started\n");
|
||||
break;
|
||||
}
|
||||
|
||||
// Close all subscriptions
|
||||
for (int i = 0; i < subscription_count; i++) {
|
||||
if (subscriptions[i]) {
|
||||
nostr_pool_subscription_close(subscriptions[i]);
|
||||
}
|
||||
}
|
||||
free(subscriptions);
|
||||
subscriptions = NULL;
|
||||
subscription_count = 0;
|
||||
subscription_capacity = 0;
|
||||
|
||||
nostr_relay_pool_destroy(pool);
|
||||
pool = NULL;
|
||||
|
||||
printf("✅ Pool stopped\n");
|
||||
|
||||
now = time(NULL);
|
||||
ctime_r(&now, timestamp);
|
||||
timestamp[24] = '\0';
|
||||
dprintf(log_fd, "[%s] 🛑 Pool stopped\n\n", timestamp);
|
||||
break;
|
||||
}
|
||||
|
||||
case '3': { // Add relay
|
||||
if (!pool) {
|
||||
printf("❌ Pool not started\n");
|
||||
break;
|
||||
}
|
||||
|
||||
char* url = get_input("Enter relay URL", "wss://relay.example.com");
|
||||
if (url && strlen(url) > 0) {
|
||||
if (nostr_relay_pool_add_relay(pool, url) == NOSTR_SUCCESS) {
|
||||
printf("✅ Relay added: %s\n", url);
|
||||
|
||||
now = time(NULL);
|
||||
ctime_r(&now, timestamp);
|
||||
timestamp[24] = '\0';
|
||||
dprintf(log_fd, "[%s] ➕ Relay added: %s\n\n", timestamp, url);
|
||||
} else {
|
||||
printf("❌ Failed to add relay\n");
|
||||
}
|
||||
}
|
||||
free(url);
|
||||
break;
|
||||
}
|
||||
|
||||
case '4': { // Remove relay
|
||||
if (!pool) {
|
||||
printf("❌ Pool not started\n");
|
||||
break;
|
||||
}
|
||||
|
||||
char* url = get_input("Enter relay URL to remove", NULL);
|
||||
if (url && strlen(url) > 0) {
|
||||
if (nostr_relay_pool_remove_relay(pool, url) == NOSTR_SUCCESS) {
|
||||
printf("✅ Relay removed: %s\n", url);
|
||||
|
||||
now = time(NULL);
|
||||
ctime_r(&now, timestamp);
|
||||
timestamp[24] = '\0';
|
||||
dprintf(log_fd, "[%s] ➖ Relay removed: %s\n\n", timestamp, url);
|
||||
} else {
|
||||
printf("❌ Failed to remove relay\n");
|
||||
}
|
||||
}
|
||||
free(url);
|
||||
break;
|
||||
}
|
||||
|
||||
case '5': // Add subscription
|
||||
add_subscription();
|
||||
break;
|
||||
|
||||
case '6': // Remove subscription
|
||||
remove_subscription();
|
||||
break;
|
||||
|
||||
case '7': // Show status
|
||||
show_pool_status();
|
||||
break;
|
||||
|
||||
case '8': { // Test reconnection
|
||||
if (!pool) {
|
||||
printf("❌ Pool not started\n");
|
||||
break;
|
||||
}
|
||||
|
||||
char** relay_urls = NULL;
|
||||
nostr_pool_relay_status_t* statuses = NULL;
|
||||
int relay_count = nostr_relay_pool_list_relays(pool, &relay_urls, &statuses);
|
||||
|
||||
if (relay_count <= 0) {
|
||||
printf("❌ No relays in pool\n");
|
||||
break;
|
||||
}
|
||||
|
||||
printf("\n--- Test Reconnection ---\n");
|
||||
printf("Available relays:\n");
|
||||
for (int i = 0; i < relay_count; i++) {
|
||||
printf("%d. %s (%s)\n", i + 1, relay_urls[i],
|
||||
statuses[i] == NOSTR_POOL_RELAY_CONNECTED ? "CONNECTED" : "NOT CONNECTED");
|
||||
}
|
||||
|
||||
char* choice_input = get_input("Enter relay number to test reconnection with", NULL);
|
||||
if (!choice_input || strlen(choice_input) == 0) {
|
||||
for (int i = 0; i < relay_count; i++) free(relay_urls[i]);
|
||||
free(relay_urls);
|
||||
free(statuses);
|
||||
free(choice_input);
|
||||
break;
|
||||
}
|
||||
|
||||
int choice = atoi(choice_input) - 1;
|
||||
free(choice_input);
|
||||
|
||||
if (choice < 0 || choice >= relay_count) {
|
||||
printf("❌ Invalid relay number\n");
|
||||
for (int i = 0; i < relay_count; i++) free(relay_urls[i]);
|
||||
free(relay_urls);
|
||||
free(statuses);
|
||||
break;
|
||||
}
|
||||
|
||||
printf("🔄 Testing reconnection with %s...\n", relay_urls[choice]);
|
||||
printf(" The pool is configured with automatic reconnection enabled.\n");
|
||||
printf(" If the connection drops, it will automatically attempt to reconnect\n");
|
||||
printf(" with exponential backoff (1s → 2s → 4s → 8s → 16s → 30s max).\n");
|
||||
printf(" Connection health is monitored with ping/pong every 30 seconds.\n");
|
||||
|
||||
time_t now = time(NULL);
|
||||
char timestamp[26];
|
||||
ctime_r(&now, timestamp);
|
||||
timestamp[24] = '\0';
|
||||
dprintf(log_fd, "[%s] 🔄 TEST: Testing reconnection behavior with %s\n", timestamp, relay_urls[choice]);
|
||||
dprintf(log_fd, " Pool configured with: auto-reconnect=ON, max_attempts=10, ping_interval=30s\n\n");
|
||||
|
||||
printf("✅ Reconnection test initiated. Monitor the status and logs for reconnection activity.\n");
|
||||
|
||||
for (int i = 0; i < relay_count; i++) free(relay_urls[i]);
|
||||
free(relay_urls);
|
||||
free(statuses);
|
||||
break;
|
||||
}
|
||||
|
||||
case '9': // Exit
|
||||
running = 0;
|
||||
break;
|
||||
|
||||
default:
|
||||
printf("❌ Invalid choice\n");
|
||||
break;
|
||||
}
|
||||
}
|
||||
|
||||
printf("\n🧹 Cleaning up...\n");
|
||||
|
||||
// Stop polling thread
|
||||
running = 0;
|
||||
pthread_join(poll_thread, NULL);
|
||||
|
||||
// Clean up pool and subscriptions
|
||||
if (pool) {
|
||||
for (int i = 0; i < subscription_count; i++) {
|
||||
if (subscriptions[i]) {
|
||||
nostr_pool_subscription_close(subscriptions[i]);
|
||||
}
|
||||
}
|
||||
free(subscriptions);
|
||||
nostr_relay_pool_destroy(pool);
|
||||
printf("✅ Pool destroyed\n");
|
||||
}
|
||||
|
||||
// Cleanup
|
||||
nostr_cleanup();
|
||||
close(log_fd);
|
||||
|
||||
printf("👋 Test completed\n");
|
||||
return 0;
|
||||
}
|
||||
@@ -0,0 +1,254 @@
|
||||
/*
|
||||
* Relay Pool Test Program
|
||||
*
|
||||
* Tests the nostr_relay_pool functionality with persistent connections
|
||||
* and subscriptions. Prints events as they arrive and shows connection status.
|
||||
*
|
||||
* Usage: ./pool_test
|
||||
* Press Ctrl+C to exit
|
||||
*/
|
||||
|
||||
#define _POSIX_C_SOURCE 200809L
|
||||
#include <stdio.h>
|
||||
#include <stdlib.h>
|
||||
#include <string.h>
|
||||
#include <signal.h>
|
||||
#include <time.h>
|
||||
#include <unistd.h>
|
||||
#include "../nostr_core/nostr_core.h"
|
||||
#include "../cjson/cJSON.h"
|
||||
|
||||
// Global variables for signal handling
|
||||
volatile sig_atomic_t running = 1;
|
||||
time_t last_status_time = 0;
|
||||
|
||||
// Signal handler for clean shutdown
|
||||
void signal_handler(int signum) {
|
||||
(void)signum; // Suppress unused parameter warning
|
||||
printf("\n🛑 Received signal, shutting down...\n");
|
||||
running = 0;
|
||||
}
|
||||
|
||||
// Event callback - called when an event is received
|
||||
void on_event(cJSON* event, const char* relay_url, void* user_data) {
|
||||
(void)user_data; // Suppress unused parameter warning
|
||||
|
||||
// Extract basic event information
|
||||
cJSON* id = cJSON_GetObjectItem(event, "id");
|
||||
cJSON* pubkey = cJSON_GetObjectItem(event, "pubkey");
|
||||
cJSON* created_at = cJSON_GetObjectItem(event, "created_at");
|
||||
cJSON* kind = cJSON_GetObjectItem(event, "kind");
|
||||
cJSON* content = cJSON_GetObjectItem(event, "content");
|
||||
|
||||
printf("\n📨 EVENT from %s\n", relay_url);
|
||||
printf("├── ID: %.12s...\n", id && cJSON_IsString(id) ? cJSON_GetStringValue(id) : "unknown");
|
||||
printf("├── Pubkey: %.12s...\n", pubkey && cJSON_IsString(pubkey) ? cJSON_GetStringValue(pubkey) : "unknown");
|
||||
printf("├── Kind: %d\n", kind && cJSON_IsNumber(kind) ? (int)cJSON_GetNumberValue(kind) : -1);
|
||||
printf("├── Created: %lld\n", created_at && cJSON_IsNumber(created_at) ? (long long)cJSON_GetNumberValue(created_at) : 0);
|
||||
|
||||
// Truncate content if too long
|
||||
if (content && cJSON_IsString(content)) {
|
||||
const char* content_str = cJSON_GetStringValue(content);
|
||||
size_t content_len = strlen(content_str);
|
||||
if (content_len > 100) {
|
||||
printf("└── Content: %.97s...\n", content_str);
|
||||
} else {
|
||||
printf("└── Content: %s\n", content_str);
|
||||
}
|
||||
} else {
|
||||
printf("└── Content: (empty)\n");
|
||||
}
|
||||
fflush(stdout);
|
||||
}
|
||||
|
||||
// EOSE callback - called when End of Stored Events is received
|
||||
void on_eose(cJSON** events, int event_count, void* user_data) {
|
||||
(void)user_data; // Suppress unused parameter warning
|
||||
printf("📋 EOSE received - %d events collected\n", event_count);
|
||||
|
||||
// Log collected events if any
|
||||
for (int i = 0; i < event_count; i++) {
|
||||
cJSON* id = cJSON_GetObjectItem(events[i], "id");
|
||||
if (id && cJSON_IsString(id)) {
|
||||
printf(" Event %d: %.12s...\n", i + 1, cJSON_GetStringValue(id));
|
||||
}
|
||||
}
|
||||
fflush(stdout);
|
||||
}
|
||||
|
||||
// Print connection status for all relays
|
||||
void print_relay_status(nostr_relay_pool_t* pool) {
|
||||
char** relay_urls = NULL;
|
||||
nostr_pool_relay_status_t* statuses = NULL;
|
||||
int relay_count = nostr_relay_pool_list_relays(pool, &relay_urls, &statuses);
|
||||
|
||||
if (relay_count <= 0) {
|
||||
printf("❌ No relays in pool\n");
|
||||
return;
|
||||
}
|
||||
|
||||
printf("\n📊 RELAY STATUS (%d relays):\n", relay_count);
|
||||
for (int i = 0; i < relay_count; i++) {
|
||||
const char* status_str;
|
||||
switch (statuses[i]) {
|
||||
case NOSTR_POOL_RELAY_CONNECTED:
|
||||
status_str = "🟢 CONNECTED";
|
||||
break;
|
||||
case NOSTR_POOL_RELAY_CONNECTING:
|
||||
status_str = "🟡 CONNECTING";
|
||||
break;
|
||||
case NOSTR_POOL_RELAY_DISCONNECTED:
|
||||
status_str = "⚪ DISCONNECTED";
|
||||
break;
|
||||
case NOSTR_POOL_RELAY_ERROR:
|
||||
status_str = "🔴 ERROR";
|
||||
break;
|
||||
default:
|
||||
status_str = "❓ UNKNOWN";
|
||||
break;
|
||||
}
|
||||
|
||||
printf("├── %s: %s\n", relay_urls[i], status_str);
|
||||
|
||||
// Show additional stats if available
|
||||
const nostr_relay_stats_t* stats = nostr_relay_pool_get_relay_stats(pool, relay_urls[i]);
|
||||
if (stats) {
|
||||
printf("│ ├── Events received: %d\n", stats->events_received);
|
||||
printf("│ ├── Connection attempts: %d\n", stats->connection_attempts);
|
||||
printf("│ └── Connection failures: %d\n", stats->connection_failures);
|
||||
}
|
||||
|
||||
free(relay_urls[i]);
|
||||
}
|
||||
printf("\n");
|
||||
free(relay_urls);
|
||||
free(statuses);
|
||||
fflush(stdout);
|
||||
}
|
||||
|
||||
int main() {
|
||||
printf("🔗 NOSTR Relay Pool Test\n");
|
||||
printf("========================\n");
|
||||
printf("Testing persistent relay connections with subscriptions.\n");
|
||||
printf("Press Ctrl+C to exit.\n\n");
|
||||
|
||||
// Initialize NOSTR library
|
||||
if (nostr_init() != NOSTR_SUCCESS) {
|
||||
fprintf(stderr, "❌ Failed to initialize NOSTR library\n");
|
||||
return 1;
|
||||
}
|
||||
|
||||
// Setup signal handler for clean shutdown
|
||||
signal(SIGINT, signal_handler);
|
||||
signal(SIGTERM, signal_handler);
|
||||
|
||||
// Create relay pool with default configuration
|
||||
nostr_pool_reconnect_config_t* config = nostr_pool_reconnect_config_default();
|
||||
nostr_relay_pool_t* pool = nostr_relay_pool_create(config);
|
||||
if (!pool) {
|
||||
fprintf(stderr, "❌ Failed to create relay pool\n");
|
||||
nostr_cleanup();
|
||||
return 1;
|
||||
}
|
||||
|
||||
// Add relays to the pool
|
||||
const char* relay_urls[] = {
|
||||
"wss://nostr.mom",
|
||||
"wss://relay.laantungir.net",
|
||||
"wss://nos.lol"
|
||||
};
|
||||
int relay_count = 3;
|
||||
|
||||
printf("📡 Adding %d relays to pool:\n", relay_count);
|
||||
for (int i = 0; i < relay_count; i++) {
|
||||
printf("├── %s\n", relay_urls[i]);
|
||||
if (nostr_relay_pool_add_relay(pool, relay_urls[i]) != NOSTR_SUCCESS) {
|
||||
printf("│ ❌ Failed to add relay\n");
|
||||
} else {
|
||||
printf("│ ✅ Added successfully\n");
|
||||
}
|
||||
}
|
||||
printf("\n");
|
||||
|
||||
// Create filter for subscription (kind 1 events - text notes)
|
||||
cJSON* filter = cJSON_CreateObject();
|
||||
cJSON* kinds = cJSON_CreateArray();
|
||||
cJSON_AddItemToArray(kinds, cJSON_CreateNumber(1));
|
||||
cJSON_AddItemToObject(filter, "kinds", kinds);
|
||||
cJSON_AddItemToObject(filter, "limit", cJSON_CreateNumber(10)); // Limit to 10 events per relay
|
||||
|
||||
printf("🔍 Creating subscription with filter:\n");
|
||||
char* filter_json = cJSON_Print(filter);
|
||||
printf("%s\n\n", filter_json);
|
||||
free(filter_json);
|
||||
|
||||
// Create subscription with new parameters
|
||||
nostr_pool_subscription_t* subscription = nostr_relay_pool_subscribe(
|
||||
pool,
|
||||
relay_urls,
|
||||
relay_count,
|
||||
filter,
|
||||
on_event, // Event callback
|
||||
on_eose, // EOSE callback
|
||||
NULL, // User data (not used)
|
||||
0, // close_on_eose (false - keep subscription open)
|
||||
1, // enable_deduplication
|
||||
NOSTR_POOL_EOSE_FULL_SET, // result_mode
|
||||
30, // relay_timeout_seconds
|
||||
60 // eose_timeout_seconds
|
||||
);
|
||||
|
||||
if (!subscription) {
|
||||
fprintf(stderr, "❌ Failed to create subscription\n");
|
||||
cJSON_Delete(filter);
|
||||
nostr_relay_pool_destroy(pool);
|
||||
nostr_cleanup();
|
||||
return 1;
|
||||
}
|
||||
|
||||
printf("✅ Subscription created successfully\n");
|
||||
printf("🎯 Listening for events... (Ctrl+C to exit)\n\n");
|
||||
|
||||
// Record start time for status updates
|
||||
last_status_time = time(NULL);
|
||||
|
||||
// Main event loop
|
||||
while (running) {
|
||||
// Poll for events (100ms timeout)
|
||||
int events_processed = nostr_relay_pool_poll(pool, 100);
|
||||
|
||||
// Check if we should print status (every 30 seconds)
|
||||
time_t current_time = time(NULL);
|
||||
if (current_time - last_status_time >= 30) {
|
||||
print_relay_status(pool);
|
||||
last_status_time = current_time;
|
||||
}
|
||||
|
||||
// Small delay to prevent busy waiting
|
||||
if (events_processed == 0) {
|
||||
struct timespec ts = {0, 10000000}; // 10ms
|
||||
nanosleep(&ts, NULL);
|
||||
}
|
||||
}
|
||||
|
||||
printf("\n🧹 Cleaning up...\n");
|
||||
|
||||
// Close subscription
|
||||
if (subscription) {
|
||||
nostr_pool_subscription_close(subscription);
|
||||
printf("✅ Subscription closed\n");
|
||||
}
|
||||
|
||||
// Destroy pool
|
||||
nostr_relay_pool_destroy(pool);
|
||||
printf("✅ Relay pool destroyed\n");
|
||||
|
||||
// Cleanup JSON
|
||||
cJSON_Delete(filter);
|
||||
|
||||
// Cleanup library
|
||||
nostr_cleanup();
|
||||
|
||||
printf("👋 Test completed successfully\n");
|
||||
return 0;
|
||||
}
|
||||
@@ -10,7 +10,7 @@
|
||||
#include <stdio.h>
|
||||
#include <stdlib.h>
|
||||
#include <time.h>
|
||||
#include "../nostr_core/nostr_common.h"
|
||||
#include "../nostr_core/nostr_core.h"
|
||||
#include "../cjson/cJSON.h"
|
||||
|
||||
|
||||
@@ -80,7 +80,7 @@ int main() {
|
||||
const char* filter_json =
|
||||
"{"
|
||||
" \"kinds\": [1],"
|
||||
" \"limit\": 1"
|
||||
" \"limit\": 4"
|
||||
"}";
|
||||
|
||||
// Alternative filter examples (comment out the one above, uncomment one below):
|
||||
@@ -133,7 +133,8 @@ int main() {
|
||||
|
||||
cJSON** results = synchronous_query_relays_with_progress(
|
||||
test_relays, relay_count, filter, test_mode,
|
||||
&result_count, 5, progress_callback, NULL
|
||||
&result_count, 5, progress_callback, NULL,
|
||||
1, NULL // nip42_enabled = true, private_key = NULL (no auth)
|
||||
);
|
||||
|
||||
time_t end_time = time(NULL);
|
||||
|
||||
@@ -1,103 +0,0 @@
|
||||
/*
|
||||
* WebSocket SSL Test - Test OpenSSL WebSocket implementation
|
||||
* Connect to a NOSTR relay and fetch one type 1 event
|
||||
*/
|
||||
|
||||
#include <stdio.h>
|
||||
#include <stdlib.h>
|
||||
|
||||
#include "../cjson/cJSON.h"
|
||||
#include "../nostr_core/nostr_common.h"
|
||||
|
||||
// Progress callback to show connection status
|
||||
static void progress_callback(
|
||||
const char* relay_url,
|
||||
const char* status,
|
||||
const char* event_id,
|
||||
int events_received,
|
||||
int total_relays,
|
||||
int completed_relays,
|
||||
void* user_data)
|
||||
{
|
||||
printf("Progress: %s - %s", relay_url ? relay_url : "Summary", status);
|
||||
if (event_id) {
|
||||
printf(" (Event: %.12s...)", event_id);
|
||||
}
|
||||
printf(" [%d/%d events, %d/%d relays]\n",
|
||||
events_received, *(int*)user_data, completed_relays, total_relays);
|
||||
}
|
||||
|
||||
int main() {
|
||||
printf("WebSocket SSL Test - Testing OpenSSL WebSocket with NOSTR relay\n");
|
||||
printf("================================================================\n");
|
||||
|
||||
// Initialize NOSTR library
|
||||
if (nostr_init() != NOSTR_SUCCESS) {
|
||||
printf("❌ Failed to initialize NOSTR library\n");
|
||||
return 1;
|
||||
}
|
||||
|
||||
printf("✅ NOSTR library initialized\n");
|
||||
|
||||
// Setup relay and filter
|
||||
const char* relay_urls[] = {"wss://nostr.mom"};
|
||||
int relay_count = 1;
|
||||
|
||||
// Create filter for type 1 events (text notes), limit to 1 event
|
||||
cJSON* filter = cJSON_CreateObject();
|
||||
cJSON* kinds = cJSON_CreateArray();
|
||||
cJSON_AddItemToArray(kinds, cJSON_CreateNumber(1));
|
||||
cJSON_AddItemToObject(filter, "kinds", kinds);
|
||||
cJSON_AddItemToObject(filter, "limit", cJSON_CreateNumber(1));
|
||||
|
||||
printf("📡 Connecting to %s...\n", relay_urls[0]);
|
||||
printf("🔍 Requesting 1 type 1 event (text note)...\n\n");
|
||||
|
||||
// Query the relay
|
||||
int result_count = 0;
|
||||
int expected_events = 1;
|
||||
cJSON** events = synchronous_query_relays_with_progress(
|
||||
relay_urls,
|
||||
relay_count,
|
||||
filter,
|
||||
RELAY_QUERY_FIRST_RESULT, // Return as soon as we get the first event
|
||||
&result_count,
|
||||
10, // 10 second timeout
|
||||
progress_callback,
|
||||
&expected_events
|
||||
);
|
||||
|
||||
// Process results
|
||||
if (events && result_count > 0) {
|
||||
printf("\n✅ Successfully received %d event(s)!\n", result_count);
|
||||
printf("📄 Raw JSON event data:\n");
|
||||
printf("========================\n");
|
||||
|
||||
for (int i = 0; i < result_count; i++) {
|
||||
char* json_string = cJSON_Print(events[i]);
|
||||
if (json_string) {
|
||||
printf("%s\n\n", json_string);
|
||||
free(json_string);
|
||||
}
|
||||
cJSON_Delete(events[i]);
|
||||
}
|
||||
free(events);
|
||||
|
||||
printf("🎉 WebSocket SSL Test PASSED - OpenSSL WebSocket working correctly!\n");
|
||||
} else {
|
||||
printf("\n❌ No events received or query failed\n");
|
||||
printf("❌ WebSocket SSL Test FAILED\n");
|
||||
|
||||
// Cleanup and return error
|
||||
cJSON_Delete(filter);
|
||||
nostr_cleanup();
|
||||
return 1;
|
||||
}
|
||||
|
||||
// Cleanup
|
||||
cJSON_Delete(filter);
|
||||
nostr_cleanup();
|
||||
|
||||
printf("✅ WebSocket connection and TLS working with OpenSSL\n");
|
||||
return 0;
|
||||
}
|
||||
Reference in New Issue
Block a user