Compare commits

..

9 Commits

Author SHA1 Message Date
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
Your Name
2d93c2f819 v0.0.4 - nip09 implemented 2025-09-05 10:59:10 -04:00
Your Name
ce7f7ad11b Most of the way through nip01 2025-09-05 09:49:48 -04:00
Your Name
662feab881 Basic relay functionality completed 2025-09-04 07:10:13 -04:00
22 changed files with 6009 additions and 273 deletions

2
.gitignore vendored
View File

@@ -1 +1,3 @@
nostr_core_lib/
nips/
build/

106
Makefile
View File

@@ -5,27 +5,108 @@ 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
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
# Build the relay
$(TARGET): $(MAIN_SRC) $(NOSTR_CORE_LIB)
@echo "Compiling C-Relay..."
$(TARGET): $(BUILD_DIR) $(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) $(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) $(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 +119,7 @@ init-db:
# Clean build artifacts
clean:
rm -f $(TARGET)
rm -rf $(BUILD_DIR)
@echo "Clean complete"
# Clean everything including nostr_core_lib
@@ -56,17 +137,26 @@ help:
@echo "C-Relay Build System"
@echo ""
@echo "Targets:"
@echo " all Build the relay (default)"
@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 # 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"
.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

View File

@@ -1,4 +1,22 @@
A nostr relay in C with sqlite on the back end.
<!--
NOTE FOR ASSISTANTS: When updating the NIPs checklist below, ONLY change [ ] to [x] to mark as complete.
Do NOT modify the formatting, add emojis, or change the text. Keep the simple format consistent.
-->
### [NIPs](https://github.com/nostr-protocol/nips)
- [x] NIP-01: Basic protocol flow implementation
- [x] NIP-09: Event deletion
- [x] NIP-11: Relay information document
- [x] NIP-13: Proof of Work
- [x] NIP-15: End of Stored Events Notice
- [x] NIP-20: Command Results
- [x] NIP-33: Parameterized Replaceable Events
- [x] NIP-40: Expiration Timestamp
- [ ] NIP-42: Authentication of clients to relays
- [ ] NIP-45: Counting results.
- [ ] NIP-50: Keywords filter.
- [ ] NIP-70: Protected Events

387
admin_spec.md Normal file
View File

@@ -0,0 +1,387 @@
# Ginxsom Admin System - Comprehensive Specification
## Overview
The Ginxsom admin system provides both programmatic (API-based) and interactive (web-based) administration capabilities for the Ginxsom Blossom server. The system is designed around Nostr-based authentication and supports multiple administration workflows including first-run setup, ongoing configuration management, and operational monitoring.
## Architecture Components
### 1. Configuration System
- **File-based configuration**: Signed Nostr events stored as JSON files following XDG Base Directory specification
- **Database configuration**: Key-value pairs stored in SQLite for runtime configuration
- **Interactive setup**: Command-line wizard for initial server configuration
- **Manual setup**: Scripts for generating signed configuration events
### 2. Authentication & Authorization
- **Nostr-based auth**: All admin operations require valid Nostr event signatures
- **Admin pubkey verification**: Only configured admin public keys can perform admin operations
- **Event validation**: Full cryptographic verification of Nostr events including structure, signature, and expiration
- **Method-specific authorization**: Different event types for different operations (upload, admin, delete, etc.)
### 3. API System
- **RESTful endpoints**: `/api/*` routes for programmatic administration
- **Command-line testing**: Complete test suite using `nak` and `curl`
- **JSON responses**: Structured data for all admin operations
- **CORS support**: Cross-origin requests for web admin interface
### 4. Web Interface (Future)
- **Single-page application**: Self-contained HTML file with inline CSS/JS
- **Real-time monitoring**: Statistics and system health dashboards
- **Configuration management**: GUI for server settings
- **File management**: Browse and manage uploaded blobs
## Configuration System Architecture
### File-based Configuration (Priority 1)
**Location**: Follows XDG Base Directory Specification
- `$XDG_CONFIG_HOME/ginxsom/ginxsom_config_event.json`
- Falls back to `$HOME/.config/ginxsom/ginxsom_config_event.json`
**Format**: Signed Nostr event containing server configuration
```json
{
"kind": 33333,
"created_at": 1704067200,
"tags": [
["server_privkey", "server_private_key_hex"],
["cdn_origin", "https://cdn.example.com"],
["max_file_size", "104857600"],
["nip94_enabled", "true"]
],
"content": "Ginxsom server configuration",
"pubkey": "admin_public_key_hex",
"id": "event_id_hash",
"sig": "event_signature"
}
```
**Loading Process**:
1. Check for file-based config at XDG location
2. Validate Nostr event structure and signature
3. Extract configuration from event tags
4. Apply settings to server (database storage)
5. Fall back to database-only config if file missing/invalid
### Database Configuration (Priority 2)
**Table**: `server_config`
```sql
CREATE TABLE server_config (
key TEXT PRIMARY KEY,
value TEXT NOT NULL,
description TEXT,
created_at TIMESTAMP DEFAULT CURRENT_TIMESTAMP,
updated_at TIMESTAMP DEFAULT CURRENT_TIMESTAMP
);
```
**Key Configuration Items**:
- `admin_pubkey`: Authorized admin public key
- `admin_enabled`: Enable/disable admin interface
- `cdn_origin`: Base URL for blob access
- `max_file_size`: Maximum upload size in bytes
- `nip94_enabled`: Enable NIP-94 metadata emission
- `auth_rules_enabled`: Enable authentication rules system
### Setup Workflows
#### Interactive Setup (Command Line)
```bash
# First-run detection
if [[ ! -f "$XDG_CONFIG_HOME/ginxsom/ginxsom_config_event.json" ]]; then
echo "=== Ginxsom First-Time Setup Required ==="
echo "1. Run interactive setup wizard"
echo "2. Exit and create config manually"
read -p "Choice (1/2): " choice
if [[ "$choice" == "1" ]]; then
./scripts/setup.sh
else
echo "Manual setup: Run ./scripts/generate_config.sh"
exit 1
fi
fi
```
#### Manual Setup (Script-based)
```bash
# Generate configuration event
./scripts/generate_config.sh --admin-key <admin_pubkey> \
--server-key <server_privkey> \
--cdn-origin "https://cdn.example.com" \
--output "$XDG_CONFIG_HOME/ginxsom/ginxsom_config_event.json"
```
### C Implementation Functions
#### Configuration Loading
```c
// Get XDG-compliant config file path
int get_config_file_path(char* path, size_t path_size);
// Load and validate config event from file
int load_server_config(const char* config_path);
// Extract config from validated event and apply to server
int apply_config_from_event(cJSON* event);
// Interactive setup runner for first-run
int run_interactive_setup(const char* config_path);
```
#### Security Features
- Server private key stored only in memory (never in database)
- Config file must be signed Nostr event
- Full cryptographic validation of config events
- Admin pubkey verification for all operations
## Admin API Specification
### Authentication Model
All admin API endpoints (except `/api/health`) require Nostr authentication:
**Authorization Header Format**:
```
Authorization: Nostr <base64-encoded-event>
```
**Required Event Structure**:
```json
{
"kind": 24242,
"created_at": 1704067200,
"tags": [
["t", "GET"],
["expiration", "1704070800"]
],
"content": "admin_request",
"pubkey": "admin_public_key",
"id": "event_id",
"sig": "event_signature"
}
```
### API Endpoints
#### GET /api/health
**Purpose**: System health check (no authentication required)
**Response**:
```json
{
"status": "success",
"data": {
"database": "connected",
"blob_directory": "accessible",
"server_time": 1704067200,
"uptime": 3600,
"disk_usage": {
"total_bytes": 1073741824,
"used_bytes": 536870912,
"available_bytes": 536870912,
"usage_percent": 50.0
}
}
}
```
#### GET /api/stats
**Purpose**: Server statistics and metrics
**Authentication**: Required (admin pubkey)
**Response**:
```json
{
"status": "success",
"data": {
"total_files": 1234,
"total_bytes": 104857600,
"total_size_mb": 100.0,
"unique_uploaders": 56,
"first_upload": 1693929600,
"last_upload": 1704067200,
"avg_file_size": 85049,
"file_types": {
"image/png": 45,
"image/jpeg": 32,
"application/pdf": 12,
"other": 8
}
}
}
```
#### GET /api/config
**Purpose**: Retrieve current server configuration
**Authentication**: Required (admin pubkey)
**Response**:
```json
{
"status": "success",
"data": {
"cdn_origin": "http://localhost:9001",
"max_file_size": "104857600",
"nip94_enabled": "true",
"auth_rules_enabled": "false",
"auth_cache_ttl": "300"
}
}
```
#### PUT /api/config
**Purpose**: Update server configuration
**Authentication**: Required (admin pubkey)
**Request Body**:
```json
{
"max_file_size": "209715200",
"nip94_enabled": "true",
"cdn_origin": "https://cdn.example.com"
}
```
**Response**:
```json
{
"status": "success",
"message": "Configuration updated successfully",
"updated_keys": ["max_file_size", "cdn_origin"]
}
```
#### GET /api/files
**Purpose**: List recent files with pagination
**Authentication**: Required (admin pubkey)
**Parameters**:
- `limit` (default: 50): Number of files to return
- `offset` (default: 0): Pagination offset
**Response**:
```json
{
"status": "success",
"data": {
"files": [
{
"sha256": "b1674191a88ec5cdd733e4240a81803105dc412d6c6708d53ab94fc248f4f553",
"size": 184292,
"type": "application/pdf",
"uploaded_at": 1725105921,
"uploader_pubkey": "79be667ef9dcbbac55a06295ce870b07029bfcdb2dce28d959f2815b16f81798",
"filename": "document.pdf",
"url": "http://localhost:9001/b1674191a88ec5cdd733e4240a81803105dc412d6c6708d53ab94fc248f4f553.pdf"
}
],
"total": 1234,
"limit": 50,
"offset": 0
}
}
```
## Implementation Status
### ✅ Completed Components
1. **Database-based configuration loading** - Implemented in main.c
2. **Admin API authentication system** - Implemented in admin_api.c
3. **Nostr event validation** - Full cryptographic verification
4. **Admin pubkey verification** - Database-backed authorization
5. **Basic API endpoints** - Health, stats, config, files
### ✅ Recently Completed Components
1. **File-based configuration system** - Fully implemented in main.c with XDG compliance
2. **Interactive setup wizard** - Complete shell script with guided setup process (`scripts/setup.sh`)
3. **Manual config generation** - Full-featured command-line config generator (`scripts/generate_config.sh`)
4. **Testing infrastructure** - Comprehensive admin API test suite (`scripts/test_admin.sh`)
5. **Documentation system** - Complete setup and usage documentation (`scripts/README.md`)
### 📋 Planned Components
1. **Web admin interface** - Single-page HTML application
2. **Enhanced monitoring** - Real-time statistics dashboard
3. **Bulk operations** - Multi-file management APIs
4. **Configuration validation** - Advanced config checking
5. **Audit logging** - Admin action tracking
## Setup Instructions
### 1. Enable Admin Interface
```bash
# Configure admin pubkey and enable interface
sqlite3 db/ginxsom.db << EOF
INSERT OR REPLACE INTO server_config (key, value, description) VALUES
('admin_pubkey', 'your_admin_public_key_here', 'Authorized admin public key'),
('admin_enabled', 'true', 'Enable admin interface');
EOF
```
### 2. Test API Access
```bash
# Generate admin authentication event
ADMIN_PRIVKEY="your_admin_private_key"
EVENT=$(nak event -k 24242 -c "admin_request" \
--tag t="GET" \
--tag expiration="$(date -d '+1 hour' +%s)" \
--sec "$ADMIN_PRIVKEY")
# Test admin API
AUTH_HEADER="Nostr $(echo "$EVENT" | base64 -w 0)"
curl -H "Authorization: $AUTH_HEADER" http://localhost:9001/api/stats
```
### 3. Configure File-based Setup (Future)
```bash
# Create XDG config directory
mkdir -p "$XDG_CONFIG_HOME/ginxsom"
# Generate signed config event
./scripts/generate_config.sh \
--admin-key "your_admin_pubkey" \
--server-key "generated_server_privkey" \
--output "$XDG_CONFIG_HOME/ginxsom/ginxsom_config_event.json"
```
## Security Considerations
### Authentication Security
- **Event expiration**: All admin events must include expiration timestamps
- **Signature validation**: Full secp256k1 cryptographic verification
- **Replay protection**: Event IDs tracked to prevent reuse
- **Admin key rotation**: Support for updating admin pubkeys
### Configuration Security
- **File permissions**: Config files should be readable only by server user
- **Private key handling**: Server private keys never stored in database
- **Config validation**: All configuration changes validated before application
- **Backup verification**: Config events cryptographically verifiable
### Operational Security
- **Access logging**: All admin operations logged with timestamps
- **Rate limiting**: API endpoints protected against abuse
- **Input validation**: All user input sanitized and validated
- **Database security**: Prepared statements prevent SQL injection
## Future Enhancements
### 1. Web Admin Interface
- Self-contained HTML file with inline CSS/JavaScript
- Real-time monitoring dashboards
- Visual configuration management
- File upload/management interface
### 2. Advanced Monitoring
- Performance metrics collection
- Alert system for critical events
- Historical data trending
- Resource usage tracking
### 3. Multi-admin Support
- Multiple authorized admin pubkeys
- Role-based permissions (read-only vs full admin)
- Admin action audit trails
- Delegation capabilities
### 4. Integration Features
- Nostr relay integration for admin events
- Webhook notifications for admin actions
- External authentication providers
- API key management for programmatic access
This specification represents the current understanding and planned development of the Ginxsom admin system, focusing on security, usability, and maintainability.

391
build_and_push.sh Executable file
View File

@@ -0,0 +1,391 @@
#!/bin/bash
set -e
# Colors for output
RED='\033[0;31m'
GREEN='\033[0;32m'
YELLOW='\033[1;33m'
BLUE='\033[0;34m'
NC='\033[0m'
print_status() { echo -e "${BLUE}[INFO]${NC} $1"; }
print_success() { echo -e "${GREEN}[SUCCESS]${NC} $1"; }
print_warning() { echo -e "${YELLOW}[WARNING]${NC} $1"; }
print_error() { echo -e "${RED}[ERROR]${NC} $1"; }
# Global variables
COMMIT_MESSAGE=""
RELEASE_MODE=false
# Parse command line arguments
while [[ $# -gt 0 ]]; do
case $1 in
-r|--release)
RELEASE_MODE=true
shift
;;
-h|--help)
show_usage
exit 0
;;
*)
# First non-flag argument is the commit message
if [[ -z "$COMMIT_MESSAGE" ]]; then
COMMIT_MESSAGE="$1"
fi
shift
;;
esac
done
show_usage() {
echo "C-Relay Build and Push Script"
echo ""
echo "Usage:"
echo " $0 \"commit message\" - Default: compile, increment patch, commit & push"
echo " $0 -r \"commit message\" - Release: compile x86+arm64, increment minor, create release"
echo ""
echo "Examples:"
echo " $0 \"Fixed event validation bug\""
echo " $0 --release \"Major release with new features\""
echo ""
echo "Default Mode (patch increment):"
echo " - Compile C-Relay"
echo " - Increment patch version (v1.2.3 → v1.2.4)"
echo " - Git add, commit with message, and push"
echo ""
echo "Release Mode (-r flag):"
echo " - Compile C-Relay for x86_64 and arm64"
echo " - Increment minor version, zero patch (v1.2.3 → v1.3.0)"
echo " - Git add, commit, push, and create Gitea release"
echo ""
echo "Requirements for Release Mode:"
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"
}
# Validate inputs
if [[ -z "$COMMIT_MESSAGE" ]]; then
print_error "Commit message is required"
echo ""
show_usage
exit 1
fi
# Check if we're in a git repository
check_git_repo() {
if ! git rev-parse --git-dir > /dev/null 2>&1; then
print_error "Not in a git repository"
exit 1
fi
}
# Function to get current version and increment appropriately
increment_version() {
local increment_type="$1" # "patch" or "minor"
print_status "Getting current version..."
# Get the highest version tag (not chronologically latest)
LATEST_TAG=$(git tag -l 'v*.*.*' | sort -V | tail -n 1 || echo "")
if [[ -z "$LATEST_TAG" ]]; then
LATEST_TAG="v0.0.0"
print_warning "No version tags found, starting from $LATEST_TAG"
fi
# Extract version components (remove 'v' prefix)
VERSION=${LATEST_TAG#v}
# Parse major.minor.patch using regex
if [[ $VERSION =~ ^([0-9]+)\.([0-9]+)\.([0-9]+)$ ]]; then
MAJOR=${BASH_REMATCH[1]}
MINOR=${BASH_REMATCH[2]}
PATCH=${BASH_REMATCH[3]}
else
print_error "Invalid version format in tag: $LATEST_TAG"
print_error "Expected format: v0.1.0"
exit 1
fi
# Increment version based on type
if [[ "$increment_type" == "minor" ]]; then
# Minor release: increment minor, zero patch
NEW_MINOR=$((MINOR + 1))
NEW_PATCH=0
NEW_VERSION="v${MAJOR}.${NEW_MINOR}.${NEW_PATCH}"
print_status "Release mode: incrementing minor version"
else
# Default: increment patch
NEW_PATCH=$((PATCH + 1))
NEW_VERSION="v${MAJOR}.${MINOR}.${NEW_PATCH}"
print_status "Default mode: incrementing patch version"
fi
print_status "Current version: $LATEST_TAG"
print_status "New version: $NEW_VERSION"
# Export for use in other functions
export NEW_VERSION
}
# Function to compile the C-Relay project
compile_project() {
print_status "Compiling C-Relay..."
# Clean previous build
if make clean > /dev/null 2>&1; then
print_success "Cleaned previous build"
else
print_warning "Clean failed or no Makefile found"
fi
# Compile the project
if make > /dev/null 2>&1; then
print_success "C-Relay compiled successfully"
else
print_error "Compilation failed"
exit 1
fi
}
# Function to build release binaries
build_release_binaries() {
print_status "Building release binaries..."
# Build x86_64 version
print_status "Building x86_64 version..."
make clean > /dev/null 2>&1
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"
exit 1
fi
else
print_error "x86_64 build failed"
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_warning "ARM64 binary not found after compilation"
fi
else
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
make clean > /dev/null 2>&1
make > /dev/null 2>&1
}
# Function to commit and push changes
git_commit_and_push() {
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
# Create new git tag
if git tag "$NEW_VERSION" > /dev/null 2>&1; then
print_success "Created tag: $NEW_VERSION"
else
print_warning "Tag $NEW_VERSION already exists"
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
if git push --tags > /dev/null 2>&1; then
print_success "Pushed tags"
else
print_warning "Failed to push tags"
fi
}
# Function to create Gitea release
create_gitea_release() {
print_status "Creating Gitea release..."
# Check for Gitea token
if [[ ! -f "$HOME/.gitea_token" ]]; then
print_warning "No ~/.gitea_token found. Skipping release creation."
print_warning "Create ~/.gitea_token with your Gitea access token to enable releases."
return 0
fi
local token=$(cat "$HOME/.gitea_token" | tr -d '\n\r')
local api_url="https://git.laantungir.net/api/v1/repos/laantungir/c-relay"
# Create release
print_status "Creating release $NEW_VERSION..."
local response=$(curl -s -X POST "$api_url/releases" \
-H "Authorization: token $token" \
-H "Content-Type: application/json" \
-d "{\"tag_name\": \"$NEW_VERSION\", \"name\": \"$NEW_VERSION\", \"body\": \"$COMMIT_MESSAGE\"}")
if echo "$response" | grep -q '"id"'; then
print_success "Created release $NEW_VERSION"
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_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
}
# Function to upload release binaries
upload_release_binaries() {
local api_url="$1"
local token="$2"
# 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..."
if curl -s -X POST "$api_url/releases/$release_id/assets" \
-H "Authorization: token $token" \
-F "attachment=@c-relay-x86_64;filename=c-relay-${NEW_VERSION}-linux-x86_64" > /dev/null; then
print_success "Uploaded x86_64 binary"
else
print_warning "Failed to upload x86_64 binary"
fi
fi
# Upload ARM64 binary
if [[ -f "c-relay-arm64" ]]; then
print_status "Uploading ARM64 binary..."
if curl -s -X POST "$api_url/releases/$release_id/assets" \
-H "Authorization: token $token" \
-F "attachment=@c-relay-arm64;filename=c-relay-${NEW_VERSION}-linux-arm64" > /dev/null; then
print_success "Uploaded ARM64 binary"
else
print_warning "Failed to upload ARM64 binary"
fi
fi
}
# Function to clean up release binaries
cleanup_release_binaries() {
if [[ -f "c-relay-x86_64" ]]; then
rm -f c-relay-x86_64
print_status "Cleaned up x86_64 binary"
fi
if [[ -f "c-relay-arm64" ]]; then
rm -f c-relay-arm64
print_status "Cleaned up ARM64 binary"
fi
}
# Main execution
main() {
print_status "C-Relay Build and Push Script"
# Check prerequisites
check_git_repo
if [[ "$RELEASE_MODE" == true ]]; then
print_status "=== RELEASE MODE ==="
# Increment minor version for releases
increment_version "minor"
# Compile project first
compile_project
# Build release binaries
build_release_binaries
# Commit and push
git_commit_and_push
# Create Gitea release with binaries
create_gitea_release
# Cleanup
cleanup_release_binaries
print_success "Release $NEW_VERSION completed successfully!"
print_status "Binaries uploaded to Gitea release"
else
print_status "=== DEFAULT MODE ==="
# Increment patch version for regular commits
increment_version "patch"
# Compile project
compile_project
# Commit and push
git_commit_and_push
print_success "Build and push completed successfully!"
print_status "Version $NEW_VERSION pushed to repository"
fi
}
# Execute main function
main

Binary file not shown.

Binary file not shown.

Binary file not shown.

View File

@@ -115,7 +115,7 @@ verify_database() {
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 ('event', 'tag');")
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"
else

View File

@@ -1,66 +1,181 @@
-- C Nostr Relay Database Schema
-- Simplified schema with just event and tag tables
-- SQLite database for storing Nostr events
-- SQLite schema for storing Nostr events with JSON tags support
-- ============================================================================
-- DATABASE SETTINGS
-- ============================================================================
-- Schema version tracking
PRAGMA user_version = 2;
PRAGMA encoding = "UTF-8";
PRAGMA journal_mode = WAL;
PRAGMA auto_vacuum = FULL;
PRAGMA synchronous = NORMAL;
-- Enable foreign key support
PRAGMA foreign_keys = ON;
-- ============================================================================
-- EVENT TABLE
-- ============================================================================
-- Optimize for performance
PRAGMA journal_mode = WAL;
PRAGMA synchronous = NORMAL;
PRAGMA cache_size = 10000;
-- Main event table - stores all Nostr events
CREATE TABLE IF NOT EXISTS event (
-- Core events table with hybrid single-table design
CREATE TABLE events (
id TEXT PRIMARY KEY, -- Nostr event ID (hex string)
pubkey TEXT NOT NULL, -- Public key of event author (hex string)
created_at INTEGER NOT NULL, -- Event creation timestamp (Unix timestamp)
kind INTEGER NOT NULL, -- Event kind (0-65535)
event_type TEXT NOT NULL CHECK (event_type IN ('regular', 'replaceable', 'ephemeral', 'addressable')),
content TEXT NOT NULL, -- Event content (text content only)
sig TEXT NOT NULL -- Event signature (hex string)
sig TEXT NOT NULL, -- Event signature (hex string)
tags JSON NOT NULL DEFAULT '[]', -- Event tags as JSON array
first_seen INTEGER NOT NULL DEFAULT (strftime('%s', 'now')) -- When relay received event
);
-- Event indexes for performance
CREATE INDEX IF NOT EXISTS event_pubkey_index ON event(pubkey);
CREATE INDEX IF NOT EXISTS event_created_at_index ON event(created_at);
CREATE INDEX IF NOT EXISTS event_kind_index ON event(kind);
CREATE INDEX IF NOT EXISTS event_pubkey_created_at_index ON event(pubkey, created_at);
CREATE INDEX IF NOT EXISTS event_kind_created_at_index ON event(kind, created_at);
-- Core performance indexes
CREATE INDEX idx_events_pubkey ON events(pubkey);
CREATE INDEX idx_events_kind ON events(kind);
CREATE INDEX idx_events_created_at ON events(created_at DESC);
CREATE INDEX idx_events_event_type ON events(event_type);
-- ============================================================================
-- TAG TABLE
-- ============================================================================
-- Composite indexes for common query patterns
CREATE INDEX idx_events_kind_created_at ON events(kind, created_at DESC);
CREATE INDEX idx_events_pubkey_created_at ON events(pubkey, created_at DESC);
CREATE INDEX idx_events_pubkey_kind ON events(pubkey, kind);
-- Tag table for storing event tags
CREATE TABLE IF NOT EXISTS tag (
id TEXT NOT NULL, -- Nostr event ID (references event.id)
name TEXT NOT NULL, -- Tag name (e.g., "e", "p", "d")
value TEXT NOT NULL, -- Tag value
parameters TEXT, -- Additional tag parameters (JSON string)
FOREIGN KEY(id) REFERENCES event(id) ON UPDATE CASCADE ON DELETE CASCADE
-- Schema information table
CREATE TABLE schema_info (
key TEXT PRIMARY KEY,
value TEXT NOT NULL,
updated_at INTEGER NOT NULL DEFAULT (strftime('%s', 'now'))
);
-- Tag indexes for performance
CREATE INDEX IF NOT EXISTS tag_id_index ON tag(id);
CREATE INDEX IF NOT EXISTS tag_name_index ON tag(name);
CREATE INDEX IF NOT EXISTS tag_name_value_index ON tag(name, value);
CREATE INDEX IF NOT EXISTS tag_id_name_index ON tag(id, name);
-- Insert schema metadata
INSERT INTO schema_info (key, value) VALUES
('version', '2'),
('description', 'Hybrid single-table Nostr relay schema with JSON tags'),
('created_at', strftime('%s', 'now'));
-- ============================================================================
-- PERFORMANCE OPTIMIZATIONS
-- ============================================================================
-- Helper views for common queries
CREATE VIEW recent_events AS
SELECT id, pubkey, created_at, kind, event_type, content
FROM events
WHERE event_type != 'ephemeral'
ORDER BY created_at DESC
LIMIT 1000;
-- Enable query planner optimizations
PRAGMA optimize;
CREATE VIEW event_stats AS
SELECT
event_type,
COUNT(*) as count,
AVG(length(content)) as avg_content_length,
MIN(created_at) as earliest,
MAX(created_at) as latest
FROM events
GROUP BY event_type;
-- Set recommended pragmas for performance
PRAGMA main.synchronous = NORMAL;
PRAGMA foreign_keys = ON;
PRAGMA temp_store = 2; -- use memory for temp tables
PRAGMA main.cache_size = 10000; -- 40MB cache per connection
-- Optimization: Trigger for automatic cleanup of ephemeral events older than 1 hour
CREATE TRIGGER cleanup_ephemeral_events
AFTER INSERT ON events
WHEN NEW.event_type = 'ephemeral'
BEGIN
DELETE FROM events
WHERE event_type = 'ephemeral'
AND first_seen < (strftime('%s', 'now') - 3600);
END;
-- Replaceable event handling trigger
CREATE TRIGGER handle_replaceable_events
AFTER INSERT ON events
WHEN NEW.event_type = 'replaceable'
BEGIN
DELETE FROM events
WHERE pubkey = NEW.pubkey
AND kind = NEW.kind
AND event_type = 'replaceable'
AND id != NEW.id;
END;
-- Persistent Subscriptions Logging Tables (Phase 2)
-- Optional database logging for subscription analytics and debugging
-- Subscription events log
CREATE TABLE subscription_events (
id INTEGER PRIMARY KEY AUTOINCREMENT,
subscription_id TEXT NOT NULL, -- Subscription ID from client
client_ip TEXT NOT NULL, -- Client IP address
event_type TEXT NOT NULL CHECK (event_type IN ('created', 'closed', 'expired', 'disconnected')),
filter_json TEXT, -- JSON representation of filters (for created events)
events_sent INTEGER DEFAULT 0, -- Number of events sent to this subscription
created_at INTEGER NOT NULL DEFAULT (strftime('%s', 'now')),
ended_at INTEGER, -- When subscription ended (for closed/expired/disconnected)
duration INTEGER -- Computed: ended_at - created_at
);
-- Subscription metrics summary
CREATE TABLE subscription_metrics (
id INTEGER PRIMARY KEY AUTOINCREMENT,
date TEXT NOT NULL, -- Date (YYYY-MM-DD)
total_created INTEGER DEFAULT 0, -- Total subscriptions created
total_closed INTEGER DEFAULT 0, -- Total subscriptions closed
total_events_broadcast INTEGER DEFAULT 0, -- Total events broadcast
avg_duration REAL DEFAULT 0, -- Average subscription duration
peak_concurrent INTEGER DEFAULT 0, -- Peak concurrent subscriptions
updated_at INTEGER NOT NULL DEFAULT (strftime('%s', 'now')),
UNIQUE(date)
);
-- Event broadcasting log (optional, for detailed analytics)
CREATE TABLE event_broadcasts (
id INTEGER PRIMARY KEY AUTOINCREMENT,
event_id TEXT NOT NULL, -- Event ID that was broadcast
subscription_id TEXT NOT NULL, -- Subscription that received it
client_ip TEXT NOT NULL, -- Client IP
broadcast_at INTEGER NOT NULL DEFAULT (strftime('%s', 'now')),
FOREIGN KEY (event_id) REFERENCES events(id)
);
-- Indexes for subscription logging performance
CREATE INDEX idx_subscription_events_id ON subscription_events(subscription_id);
CREATE INDEX idx_subscription_events_type ON subscription_events(event_type);
CREATE INDEX idx_subscription_events_created ON subscription_events(created_at DESC);
CREATE INDEX idx_subscription_events_client ON subscription_events(client_ip);
CREATE INDEX idx_subscription_metrics_date ON subscription_metrics(date DESC);
CREATE INDEX idx_event_broadcasts_event ON event_broadcasts(event_id);
CREATE INDEX idx_event_broadcasts_sub ON event_broadcasts(subscription_id);
CREATE INDEX idx_event_broadcasts_time ON event_broadcasts(broadcast_at DESC);
-- Trigger to update subscription duration when ended
CREATE TRIGGER update_subscription_duration
AFTER UPDATE OF ended_at ON subscription_events
WHEN NEW.ended_at IS NOT NULL AND OLD.ended_at IS NULL
BEGIN
UPDATE subscription_events
SET duration = NEW.ended_at - NEW.created_at
WHERE id = NEW.id;
END;
-- View for subscription analytics
CREATE VIEW subscription_analytics AS
SELECT
date(created_at, 'unixepoch') as date,
COUNT(*) as subscriptions_created,
COUNT(CASE WHEN ended_at IS NOT NULL THEN 1 END) as subscriptions_ended,
AVG(CASE WHEN duration IS NOT NULL THEN duration END) as avg_duration_seconds,
MAX(events_sent) as max_events_sent,
AVG(events_sent) as avg_events_sent,
COUNT(DISTINCT client_ip) as unique_clients
FROM subscription_events
GROUP BY date(created_at, 'unixepoch')
ORDER BY date DESC;
-- View for current active subscriptions (from log perspective)
CREATE VIEW active_subscriptions_log AS
SELECT
subscription_id,
client_ip,
filter_json,
events_sent,
created_at,
(strftime('%s', 'now') - created_at) as duration_seconds
FROM subscription_events
WHERE event_type = 'created'
AND subscription_id NOT IN (
SELECT subscription_id FROM subscription_events
WHERE event_type IN ('closed', 'expired', 'disconnected')
);

View File

@@ -15,9 +15,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 +38,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 +48,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 +68,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"
@@ -78,9 +91,10 @@ if ps -p "$RELAY_PID" >/dev/null 2>&1; then
echo $RELAY_PID > relay.pid
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"

View File

@@ -1,11 +1,83 @@
=== C Nostr Relay Server ===
[SUCCESS] Database connection established
[SUCCESS] Relay information initialized with default values
[INFO] Initializing NIP-13 Proof of Work configuration
[INFO] PoW Configuration: enabled=true, min_difficulty=0, validation_flags=0x1, mode=full
[INFO] Initializing NIP-40 Expiration Timestamp configuration
[INFO] Expiration Configuration: enabled=true, strict_mode=true, filter_responses=true, grace_period=300 seconds
[INFO] Starting relay server...
[INFO] Starting libwebsockets-based Nostr relay server...
[SUCCESS] WebSocket relay started on ws://127.0.0.1:8888
[INFO] HTTP request received
[INFO] Handling NIP-11 relay information request
[SUCCESS] NIP-11 relay information served successfully
[INFO] HTTP request received
[INFO] Handling NIP-11 relay information request
[SUCCESS] NIP-11 relay information served successfully
[INFO] WebSocket connection established
[INFO] Received WebSocket message
[INFO] Handling EVENT message
[INFO] Handling EVENT message with full NIP-01 validation
[SUCCESS] Event stored in database
[SUCCESS] Event stored successfully
[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
[WARNING] Event rejected: expired timestamp
[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] Handling EVENT message with full NIP-01 validation
[WARNING] Event rejected: expired timestamp
[INFO] WebSocket connection closed
[INFO] WebSocket connection established
[INFO] Received WebSocket message
[INFO] Handling REQ message for persistent subscription
[INFO] Added subscription 'filter_test' (total: 1)
[INFO] Executing SQL: SELECT id, pubkey, created_at, kind, content, sig, tags FROM events WHERE 1=1 AND kind IN (1) ORDER BY created_at DESC LIMIT 10
[INFO] Query returned 10 rows
[INFO] Total events sent: 10
[INFO] Received WebSocket message
[INFO] Removed subscription 'filter_test' (total: 0)
[INFO] Closed subscription: filter_test
[INFO] WebSocket connection closed
[WARNING] Subscription '<27><><15>a' not found for removal
[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] HTTP request received
[INFO] Handling NIP-11 relay information request
[SUCCESS] NIP-11 relay information served successfully

View File

@@ -1 +1 @@
320933
743964

BIN
src/main

Binary file not shown.

2742
src/main.c

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

View File

@@ -1,7 +1,7 @@
#!/bin/bash
# Simple C-Relay Test - Create type 1 event and upload to relay
# Uses nak to generate and publish a single event
# Comprehensive C-Relay Test - Test event types and subscriptions
# Uses nak to generate and publish various event types, then tests subscriptions
set -e # Exit on any error
@@ -16,7 +16,6 @@ RESET='\033[0m'
# Test configuration
RELAY_URL="ws://127.0.0.1:8888"
TEST_PRIVATE_KEY="nsec1j4c6269y9w0q2er2xjw8sv2ehyrtfxq3jwgdlxj6qfn8z4gjsq5qfvfk99"
TEST_CONTENT="Hello from C-Relay test!"
# Print functions
print_header() {
@@ -39,74 +38,320 @@ print_info() {
echo -e "${BLUE}[INFO]${RESET} $1"
}
# Main test function
run_test() {
print_header "C-Relay Simple Test"
print_warning() {
echo -e "${YELLOW}[WARNING]${RESET} $1"
}
# Check if nak is available
# Global arrays to store event IDs for subscription tests
declare -a REGULAR_EVENT_IDS=()
declare -a REPLACEABLE_EVENT_IDS=()
declare -a EPHEMERAL_EVENT_IDS=()
declare -a ADDRESSABLE_EVENT_IDS=()
# Helper function to publish event and extract ID
publish_event() {
local event_json="$1"
local event_type="$2"
local description="$3"
# Extract event ID
local event_id=$(echo "$event_json" | jq -r '.id' 2>/dev/null)
if [[ "$event_id" == "null" || -z "$event_id" ]]; then
print_error "Could not extract event ID from $description"
return 1
fi
print_info "Publishing $description..."
# Create EVENT message in Nostr format
local event_message="[\"EVENT\",$event_json]"
# Publish to relay
local response=""
if command -v websocat &> /dev/null; then
response=$(echo "$event_message" | timeout 5s websocat "$RELAY_URL" 2>&1 || echo "Connection failed")
else
print_error "websocat not found - required for testing"
return 1
fi
# Check response
if [[ "$response" == *"Connection failed"* ]]; then
print_error "Failed to connect to relay for $description"
return 1
elif [[ "$response" == *"true"* ]]; then
print_success "$description uploaded (ID: ${event_id:0:16}...)"
# Store event ID in appropriate array
case "$event_type" in
"regular") REGULAR_EVENT_IDS+=("$event_id") ;;
"replaceable") REPLACEABLE_EVENT_IDS+=("$event_id") ;;
"ephemeral") EPHEMERAL_EVENT_IDS+=("$event_id") ;;
"addressable") ADDRESSABLE_EVENT_IDS+=("$event_id") ;;
esac
echo # Add blank line for readability
return 0
else
print_warning "$description might have failed: $response"
echo # Add blank line for readability
return 1
fi
}
# Helper function to publish invalid event and expect rejection
publish_invalid_event() {
local event_json="$1"
local description="$2"
local expected_error="$3"
print_info "Publishing invalid $description..."
# Create EVENT message in Nostr format
local event_message="[\"EVENT\",$event_json]"
# Publish to relay
local response=""
if command -v websocat &> /dev/null; then
response=$(echo "$event_message" | timeout 5s websocat "$RELAY_URL" 2>&1 || echo "Connection failed")
else
print_error "websocat not found - required for testing"
return 1
fi
# Check response - should contain "false" and error message
if [[ "$response" == *"Connection failed"* ]]; then
print_error "Failed to connect to relay for $description"
return 1
elif [[ "$response" == *"false"* ]]; then
# Extract error message
local error_msg=$(echo "$response" | grep -o '"[^"]*invalid[^"]*"' | head -1 | sed 's/"//g' 2>/dev/null || echo "rejected")
print_success "$description correctly rejected: $error_msg"
echo # Add blank line for readability
return 0
elif [[ "$response" == *"true"* ]]; then
print_error "$description was incorrectly accepted (should have been rejected)"
echo # Add blank line for readability
return 1
else
print_warning "$description response unclear: $response"
echo # Add blank line for readability
return 1
fi
}
# Test subscription with filters
test_subscription() {
local sub_id="$1"
local filter="$2"
local description="$3"
local expected_count="$4"
print_step "Testing subscription: $description"
# Create REQ message
local req_message="[\"REQ\",\"$sub_id\",$filter]"
print_info "Testing filter: $filter"
# Send subscription and collect events
local response=""
if command -v websocat &> /dev/null; then
response=$(echo -e "$req_message\n[\"CLOSE\",\"$sub_id\"]" | timeout 3s websocat "$RELAY_URL" 2>/dev/null || echo "")
fi
# Count EVENT responses (lines containing ["EVENT","sub_id",...])
local event_count=0
if [[ -n "$response" ]]; then
event_count=$(echo "$response" | grep -c "\"EVENT\"" 2>/dev/null || echo "0")
fi
if [[ "$expected_count" == "any" ]]; then
if [[ $event_count -gt 0 ]]; then
print_success "$description - Found $event_count events"
else
print_warning "$description - No events found"
fi
elif [[ $event_count -eq $expected_count ]]; then
print_success "$description - Found expected $event_count events"
else
print_warning "$description - Expected $expected_count events, found $event_count"
fi
# Show a few sample events for verification (first 2)
if [[ $event_count -gt 0 && "$description" == "All events" ]]; then
print_info "Sample events (first 2):"
echo "$response" | grep "\"EVENT\"" | head -2 | while IFS= read -r line; do
local event_content=$(echo "$line" | jq -r '.[2].content' 2>/dev/null || echo "N/A")
local event_kind=$(echo "$line" | jq -r '.[2].kind' 2>/dev/null || echo "N/A")
local event_id=$(echo "$line" | jq -r '.[2].id' 2>/dev/null || echo "N/A")
echo " - ID: ${event_id:0:16}... Kind: $event_kind Content: ${event_content:0:30}..."
done
fi
echo # Add blank line for readability
return 0
}
# Main test function
run_comprehensive_test() {
print_header "C-Relay Comprehensive Test"
# Check dependencies
print_step "Checking dependencies..."
if ! command -v nak &> /dev/null; then
print_error "nak command not found"
print_info "Please install nak: go install github.com/fiatjaf/nak@latest"
return 1
fi
print_success "nak found"
# Step 1: Create type 1 event with nak including tags
print_step "Creating type 1 event with nak and tags..."
local event_json
if ! event_json=$(nak event --sec "$TEST_PRIVATE_KEY" -c "$TEST_CONTENT" -k 1 --ts $(date +%s) -e "test_event_id" -p "test_pubkey" -t "subject=Test Event" 2>/dev/null); then
print_error "Failed to generate event with nak"
if ! command -v websocat &> /dev/null; then
print_error "websocat command not found"
print_info "Please install websocat for testing"
return 1
fi
if ! command -v jq &> /dev/null; then
print_error "jq command not found"
print_info "Please install jq for JSON processing"
return 1
fi
print_success "All dependencies found"
print_success "Event created successfully"
print_header "FULL EVENT JSON"
echo "$event_json" | jq . 2>/dev/null || echo "$event_json"
echo
print_header "PHASE 1: Publishing Various Event Types"
# Step 2: Upload to C-Relay
print_step "Uploading event to C-Relay at $RELAY_URL..."
# Test 1: Regular Events (kind 1)
print_step "Creating regular events (kind 1)..."
local regular1=$(nak event --sec "$TEST_PRIVATE_KEY" -c "Regular event #1" -k 1 --ts $(($(date +%s) - 100)) -t "type=regular" -t "test=phase1" 2>/dev/null)
local regular2=$(nak event --sec "$TEST_PRIVATE_KEY" -c "Regular event #2 with tags" -k 1 --ts $(($(date +%s) - 90)) -e "previous_event_id" -p "test_pubkey" -t "type=regular" -t "test=phase1" 2>/dev/null)
# Create EVENT message in Nostr format
local event_message="[\"EVENT\",$event_json]"
publish_event "$regular1" "regular" "Regular event #1"
publish_event "$regular2" "regular" "Regular event #2"
# Use websocat or wscat to send to relay if available
local response=""
if command -v websocat &> /dev/null; then
print_info "Using websocat to connect to relay..."
response=$(echo "$event_message" | timeout 5s websocat "$RELAY_URL" 2>&1 || echo "Connection failed")
elif command -v wscat &> /dev/null; then
print_info "Using wscat to connect to relay..."
response=$(echo "$event_message" | timeout 5s wscat -c "$RELAY_URL" 2>&1 || echo "Connection failed")
# Test 2: Replaceable Events (kind 0 - metadata)
print_step "Creating replaceable events (kind 0)..."
local replaceable1=$(nak event --sec "$TEST_PRIVATE_KEY" -c '{"name":"Test User","about":"Testing C-Relay"}' -k 0 --ts $(($(date +%s) - 80)) -t "type=replaceable" 2>/dev/null)
local replaceable2=$(nak event --sec "$TEST_PRIVATE_KEY" -c '{"name":"Test User Updated","about":"Updated profile"}' -k 0 --ts $(($(date +%s) - 70)) -t "type=replaceable" 2>/dev/null)
publish_event "$replaceable1" "replaceable" "Replaceable event #1 (metadata)"
publish_event "$replaceable2" "replaceable" "Replaceable event #2 (metadata update)"
# Test 3: Ephemeral Events (kind 20000+)
print_step "Creating ephemeral events (kind 20001)..."
local ephemeral1=$(nak event --sec "$TEST_PRIVATE_KEY" -c "Ephemeral event - should not be stored permanently" -k 20001 --ts $(date +%s) -t "type=ephemeral" 2>/dev/null)
publish_event "$ephemeral1" "ephemeral" "Ephemeral event"
# Test 4: Addressable Events (kind 30000+)
print_step "Creating addressable events (kind 30001)..."
local addressable1=$(nak event --sec "$TEST_PRIVATE_KEY" -c "Addressable event with d-tag" -k 30001 --ts $(($(date +%s) - 50)) -t "d=test-article" -t "type=addressable" 2>/dev/null)
local addressable2=$(nak event --sec "$TEST_PRIVATE_KEY" -c "Updated addressable event" -k 30001 --ts $(($(date +%s) - 40)) -t "d=test-article" -t "type=addressable" -t "updated=true" 2>/dev/null)
publish_event "$addressable1" "addressable" "Addressable event #1"
publish_event "$addressable2" "addressable" "Addressable event #2 (update)"
# Brief pause to let events settle
sleep 2
print_header "PHASE 2: Testing Invalid Events (NIP-01 Validation)"
print_step "Testing various invalid events that should be rejected..."
# Test 1: Event with invalid JSON structure (malformed)
local malformed_event='{"id":"invalid","pubkey":"invalid_pubkey","created_at":"not_a_number","kind":1,"tags":[],"content":"test"}'
publish_invalid_event "$malformed_event" "malformed event with invalid created_at" "invalid"
# Test 2: Event with missing required fields
local missing_field_event='{"id":"test123","pubkey":"valid_pubkey","kind":1,"tags":[],"content":"test"}'
publish_invalid_event "$missing_field_event" "event missing created_at and sig" "invalid"
# Test 3: Event with invalid pubkey format (not hex)
local invalid_pubkey_event='{"id":"abc123","pubkey":"not_valid_hex_pubkey","created_at":1234567890,"kind":1,"tags":[],"content":"test","sig":"fake_sig"}'
publish_invalid_event "$invalid_pubkey_event" "event with invalid pubkey format" "invalid"
# Test 4: Event with invalid event ID format
local invalid_id_event='{"id":"not_64_char_hex","pubkey":"0123456789abcdef0123456789abcdef0123456789abcdef0123456789abcdef","created_at":1234567890,"kind":1,"tags":[],"content":"test","sig":"0123456789abcdef0123456789abcdef0123456789abcdef0123456789abcdef0123456789abcdef0123456789abcdef0123456789abcdef0123456789abcdef"}'
publish_invalid_event "$invalid_id_event" "event with invalid ID format" "invalid"
# Test 5: Event with invalid signature
local invalid_sig_event='{"id":"0123456789abcdef0123456789abcdef0123456789abcdef0123456789abcdef","pubkey":"0123456789abcdef0123456789abcdef0123456789abcdef0123456789abcdef","created_at":1234567890,"kind":1,"tags":[],"content":"test","sig":"invalid_signature_format"}'
publish_invalid_event "$invalid_sig_event" "event with invalid signature format" "invalid"
# Test 6: Event with invalid kind (negative)
local invalid_kind_event='{"id":"0123456789abcdef0123456789abcdef0123456789abcdef0123456789abcdef","pubkey":"0123456789abcdef0123456789abcdef0123456789abcdef0123456789abcdef","created_at":1234567890,"kind":-1,"tags":[],"content":"test","sig":"0123456789abcdef0123456789abcdef0123456789abcdef0123456789abcdef0123456789abcdef0123456789abcdef0123456789abcdef0123456789abcdef"}'
publish_invalid_event "$invalid_kind_event" "event with negative kind" "invalid"
# Test 7: Event with invalid tags format (not array)
local invalid_tags_event='{"id":"0123456789abcdef0123456789abcdef0123456789abcdef0123456789abcdef","pubkey":"0123456789abcdef0123456789abcdef0123456789abcdef0123456789abcdef","created_at":1234567890,"kind":1,"tags":"not_an_array","content":"test","sig":"0123456789abcdef0123456789abcdef0123456789abcdef0123456789abcdef0123456789abcdef0123456789abcdef0123456789abcdef0123456789abcdef"}'
publish_invalid_event "$invalid_tags_event" "event with invalid tags format" "invalid"
print_success "Invalid event tests completed - all should have been rejected"
print_header "PHASE 3: Testing Subscriptions and Filters"
# Test subscription filters
print_step "Testing various subscription filters..."
# Test 1: Get all events
test_subscription "test_all" '{}' "All events" "any"
# Test 2: Get events by kind
test_subscription "test_kind1" '{"kinds":[1]}' "Kind 1 events only" "2"
test_subscription "test_kind0" '{"kinds":[0]}' "Kind 0 events only" "any"
# Test 3: Get events by author (pubkey)
local test_pubkey=$(echo "$regular1" | jq -r '.pubkey' 2>/dev/null)
test_subscription "test_author" "{\"authors\":[\"$test_pubkey\"]}" "Events by specific author" "any"
# Test 4: Get recent events (time-based)
local recent_timestamp=$(($(date +%s) - 200))
test_subscription "test_recent" "{\"since\":$recent_timestamp}" "Recent events" "any"
# Test 5: Get events with specific tags
test_subscription "test_tag_type" '{"#type":["regular"]}' "Events with type=regular tag" "any"
# Test 6: Multiple kinds
test_subscription "test_multi_kinds" '{"kinds":[0,1]}' "Multiple kinds (0,1)" "any"
# Test 7: Limit results
test_subscription "test_limit" '{"kinds":[1],"limit":1}' "Limited to 1 event" "1"
print_header "PHASE 4: Database Verification"
# Check what's actually stored in the database
print_step "Verifying database contents..."
if command -v sqlite3 &> /dev/null; then
print_info "Events by type in database:"
sqlite3 db/c_nostr_relay.db "SELECT event_type, COUNT(*) as count FROM events GROUP BY event_type;" | while read line; do
echo " $line"
done
print_info "Recent events in database:"
sqlite3 db/c_nostr_relay.db "SELECT substr(id, 1, 16) || '...' as short_id, event_type, kind, substr(content, 1, 30) || '...' as short_content FROM events ORDER BY created_at DESC LIMIT 5;" | while read line; do
echo " $line"
done
print_success "Database verification complete"
else
# Fallback: use nak publish
print_info "Using nak to publish event..."
response=$(echo "$event_json" | nak event --relay "$RELAY_URL" 2>&1 || echo "Publish failed")
print_warning "sqlite3 not available for database verification"
fi
print_header "FULL RELAY RESPONSE"
echo "$response"
echo
if [[ "$response" == *"Connection failed"* ]] || [[ "$response" == *"Publish failed"* ]]; then
print_error "Failed to connect to relay or publish event"
print_info "Make sure the relay is running: ./make_and_restart_relay.sh"
return 1
else
print_success "Event uploaded to relay"
return 0
fi
}
# Run the test
if run_test; then
# Run the comprehensive test
print_header "Starting C-Relay Comprehensive Test Suite with NIP-01 Validation"
echo
if run_comprehensive_test; then
echo
print_success "All tests completed successfully!"
print_info "The C-Relay with full NIP-01 validation is working correctly"
print_info "✅ Event validation, signature verification, and error handling all working"
echo
print_success "Test completed successfully"
exit 0
else
echo
print_error "Test failed"
print_error "Some tests failed"
exit 1
fi

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

386
tests/9_nip_delete_test.sh Executable file
View File

@@ -0,0 +1,386 @@
#!/bin/bash
# NIP-09 Event Deletion Request Test for C-Relay
# Tests deletion request functionality - assumes relay is already running
# Based on the pattern from 1_nip_test.sh
set -e
# 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"
TEST_PRIVATE_KEY="nsec1j4c6269y9w0q2er2xjw8sv2ehyrtfxq3jwgdlxj6qfn8z4gjsq5qfvfk99"
# 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"
}
# Helper function to publish event and extract ID
publish_event() {
local event_json="$1"
local description="$2"
# Extract event ID
local event_id=$(echo "$event_json" | jq -r '.id' 2>/dev/null)
if [[ "$event_id" == "null" || -z "$event_id" ]]; then
print_error "Could not extract event ID from $description"
return 1
fi
print_info "Publishing $description..."
# Create EVENT message in Nostr format
local event_message="[\"EVENT\",$event_json]"
# Publish to relay
local response=""
if command -v websocat &> /dev/null; then
response=$(echo "$event_message" | timeout 5s websocat "$RELAY_URL" 2>&1 || echo "Connection failed")
else
print_error "websocat not found - required for testing"
return 1
fi
# Check response
if [[ "$response" == *"Connection failed"* ]]; then
print_error "Failed to connect to relay for $description"
return 1
elif [[ "$response" == *"true"* ]]; then
print_success "$description uploaded (ID: ${event_id:0:16}...)"
echo "$event_id"
return 0
else
print_warning "$description might have failed: $response"
echo ""
return 1
fi
}
# Helper function to publish deletion request
publish_deletion_request() {
local deletion_event_json="$1"
local description="$2"
# Extract event ID
local event_id=$(echo "$deletion_event_json" | jq -r '.id' 2>/dev/null)
if [[ "$event_id" == "null" || -z "$event_id" ]]; then
print_error "Could not extract event ID from $description"
return 1
fi
print_info "Publishing $description..."
# Create EVENT message in Nostr format
local event_message="[\"EVENT\",$deletion_event_json]"
# Publish to relay
local response=""
if command -v websocat &> /dev/null; then
response=$(echo "$event_message" | timeout 5s websocat "$RELAY_URL" 2>&1 || echo "Connection failed")
else
print_error "websocat not found - required for testing"
return 1
fi
# Check response
if [[ "$response" == *"Connection failed"* ]]; then
print_error "Failed to connect to relay for $description"
return 1
elif [[ "$response" == *"true"* ]]; then
print_success "$description accepted (ID: ${event_id:0:16}...)"
echo "$event_id"
return 0
else
print_warning "$description might have failed: $response"
echo ""
return 1
fi
}
# Helper function to check if event exists via subscription
check_event_exists() {
local event_id="$1"
local sub_id="exists_$(date +%s%N | cut -c1-10)"
# Create REQ message to query for specific event ID
local req_message="[\"REQ\",\"$sub_id\",{\"ids\":[\"$event_id\"]}]"
# Send subscription and collect events
local response=""
if command -v websocat &> /dev/null; then
response=$(echo -e "$req_message\n[\"CLOSE\",\"$sub_id\"]" | timeout 3s websocat "$RELAY_URL" 2>/dev/null || echo "")
fi
# Count EVENT responses
local event_count=0
if [[ -n "$response" ]]; then
event_count=$(echo "$response" | grep -c "\"EVENT\"" 2>/dev/null || echo "0")
fi
echo "$event_count"
}
# Helper function to query events by kind
query_events_by_kind() {
local kind="$1"
local sub_id="kind${kind}_$(date +%s%N | cut -c1-10)"
# Create REQ message to query for events of specific kind
local req_message="[\"REQ\",\"$sub_id\",{\"kinds\":[$kind]}]"
# Send subscription and collect events
local response=""
if command -v websocat &> /dev/null; then
response=$(echo -e "$req_message\n[\"CLOSE\",\"$sub_id\"]" | timeout 3s websocat "$RELAY_URL" 2>/dev/null || echo "")
fi
# Count EVENT responses
local event_count=0
if [[ -n "$response" ]]; then
event_count=$(echo "$response" | grep -c "\"EVENT\"" 2>/dev/null || echo "0")
fi
echo "$event_count"
}
# Main test function
run_deletion_test() {
print_header "NIP-09 Event Deletion Request Test"
# Check dependencies
print_step "Checking dependencies..."
if ! command -v nak &> /dev/null; then
print_error "nak command not found"
print_info "Please install nak: go install github.com/fiatjaf/nak@latest"
return 1
fi
if ! command -v websocat &> /dev/null; then
print_error "websocat command not found"
print_info "Please install websocat for testing"
return 1
fi
if ! command -v jq &> /dev/null; then
print_error "jq command not found"
print_info "Please install jq for JSON processing"
return 1
fi
print_success "All dependencies found"
print_header "PHASE 1: Publishing Events to be Deleted"
# Create test events that will be deleted
print_step "Creating events for deletion testing..."
# Create regular events (kind 1) - these will be deleted by ID
local event1=$(nak event --sec "$TEST_PRIVATE_KEY" -c "Event to be deleted #1" -k 1 --ts $(($(date +%s) - 100)) -t "type=test" -t "phase=deletion" 2>/dev/null)
local event2=$(nak event --sec "$TEST_PRIVATE_KEY" -c "Event to be deleted #2" -k 1 --ts $(($(date +%s) - 90)) -t "type=test" -t "phase=deletion" 2>/dev/null)
# Publish the events
event1_id=$(publish_event "$event1" "Event to be deleted #1")
if [[ -z "$event1_id" ]]; then
print_error "Failed to publish test event #1"
return 1
fi
event2_id=$(publish_event "$event2" "Event to be deleted #2")
if [[ -z "$event2_id" ]]; then
print_error "Failed to publish test event #2"
return 1
fi
# Create an addressable event (kind 30001) - will be deleted by address
local addr_event=$(nak event --sec "$TEST_PRIVATE_KEY" -c "Addressable event to be deleted" -k 30001 --ts $(($(date +%s) - 80)) -t "d=test-delete" -t "type=addressable" 2>/dev/null)
addr_event_id=$(publish_event "$addr_event" "Addressable event to be deleted")
if [[ -z "$addr_event_id" ]]; then
print_error "Failed to publish addressable test event"
return 1
fi
# Create an event by a different author (to test unauthorized deletion)
local different_key="nsec1234567890abcdef1234567890abcdef1234567890abcdef1234567890ab"
local unauth_event=$(nak event --sec "$different_key" -c "Event by different author" -k 1 --ts $(($(date +%s) - 70)) -t "type=unauthorized" 2>/dev/null)
unauth_event_id=$(publish_event "$unauth_event" "Event by different author")
if [[ -z "$unauth_event_id" ]]; then
print_warning "Failed to publish unauthorized test event - continuing anyway"
fi
# Let events settle
sleep 2
print_header "PHASE 2: Testing Event Deletion by ID"
print_step "Verifying events exist before deletion..."
local event1_before=$(check_event_exists "$event1_id")
local event2_before=$(check_event_exists "$event2_id")
print_info "Event1 exists: $event1_before, Event2 exists: $event2_before"
# Create deletion request targeting the two events by ID
print_step "Creating deletion request for events by ID..."
local deletion_by_id=$(nak event --sec "$TEST_PRIVATE_KEY" -c "Deleting events by ID" -k 5 --ts $(date +%s) -e "$event1_id" -e "$event2_id" -t "k=1" 2>/dev/null)
deletion_id=$(publish_deletion_request "$deletion_by_id" "Deletion request for events by ID")
if [[ -z "$deletion_id" ]]; then
print_error "Failed to publish deletion request"
return 1
fi
# Wait for deletion to process
sleep 3
# Check if events were deleted
print_step "Verifying events were deleted..."
local event1_after=$(check_event_exists "$event1_id")
local event2_after=$(check_event_exists "$event2_id")
print_info "Event1 exists after deletion: $event1_after, Event2 exists after deletion: $event2_after"
if [[ "$event1_after" == "0" && "$event2_after" == "0" ]]; then
print_success "✓ Events successfully deleted by ID"
else
print_error "✗ Events were not properly deleted"
fi
print_header "PHASE 3: Testing Address-based Deletion"
if [[ -n "$addr_event_id" ]]; then
print_step "Verifying addressable event exists before deletion..."
local addr_before=$(check_event_exists "$addr_event_id")
print_info "Addressable event exists: $addr_before"
# Create deletion request for addressable event using 'a' tag
print_step "Creating deletion request for addressable event..."
local test_pubkey=$(echo "$addr_event" | jq -r '.pubkey' 2>/dev/null)
local deletion_by_addr=$(nak event --sec "$TEST_PRIVATE_KEY" -c "Deleting addressable event" -k 5 --ts $(date +%s) -t "a=30001:${test_pubkey}:test-delete" -t "k=30001" 2>/dev/null)
addr_deletion_id=$(publish_deletion_request "$deletion_by_addr" "Deletion request for addressable event")
if [[ -n "$addr_deletion_id" ]]; then
# Wait for deletion to process
sleep 3
# Check if addressable event was deleted
print_step "Verifying addressable event was deleted..."
local addr_after=$(check_event_exists "$addr_event_id")
print_info "Addressable event exists after deletion: $addr_after"
if [[ "$addr_after" == "0" ]]; then
print_success "✓ Addressable event successfully deleted"
else
print_error "✗ Addressable event was not properly deleted"
fi
fi
fi
print_header "PHASE 4: Testing Unauthorized Deletion"
if [[ -n "$unauth_event_id" ]]; then
print_step "Testing unauthorized deletion attempt..."
# Try to delete the unauthorized event (should fail)
local unauth_deletion=$(nak event --sec "$TEST_PRIVATE_KEY" -c "Attempting unauthorized deletion" -k 5 --ts $(date +%s) -e "$unauth_event_id" -t "k=1" 2>/dev/null)
unauth_deletion_id=$(publish_deletion_request "$unauth_deletion" "Unauthorized deletion request")
if [[ -n "$unauth_deletion_id" ]]; then
# Wait for processing
sleep 3
# Check if unauthorized event still exists (should still exist)
local unauth_after=$(check_event_exists "$unauth_event_id")
print_info "Unauthorized event exists after deletion attempt: $unauth_after"
if [[ "$unauth_after" == "1" ]]; then
print_success "✓ Unauthorized deletion properly rejected - event still exists"
else
print_error "✗ Unauthorized deletion succeeded - security vulnerability!"
fi
fi
fi
print_header "PHASE 5: Testing Invalid Deletion Requests"
print_step "Testing deletion request with no targets..."
# Create deletion request with no 'e' or 'a' tags (should be rejected)
local invalid_deletion='{"id":"0123456789abcdef0123456789abcdef0123456789abcdef0123456789abcdef","pubkey":"aa4fc8665f5696e33db7e1a572e3b0f5b3d615837b0f362dcb1c8068b098c7b4","created_at":'$(date +%s)',"kind":5,"tags":[["k","1"]],"content":"Invalid deletion request with no targets","sig":"0123456789abcdef0123456789abcdef0123456789abcdef0123456789abcdef0123456789abcdef0123456789abcdef0123456789abcdef0123456789abcdef"}'
# Create EVENT message in Nostr format
local invalid_message="[\"EVENT\",$invalid_deletion]"
# Publish to relay
local invalid_response=""
if command -v websocat &> /dev/null; then
invalid_response=$(echo "$invalid_message" | timeout 5s websocat "$RELAY_URL" 2>&1 || echo "Connection failed")
fi
# Check response - should be rejected
if [[ "$invalid_response" == *"false"* ]]; then
print_success "✓ Invalid deletion request properly rejected"
elif [[ "$invalid_response" == *"true"* ]]; then
print_warning "⚠ Invalid deletion request was accepted (should have been rejected)"
else
print_info "Invalid deletion request response: $invalid_response"
fi
print_header "PHASE 6: Verification"
# Verify deletion requests themselves are stored
print_step "Verifying deletion requests are stored..."
local deletion_count=$(query_events_by_kind 5)
print_info "Deletion requests accessible via query: $deletion_count"
if [[ "$deletion_count" -gt 0 ]]; then
print_success "✓ Deletion requests properly stored and queryable"
else
print_warning "⚠ No deletion requests found via query"
fi
return 0
}
# Run the test
print_header "Starting NIP-09 Event Deletion Request Test Suite"
echo
if run_deletion_test; then
echo
print_success "All NIP-09 deletion tests completed successfully!"
print_info "The C-Relay NIP-09 implementation is working correctly"
print_info "✅ Event deletion by ID working"
print_info "✅ Address-based deletion working"
print_info "✅ Authorization validation working"
print_info "✅ Invalid deletion rejection working"
echo
exit 0
else
echo
print_error "Some NIP-09 tests failed"
exit 1
fi

199
tests/subscribe_all.sh Executable file
View File

@@ -0,0 +1,199 @@
#!/bin/bash
# Persistent Subscription Test Script
# Subscribes to all events in the relay and prints them as they arrive in real-time
# This tests the persistent subscription functionality of the C-Relay
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="ws://127.0.0.1:8888"
SUBSCRIPTION_ID="persistent_test_$(date +%s)"
# Print functions
print_header() {
echo -e "${BLUE}${BOLD}=== $1 ===${RESET}"
}
print_info() {
echo -e "${BLUE}[INFO]${RESET} $1"
}
print_success() {
echo -e "${GREEN}${RESET} $1"
}
print_error() {
echo -e "${RED}${RESET} $1"
}
print_warning() {
echo -e "${YELLOW}[WARNING]${RESET} $1"
}
print_event() {
echo -e "${GREEN}[EVENT]${RESET} $1"
}
# Cleanup function
cleanup() {
print_info "Cleaning up..."
if [[ -n "$WEBSOCAT_PID" ]]; then
kill "$WEBSOCAT_PID" 2>/dev/null || true
wait "$WEBSOCAT_PID" 2>/dev/null || true
fi
# Send CLOSE message to clean up subscription on relay
if command -v websocat &> /dev/null; then
echo "[\"CLOSE\",\"$SUBSCRIPTION_ID\"]" | timeout 2s websocat "$RELAY_URL" 2>/dev/null || true
fi
print_info "Cleanup complete"
exit 0
}
# Set up signal handlers
trap cleanup SIGINT SIGTERM
# Parse events from relay responses
parse_events() {
while IFS= read -r line; do
# Check if this is an EVENT message
if echo "$line" | jq -e '. | type == "array" and length >= 3 and .[0] == "EVENT"' >/dev/null 2>&1; then
# Extract event details
local event_id=$(echo "$line" | jq -r '.[2].id' 2>/dev/null || echo "unknown")
local event_kind=$(echo "$line" | jq -r '.[2].kind' 2>/dev/null || echo "unknown")
local event_content=$(echo "$line" | jq -r '.[2].content' 2>/dev/null || echo "")
local event_pubkey=$(echo "$line" | jq -r '.[2].pubkey' 2>/dev/null || echo "unknown")
local event_created_at=$(echo "$line" | jq -r '.[2].created_at' 2>/dev/null || echo "unknown")
local event_tags=$(echo "$line" | jq -r '.[2].tags | length' 2>/dev/null || echo "0")
# Convert timestamp to readable format
local readable_time="unknown"
if [[ "$event_created_at" != "unknown" && "$event_created_at" =~ ^[0-9]+$ ]]; then
readable_time=$(date -d "@$event_created_at" "+%Y-%m-%d %H:%M:%S" 2>/dev/null || echo "$event_created_at")
fi
# Print formatted event
print_event "Kind: $event_kind | ID: ${event_id:0:16}... | Author: ${event_pubkey:0:16}..."
echo -e " ${YELLOW}Time:${RESET} $readable_time | ${YELLOW}Tags:${RESET} $event_tags"
# Show content (truncated if too long)
if [[ -n "$event_content" ]]; then
local truncated_content="${event_content:0:100}"
if [[ ${#event_content} -gt 100 ]]; then
truncated_content="${truncated_content}..."
fi
echo -e " ${YELLOW}Content:${RESET} $truncated_content"
fi
echo # Blank line for readability
elif echo "$line" | jq -e '. | type == "array" and length >= 2 and .[0] == "EOSE"' >/dev/null 2>&1; then
# End of stored events
local sub_id=$(echo "$line" | jq -r '.[1]' 2>/dev/null)
print_info "End of stored events for subscription: $sub_id"
print_success "Persistent subscription is now active - waiting for new events..."
echo
elif echo "$line" | jq -e '. | type == "array" and length >= 3 and .[0] == "CLOSED"' >/dev/null 2>&1; then
# Subscription closed
local sub_id=$(echo "$line" | jq -r '.[1]' 2>/dev/null)
local reason=$(echo "$line" | jq -r '.[2]' 2>/dev/null)
print_warning "Subscription $sub_id was closed: $reason"
elif echo "$line" | jq -e '. | type == "array" and length >= 4 and .[0] == "OK"' >/dev/null 2>&1; then
# OK response to event publishing
local event_id=$(echo "$line" | jq -r '.[1]' 2>/dev/null)
local success=$(echo "$line" | jq -r '.[2]' 2>/dev/null)
local message=$(echo "$line" | jq -r '.[3]' 2>/dev/null)
if [[ "$success" == "true" ]]; then
print_success "Event published: ${event_id:0:16}..."
else
print_error "Event publish failed: ${event_id:0:16}... - $message"
fi
else
# Unknown message type - just show it
print_info "Relay message: $line"
fi
done
}
# Main function
main() {
print_header "Persistent Subscription Test - Subscribe to All Events"
# Check dependencies
if ! command -v websocat &> /dev/null; then
print_error "websocat command not found"
print_info "Please install websocat for testing"
return 1
fi
if ! command -v jq &> /dev/null; then
print_error "jq command not found"
print_info "Please install jq for JSON processing"
return 1
fi
print_info "Subscription ID: $SUBSCRIPTION_ID"
print_info "Relay URL: $RELAY_URL"
print_info "Filter: {} (all events)"
echo
# Create REQ message to subscribe to all events
local req_message="[\"REQ\",\"$SUBSCRIPTION_ID\",{}]"
print_info "Establishing persistent subscription..."
print_info "Press Ctrl+C to stop and cleanup"
echo
# Start websocat connection and keep it open
{
echo "$req_message"
# Keep the connection alive by sleeping indefinitely
# The connection will receive events as they come in
while true; do
sleep 1
done
} | websocat "$RELAY_URL" | parse_events &
# Store the background process ID
WEBSOCAT_PID=$!
# Wait for the background process (which runs indefinitely)
# This will exit when we get a signal (Ctrl+C)
wait "$WEBSOCAT_PID" 2>/dev/null || true
}
# Usage information
usage() {
echo "Usage: $0"
echo
echo "This script creates a persistent subscription to all events on the relay"
echo "and displays them in real-time as they arrive. Perfect for testing"
echo "the persistent subscription functionality."
echo
echo "To test:"
echo "1. Run this script in one terminal"
echo "2. Run 'tests/1_nip_test.sh' in another terminal"
echo "3. Watch events appear in real-time in this terminal"
echo
echo "Press Ctrl+C to stop and cleanup the subscription."
}
# Handle help flag
if [[ "$1" == "-h" || "$1" == "--help" ]]; then
usage
exit 0
fi
# Run main function
main "$@"