Compare commits

..

12 Commits

Author SHA1 Message Date
Your Name
70c91ec858 Fix version.h generation timing in build script 2025-09-06 05:09:35 -04:00
Your Name
b7c4609c2d Fix tag push failure in build script 2025-09-06 05:06:03 -04:00
Your Name
7f69367666 v0.2.5 - Fixed versioning 2025-09-06 05:01:26 -04:00
Your Name
fa17aa1f78 v0.2.4 - Clean up config issues 2025-09-06 04:40:59 -04:00
Your Name
7e560b4247 Remove relay.log from tracking (already in .gitignore) 2025-09-05 19:51:47 -04:00
Your Name
9a29ea51e3 v0.2.2 - Working on config setup 2025-09-05 19:48:49 -04:00
Your Name
6c10713e18 v0.2.1 - Nip-40 implemented 2025-09-05 14:45:32 -04:00
Your Name
b810982a17 v0.2.0 - Nip13 implemented 2025-09-05 14:14:15 -04:00
Your Name
23c95fd2ea v0.1.1 - -release 2025-09-05 13:00:42 -04:00
Your Name
e96957f91b v0.1.0 - New minor version 2025-09-05 11:32:31 -04:00
Your Name
de3e7c75a5 v0.2.0 - New minor version 2025-09-05 11:27:32 -04:00
Your Name
646adac981 v0.1.0 - New minor version 2025-09-05 11:26:08 -04:00
25 changed files with 4741 additions and 260 deletions

4
.gitignore vendored
View File

@@ -1,2 +1,6 @@
nostr_core_lib/
nips/
build/
relay.log
Trash/
src/version.h

1
.roo/rules-code/rules.md Normal file
View File

@@ -0,0 +1 @@
Use ./make_and_restart_relay.sh instead of make to build the project.

175
Makefile
View File

@@ -5,27 +5,157 @@ CFLAGS = -Wall -Wextra -std=c99 -g -O2
INCLUDES = -I. -Inostr_core_lib -Inostr_core_lib/nostr_core -Inostr_core_lib/cjson -Inostr_core_lib/nostr_websocket
LIBS = -lsqlite3 -lwebsockets -lz -ldl -lpthread -lm -L/usr/local/lib -lsecp256k1 -lssl -lcrypto -L/usr/local/lib -lcurl
# Build directory
BUILD_DIR = build
# Source files
MAIN_SRC = src/main.c
MAIN_SRC = src/main.c src/config.c
NOSTR_CORE_LIB = nostr_core_lib/libnostr_core_x64.a
# Target binary
TARGET = src/main
# Architecture detection
ARCH = $(shell uname -m)
ifeq ($(ARCH),x86_64)
TARGET = $(BUILD_DIR)/c_relay_x86
else ifeq ($(ARCH),aarch64)
TARGET = $(BUILD_DIR)/c_relay_arm64
else ifeq ($(ARCH),arm64)
TARGET = $(BUILD_DIR)/c_relay_arm64
else
TARGET = $(BUILD_DIR)/c_relay_$(ARCH)
endif
# Default target
all: $(TARGET)
# Create build directory
$(BUILD_DIR):
mkdir -p $(BUILD_DIR)
# Check if nostr_core_lib is built
$(NOSTR_CORE_LIB):
@echo "Building nostr_core_lib..."
cd nostr_core_lib && ./build.sh
# Generate version.h from git tags
src/version.h:
@if [ -d .git ]; then \
echo "Generating version.h from git tags..."; \
VERSION=$$(git describe --tags --always --dirty 2>/dev/null || echo "unknown"); \
if echo "$$VERSION" | grep -q "^v[0-9]"; then \
CLEAN_VERSION=$$(echo "$$VERSION" | sed 's/^v//'); \
MAJOR=$$(echo "$$CLEAN_VERSION" | cut -d. -f1); \
MINOR=$$(echo "$$CLEAN_VERSION" | cut -d. -f2); \
PATCH=$$(echo "$$CLEAN_VERSION" | cut -d. -f3 | cut -d- -f1); \
else \
CLEAN_VERSION="0.0.0-$$VERSION"; \
MAJOR=0; MINOR=0; PATCH=0; \
fi; \
echo "/* Auto-generated version information */" > src/version.h; \
echo "#ifndef VERSION_H" >> src/version.h; \
echo "#define VERSION_H" >> src/version.h; \
echo "" >> src/version.h; \
echo "#define VERSION \"$$VERSION\"" >> src/version.h; \
echo "#define VERSION_MAJOR $$MAJOR" >> src/version.h; \
echo "#define VERSION_MINOR $$MINOR" >> src/version.h; \
echo "#define VERSION_PATCH $$PATCH" >> src/version.h; \
echo "" >> src/version.h; \
echo "#endif /* VERSION_H */" >> src/version.h; \
echo "Generated version.h with version: $$VERSION"; \
elif [ ! -f src/version.h ]; then \
echo "Git not available and version.h missing, creating fallback version.h..."; \
VERSION="unknown"; \
echo "/* Auto-generated version information */" > src/version.h; \
echo "#ifndef VERSION_H" >> src/version.h; \
echo "#define VERSION_H" >> src/version.h; \
echo "" >> src/version.h; \
echo "#define VERSION \"$$VERSION\"" >> src/version.h; \
echo "#define VERSION_MAJOR 0" >> src/version.h; \
echo "#define VERSION_MINOR 0" >> src/version.h; \
echo "#define VERSION_PATCH 0" >> src/version.h; \
echo "" >> src/version.h; \
echo "#endif /* VERSION_H */" >> src/version.h; \
echo "Created fallback version.h with version: $$VERSION"; \
else \
echo "Git not available, preserving existing version.h"; \
fi
# Force version.h regeneration (useful for development)
force-version:
@echo "Force regenerating version.h..."
@rm -f src/version.h
@$(MAKE) src/version.h
# Build the relay
$(TARGET): $(MAIN_SRC) $(NOSTR_CORE_LIB)
@echo "Compiling C-Relay..."
$(TARGET): $(BUILD_DIR) src/version.h $(MAIN_SRC) $(NOSTR_CORE_LIB)
@echo "Compiling C-Relay for architecture: $(ARCH)"
$(CC) $(CFLAGS) $(INCLUDES) $(MAIN_SRC) -o $(TARGET) $(NOSTR_CORE_LIB) $(LIBS)
@echo "Build complete: $(TARGET)"
# Build for specific architectures
x86: $(BUILD_DIR) src/version.h $(MAIN_SRC) $(NOSTR_CORE_LIB)
@echo "Building C-Relay for x86_64..."
$(CC) $(CFLAGS) $(INCLUDES) $(MAIN_SRC) -o $(BUILD_DIR)/c_relay_x86 $(NOSTR_CORE_LIB) $(LIBS)
@echo "Build complete: $(BUILD_DIR)/c_relay_x86"
arm64: $(BUILD_DIR) src/version.h $(MAIN_SRC) $(NOSTR_CORE_LIB)
@echo "Cross-compiling C-Relay for ARM64..."
@if ! command -v aarch64-linux-gnu-gcc >/dev/null 2>&1; then \
echo "ERROR: ARM64 cross-compiler not found."; \
echo "Install with: make install-cross-tools"; \
echo "Or install manually: sudo apt install gcc-aarch64-linux-gnu"; \
exit 1; \
fi
@echo "Checking for ARM64 development libraries..."
@if ! dpkg -l | grep -q "libssl-dev:arm64\|libsqlite3-dev:arm64"; then \
echo "ERROR: ARM64 libraries not found. Cross-compilation requires ARM64 versions of:"; \
echo " - libssl-dev:arm64"; \
echo " - libsqlite3-dev:arm64"; \
echo " - libwebsockets-dev:arm64"; \
echo " - libsecp256k1-dev:arm64"; \
echo " - zlib1g-dev:arm64"; \
echo " - libcurl4-openssl-dev:arm64"; \
echo ""; \
echo "Install ARM64 libraries with: make install-arm64-deps"; \
echo "Or use Docker for cross-platform builds."; \
exit 1; \
fi
@echo "Using aarch64-linux-gnu-gcc with ARM64 libraries..."
PKG_CONFIG_PATH=/usr/lib/aarch64-linux-gnu/pkgconfig:/usr/share/pkgconfig \
aarch64-linux-gnu-gcc $(CFLAGS) $(INCLUDES) $(MAIN_SRC) -o $(BUILD_DIR)/c_relay_arm64 $(NOSTR_CORE_LIB) \
-L/usr/lib/aarch64-linux-gnu $(LIBS)
@echo "Build complete: $(BUILD_DIR)/c_relay_arm64"
# Install ARM64 cross-compilation dependencies
install-arm64-deps:
@echo "Installing ARM64 cross-compilation dependencies..."
@echo "This requires adding ARM64 architecture and installing cross-libraries..."
sudo dpkg --add-architecture arm64
sudo apt update
sudo apt install -y \
gcc-aarch64-linux-gnu \
libc6-dev-arm64-cross \
libssl-dev:arm64 \
libsqlite3-dev:arm64 \
zlib1g-dev:arm64 \
libcurl4-openssl-dev:arm64
@echo "Note: libwebsockets-dev:arm64 and libsecp256k1-dev:arm64 may need manual building"
# Install cross-compilation tools
install-cross-tools:
@echo "Installing cross-compilation tools..."
sudo apt update
sudo apt install -y gcc-aarch64-linux-gnu libc6-dev-arm64-cross
# Check what architectures we can actually build
check-toolchain:
@echo "Checking available toolchains:"
@echo "Native compiler: $(shell $(CC) --version | head -1)"
@if command -v aarch64-linux-gnu-gcc >/dev/null 2>&1; then \
echo "ARM64 cross-compiler: $(shell aarch64-linux-gnu-gcc --version | head -1)"; \
else \
echo "ARM64 cross-compiler: NOT INSTALLED (install with 'make install-cross-tools')"; \
fi
# Run tests
test: $(TARGET)
@echo "Running tests..."
@@ -38,7 +168,8 @@ init-db:
# Clean build artifacts
clean:
rm -f $(TARGET)
rm -rf $(BUILD_DIR)
rm -f src/version.h
@echo "Clean complete"
# Clean everything including nostr_core_lib
@@ -56,17 +187,27 @@ help:
@echo "C-Relay Build System"
@echo ""
@echo "Targets:"
@echo " all Build the relay (default)"
@echo " test Build and run tests"
@echo " init-db Initialize the database"
@echo " clean Clean build artifacts"
@echo " clean-all Clean everything including dependencies"
@echo " install-deps Install system dependencies"
@echo " help Show this help"
@echo " all Build the relay for current architecture (default)"
@echo " x86 Build specifically for x86_64"
@echo " arm64 Build for ARM64 (requires cross-compilation setup)"
@echo " test Build and run tests"
@echo " init-db Initialize the database"
@echo " clean Clean build artifacts"
@echo " clean-all Clean everything including dependencies"
@echo " install-deps Install system dependencies"
@echo " install-cross-tools Install basic ARM64 cross-compiler"
@echo " install-arm64-deps Install ARM64 cross-compilation libraries"
@echo " check-toolchain Check available compilers"
@echo " help Show this help"
@echo ""
@echo "Usage:"
@echo " make # Build the relay"
@echo " make test # Run tests"
@echo " make init-db # Set up database"
@echo " make # Build the relay for current arch"
@echo " make x86 # Build for x86_64"
@echo " make arm64 # Build for ARM64 (fails if cross-compilation not set up)"
@echo " make install-arm64-deps # Install full ARM64 cross-compilation setup"
@echo " make check-toolchain # Check what compilers are available"
@echo " make test # Run tests"
@echo " make init-db # Set up database"
@echo " make force-version # Force regenerate version.h from git"
.PHONY: all test init-db clean clean-all install-deps help
.PHONY: all x86 arm64 test init-db clean clean-all install-deps install-cross-tools install-arm64-deps check-toolchain help force-version

View File

@@ -10,20 +10,13 @@ Do NOT modify the formatting, add emojis, or change the text. Keep the simple fo
- [x] NIP-01: Basic protocol flow implementation
- [x] NIP-09: Event deletion
- [ ] NIP-11: Relay information document
- [ ] NIP-12: Generic tag queries
- [ ] NIP-13: Proof of Work
- [x] NIP-11: Relay information document
- [x] NIP-13: Proof of Work
- [x] NIP-15: End of Stored Events Notice
- [ ] NIP-16: Event Treatment
- [x] NIP-20: Command Results
- [ ] NIP-22: Event `created_at` Limits
- [ ] NIP-25: Reactions
- [ ] NIP-26: Delegated Event Signing
- [ ] NIP-28: Public Chat
- [ ] NIP-33: Parameterized Replaceable Events
- [ ] NIP-40: Expiration Timestamp
- [x] NIP-33: Parameterized Replaceable Events
- [x] NIP-40: Expiration Timestamp
- [ ] NIP-42: Authentication of clients to relays
- [ ] NIP-45: Counting results. [experimental](#count)
- [ ] NIP-50: Keywords filter. [experimental](#search)
- [ ] NIP-45: Counting results.
- [ ] NIP-50: Keywords filter.
- [ ] NIP-70: Protected Events

View File

@@ -60,7 +60,7 @@ show_usage() {
echo " - Git add, commit, push, and create Gitea release"
echo ""
echo "Requirements for Release Mode:"
echo " - ARM64 cross-compiler: sudo apt install gcc-aarch64-linux-gnu"
echo " - For ARM64 builds: make install-arm64-deps (optional - will build x86_64 only if missing)"
echo " - Gitea token in ~/.gitea_token for release uploads"
}
@@ -139,6 +139,13 @@ compile_project() {
print_warning "Clean failed or no Makefile found"
fi
# Force regenerate version.h to pick up new tags
if make force-version > /dev/null 2>&1; then
print_success "Regenerated version.h"
else
print_warning "Failed to regenerate version.h"
fi
# Compile the project
if make > /dev/null 2>&1; then
print_success "C-Relay compiled successfully"
@@ -148,16 +155,6 @@ compile_project() {
fi
}
# Check for ARM64 cross-compiler
check_cross_compiler() {
if ! command -v aarch64-linux-gnu-gcc > /dev/null 2>&1; then
print_error "ARM64/AArch64 cross-compiler not found!"
print_error "Install with: sudo apt install gcc-aarch64-linux-gnu"
return 1
fi
return 0
}
# Function to build release binaries
build_release_binaries() {
print_status "Building release binaries..."
@@ -165,9 +162,9 @@ build_release_binaries() {
# Build x86_64 version
print_status "Building x86_64 version..."
make clean > /dev/null 2>&1
if make CC=gcc > /dev/null 2>&1; then
if [[ -f "src/main" ]]; then
cp src/main c-relay-x86_64
if make x86 > /dev/null 2>&1; then
if [[ -f "build/c_relay_x86" ]]; then
cp build/c_relay_x86 c-relay-x86_64
print_success "x86_64 binary created: c-relay-x86_64"
else
print_error "x86_64 binary not found after compilation"
@@ -178,25 +175,19 @@ build_release_binaries() {
exit 1
fi
# Check for ARM64 cross-compiler
if check_cross_compiler; then
# Build ARM64 version
print_status "Building ARM64 version..."
make clean > /dev/null 2>&1
if make CC=aarch64-linux-gnu-gcc > /dev/null 2>&1; then
if [[ -f "src/main" ]]; then
cp src/main c-relay-arm64
print_success "ARM64 binary created: c-relay-arm64"
else
print_error "ARM64 binary not found after compilation"
exit 1
fi
# Try to build ARM64 version
print_status "Attempting ARM64 build..."
make clean > /dev/null 2>&1
if make arm64 > /dev/null 2>&1; then
if [[ -f "build/c_relay_arm64" ]]; then
cp build/c_relay_arm64 c-relay-arm64
print_success "ARM64 binary created: c-relay-arm64"
else
print_error "ARM64 build failed"
exit 1
print_warning "ARM64 binary not found after compilation"
fi
else
print_warning "ARM64 cross-compiler not available, skipping ARM64 build"
print_warning "ARM64 build failed - ARM64 cross-compilation not properly set up"
print_status "Only x86_64 binary will be included in release"
fi
# Restore normal build
@@ -245,10 +236,55 @@ git_commit_and_push() {
exit 1
fi
if git push --tags > /dev/null 2>&1; then
print_success "Pushed tags"
# Push only the new tag to avoid conflicts with existing tags
if git push origin "$NEW_VERSION" > /dev/null 2>&1; then
print_success "Pushed tag: $NEW_VERSION"
else
print_warning "Failed to push tags"
print_error "Failed to push tag: $NEW_VERSION"
exit 1
fi
}
# Function to commit and push changes without creating a tag (tag already created)
git_commit_and_push_no_tag() {
print_status "Preparing git commit..."
# Stage all changes
if git add . > /dev/null 2>&1; then
print_success "Staged all changes"
else
print_error "Failed to stage changes"
exit 1
fi
# Check if there are changes to commit
if git diff --staged --quiet; then
print_warning "No changes to commit"
else
# Commit changes
if git commit -m "$NEW_VERSION - $COMMIT_MESSAGE" > /dev/null 2>&1; then
print_success "Committed changes"
else
print_error "Failed to commit changes"
exit 1
fi
fi
# Push changes and tags
print_status "Pushing to remote repository..."
if git push > /dev/null 2>&1; then
print_success "Pushed changes"
else
print_error "Failed to push changes"
exit 1
fi
# Push only the new tag to avoid conflicts with existing tags
if git push origin "$NEW_VERSION" > /dev/null 2>&1; then
print_success "Pushed tag: $NEW_VERSION"
else
print_error "Failed to push tag: $NEW_VERSION"
exit 1
fi
}
@@ -264,7 +300,7 @@ create_gitea_release() {
fi
local token=$(cat "$HOME/.gitea_token" | tr -d '\n\r')
local api_url="https://git.laantungir.net/api/v1/repos/teknari/c-relay"
local api_url="https://git.laantungir.net/api/v1/repos/laantungir/c-relay"
# Create release
print_status "Creating release $NEW_VERSION..."
@@ -275,13 +311,24 @@ create_gitea_release() {
if echo "$response" | grep -q '"id"'; then
print_success "Created release $NEW_VERSION"
# Upload binaries
upload_release_binaries "$api_url" "$token"
elif echo "$response" | grep -q "already exists"; then
print_warning "Release $NEW_VERSION already exists"
upload_release_binaries "$api_url" "$token"
else
print_warning "Release may already exist or creation failed"
print_status "Attempting to upload to existing release..."
upload_release_binaries "$api_url" "$token"
print_error "Failed to create release $NEW_VERSION"
print_error "Response: $response"
# Try to check if the release exists anyway
print_status "Checking if release exists..."
local check_response=$(curl -s -H "Authorization: token $token" "$api_url/releases/tags/$NEW_VERSION")
if echo "$check_response" | grep -q '"id"'; then
print_warning "Release exists but creation response was unexpected"
upload_release_binaries "$api_url" "$token"
else
print_error "Release does not exist and creation failed"
return 1
fi
fi
}
@@ -290,16 +337,23 @@ upload_release_binaries() {
local api_url="$1"
local token="$2"
# Get release ID
local release_id=$(curl -s -H "Authorization: token $token" \
"$api_url/releases/tags/$NEW_VERSION" | \
grep -o '"id":[0-9]*' | head -n1 | cut -d: -f2)
# Get release ID with more robust parsing
print_status "Getting release ID for $NEW_VERSION..."
local response=$(curl -s -H "Authorization: token $token" "$api_url/releases/tags/$NEW_VERSION")
local release_id=$(echo "$response" | grep -o '"id":[0-9]*' | head -n1 | cut -d: -f2)
if [[ -z "$release_id" ]]; then
print_error "Could not get release ID for $NEW_VERSION"
print_error "API Response: $response"
# Try to list all releases to debug
print_status "Available releases:"
curl -s -H "Authorization: token $token" "$api_url/releases" | grep -o '"tag_name":"[^"]*"' | head -5
return 1
fi
print_success "Found release ID: $release_id"
# Upload x86_64 binary
if [[ -f "c-relay-x86_64" ]]; then
print_status "Uploading x86_64 binary..."
@@ -350,14 +404,23 @@ main() {
# Increment minor version for releases
increment_version "minor"
# Compile project first
# Create new git tag BEFORE compilation so version.h picks it up
if git tag "$NEW_VERSION" > /dev/null 2>&1; then
print_success "Created tag: $NEW_VERSION"
else
print_warning "Tag $NEW_VERSION already exists, removing and recreating..."
git tag -d "$NEW_VERSION" > /dev/null 2>&1
git tag "$NEW_VERSION" > /dev/null 2>&1
fi
# Compile project first (will now pick up the new tag)
compile_project
# Build release binaries
build_release_binaries
# Commit and push
git_commit_and_push
# Commit and push (but skip tag creation since we already did it)
git_commit_and_push_no_tag
# Create Gitea release with binaries
create_gitea_release
@@ -374,11 +437,20 @@ main() {
# Increment patch version for regular commits
increment_version "patch"
# Compile project
# Create new git tag BEFORE compilation so version.h picks it up
if git tag "$NEW_VERSION" > /dev/null 2>&1; then
print_success "Created tag: $NEW_VERSION"
else
print_warning "Tag $NEW_VERSION already exists, removing and recreating..."
git tag -d "$NEW_VERSION" > /dev/null 2>&1
git tag "$NEW_VERSION" > /dev/null 2>&1
fi
# Compile project (will now pick up the new tag)
compile_project
# Commit and push
git_commit_and_push
# Commit and push (but skip tag creation since we already did it)
git_commit_and_push_no_tag
print_success "Build and push completed successfully!"
print_status "Version $NEW_VERSION pushed to repository"

Binary file not shown.

Binary file not shown.

Binary file not shown.

Binary file not shown.

View File

@@ -114,12 +114,12 @@ verify_database() {
local schema_version=$(sqlite3 "$DB_PATH" "PRAGMA user_version;")
log_info "Database schema version: $schema_version"
# Check that main tables exist
local table_count=$(sqlite3 "$DB_PATH" "SELECT count(*) FROM sqlite_master WHERE type='table' AND name IN ('events', 'schema_info');")
if [ "$table_count" -eq 2 ]; then
log_success "Core tables created successfully"
# Check that main tables exist (including configuration tables)
local table_count=$(sqlite3 "$DB_PATH" "SELECT count(*) FROM sqlite_master WHERE type='table' AND name IN ('events', 'schema_info', 'server_config');")
if [ "$table_count" -eq 3 ]; then
log_success "Core tables created successfully (including configuration tables)"
else
log_error "Missing core tables (expected 2, found $table_count)"
log_error "Missing core tables (expected 3, found $table_count)"
exit 1
fi
}

View File

@@ -2,7 +2,7 @@
-- SQLite schema for storing Nostr events with JSON tags support
-- Schema version tracking
PRAGMA user_version = 2;
PRAGMA user_version = 3;
-- Enable foreign key support
PRAGMA foreign_keys = ON;
@@ -44,9 +44,9 @@ CREATE TABLE schema_info (
);
-- Insert schema metadata
INSERT INTO schema_info (key, value) VALUES
('version', '2'),
('description', 'Hybrid single-table Nostr relay schema with JSON tags'),
INSERT INTO schema_info (key, value) VALUES
('version', '3'),
('description', 'Hybrid single-table Nostr relay schema with JSON tags and configuration management'),
('created_at', strftime('%s', 'now'));
-- Helper views for common queries
@@ -178,4 +178,122 @@ WHERE event_type = 'created'
AND subscription_id NOT IN (
SELECT subscription_id FROM subscription_events
WHERE event_type IN ('closed', 'expired', 'disconnected')
);
);
-- ================================
-- CONFIGURATION MANAGEMENT TABLES
-- ================================
-- Core server configuration table
CREATE TABLE server_config (
key TEXT PRIMARY KEY, -- Configuration key (unique identifier)
value TEXT NOT NULL, -- Configuration value (stored as string)
description TEXT, -- Human-readable description
config_type TEXT DEFAULT 'user' CHECK (config_type IN ('system', 'user', 'runtime')),
data_type TEXT DEFAULT 'string' CHECK (data_type IN ('string', 'integer', 'boolean', 'json')),
validation_rules TEXT, -- JSON validation rules (optional)
is_sensitive INTEGER DEFAULT 0, -- 1 if value should be masked in logs
requires_restart INTEGER DEFAULT 0, -- 1 if change requires server restart
created_at INTEGER NOT NULL DEFAULT (strftime('%s', 'now')),
updated_at INTEGER NOT NULL DEFAULT (strftime('%s', 'now'))
);
-- Configuration change history table
CREATE TABLE config_history (
id INTEGER PRIMARY KEY AUTOINCREMENT,
config_key TEXT NOT NULL, -- Key that was changed
old_value TEXT, -- Previous value (NULL for new keys)
new_value TEXT NOT NULL, -- New value
changed_by TEXT DEFAULT 'system', -- Who made the change (system/admin/user)
change_reason TEXT, -- Optional reason for change
changed_at INTEGER NOT NULL DEFAULT (strftime('%s', 'now')),
FOREIGN KEY (config_key) REFERENCES server_config(key)
);
-- Configuration validation errors log
CREATE TABLE config_validation_log (
id INTEGER PRIMARY KEY AUTOINCREMENT,
config_key TEXT NOT NULL,
attempted_value TEXT,
validation_error TEXT NOT NULL,
error_source TEXT DEFAULT 'validation', -- validation/parsing/constraint
attempted_at INTEGER NOT NULL DEFAULT (strftime('%s', 'now'))
);
-- Cache for file-based configuration events
CREATE TABLE config_file_cache (
file_path TEXT PRIMARY KEY, -- Full path to config file
file_hash TEXT NOT NULL, -- SHA256 hash of file content
event_id TEXT, -- Nostr event ID from file
event_pubkey TEXT, -- Admin pubkey that signed event
loaded_at INTEGER NOT NULL DEFAULT (strftime('%s', 'now')),
validation_status TEXT CHECK (validation_status IN ('valid', 'invalid', 'unverified')),
validation_error TEXT -- Error details if invalid
);
-- Performance indexes for configuration tables
CREATE INDEX idx_server_config_type ON server_config(config_type);
CREATE INDEX idx_server_config_updated ON server_config(updated_at DESC);
CREATE INDEX idx_config_history_key ON config_history(config_key);
CREATE INDEX idx_config_history_time ON config_history(changed_at DESC);
CREATE INDEX idx_config_validation_key ON config_validation_log(config_key);
CREATE INDEX idx_config_validation_time ON config_validation_log(attempted_at DESC);
-- Trigger to update timestamp on configuration changes
CREATE TRIGGER update_config_timestamp
AFTER UPDATE ON server_config
BEGIN
UPDATE server_config SET updated_at = strftime('%s', 'now') WHERE key = NEW.key;
END;
-- Trigger to log configuration changes to history
CREATE TRIGGER log_config_changes
AFTER UPDATE ON server_config
WHEN OLD.value != NEW.value
BEGIN
INSERT INTO config_history (config_key, old_value, new_value, changed_by, change_reason)
VALUES (NEW.key, OLD.value, NEW.value, 'system', 'configuration update');
END;
-- Active Configuration View
CREATE VIEW active_config AS
SELECT
key,
value,
description,
config_type,
data_type,
requires_restart,
updated_at
FROM server_config
WHERE config_type IN ('system', 'user')
ORDER BY config_type, key;
-- Runtime Statistics View
CREATE VIEW runtime_stats AS
SELECT
key,
value,
description,
updated_at
FROM server_config
WHERE config_type = 'runtime'
ORDER BY key;
-- Configuration Change Summary
CREATE VIEW recent_config_changes AS
SELECT
ch.config_key,
sc.description,
ch.old_value,
ch.new_value,
ch.changed_by,
ch.change_reason,
ch.changed_at
FROM config_history ch
JOIN server_config sc ON ch.config_key = sc.key
ORDER BY ch.changed_at DESC
LIMIT 50;
-- Runtime Statistics (initialized by server on startup)
-- These will be populated when configuration system initializes

View File

@@ -0,0 +1,280 @@
# Database Configuration Schema Design
## Overview
This document outlines the database configuration schema additions for the C Nostr Relay startup config file system. The design follows the Ginxsom admin system approach with signed Nostr events and database storage.
## Schema Version Update
- Current Version: 2
- Target Version: 3
- Update: Add server configuration management tables
## Core Configuration Tables
### 1. `server_config` Table
```sql
-- Server configuration table - core configuration storage
CREATE TABLE server_config (
key TEXT PRIMARY KEY, -- Configuration key (unique identifier)
value TEXT NOT NULL, -- Configuration value (stored as string)
description TEXT, -- Human-readable description
config_type TEXT DEFAULT 'user' CHECK (config_type IN ('system', 'user', 'runtime')),
data_type TEXT DEFAULT 'string' CHECK (data_type IN ('string', 'integer', 'boolean', 'json')),
validation_rules TEXT, -- JSON validation rules (optional)
is_sensitive INTEGER DEFAULT 0, -- 1 if value should be masked in logs
requires_restart INTEGER DEFAULT 0, -- 1 if change requires server restart
created_at INTEGER NOT NULL DEFAULT (strftime('%s', 'now')),
updated_at INTEGER NOT NULL DEFAULT (strftime('%s', 'now'))
);
```
**Configuration Types:**
- `system`: Core system settings (admin keys, security)
- `user`: User-configurable settings (relay info, features)
- `runtime`: Dynamic runtime values (statistics, cache)
**Data Types:**
- `string`: Text values
- `integer`: Numeric values
- `boolean`: True/false values (stored as "true"/"false")
- `json`: JSON object/array values
### 2. `config_history` Table
```sql
-- Configuration change history table
CREATE TABLE config_history (
id INTEGER PRIMARY KEY AUTOINCREMENT,
config_key TEXT NOT NULL, -- Key that was changed
old_value TEXT, -- Previous value (NULL for new keys)
new_value TEXT NOT NULL, -- New value
changed_by TEXT DEFAULT 'system', -- Who made the change (system/admin/user)
change_reason TEXT, -- Optional reason for change
changed_at INTEGER NOT NULL DEFAULT (strftime('%s', 'now')),
FOREIGN KEY (config_key) REFERENCES server_config(key)
);
```
### 3. `config_validation_log` Table
```sql
-- Configuration validation errors log
CREATE TABLE config_validation_log (
id INTEGER PRIMARY KEY AUTOINCREMENT,
config_key TEXT NOT NULL,
attempted_value TEXT,
validation_error TEXT NOT NULL,
error_source TEXT DEFAULT 'validation', -- validation/parsing/constraint
attempted_at INTEGER NOT NULL DEFAULT (strftime('%s', 'now'))
);
```
### 4. Configuration File Cache Table
```sql
-- Cache for file-based configuration events
CREATE TABLE config_file_cache (
file_path TEXT PRIMARY KEY, -- Full path to config file
file_hash TEXT NOT NULL, -- SHA256 hash of file content
event_id TEXT, -- Nostr event ID from file
event_pubkey TEXT, -- Admin pubkey that signed event
loaded_at INTEGER NOT NULL DEFAULT (strftime('%s', 'now')),
validation_status TEXT CHECK (validation_status IN ('valid', 'invalid', 'unverified')),
validation_error TEXT -- Error details if invalid
);
```
## Indexes and Performance
```sql
-- Performance indexes for configuration tables
CREATE INDEX idx_server_config_type ON server_config(config_type);
CREATE INDEX idx_server_config_updated ON server_config(updated_at DESC);
CREATE INDEX idx_config_history_key ON config_history(config_key);
CREATE INDEX idx_config_history_time ON config_history(changed_at DESC);
CREATE INDEX idx_config_validation_key ON config_validation_log(config_key);
CREATE INDEX idx_config_validation_time ON config_validation_log(attempted_at DESC);
```
## Triggers
### Update Timestamp Trigger
```sql
-- Trigger to update timestamp on configuration changes
CREATE TRIGGER update_config_timestamp
AFTER UPDATE ON server_config
BEGIN
UPDATE server_config SET updated_at = strftime('%s', 'now') WHERE key = NEW.key;
END;
```
### Configuration History Trigger
```sql
-- Trigger to log configuration changes to history
CREATE TRIGGER log_config_changes
AFTER UPDATE ON server_config
WHEN OLD.value != NEW.value
BEGIN
INSERT INTO config_history (config_key, old_value, new_value, changed_by, change_reason)
VALUES (NEW.key, OLD.value, NEW.value, 'system', 'configuration update');
END;
```
## Default Configuration Values
### Core System Settings
```sql
INSERT OR IGNORE INTO server_config (key, value, description, config_type, data_type, requires_restart) VALUES
-- Administrative settings
('admin_pubkey', '', 'Authorized admin public key (hex)', 'system', 'string', 1),
('admin_enabled', 'false', 'Enable admin interface', 'system', 'boolean', 1),
-- Server core settings
('relay_port', '8888', 'WebSocket server port', 'user', 'integer', 1),
('database_path', 'db/c_nostr_relay.db', 'SQLite database file path', 'user', 'string', 1),
('max_connections', '100', 'Maximum concurrent connections', 'user', 'integer', 1),
-- NIP-11 Relay Information
('relay_name', 'C Nostr Relay', 'Relay name for NIP-11', 'user', 'string', 0),
('relay_description', 'High-performance C Nostr relay with SQLite storage', 'Relay description', 'user', 'string', 0),
('relay_contact', '', 'Contact information', 'user', 'string', 0),
('relay_pubkey', '', 'Relay public key', 'user', 'string', 0),
('relay_software', 'https://git.laantungir.net/laantungir/c-relay.git', 'Software URL', 'user', 'string', 0),
('relay_version', '0.2.0', 'Software version', 'user', 'string', 0),
-- NIP-13 Proof of Work
('pow_enabled', 'true', 'Enable NIP-13 Proof of Work validation', 'user', 'boolean', 0),
('pow_min_difficulty', '0', 'Minimum PoW difficulty required', 'user', 'integer', 0),
('pow_mode', 'basic', 'PoW validation mode (basic/full/strict)', 'user', 'string', 0),
-- NIP-40 Expiration Timestamp
('expiration_enabled', 'true', 'Enable NIP-40 expiration handling', 'user', 'boolean', 0),
('expiration_strict', 'true', 'Reject expired events on submission', 'user', 'boolean', 0),
('expiration_filter', 'true', 'Filter expired events from responses', 'user', 'boolean', 0),
('expiration_grace_period', '300', 'Grace period for clock skew (seconds)', 'user', 'integer', 0),
-- Subscription limits
('max_subscriptions_per_client', '20', 'Max subscriptions per client', 'user', 'integer', 0),
('max_total_subscriptions', '5000', 'Max total concurrent subscriptions', 'user', 'integer', 0),
('subscription_id_max_length', '64', 'Maximum subscription ID length', 'user', 'integer', 0),
-- Event processing limits
('max_event_tags', '100', 'Maximum tags per event', 'user', 'integer', 0),
('max_content_length', '8196', 'Maximum content length', 'user', 'integer', 0),
('max_message_length', '16384', 'Maximum message length', 'user', 'integer', 0),
-- Performance settings
('default_limit', '500', 'Default query limit', 'user', 'integer', 0),
('max_limit', '5000', 'Maximum query limit', 'user', 'integer', 0);
```
### Runtime Statistics
```sql
INSERT OR IGNORE INTO server_config (key, value, description, config_type, data_type) VALUES
-- Runtime statistics (updated by server)
('server_start_time', '0', 'Server startup timestamp', 'runtime', 'integer'),
('total_events_processed', '0', 'Total events processed', 'runtime', 'integer'),
('total_subscriptions_created', '0', 'Total subscriptions created', 'runtime', 'integer'),
('current_connections', '0', 'Current active connections', 'runtime', 'integer'),
('database_size_bytes', '0', 'Database file size in bytes', 'runtime', 'integer');
```
## Configuration Views
### Active Configuration View
```sql
CREATE VIEW active_config AS
SELECT
key,
value,
description,
config_type,
data_type,
requires_restart,
updated_at
FROM server_config
WHERE config_type IN ('system', 'user')
ORDER BY config_type, key;
```
### Runtime Statistics View
```sql
CREATE VIEW runtime_stats AS
SELECT
key,
value,
description,
updated_at
FROM server_config
WHERE config_type = 'runtime'
ORDER BY key;
```
### Configuration Change Summary
```sql
CREATE VIEW recent_config_changes AS
SELECT
ch.config_key,
sc.description,
ch.old_value,
ch.new_value,
ch.changed_by,
ch.change_reason,
ch.changed_at
FROM config_history ch
JOIN server_config sc ON ch.config_key = sc.key
ORDER BY ch.changed_at DESC
LIMIT 50;
```
## Validation Rules Format
Configuration validation rules are stored as JSON strings in the `validation_rules` column:
```json
{
"type": "integer",
"min": 1,
"max": 65535,
"required": true
}
```
```json
{
"type": "string",
"pattern": "^[0-9a-fA-F]{64}$",
"required": false,
"description": "64-character hex string"
}
```
```json
{
"type": "boolean",
"required": true
}
```
## Migration Strategy
1. **Phase 1**: Add configuration tables to existing schema
2. **Phase 2**: Populate with current hardcoded values
3. **Phase 3**: Update application code to read from database
4. **Phase 4**: Add file-based configuration loading
5. **Phase 5**: Remove hardcoded defaults and environment variable fallbacks
## Integration Points
- **Startup**: Load configuration from file → database → apply to application
- **Runtime**: Read configuration values from database cache
- **Updates**: Write changes to database → optionally update file
- **Validation**: Validate all configuration changes before applying
- **History**: Track all configuration changes for audit purposes

493
docs/file_config_design.md Normal file
View File

@@ -0,0 +1,493 @@
# File-Based Configuration Architecture Design
## Overview
This document outlines the XDG-compliant file-based configuration system for the C Nostr Relay, following the Ginxsom admin system approach using signed Nostr events.
## XDG Base Directory Specification Compliance
### File Location Strategy
**Primary Location:**
```
$XDG_CONFIG_HOME/c-relay/c_relay_config_event.json
```
**Fallback Location:**
```
$HOME/.config/c-relay/c_relay_config_event.json
```
**System-wide Fallback:**
```
/etc/c-relay/c_relay_config_event.json
```
### Directory Structure
```
$XDG_CONFIG_HOME/c-relay/
├── c_relay_config_event.json # Main configuration file
├── backup/ # Configuration backups
│ ├── c_relay_config_event.json.bak
│ └── c_relay_config_event.20241205.json
└── validation/ # Validation logs
└── config_validation.log
```
## Configuration File Format
### Signed Nostr Event Structure
The configuration file contains a signed Nostr event (kind 33334) with relay configuration:
```json
{
"kind": 33334,
"created_at": 1704067200,
"tags": [
["relay_name", "C Nostr Relay"],
["relay_description", "High-performance C Nostr relay with SQLite storage"],
["relay_port", "8888"],
["database_path", "db/c_nostr_relay.db"],
["admin_pubkey", ""],
["admin_enabled", "false"],
["pow_enabled", "true"],
["pow_min_difficulty", "0"],
["pow_mode", "basic"],
["expiration_enabled", "true"],
["expiration_strict", "true"],
["expiration_filter", "true"],
["expiration_grace_period", "300"],
["max_subscriptions_per_client", "20"],
["max_total_subscriptions", "5000"],
["max_connections", "100"],
["relay_contact", ""],
["relay_pubkey", ""],
["relay_software", "https://git.laantungir.net/laantungir/c-relay.git"],
["relay_version", "0.2.0"],
["max_event_tags", "100"],
["max_content_length", "8196"],
["max_message_length", "16384"],
["default_limit", "500"],
["max_limit", "5000"]
],
"content": "C Nostr Relay configuration event",
"pubkey": "admin_public_key_hex_64_chars",
"id": "computed_event_id_hex_64_chars",
"sig": "computed_signature_hex_128_chars"
}
```
### Event Kind Definition
**Kind 33334**: C Nostr Relay Configuration Event
- Parameterized replaceable event
- Must be signed by authorized admin pubkey
- Contains relay configuration as tags
- Validation required on load
## Configuration Loading Architecture
### Loading Priority Chain
1. **Command Line Arguments** (highest priority)
2. **File-based Configuration** (signed Nostr event)
3. **Database Configuration** (persistent storage)
4. **Environment Variables** (compatibility mode)
5. **Hardcoded Defaults** (fallback)
### Loading Process Flow
```mermaid
flowchart TD
A[Server Startup] --> B[Get Config File Path]
B --> C{File Exists?}
C -->|No| D[Check Database Config]
C -->|Yes| E[Load & Parse JSON]
E --> F[Validate Event Structure]
F --> G{Valid Event?}
G -->|No| H[Log Error & Use Database]
G -->|Yes| I[Verify Event Signature]
I --> J{Signature Valid?}
J -->|No| K[Log Error & Use Database]
J -->|Yes| L[Extract Configuration Tags]
L --> M[Apply to Database]
M --> N[Apply to Application]
D --> O[Load from Database]
H --> O
K --> O
O --> P[Apply Environment Variable Overrides]
P --> Q[Apply Command Line Overrides]
Q --> N
N --> R[Server Ready]
```
## C Implementation Architecture
### Core Data Structures
```c
// Configuration file management
typedef struct {
char file_path[512];
char file_hash[65]; // SHA256 hash
time_t last_modified;
time_t last_loaded;
int validation_status; // 0=valid, 1=invalid, 2=unverified
char validation_error[256];
} config_file_info_t;
// Configuration event structure
typedef struct {
char event_id[65];
char pubkey[65];
char signature[129];
long created_at;
int kind;
cJSON* tags;
char* content;
} config_event_t;
// Configuration management context
typedef struct {
config_file_info_t file_info;
config_event_t event;
int loaded_from_file;
int loaded_from_database;
char admin_pubkey[65];
time_t load_timestamp;
} config_context_t;
```
### Core Function Signatures
```c
// XDG path resolution
int get_config_file_path(char* path, size_t path_size);
int create_config_directories(const char* config_path);
// File operations
int load_config_from_file(const char* config_path, config_context_t* ctx);
int save_config_to_file(const char* config_path, const config_event_t* event);
int backup_config_file(const char* config_path);
// Event validation
int validate_config_event_structure(const cJSON* event);
int verify_config_event_signature(const config_event_t* event, const char* admin_pubkey);
int validate_config_tag_values(const cJSON* tags);
// Configuration extraction and application
int extract_config_from_tags(const cJSON* tags, config_context_t* ctx);
int apply_config_to_database(const config_context_t* ctx);
int apply_config_to_globals(const config_context_t* ctx);
// File monitoring and updates
int monitor_config_file_changes(const char* config_path);
int reload_config_on_change(config_context_t* ctx);
// Error handling and logging
int log_config_validation_error(const char* config_key, const char* error);
int log_config_load_event(const config_context_t* ctx, const char* source);
```
## Configuration Validation Rules
### Event Structure Validation
1. **Required Fields**: `kind`, `created_at`, `tags`, `content`, `pubkey`, `id`, `sig`
2. **Kind Validation**: Must be exactly 33334
3. **Timestamp Validation**: Must be reasonable (not too old, not future)
4. **Tags Format**: Array of string arrays `[["key", "value"], ...]`
5. **Signature Verification**: Must be signed by authorized admin pubkey
### Configuration Value Validation
```c
typedef struct {
char* key;
char* data_type; // "string", "integer", "boolean", "json"
char* validation_rule; // JSON validation rule
int required;
char* default_value;
} config_validation_rule_t;
static config_validation_rule_t validation_rules[] = {
{"relay_port", "integer", "{\"min\": 1, \"max\": 65535}", 1, "8888"},
{"pow_min_difficulty", "integer", "{\"min\": 0, \"max\": 64}", 1, "0"},
{"expiration_grace_period", "integer", "{\"min\": 0, \"max\": 86400}", 1, "300"},
{"admin_pubkey", "string", "{\"pattern\": \"^[0-9a-fA-F]{64}$\"}", 0, ""},
{"pow_enabled", "boolean", "{}", 1, "true"},
// ... more rules
};
```
### Security Validation
1. **Admin Pubkey Verification**: Only configured admin pubkeys can create config events
2. **Event ID Verification**: Event ID must match computed hash
3. **Signature Verification**: Signature must be valid for the event and pubkey
4. **Timestamp Validation**: Prevent replay attacks with old events
5. **File Permission Checks**: Config files should have appropriate permissions
## File Management Features
### Configuration File Operations
**File Creation:**
- Generate initial configuration file with default values
- Sign with admin private key
- Set appropriate file permissions (600 - owner read/write only)
**File Updates:**
- Create backup of existing file
- Validate new configuration
- Atomic file replacement (write to temp, then rename)
- Update file metadata cache
**File Monitoring:**
- Watch for file system changes using inotify (Linux)
- Reload configuration automatically when file changes
- Validate changes before applying
- Log all configuration reload events
### Backup and Recovery
**Automatic Backups:**
```
$XDG_CONFIG_HOME/c-relay/backup/
├── c_relay_config_event.json.bak # Last working config
├── c_relay_config_event.20241205-143022.json # Timestamped backups
└── c_relay_config_event.20241204-091530.json
```
**Recovery Process:**
1. Detect corrupted or invalid config file
2. Attempt to load from `.bak` backup
3. If backup fails, generate default configuration
4. Log recovery actions for audit
## Integration with Database Schema
### File-Database Synchronization
**On File Load:**
1. Parse and validate file-based configuration
2. Extract configuration values from event tags
3. Update database `server_config` table
4. Record file metadata in `config_file_cache` table
5. Log configuration changes in `config_history` table
**Configuration Priority Resolution:**
```c
char* get_config_value(const char* key, const char* default_value) {
// Priority: CLI args > File config > DB config > Env vars > Default
char* value = NULL;
// 1. Check command line overrides (if implemented)
value = get_cli_override(key);
if (value) return value;
// 2. Check database (updated from file)
value = get_database_config(key);
if (value) return value;
// 3. Check environment variables (compatibility)
value = get_env_config(key);
if (value) return value;
// 4. Return default
return strdup(default_value);
}
```
## Error Handling and Recovery
### Validation Error Handling
```c
typedef enum {
CONFIG_ERROR_NONE = 0,
CONFIG_ERROR_FILE_NOT_FOUND = 1,
CONFIG_ERROR_PARSE_FAILED = 2,
CONFIG_ERROR_INVALID_STRUCTURE = 3,
CONFIG_ERROR_SIGNATURE_INVALID = 4,
CONFIG_ERROR_UNAUTHORIZED = 5,
CONFIG_ERROR_VALUE_INVALID = 6,
CONFIG_ERROR_IO_ERROR = 7
} config_error_t;
typedef struct {
config_error_t error_code;
char error_message[256];
char config_key[64];
char invalid_value[128];
time_t error_timestamp;
} config_error_info_t;
```
### Graceful Degradation
**File Load Failure:**
1. Log detailed error information
2. Fall back to database configuration
3. Continue operation with last known good config
4. Set service status to "degraded" mode
**Validation Failure:**
1. Log validation errors with specific details
2. Skip invalid configuration items
3. Use default values for failed items
4. Continue with partial configuration
**Permission Errors:**
1. Log permission issues
2. Attempt to use fallback locations
3. Generate temporary config if needed
4. Alert administrator via logs
## Configuration Update Process
### Safe Configuration Updates
**Atomic Update Process:**
1. Create backup of current configuration
2. Write new configuration to temporary file
3. Validate new configuration completely
4. If valid, rename temporary file to active config
5. Update database with new values
6. Apply changes to running server
7. Log successful update
**Rollback Process:**
1. Detect invalid configuration at startup
2. Restore from backup file
3. Log rollback event
4. Continue with previous working configuration
### Hot Reload Support
**File Change Detection:**
```c
int monitor_config_file_changes(const char* config_path) {
// Use inotify on Linux to watch file changes
int inotify_fd = inotify_init();
int watch_fd = inotify_add_watch(inotify_fd, config_path, IN_MODIFY | IN_MOVED_TO);
// Monitor in separate thread
// On change: validate -> apply -> log
return 0;
}
```
**Runtime Configuration Updates:**
- Reload configuration on file change
- Apply non-restart-required changes immediately
- Queue restart-required changes for next restart
- Notify operators of configuration changes
## Security Considerations
### Access Control
**File Permissions:**
- Config files: 600 (owner read/write only)
- Directories: 700 (owner access only)
- Backup files: 600 (owner read/write only)
**Admin Key Management:**
- Admin private keys never stored in config files
- Only admin pubkeys stored for verification
- Support for multiple admin pubkeys
- Key rotation support
### Signature Validation
**Event Signature Verification:**
```c
int verify_config_event_signature(const config_event_t* event, const char* admin_pubkey) {
// 1. Reconstruct event for signing (without id and sig)
// 2. Compute event ID and verify against stored ID
// 3. Verify signature using admin pubkey
// 4. Check admin pubkey authorization
return NOSTR_SUCCESS;
}
```
**Anti-Replay Protection:**
- Configuration events must be newer than current
- Event timestamps validated against reasonable bounds
- Configuration history prevents replay attacks
## Implementation Phases
### Phase 1: Basic File Support
- XDG path resolution
- File loading and parsing
- Basic validation
- Database integration
### Phase 2: Security Features
- Event signature verification
- Admin pubkey management
- File permission checks
- Error handling
### Phase 3: Advanced Features
- Hot reload support
- Automatic backups
- Configuration utilities
- Interactive setup
### Phase 4: Monitoring & Management
- Configuration change monitoring
- Advanced validation rules
- Configuration audit logging
- Management utilities
## Configuration Generation Utilities
### Interactive Setup Script
```bash
#!/bin/bash
# scripts/setup_config.sh - Interactive configuration setup
create_initial_config() {
echo "=== C Nostr Relay Initial Configuration ==="
# Collect basic information
read -p "Relay name [C Nostr Relay]: " relay_name
read -p "Admin public key (hex): " admin_pubkey
read -p "Server port [8888]: " server_port
# Generate signed configuration event
./scripts/generate_config.sh \
--admin-key "$admin_pubkey" \
--relay-name "${relay_name:-C Nostr Relay}" \
--port "${server_port:-8888}" \
--output "$XDG_CONFIG_HOME/c-relay/c_relay_config_event.json"
}
```
### Configuration Validation Utility
```bash
#!/bin/bash
# scripts/validate_config.sh - Validate configuration file
validate_config_file() {
local config_file="$1"
# Check file exists and is readable
# Validate JSON structure
# Verify event signature
# Check configuration values
# Report validation results
}
```
This comprehensive file-based configuration design provides a robust, secure, and maintainable system that follows industry standards while integrating seamlessly with the existing C Nostr Relay architecture.

View File

@@ -5,6 +5,53 @@
echo "=== C Nostr Relay Build and Restart Script ==="
# Parse command line arguments
PRESERVE_CONFIG=false
HELP=false
while [[ $# -gt 0 ]]; do
case $1 in
--preserve-config|-p)
PRESERVE_CONFIG=true
shift
;;
--help|-h)
HELP=true
shift
;;
*)
echo "Unknown option: $1"
HELP=true
shift
;;
esac
done
# Show help
if [ "$HELP" = true ]; then
echo "Usage: $0 [OPTIONS]"
echo ""
echo "Options:"
echo " --preserve-config Keep existing configuration file (don't regenerate)"
echo " --help, -h Show this help message"
echo ""
echo "Default behavior: Automatically regenerates configuration file on each build"
echo " for development purposes"
exit 0
fi
# Handle configuration file regeneration
CONFIG_FILE="$HOME/.config/c-relay/c_relay_config_event.json"
if [ "$PRESERVE_CONFIG" = false ] && [ -f "$CONFIG_FILE" ]; then
echo "Removing old configuration file to trigger regeneration..."
rm -f "$CONFIG_FILE"
echo "✓ Configuration file removed - will be regenerated with latest database values"
elif [ "$PRESERVE_CONFIG" = true ] && [ -f "$CONFIG_FILE" ]; then
echo "Preserving existing configuration file as requested"
elif [ ! -f "$CONFIG_FILE" ]; then
echo "No existing configuration file found - will generate new one"
fi
# Build the project first
echo "Building project..."
make clean all
@@ -15,9 +62,22 @@ if [ $? -ne 0 ]; then
exit 1
fi
# Check if relay binary exists after build
if [ ! -f "./src/main" ]; then
echo "ERROR: Relay binary not found after build. Build may have failed."
# Check if relay binary exists after build - detect architecture
ARCH=$(uname -m)
case "$ARCH" in
x86_64)
BINARY_PATH="./build/c_relay_x86"
;;
aarch64|arm64)
BINARY_PATH="./build/c_relay_arm64"
;;
*)
BINARY_PATH="./build/c_relay_$ARCH"
;;
esac
if [ ! -f "$BINARY_PATH" ]; then
echo "ERROR: Relay binary not found at $BINARY_PATH after build. Build may have failed."
exit 1
fi
@@ -25,7 +85,7 @@ echo "Build successful. Proceeding with relay restart..."
# Kill existing relay if running
echo "Stopping any existing relay servers..."
pkill -f "./src/main" 2>/dev/null
pkill -f "c_relay_" 2>/dev/null
sleep 2 # Give time for shutdown
# Check if port is still bound
@@ -35,7 +95,7 @@ if lsof -i :8888 >/dev/null 2>&1; then
fi
# Get any remaining processes
REMAINING_PIDS=$(pgrep -f "./src/main" || echo "")
REMAINING_PIDS=$(pgrep -f "c_relay_" || echo "")
if [ -n "$REMAINING_PIDS" ]; then
echo "Force killing remaining processes: $REMAINING_PIDS"
kill -9 $REMAINING_PIDS 2>/dev/null
@@ -55,10 +115,10 @@ fi
# Start relay in background with output redirection
echo "Starting relay server..."
echo "Debug: Current processes: $(ps aux | grep './src/main' | grep -v grep || echo 'None')"
echo "Debug: Current processes: $(ps aux | grep 'c_relay_' | grep -v grep || echo 'None')"
# Start relay in background and capture its PID
./src/main > relay.log 2>&1 &
$BINARY_PATH > relay.log 2>&1 &
RELAY_PID=$!
echo "Started with PID: $RELAY_PID"
@@ -77,10 +137,24 @@ if ps -p "$RELAY_PID" >/dev/null 2>&1; then
# Save PID for debugging
echo $RELAY_PID > relay.pid
# Check if a new private key was generated and display it
sleep 1 # Give relay time to write initial logs
if grep -q "GENERATED RELAY ADMIN PRIVATE KEY" relay.log 2>/dev/null; then
echo "=== IMPORTANT: NEW ADMIN PRIVATE KEY GENERATED ==="
echo ""
# Extract and display the private key section from the log
grep -A 8 -B 2 "GENERATED RELAY ADMIN PRIVATE KEY" relay.log | head -n 12
echo ""
echo "⚠️ SAVE THIS PRIVATE KEY SECURELY - IT CONTROLS YOUR RELAY!"
echo "⚠️ This key is also logged in relay.log for reference"
echo ""
fi
echo "=== Relay server running in background ==="
echo "To kill relay: pkill -f './src/main'"
echo "To check status: ps aux | grep src/main"
echo "To kill relay: pkill -f 'c_relay_'"
echo "To check status: ps aux | grep c_relay_"
echo "To view logs: tail -f relay.log"
echo "Binary: $BINARY_PATH"
echo "Ready for Nostr client connections!"
else
echo "ERROR: Relay failed to start"

131
relay.log
View File

@@ -1,131 +0,0 @@
=== C Nostr Relay Server ===
[SUCCESS] Database connection established
[INFO] Starting relay server...
[INFO] Starting libwebsockets-based Nostr relay server...
[SUCCESS] WebSocket relay started on ws://127.0.0.1:8888
[INFO] WebSocket connection established
[INFO] Received WebSocket message
[INFO] Received WebSocket message
[INFO] Received WebSocket message
[INFO] Received WebSocket message
[INFO] Received WebSocket message
[INFO] Received WebSocket message
[INFO] Received WebSocket message
[INFO] Received WebSocket message
[INFO] Received WebSocket message
[INFO] WebSocket connection closed
[INFO] WebSocket connection established
[INFO] Received WebSocket message
[INFO] Handling EVENT message with full NIP-01 validation
[SUCCESS] Event stored in database
[SUCCESS] Event validated and stored successfully
[INFO] WebSocket connection closed
[INFO] WebSocket connection established
[INFO] Received WebSocket message
[INFO] Handling EVENT message with full NIP-01 validation
[SUCCESS] Event stored in database
[SUCCESS] Event validated and stored successfully
[INFO] WebSocket connection closed
[INFO] WebSocket connection established
[INFO] Received WebSocket message
[INFO] Handling EVENT message with full NIP-01 validation
[SUCCESS] Event stored in database
[SUCCESS] Event validated and stored successfully
[INFO] WebSocket connection closed
[INFO] WebSocket connection established
[INFO] Received WebSocket message
[INFO] Received WebSocket message
[INFO] Received WebSocket message
[INFO] Received WebSocket message
[WARNING] Subscription 'exists_1757082297' not found for removal
[INFO] Closed subscription: exists_1757082297
[INFO] WebSocket connection closed
[INFO] WebSocket connection established
[INFO] Received WebSocket message
[INFO] Received WebSocket message
[INFO] Received WebSocket message
[INFO] Received WebSocket message
[WARNING] Subscription 'exists_1757082298' not found for removal
[INFO] Closed subscription: exists_1757082298
[INFO] WebSocket connection closed
[INFO] WebSocket connection established
[INFO] Received WebSocket message
[INFO] Handling EVENT message with full NIP-01 validation
[INFO] Event not found for deletion: [INFO] ...
[INFO] Event not found for deletion: [INFO] ...
[SUCCESS] Event stored in database
[INFO] Deletion request processed: 0 events deleted
[INFO] WebSocket connection closed
[INFO] WebSocket connection established
[INFO] Received WebSocket message
[INFO] Received WebSocket message
[INFO] Received WebSocket message
[INFO] Received WebSocket message
[WARNING] Subscription 'exists_1757082301' not found for removal
[INFO] Closed subscription: exists_1757082301
[INFO] WebSocket connection closed
[INFO] WebSocket connection established
[INFO] Received WebSocket message
[INFO] Received WebSocket message
[INFO] Received WebSocket message
[INFO] Received WebSocket message
[WARNING] Subscription 'exists_1757082301' not found for removal
[INFO] Closed subscription: exists_1757082301
[INFO] WebSocket connection closed
[INFO] WebSocket connection established
[INFO] Received WebSocket message
[INFO] Received WebSocket message
[INFO] Received WebSocket message
[INFO] Received WebSocket message
[WARNING] Subscription 'exists_1757082301' not found for removal
[INFO] Closed subscription: exists_1757082301
[INFO] WebSocket connection closed
[INFO] WebSocket connection established
[INFO] Received WebSocket message
[INFO] Handling EVENT message with full NIP-01 validation
[SUCCESS] Event stored in database
[INFO] Deletion request processed: 0 events deleted
[INFO] WebSocket connection closed
[INFO] WebSocket connection established
[INFO] Received WebSocket message
[INFO] Received WebSocket message
[INFO] Received WebSocket message
[INFO] Received WebSocket message
[WARNING] Subscription 'exists_1757082305' not found for removal
[INFO] Closed subscription: exists_1757082305
[INFO] WebSocket connection closed
[INFO] WebSocket connection established
[INFO] Received WebSocket message
[INFO] Handling EVENT message with full NIP-01 validation
[INFO] Event not found for deletion: ✗ Cou...
[SUCCESS] Event stored in database
[INFO] Deletion request processed: 0 events deleted
[INFO] WebSocket connection closed
[INFO] WebSocket connection established
[INFO] Received WebSocket message
[INFO] Handling REQ message for persistent subscription
[INFO] Added subscription 'exists_1757082309' (total: 1)
[INFO] Executing SQL: SELECT id, pubkey, created_at, kind, content, sig, tags FROM events WHERE 1=1 ORDER BY created_at DESC LIMIT 500
[INFO] Query returned 25 rows
[INFO] Total events sent: 25
[INFO] Received WebSocket message
[INFO] Removed subscription 'exists_1757082309' (total: 0)
[INFO] Closed subscription: exists_1757082309
[INFO] WebSocket connection closed
[WARNING] Subscription 'z[<5B><>.Y' not found for removal
[INFO] WebSocket connection established
[INFO] Received WebSocket message
[INFO] Handling EVENT message with full NIP-01 validation
[INFO] WebSocket connection closed
[INFO] WebSocket connection established
[INFO] Received WebSocket message
[INFO] Handling REQ message for persistent subscription
[INFO] Added subscription 'kind5_1757082309' (total: 1)
[INFO] Executing SQL: SELECT id, pubkey, created_at, kind, content, sig, tags FROM events WHERE 1=1 AND kind IN (5) ORDER BY created_at DESC LIMIT 500
[INFO] Query returned 3 rows
[INFO] Total events sent: 3
[INFO] Received WebSocket message
[INFO] Removed subscription 'kind5_1757082309' (total: 0)
[INFO] Closed subscription: kind5_1757082309
[INFO] WebSocket connection closed
[WARNING] Subscription '<27>f<EFBFBD><66>.Y' not found for removal

View File

@@ -1 +1 @@
682319
871512

1001
src/config.c Normal file

File diff suppressed because it is too large Load Diff

223
src/config.h Normal file
View File

@@ -0,0 +1,223 @@
#ifndef CONFIG_H
#define CONFIG_H
#include <sqlite3.h>
#include <time.h>
#include <stddef.h>
#include <cjson/cJSON.h>
// Configuration system constants
#define CONFIG_KEY_MAX_LENGTH 64
#define CONFIG_VALUE_MAX_LENGTH 512
#define CONFIG_DESCRIPTION_MAX_LENGTH 256
#define CONFIG_XDG_DIR_NAME "c-relay"
#define CONFIG_FILE_NAME "c_relay_config_event.json"
#define CONFIG_PRIVKEY_ENV "C_RELAY_CONFIG_PRIVKEY"
#define NOSTR_PUBKEY_HEX_LENGTH 64
#define NOSTR_PRIVKEY_HEX_LENGTH 64
#define NOSTR_EVENT_ID_HEX_LENGTH 64
#define NOSTR_SIGNATURE_HEX_LENGTH 128
// Protocol and implementation constants (hardcoded - should NOT be configurable)
#define SUBSCRIPTION_ID_MAX_LENGTH 64
#define CLIENT_IP_MAX_LENGTH 64
#define RELAY_NAME_MAX_LENGTH 128
#define RELAY_DESCRIPTION_MAX_LENGTH 1024
#define RELAY_URL_MAX_LENGTH 256
#define RELAY_CONTACT_MAX_LENGTH 128
#define RELAY_PUBKEY_MAX_LENGTH 65
// Default configuration values (used as fallbacks if database config fails)
#define DEFAULT_DATABASE_PATH "db/c_nostr_relay.db"
#define DEFAULT_PORT 8888
#define DEFAULT_HOST "127.0.0.1"
#define MAX_CLIENTS 100
#define MAX_SUBSCRIPTIONS_PER_CLIENT 20
#define MAX_TOTAL_SUBSCRIPTIONS 5000
#define MAX_FILTERS_PER_SUBSCRIPTION 10
// Configuration types
typedef enum {
CONFIG_TYPE_SYSTEM = 0,
CONFIG_TYPE_USER = 1,
CONFIG_TYPE_RUNTIME = 2
} config_type_t;
// Configuration data types
typedef enum {
CONFIG_DATA_STRING = 0,
CONFIG_DATA_INTEGER = 1,
CONFIG_DATA_BOOLEAN = 2,
CONFIG_DATA_JSON = 3
} config_data_type_t;
// Configuration validation result
typedef enum {
CONFIG_VALID = 0,
CONFIG_INVALID_TYPE = 1,
CONFIG_INVALID_RANGE = 2,
CONFIG_INVALID_FORMAT = 3,
CONFIG_MISSING_REQUIRED = 4
} config_validation_result_t;
// Configuration entry structure
typedef struct {
char key[CONFIG_KEY_MAX_LENGTH];
char value[CONFIG_VALUE_MAX_LENGTH];
char description[CONFIG_DESCRIPTION_MAX_LENGTH];
config_type_t config_type;
config_data_type_t data_type;
int is_sensitive;
int requires_restart;
time_t created_at;
time_t updated_at;
} config_entry_t;
// Configuration manager state
typedef struct {
sqlite3* db;
sqlite3_stmt* get_config_stmt;
sqlite3_stmt* set_config_stmt;
sqlite3_stmt* log_change_stmt;
// Configuration loading status
int file_config_loaded;
int database_config_loaded;
time_t last_reload;
// XDG configuration directory
char config_dir_path[512];
char config_file_path[600];
} config_manager_t;
// Global configuration manager instance
extern config_manager_t g_config_manager;
// ================================
// CORE CONFIGURATION FUNCTIONS
// ================================
// Initialize configuration system
int init_configuration_system(void);
// Cleanup configuration system
void cleanup_configuration_system(void);
// Load configuration from all sources (file -> database -> defaults)
int load_configuration(void);
// Apply loaded configuration to global variables
int apply_configuration_to_globals(void);
// ================================
// DATABASE CONFIGURATION FUNCTIONS
// ================================
// Initialize database prepared statements
int init_config_database_statements(void);
// Get configuration value from database
int get_database_config(const char* key, char* value, size_t value_size);
// Set configuration value in database
int set_database_config(const char* key, const char* new_value, const char* changed_by);
// Load all configuration from database
int load_config_from_database(void);
// ================================
// FILE CONFIGURATION FUNCTIONS
// ================================
// Get XDG configuration directory path
int get_xdg_config_dir(char* path, size_t path_size);
// Check if configuration file exists
int config_file_exists(void);
// Load configuration from file
int load_config_from_file(void);
// Validate and apply Nostr configuration event
int validate_and_apply_config_event(const cJSON* event);
// Validate Nostr event structure
int validate_nostr_event_structure(const cJSON* event);
// Validate configuration tags array
int validate_config_tags(const cJSON* tags);
// Extract and apply configuration tags to database
int extract_and_apply_config_tags(const cJSON* tags);
// ================================
// CONFIGURATION ACCESS FUNCTIONS
// ================================
// Get configuration value (checks all sources: file -> database -> environment -> defaults)
const char* get_config_value(const char* key);
// Get configuration value as integer
int get_config_int(const char* key, int default_value);
// Get configuration value as boolean
int get_config_bool(const char* key, int default_value);
// Set configuration value (updates database)
int set_config_value(const char* key, const char* value);
// ================================
// CONFIGURATION VALIDATION
// ================================
// Validate configuration value
config_validation_result_t validate_config_value(const char* key, const char* value);
// Log validation error
void log_config_validation_error(const char* key, const char* value, const char* error);
// ================================
// UTILITY FUNCTIONS
// ================================
// Convert config type enum to string
const char* config_type_to_string(config_type_t type);
// Convert config data type enum to string
const char* config_data_type_to_string(config_data_type_t type);
// Convert string to config type enum
config_type_t string_to_config_type(const char* str);
// Convert string to config data type enum
config_data_type_t string_to_config_data_type(const char* str);
// Check if configuration key requires restart
int config_requires_restart(const char* key);
// ================================
// NOSTR EVENT GENERATION FUNCTIONS
// ================================
// Generate configuration file with valid Nostr event if it doesn't exist
int generate_config_file_if_missing(void);
// Create a valid Nostr configuration event from database values
cJSON* create_config_nostr_event(const char* privkey_hex);
// Generate a random private key (32 bytes as hex string)
int generate_random_privkey(char* privkey_hex, size_t buffer_size);
// Derive public key from private key (using secp256k1)
int derive_pubkey_from_privkey(const char* privkey_hex, char* pubkey_hex, size_t buffer_size);
// Create Nostr event ID (SHA256 of serialized event data)
int create_nostr_event_id(const cJSON* event, char* event_id_hex, size_t buffer_size);
// Sign Nostr event (using secp256k1 Schnorr signature)
int sign_nostr_event(const cJSON* event, const char* privkey_hex, char* signature_hex, size_t buffer_size);
// Write configuration event to file
int write_config_event_to_file(const cJSON* event);
#endif // CONFIG_H

BIN
src/main

Binary file not shown.

File diff suppressed because it is too large Load Diff

432
tests/11_nip_information.sh Executable file
View File

@@ -0,0 +1,432 @@
#!/bin/bash
# NIP-11 Relay Information Document Test
# Tests HTTP endpoint for relay information according to NIP-11 specification
set -e # Exit on any error
# Color constants
RED='\033[31m'
GREEN='\033[32m'
YELLOW='\033[33m'
BLUE='\033[34m'
BOLD='\033[1m'
RESET='\033[0m'
# Test configuration
RELAY_URL="http://127.0.0.1:8888"
RELAY_WS_URL="ws://127.0.0.1:8888"
# Print functions
print_header() {
echo -e "${BLUE}${BOLD}=== $1 ===${RESET}"
}
print_step() {
echo -e "${YELLOW}[STEP]${RESET} $1"
}
print_success() {
echo -e "${GREEN}${RESET} $1"
}
print_error() {
echo -e "${RED}${RESET} $1"
}
print_info() {
echo -e "${BLUE}[INFO]${RESET} $1"
}
print_warning() {
echo -e "${YELLOW}[WARNING]${RESET} $1"
}
# Test functions
test_http_with_correct_header() {
print_step "Testing HTTP request with correct Accept header"
local response=""
local http_code=""
if command -v curl &> /dev/null; then
# Use curl to test with proper Accept header
response=$(curl -s -H "Accept: application/nostr+json" "$RELAY_URL/" 2>/dev/null || echo "")
http_code=$(curl -s -o /dev/null -w "%{http_code}" -H "Accept: application/nostr+json" "$RELAY_URL/" 2>/dev/null || echo "000")
else
print_error "curl command not found - required for NIP-11 testing"
return 1
fi
if [[ "$http_code" == "200" ]]; then
print_success "HTTP 200 OK received with correct Accept header"
# Validate JSON response
if echo "$response" | jq . >/dev/null 2>&1; then
print_success "Response is valid JSON"
return 0
else
print_error "Response is not valid JSON"
return 1
fi
else
print_error "Expected HTTP 200, got HTTP $http_code"
return 1
fi
}
test_http_without_header() {
print_step "Testing HTTP request without Accept header (should return 406)"
local http_code=""
if command -v curl &> /dev/null; then
http_code=$(curl -s -o /dev/null -w "%{http_code}" "$RELAY_URL/" 2>/dev/null || echo "000")
else
print_error "curl command not found - required for NIP-11 testing"
return 1
fi
if [[ "$http_code" == "406" ]]; then
print_success "HTTP 406 Not Acceptable received without proper Accept header"
return 0
else
print_error "Expected HTTP 406, got HTTP $http_code"
return 1
fi
}
test_http_with_wrong_header() {
print_step "Testing HTTP request with wrong Accept header (should return 406)"
local http_code=""
if command -v curl &> /dev/null; then
http_code=$(curl -s -o /dev/null -w "%{http_code}" -H "Accept: application/json" "$RELAY_URL/" 2>/dev/null || echo "000")
else
print_error "curl command not found - required for NIP-11 testing"
return 1
fi
if [[ "$http_code" == "406" ]]; then
print_success "HTTP 406 Not Acceptable received with wrong Accept header"
return 0
else
print_error "Expected HTTP 406, got HTTP $http_code"
return 1
fi
}
test_cors_headers() {
print_step "Testing CORS headers presence"
local headers=""
if command -v curl &> /dev/null; then
headers=$(curl -s -I -H "Accept: application/nostr+json" "$RELAY_URL/" 2>/dev/null || echo "")
else
print_error "curl command not found - required for NIP-11 testing"
return 1
fi
local cors_origin_found=false
local cors_headers_found=false
local cors_methods_found=false
if echo "$headers" | grep -qi "access-control-allow-origin"; then
cors_origin_found=true
print_success "Access-Control-Allow-Origin header found"
fi
if echo "$headers" | grep -qi "access-control-allow-headers"; then
cors_headers_found=true
print_success "Access-Control-Allow-Headers header found"
fi
if echo "$headers" | grep -qi "access-control-allow-methods"; then
cors_methods_found=true
print_success "Access-Control-Allow-Methods header found"
fi
if [[ "$cors_origin_found" == true && "$cors_headers_found" == true && "$cors_methods_found" == true ]]; then
print_success "All required CORS headers present"
return 0
else
print_error "Missing CORS headers"
return 1
fi
}
test_json_structure() {
print_step "Testing NIP-11 JSON structure and required fields"
local response=""
if command -v curl &> /dev/null; then
response=$(curl -s -H "Accept: application/nostr+json" "$RELAY_URL/" 2>/dev/null || echo "")
else
print_error "curl command not found - required for NIP-11 testing"
return 1
fi
if [[ -z "$response" ]]; then
print_error "Empty response received"
return 1
fi
# Validate JSON structure using jq
if ! echo "$response" | jq . >/dev/null 2>&1; then
print_error "Response is not valid JSON"
return 1
fi
print_success "Valid JSON structure confirmed"
# Check for required fields
local required_checks=0
local total_checks=0
# Test name field
((total_checks++))
if echo "$response" | jq -e '.name' >/dev/null 2>&1; then
local name=$(echo "$response" | jq -r '.name')
print_success "Name field present: $name"
((required_checks++))
else
print_warning "Name field missing (optional)"
fi
# Test supported_nips field (required)
((total_checks++))
if echo "$response" | jq -e '.supported_nips' >/dev/null 2>&1; then
local nips=$(echo "$response" | jq -r '.supported_nips | @json')
print_success "Supported NIPs field present: $nips"
((required_checks++))
# Verify NIP-11 is in the supported list
if echo "$response" | jq -e '.supported_nips | contains([11])' >/dev/null 2>&1; then
print_success "NIP-11 correctly listed in supported NIPs"
else
print_warning "NIP-11 not found in supported NIPs list"
fi
else
print_error "Supported NIPs field missing (should be present)"
fi
# Test software field
((total_checks++))
if echo "$response" | jq -e '.software' >/dev/null 2>&1; then
local software=$(echo "$response" | jq -r '.software')
print_success "Software field present: $software"
((required_checks++))
else
print_warning "Software field missing (optional)"
fi
# Test version field
((total_checks++))
if echo "$response" | jq -e '.version' >/dev/null 2>&1; then
local version=$(echo "$response" | jq -r '.version')
print_success "Version field present: $version"
((required_checks++))
else
print_warning "Version field missing (optional)"
fi
# Test limitation object
((total_checks++))
if echo "$response" | jq -e '.limitation' >/dev/null 2>&1; then
print_success "Limitation object present"
((required_checks++))
# Check some common limitation fields
if echo "$response" | jq -e '.limitation.max_message_length' >/dev/null 2>&1; then
local max_msg=$(echo "$response" | jq -r '.limitation.max_message_length')
print_info " max_message_length: $max_msg"
fi
if echo "$response" | jq -e '.limitation.max_subscriptions' >/dev/null 2>&1; then
local max_subs=$(echo "$response" | jq -r '.limitation.max_subscriptions')
print_info " max_subscriptions: $max_subs"
fi
else
print_warning "Limitation object missing (recommended)"
fi
# Test description field
if echo "$response" | jq -e '.description' >/dev/null 2>&1; then
local description=$(echo "$response" | jq -r '.description')
print_success "Description field present: ${description:0:50}..."
else
print_warning "Description field missing (optional)"
fi
print_info "JSON structure validation: $required_checks/$total_checks core fields present"
return 0
}
test_content_type_header() {
print_step "Testing Content-Type header"
local headers=""
if command -v curl &> /dev/null; then
headers=$(curl -s -I -H "Accept: application/nostr+json" "$RELAY_URL/" 2>/dev/null || echo "")
else
print_error "curl command not found - required for NIP-11 testing"
return 1
fi
if echo "$headers" | grep -qi "content-type.*application/nostr+json"; then
print_success "Correct Content-Type header: application/nostr+json"
return 0
else
print_warning "Content-Type header not exactly 'application/nostr+json'"
echo "$headers" | grep -i "content-type" | head -1
return 1
fi
}
test_non_root_path() {
print_step "Testing non-root path (should return 404)"
local http_code=""
if command -v curl &> /dev/null; then
http_code=$(curl -s -o /dev/null -w "%{http_code}" -H "Accept: application/nostr+json" "$RELAY_URL/nonexistent" 2>/dev/null || echo "000")
else
print_error "curl command not found - required for NIP-11 testing"
return 1
fi
if [[ "$http_code" == "404" ]]; then
print_success "HTTP 404 Not Found received for non-root path"
return 0
else
print_error "Expected HTTP 404 for non-root path, got HTTP $http_code"
return 1
fi
}
test_websocket_still_works() {
print_step "Testing that WebSocket functionality still works on same port"
if ! command -v websocat &> /dev/null; then
print_warning "websocat not available - skipping WebSocket test"
return 0
fi
# Try to connect to WebSocket and send a simple REQ
local response=""
response=$(echo '["REQ","test_ws_nip11",{}]' | timeout 3s websocat "$RELAY_WS_URL" 2>/dev/null || echo "Connection failed")
if [[ "$response" == *"Connection failed"* ]]; then
print_error "WebSocket connection failed"
return 1
elif [[ "$response" == *"EOSE"* ]]; then
print_success "WebSocket still functional - received EOSE response"
return 0
else
print_warning "WebSocket response unclear, but connection succeeded"
return 0
fi
}
# Main test function
run_nip11_tests() {
print_header "NIP-11 Relay Information Document Tests"
# Check dependencies
print_step "Checking dependencies..."
if ! command -v curl &> /dev/null; then
print_error "curl command not found - required for NIP-11 HTTP testing"
return 1
fi
if ! command -v jq &> /dev/null; then
print_error "jq command not found - required for JSON validation"
return 1
fi
print_success "All dependencies found"
print_header "PHASE 1: Basic HTTP Functionality"
# Test 1: Correct Accept header
if ! test_http_with_correct_header; then
return 1
fi
# Test 2: Missing Accept header
if ! test_http_without_header; then
return 1
fi
# Test 3: Wrong Accept header
if ! test_http_with_wrong_header; then
return 1
fi
print_header "PHASE 2: HTTP Headers Validation"
# Test 4: CORS headers
if ! test_cors_headers; then
return 1
fi
# Test 5: Content-Type header
if ! test_content_type_header; then
return 1
fi
print_header "PHASE 3: JSON Structure Validation"
# Test 6: JSON structure and required fields
if ! test_json_structure; then
return 1
fi
print_header "PHASE 4: Additional Endpoint Behavior"
# Test 7: Non-root paths
if ! test_non_root_path; then
return 1
fi
# Test 8: WebSocket compatibility
if ! test_websocket_still_works; then
return 1
fi
print_header "PHASE 5: NIP-11 Compliance Summary"
# Final validation - get the actual response and display it
print_step "Displaying complete NIP-11 response..."
local response=""
if command -v curl &> /dev/null; then
response=$(curl -s -H "Accept: application/nostr+json" "$RELAY_URL/" 2>/dev/null || echo "")
if [[ -n "$response" ]] && echo "$response" | jq . >/dev/null 2>&1; then
echo "$response" | jq .
else
print_error "Failed to retrieve or parse final response"
fi
fi
print_success "All NIP-11 tests passed!"
return 0
}
# Main execution
print_header "Starting NIP-11 Relay Information Document Test Suite"
echo
if run_nip11_tests; then
echo
print_success "All NIP-11 tests completed successfully!"
print_info "The C-Relay NIP-11 implementation is fully compliant"
print_info "✅ HTTP endpoint, Accept header validation, CORS, and JSON structure all working"
echo
exit 0
else
echo
print_error "Some NIP-11 tests failed"
exit 1
fi

384
tests/13_nip_test.sh Executable file
View File

@@ -0,0 +1,384 @@
#!/bin/bash
# NIP-13 Proof of Work Validation Test Suite for C Nostr Relay
# Tests PoW validation in the relay's event processing pipeline
# Based on nostr_core_lib/tests/nip13_test.c
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'
# Test configuration
RELAY_URL="ws://127.0.0.1:8888"
HTTP_URL="http://127.0.0.1:8888"
TEST_COUNT=0
PASSED_COUNT=0
FAILED_COUNT=0
# Test results tracking
declare -a TEST_RESULTS=()
print_info() {
echo -e "${BLUE}[INFO]${RESET} $1"
}
print_success() {
echo -e "${GREEN}${BOLD}[SUCCESS]${RESET} $1"
}
print_warning() {
echo -e "${YELLOW}[WARNING]${RESET} $1"
}
print_error() {
echo -e "${RED}${BOLD}[ERROR]${RESET} $1"
}
print_test_header() {
TEST_COUNT=$((TEST_COUNT + 1))
echo ""
echo -e "${BOLD}=== TEST $TEST_COUNT: $1 ===${RESET}"
}
record_test_result() {
local test_name="$1"
local result="$2"
local details="$3"
TEST_RESULTS+=("$test_name|$result|$details")
if [ "$result" = "PASS" ]; then
PASSED_COUNT=$((PASSED_COUNT + 1))
print_success "PASS: $test_name"
else
FAILED_COUNT=$((FAILED_COUNT + 1))
print_error "FAIL: $test_name"
if [ -n "$details" ]; then
echo " Details: $details"
fi
fi
}
# Check if relay is running
check_relay_running() {
print_info "Checking if relay is running..."
if ! curl -s -H "Accept: application/nostr+json" "$HTTP_URL/" >/dev/null 2>&1; then
print_error "Relay is not running or not accessible at $HTTP_URL"
print_info "Please start the relay with: ./make_and_restart_relay.sh"
exit 1
fi
print_success "Relay is running and accessible"
}
# Test NIP-11 relay information includes NIP-13
test_nip11_pow_support() {
print_test_header "NIP-11 PoW Support Advertisement"
print_info "Fetching relay information..."
RELAY_INFO=$(curl -s -H "Accept: application/nostr+json" "$HTTP_URL/")
echo "Relay Info Response:"
echo "$RELAY_INFO" | jq '.'
echo ""
# Check if NIP-13 is in supported_nips
if echo "$RELAY_INFO" | jq -e '.supported_nips | index(13)' >/dev/null 2>&1; then
print_success "✓ NIP-13 found in supported_nips array"
NIP13_SUPPORTED=true
else
print_error "✗ NIP-13 not found in supported_nips array"
NIP13_SUPPORTED=false
fi
# Check if min_pow_difficulty is present
MIN_POW_DIFF=$(echo "$RELAY_INFO" | jq -r '.limitation.min_pow_difficulty // "missing"')
if [ "$MIN_POW_DIFF" != "missing" ]; then
print_success "✓ min_pow_difficulty found: $MIN_POW_DIFF"
MIN_POW_PRESENT=true
else
print_error "✗ min_pow_difficulty not found in limitations"
MIN_POW_PRESENT=false
fi
if [ "$NIP13_SUPPORTED" = true ] && [ "$MIN_POW_PRESENT" = true ]; then
record_test_result "NIP-11 PoW Support Advertisement" "PASS" "NIP-13 supported, min_pow_difficulty=$MIN_POW_DIFF"
return 0
else
record_test_result "NIP-11 PoW Support Advertisement" "FAIL" "Missing NIP-13 support or min_pow_difficulty"
return 1
fi
}
# Test event submission without PoW (should be accepted when min_difficulty=0)
test_event_without_pow() {
print_test_header "Event Submission Without PoW (min_difficulty=0)"
# Create a simple event without PoW
print_info "Generating test event without PoW..."
# Use nak to generate a simple event
if ! command -v nak &> /dev/null; then
print_warning "nak command not found - skipping PoW generation tests"
record_test_result "Event Submission Without PoW" "SKIP" "nak not available"
return 0
fi
# Generate event without PoW using direct private key
PRIVATE_KEY="91ba716fa9e7ea2fcbad360cf4f8e0d312f73984da63d90f524ad61a6a1e7dbe"
EVENT_JSON=$(nak event --sec "$PRIVATE_KEY" -c "Test event without PoW" --ts $(date +%s))
print_info "Generated event:"
echo "$EVENT_JSON" | jq '.'
echo ""
# Send event to relay via WebSocket using websocat
print_info "Sending event to relay..."
# Create EVENT message in Nostr format
EVENT_MESSAGE="[\"EVENT\",$EVENT_JSON]"
# Send to relay and capture response
if command -v websocat &> /dev/null; then
RESPONSE=$(echo "$EVENT_MESSAGE" | timeout 5s websocat "$RELAY_URL" 2>&1 || echo "Connection failed")
print_info "Relay response: $RESPONSE"
if [[ "$RESPONSE" == *"Connection failed"* ]]; then
print_error "✗ Failed to connect to relay"
record_test_result "Event Submission Without PoW" "FAIL" "Connection failed"
return 1
elif [[ "$RESPONSE" == *"true"* ]]; then
print_success "✓ Event without PoW accepted (expected when min_difficulty=0)"
record_test_result "Event Submission Without PoW" "PASS" "Event accepted as expected"
return 0
else
print_error "✗ Event without PoW rejected (unexpected when min_difficulty=0)"
record_test_result "Event Submission Without PoW" "FAIL" "Event rejected: $RESPONSE"
return 1
fi
else
print_error "websocat not found - required for testing"
record_test_result "Event Submission Without PoW" "SKIP" "websocat not available"
return 0
fi
}
# Test event with valid PoW
test_event_with_pow() {
print_test_header "Event Submission With Valid PoW"
if ! command -v nak &> /dev/null; then
print_warning "nak command not found - skipping PoW validation tests"
record_test_result "Event Submission With Valid PoW" "SKIP" "nak not available"
return 0
fi
print_info "Generating event with PoW difficulty 8..."
# Generate event with PoW (difficulty 8 for reasonable test time) using direct private key
PRIVATE_KEY="91ba716fa9e7ea2fcbad360cf4f8e0d312f73984da63d90f524ad61a6a1e7dbe"
POW_EVENT_JSON=$(nak event --sec "$PRIVATE_KEY" -c "Test event with PoW difficulty 8" --pow 8 --ts $(date +%s))
if [ -z "$POW_EVENT_JSON" ]; then
print_error "Failed to generate PoW event"
record_test_result "Event Submission With Valid PoW" "FAIL" "PoW event generation failed"
return 1
fi
print_info "Generated PoW event:"
echo "$POW_EVENT_JSON" | jq '.'
echo ""
# Extract nonce info for verification
NONCE_TAG=$(echo "$POW_EVENT_JSON" | jq -r '.tags[] | select(.[0] == "nonce") | .[1]' 2>/dev/null || echo "")
TARGET_DIFF=$(echo "$POW_EVENT_JSON" | jq -r '.tags[] | select(.[0] == "nonce") | .[2]' 2>/dev/null || echo "")
if [ -n "$NONCE_TAG" ] && [ -n "$TARGET_DIFF" ]; then
print_info "PoW details: nonce=$NONCE_TAG, target_difficulty=$TARGET_DIFF"
fi
# Send event to relay via WebSocket using websocat
print_info "Sending PoW event to relay..."
# Create EVENT message in Nostr format
POW_EVENT_MESSAGE="[\"EVENT\",$POW_EVENT_JSON]"
# Send to relay and capture response
if command -v websocat &> /dev/null; then
RESPONSE=$(echo "$POW_EVENT_MESSAGE" | timeout 10s websocat "$RELAY_URL" 2>&1 || echo "Connection failed")
print_info "Relay response: $RESPONSE"
if [[ "$RESPONSE" == *"Connection failed"* ]]; then
print_error "✗ Failed to connect to relay"
record_test_result "Event Submission With Valid PoW" "FAIL" "Connection failed"
return 1
elif [[ "$RESPONSE" == *"true"* ]]; then
print_success "✓ Event with valid PoW accepted"
record_test_result "Event Submission With Valid PoW" "PASS" "PoW event accepted"
return 0
else
print_error "✗ Event with valid PoW rejected"
record_test_result "Event Submission With Valid PoW" "FAIL" "PoW event rejected: $RESPONSE"
return 1
fi
else
print_error "websocat not found - required for testing"
record_test_result "Event Submission With Valid PoW" "SKIP" "websocat not available"
return 0
fi
}
# Test relay configuration with environment variables
test_pow_configuration() {
print_test_header "PoW Configuration Via Environment Variables"
print_info "Testing different PoW configurations requires relay restart"
print_info "Current configuration from logs:"
if [ -f "relay.log" ]; then
grep "PoW Configuration:" relay.log | tail -1
else
print_warning "No relay.log found"
fi
# Test current configuration values
RELAY_INFO=$(curl -s -H "Accept: application/nostr+json" "$HTTP_URL/")
MIN_POW_DIFF=$(echo "$RELAY_INFO" | jq -r '.limitation.min_pow_difficulty')
print_info "Current min_pow_difficulty from NIP-11: $MIN_POW_DIFF"
# For now, just verify the configuration is readable
if [ "$MIN_POW_DIFF" != "null" ] && [ "$MIN_POW_DIFF" != "missing" ]; then
print_success "✓ PoW configuration is accessible via NIP-11"
record_test_result "PoW Configuration Via Environment Variables" "PASS" "min_pow_difficulty=$MIN_POW_DIFF"
return 0
else
print_error "✗ PoW configuration not accessible"
record_test_result "PoW Configuration Via Environment Variables" "FAIL" "Cannot read min_pow_difficulty"
return 1
fi
}
# Test NIP-13 reference event validation
test_nip13_reference_event() {
print_test_header "NIP-13 Reference Event Validation"
# This is the official NIP-13 reference event
NIP13_REF_EVENT='{"id":"000006d8c378af1779d2feebc7603a125d99eca0ccf1085959b307f64e5dd358","pubkey":"a48380f4cfcc1ad5378294fcac36439770f9c878dd880ffa94bb74ea54a6f243","created_at":1651794653,"kind":1,"tags":[["nonce","776797","20"]],"content":"It'\''s just me mining my own business","sig":"284622fc0a3f4f1303455d5175f7ba962a3300d136085b9566801bc2e0699de0c7e31e44c81fb40ad9049173742e904713c3594a1da0fc5d2382a25c11aba977"}'
print_info "Testing NIP-13 reference event from specification:"
echo "$NIP13_REF_EVENT" | jq '.'
echo ""
# Send reference event to relay via WebSocket using websocat
print_info "Sending NIP-13 reference event to relay..."
# Create EVENT message in Nostr format
REF_EVENT_MESSAGE="[\"EVENT\",$NIP13_REF_EVENT]"
# Send to relay and capture response
if command -v websocat &> /dev/null; then
RESPONSE=$(echo "$REF_EVENT_MESSAGE" | timeout 10s websocat "$RELAY_URL" 2>&1 || echo "Connection failed")
print_info "Relay response: $RESPONSE"
if [[ "$RESPONSE" == *"Connection failed"* ]] || [[ -z "$RESPONSE" ]]; then
print_error "✗ Failed to connect to relay or no response"
record_test_result "NIP-13 Reference Event Validation" "FAIL" "Connection failed or timeout"
return 1
elif [[ "$RESPONSE" == *"true"* ]]; then
print_success "✓ NIP-13 reference event accepted"
record_test_result "NIP-13 Reference Event Validation" "PASS" "Reference event accepted"
return 0
else
print_error "✗ NIP-13 reference event rejected"
record_test_result "NIP-13 Reference Event Validation" "FAIL" "Reference event rejected: $RESPONSE"
return 1
fi
else
print_error "websocat not found - required for testing"
record_test_result "NIP-13 Reference Event Validation" "SKIP" "websocat not available"
return 0
fi
}
# Print test summary
print_test_summary() {
echo ""
echo -e "${BOLD}=== TEST SUMMARY ===${RESET}"
echo "Total tests run: $TEST_COUNT"
echo -e "${GREEN}Passed: $PASSED_COUNT${RESET}"
echo -e "${RED}Failed: $FAILED_COUNT${RESET}"
if [ $FAILED_COUNT -gt 0 ]; then
echo ""
echo -e "${RED}${BOLD}Failed tests:${RESET}"
for result in "${TEST_RESULTS[@]}"; do
IFS='|' read -r name status details <<< "$result"
if [ "$status" = "FAIL" ]; then
echo -e " ${RED}$name${RESET}"
if [ -n "$details" ]; then
echo " $details"
fi
fi
done
fi
echo ""
if [ $FAILED_COUNT -eq 0 ]; then
echo -e "${GREEN}${BOLD}🎉 ALL TESTS PASSED!${RESET}"
echo -e "${GREEN}✅ NIP-13 PoW validation is working correctly in the relay${RESET}"
return 0
else
echo -e "${RED}${BOLD}❌ SOME TESTS FAILED${RESET}"
echo "Please review the output above and check relay logs for more details."
return 1
fi
}
# Main test execution
main() {
echo -e "${BOLD}=== NIP-13 Proof of Work Relay Test Suite ===${RESET}"
echo "Testing NIP-13 PoW validation in the C Nostr Relay"
echo "Relay URL: $RELAY_URL"
echo ""
# Check prerequisites
if ! command -v curl &> /dev/null; then
print_error "curl is required but not installed"
exit 1
fi
if ! command -v jq &> /dev/null; then
print_error "jq is required but not installed"
exit 1
fi
if ! command -v websocat &> /dev/null; then
print_warning "websocat not found - WebSocket tests will be skipped"
fi
# Run tests
check_relay_running
test_nip11_pow_support
test_event_without_pow
test_event_with_pow
test_pow_configuration
test_nip13_reference_event
# Print summary
print_test_summary
exit $?
}
# Run main function
main "$@"

539
tests/40_nip_test.sh Executable file
View File

@@ -0,0 +1,539 @@
#!/bin/bash
# NIP-40 Expiration Timestamp Test Suite for C Nostr Relay
# Tests expiration timestamp handling in the relay's event processing pipeline
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'
# Test configuration
RELAY_URL="ws://127.0.0.1:8888"
HTTP_URL="http://127.0.0.1:8888"
TEST_COUNT=0
PASSED_COUNT=0
FAILED_COUNT=0
# Test results tracking
declare -a TEST_RESULTS=()
print_info() {
echo -e "${BLUE}[INFO]${RESET} $1"
}
print_success() {
echo -e "${GREEN}${BOLD}[SUCCESS]${RESET} $1"
}
print_warning() {
echo -e "${YELLOW}[WARNING]${RESET} $1"
}
print_error() {
echo -e "${RED}${BOLD}[ERROR]${RESET} $1"
}
print_test_header() {
TEST_COUNT=$((TEST_COUNT + 1))
echo ""
echo -e "${BOLD}=== TEST $TEST_COUNT: $1 ===${RESET}"
}
record_test_result() {
local test_name="$1"
local result="$2"
local details="$3"
TEST_RESULTS+=("$test_name|$result|$details")
if [ "$result" = "PASS" ]; then
PASSED_COUNT=$((PASSED_COUNT + 1))
print_success "PASS: $test_name"
else
FAILED_COUNT=$((FAILED_COUNT + 1))
print_error "FAIL: $test_name"
if [ -n "$details" ]; then
echo " Details: $details"
fi
fi
}
# Check if relay is running
check_relay_running() {
print_info "Checking if relay is running..."
if ! curl -s -H "Accept: application/nostr+json" "$HTTP_URL/" >/dev/null 2>&1; then
print_error "Relay is not running or not accessible at $HTTP_URL"
print_info "Please start the relay with: ./make_and_restart_relay.sh"
exit 1
fi
print_success "Relay is running and accessible"
}
# Test NIP-11 relay information includes NIP-40
test_nip11_expiration_support() {
print_test_header "NIP-11 Expiration Support Advertisement"
print_info "Fetching relay information..."
RELAY_INFO=$(curl -s -H "Accept: application/nostr+json" "$HTTP_URL/")
echo "Relay Info Response:"
echo "$RELAY_INFO" | jq '.'
echo ""
# Check if NIP-40 is in supported_nips
if echo "$RELAY_INFO" | jq -e '.supported_nips | index(40)' >/dev/null 2>&1; then
print_success "✓ NIP-40 found in supported_nips array"
NIP40_SUPPORTED=true
else
print_error "✗ NIP-40 not found in supported_nips array"
NIP40_SUPPORTED=false
fi
if [ "$NIP40_SUPPORTED" = true ]; then
record_test_result "NIP-11 Expiration Support Advertisement" "PASS" "NIP-40 advertised in relay info"
return 0
else
record_test_result "NIP-11 Expiration Support Advertisement" "FAIL" "NIP-40 not advertised"
return 1
fi
}
# Helper function to create event with expiration tag
create_event_with_expiration() {
local content="$1"
local expiration_timestamp="$2"
local private_key="91ba716fa9e7ea2fcbad360cf4f8e0d312f73984da63d90f524ad61a6a1e7dbe"
if ! command -v nak &> /dev/null; then
echo ""
return 1
fi
# Create event with expiration tag
nak event --sec "$private_key" -c "$content" -t "expiration=$expiration_timestamp" --ts $(date +%s)
}
# Helper function to send event and check response
send_event_and_check() {
local event_json="$1"
local expected_result="$2" # "accept" or "reject"
local description="$3"
if [ -z "$event_json" ]; then
return 1
fi
# Create EVENT message
local event_message="[\"EVENT\",$event_json]"
# Send to relay
if command -v websocat &> /dev/null; then
local response=$(echo "$event_message" | timeout 5s websocat "$RELAY_URL" 2>&1 || echo "Connection failed")
print_info "Relay response: $response"
if [[ "$response" == *"Connection failed"* ]]; then
print_error "✗ Failed to connect to relay"
return 1
elif [[ "$expected_result" == "accept" && "$response" == *"true"* ]]; then
print_success "$description accepted as expected"
return 0
elif [[ "$expected_result" == "reject" && "$response" == *"false"* ]]; then
print_success "$description rejected as expected"
return 0
elif [[ "$expected_result" == "accept" && "$response" == *"false"* ]]; then
print_error "$description unexpectedly rejected: $response"
return 1
elif [[ "$expected_result" == "reject" && "$response" == *"true"* ]]; then
print_error "$description unexpectedly accepted: $response"
return 1
else
print_warning "? Unclear response for $description: $response"
return 1
fi
else
print_error "websocat not found - required for testing"
return 1
fi
}
# Test event without expiration tag
test_event_without_expiration() {
print_test_header "Event Submission Without Expiration Tag"
if ! command -v nak &> /dev/null; then
print_warning "nak command not found - skipping expiration tests"
record_test_result "Event Submission Without Expiration Tag" "SKIP" "nak not available"
return 0
fi
print_info "Creating event without expiration tag..."
local private_key="91ba716fa9e7ea2fcbad360cf4f8e0d312f73984da63d90f524ad61a6a1e7dbe"
local event_json=$(nak event --sec "$private_key" -c "Test event without expiration" --ts $(date +%s))
print_info "Generated event:"
echo "$event_json" | jq '.'
echo ""
if send_event_and_check "$event_json" "accept" "Event without expiration tag"; then
record_test_result "Event Submission Without Expiration Tag" "PASS" "Non-expiring event accepted"
return 0
else
record_test_result "Event Submission Without Expiration Tag" "FAIL" "Non-expiring event handling failed"
return 1
fi
}
# Test event with future expiration (should be accepted)
test_event_with_future_expiration() {
print_test_header "Event Submission With Future Expiration"
if ! command -v nak &> /dev/null; then
record_test_result "Event Submission With Future Expiration" "SKIP" "nak not available"
return 0
fi
print_info "Creating event with future expiration (1 hour from now)..."
local future_timestamp=$(($(date +%s) + 3600)) # 1 hour from now
local event_json=$(create_event_with_expiration "Test event expiring in 1 hour" "$future_timestamp")
if [ -z "$event_json" ]; then
record_test_result "Event Submission With Future Expiration" "FAIL" "Failed to create event"
return 1
fi
print_info "Generated event (expires at $future_timestamp):"
echo "$event_json" | jq '.'
echo ""
if send_event_and_check "$event_json" "accept" "Event with future expiration"; then
record_test_result "Event Submission With Future Expiration" "PASS" "Future-expiring event accepted"
return 0
else
record_test_result "Event Submission With Future Expiration" "FAIL" "Future-expiring event rejected"
return 1
fi
}
# Test event with past expiration (should be rejected in strict mode)
test_event_with_past_expiration() {
print_test_header "Event Submission With Past Expiration"
if ! command -v nak &> /dev/null; then
record_test_result "Event Submission With Past Expiration" "SKIP" "nak not available"
return 0
fi
print_info "Creating event with past expiration (1 hour ago)..."
local past_timestamp=$(($(date +%s) - 3600)) # 1 hour ago
local event_json=$(create_event_with_expiration "Test event expired 1 hour ago" "$past_timestamp")
if [ -z "$event_json" ]; then
record_test_result "Event Submission With Past Expiration" "FAIL" "Failed to create event"
return 1
fi
print_info "Generated event (expired at $past_timestamp):"
echo "$event_json" | jq '.'
echo ""
# In strict mode (default), this should be rejected
if send_event_and_check "$event_json" "reject" "Event with past expiration"; then
record_test_result "Event Submission With Past Expiration" "PASS" "Expired event correctly rejected in strict mode"
return 0
else
record_test_result "Event Submission With Past Expiration" "FAIL" "Expired event handling failed"
return 1
fi
}
# Test event with expiration within grace period
test_event_within_grace_period() {
print_test_header "Event Submission Within Grace Period"
if ! command -v nak &> /dev/null; then
record_test_result "Event Submission Within Grace Period" "SKIP" "nak not available"
return 0
fi
print_info "Creating event with expiration within grace period (2 minutes ago, grace period is 5 minutes)..."
local grace_timestamp=$(($(date +%s) - 120)) # 2 minutes ago (within 5 minute grace period)
local event_json=$(create_event_with_expiration "Test event within grace period" "$grace_timestamp")
if [ -z "$event_json" ]; then
record_test_result "Event Submission Within Grace Period" "FAIL" "Failed to create event"
return 1
fi
print_info "Generated event (expired at $grace_timestamp, within grace period):"
echo "$event_json" | jq '.'
echo ""
# Should be accepted due to grace period
if send_event_and_check "$event_json" "accept" "Event within grace period"; then
record_test_result "Event Submission Within Grace Period" "PASS" "Event within grace period accepted"
return 0
else
record_test_result "Event Submission Within Grace Period" "FAIL" "Grace period handling failed"
return 1
fi
}
# Test event filtering in subscriptions
test_expiration_filtering_in_subscriptions() {
print_test_header "Expiration Filtering in Subscriptions"
if ! command -v nak &> /dev/null || ! command -v websocat &> /dev/null; then
record_test_result "Expiration Filtering in Subscriptions" "SKIP" "Required tools not available"
return 0
fi
print_info "Setting up test events for subscription filtering..."
# First, create a few events with different expiration times
local private_key="91ba716fa9e7ea2fcbad360cf4f8e0d312f73984da63d90f524ad61a6a1e7dbe"
# Event 1: No expiration (should be returned)
local event1=$(nak event --sec "$private_key" -c "Event without expiration for filtering test" --ts $(date +%s))
# Event 2: Future expiration (should be returned)
local future_timestamp=$(($(date +%s) + 1800)) # 30 minutes from now
local event2=$(create_event_with_expiration "Event with future expiration for filtering test" "$future_timestamp")
# Event 3: Past expiration (should NOT be returned if filtering is enabled)
local past_timestamp=$(($(date +%s) - 3600)) # 1 hour ago
local event3=$(create_event_with_expiration "Event with past expiration for filtering test" "$past_timestamp")
print_info "Publishing test events..."
# Note: We expect event3 to be rejected on submission in strict mode,
# so we'll create it with a slightly more recent expiration that might get through
local recent_past=$(($(date +%s) - 600)) # 10 minutes ago (outside grace period)
local event3_recent=$(create_event_with_expiration "Recently expired event for filtering test" "$recent_past")
# Try to submit all events (some may be rejected)
echo "[\"EVENT\",$event1]" | timeout 3s websocat "$RELAY_URL" >/dev/null 2>&1 || true
echo "[\"EVENT\",$event2]" | timeout 3s websocat "$RELAY_URL" >/dev/null 2>&1 || true
echo "[\"EVENT\",$event3_recent]" | timeout 3s websocat "$RELAY_URL" >/dev/null 2>&1 || true
sleep 2 # Let events settle
print_info "Testing subscription filtering..."
# Create subscription for recent events
local req_message='["REQ","filter_test",{"kinds":[1],"limit":10}]'
local response=$(echo -e "$req_message\n[\"CLOSE\",\"filter_test\"]" | timeout 5s websocat "$RELAY_URL" 2>/dev/null || echo "")
print_info "Subscription response:"
echo "$response"
echo ""
# Count events that contain our test content
local no_exp_count=0
local future_exp_count=0
local past_exp_count=0
if echo "$response" | grep -q "Event without expiration for filtering test"; then
no_exp_count=1
print_success "✓ Event without expiration found in subscription results"
fi
if echo "$response" | grep -q "Event with future expiration for filtering test"; then
future_exp_count=1
print_success "✓ Event with future expiration found in subscription results"
fi
if echo "$response" | grep -q "Recently expired event for filtering test"; then
past_exp_count=1
print_warning "✗ Recently expired event found in subscription results (should be filtered)"
else
print_success "✓ Recently expired event properly filtered from subscription results"
fi
# Evaluate results
local expected_events=$((no_exp_count + future_exp_count))
if [ $expected_events -ge 1 ] && [ $past_exp_count -eq 0 ]; then
record_test_result "Expiration Filtering in Subscriptions" "PASS" "Expired events properly filtered from subscriptions"
return 0
else
record_test_result "Expiration Filtering in Subscriptions" "FAIL" "Expiration filtering not working properly in subscriptions"
return 1
fi
}
# Test malformed expiration tags
test_malformed_expiration_tags() {
print_test_header "Handling of Malformed Expiration Tags"
if ! command -v nak &> /dev/null; then
record_test_result "Handling of Malformed Expiration Tags" "SKIP" "nak not available"
return 0
fi
print_info "Testing events with malformed expiration tags..."
local private_key="91ba716fa9e7ea2fcbad360cf4f8e0d312f73984da63d90f524ad61a6a1e7dbe"
# Test 1: Non-numeric expiration value
local event1=$(nak event --sec "$private_key" -c "Event with non-numeric expiration" -t "expiration=not_a_number" --ts $(date +%s))
# Test 2: Empty expiration value
local event2=$(nak event --sec "$private_key" -c "Event with empty expiration" -t "expiration=" --ts $(date +%s))
print_info "Testing non-numeric expiration value..."
if send_event_and_check "$event1" "accept" "Event with non-numeric expiration (should be treated as no expiration)"; then
print_success "✓ Non-numeric expiration handled gracefully"
malformed_test1=true
else
malformed_test1=false
fi
print_info "Testing empty expiration value..."
if send_event_and_check "$event2" "accept" "Event with empty expiration (should be treated as no expiration)"; then
print_success "✓ Empty expiration handled gracefully"
malformed_test2=true
else
malformed_test2=false
fi
if [ "$malformed_test1" = true ] && [ "$malformed_test2" = true ]; then
record_test_result "Handling of Malformed Expiration Tags" "PASS" "Malformed expiration tags handled gracefully"
return 0
else
record_test_result "Handling of Malformed Expiration Tags" "FAIL" "Malformed expiration tag handling failed"
return 1
fi
}
# Test configuration via environment variables
test_expiration_configuration() {
print_test_header "Expiration Configuration Via Environment Variables"
print_info "Testing expiration configuration from relay logs..."
if [ -f "relay.log" ]; then
print_info "Current configuration from logs:"
grep "Expiration Configuration:" relay.log | tail -1 || print_warning "No expiration configuration found in logs"
else
print_warning "No relay.log found"
fi
# The relay should be running with default configuration
print_info "Default configuration should be:"
print_info " enabled=true"
print_info " strict_mode=true (rejects expired events on submission)"
print_info " filter_responses=true (filters expired events from responses)"
print_info " grace_period=300 seconds (5 minutes)"
# Test current behavior matches expected default configuration
print_info "Configuration test based on observed behavior:"
# Check if NIP-40 is advertised (indicates enabled=true)
if curl -s -H "Accept: application/nostr+json" "$HTTP_URL/" | jq -e '.supported_nips | index(40)' >/dev/null 2>&1; then
print_success "✓ NIP-40 support advertised (enabled=true)"
config_test=true
else
print_error "✗ NIP-40 not advertised (may be disabled)"
config_test=false
fi
if [ "$config_test" = true ]; then
record_test_result "Expiration Configuration Via Environment Variables" "PASS" "Expiration configuration is accessible and working"
return 0
else
record_test_result "Expiration Configuration Via Environment Variables" "FAIL" "Expiration configuration issues detected"
return 1
fi
}
# Print test summary
print_test_summary() {
echo ""
echo -e "${BOLD}=== TEST SUMMARY ===${RESET}"
echo "Total tests run: $TEST_COUNT"
echo -e "${GREEN}Passed: $PASSED_COUNT${RESET}"
echo -e "${RED}Failed: $FAILED_COUNT${RESET}"
if [ $FAILED_COUNT -gt 0 ]; then
echo ""
echo -e "${RED}${BOLD}Failed tests:${RESET}"
for result in "${TEST_RESULTS[@]}"; do
IFS='|' read -r name status details <<< "$result"
if [ "$status" = "FAIL" ]; then
echo -e " ${RED}$name${RESET}"
if [ -n "$details" ]; then
echo " $details"
fi
fi
done
fi
echo ""
if [ $FAILED_COUNT -eq 0 ]; then
echo -e "${GREEN}${BOLD}🎉 ALL TESTS PASSED!${RESET}"
echo -e "${GREEN}✅ NIP-40 Expiration Timestamp support is working correctly in the relay${RESET}"
return 0
else
echo -e "${RED}${BOLD}❌ SOME TESTS FAILED${RESET}"
echo "Please review the output above and check relay logs for more details."
return 1
fi
}
# Main test execution
main() {
echo -e "${BOLD}=== NIP-40 Expiration Timestamp Relay Test Suite ===${RESET}"
echo "Testing NIP-40 Expiration Timestamp support in the C Nostr Relay"
echo "Relay URL: $RELAY_URL"
echo ""
# Check prerequisites
if ! command -v curl &> /dev/null; then
print_error "curl is required but not installed"
exit 1
fi
if ! command -v jq &> /dev/null; then
print_error "jq is required but not installed"
exit 1
fi
if ! command -v websocat &> /dev/null; then
print_warning "websocat not found - WebSocket tests will be skipped"
fi
if ! command -v nak &> /dev/null; then
print_warning "nak not found - Event generation tests will be skipped"
print_info "Install with: go install github.com/fiatjaf/nak@latest"
fi
# Run tests
check_relay_running
test_nip11_expiration_support
test_event_without_expiration
test_event_with_future_expiration
test_event_with_past_expiration
test_event_within_grace_period
test_expiration_filtering_in_subscriptions
test_malformed_expiration_tags
test_expiration_configuration
# Print summary
print_test_summary
exit $?
}
# Run main function
main "$@"