Compare commits

...
4 Commits
129 changed files with 4425 additions and 9693 deletions
+11 -11
View File
@@ -1,12 +1,12 @@
# AGENTS.md - AI Agent Integration Guide for Architect Mode
**Project-Specific Information for AI Agents Working with C-Relay in Architect Mode**
**Project-Specific Information for AI Agents Working with C-Relay-PG in Architect Mode**
## Critical Architecture Understanding
### System Architecture Overview
C-Relay implements a **unique event-based configuration architecture** that fundamentally differs from traditional Nostr relays:
C-Relay-PG implements a **unique event-based configuration architecture** that fundamentally differs from traditional Nostr relays:
```
┌─────────────────┐ ┌──────────────────┐ ┌─────────────────┐
@@ -51,9 +51,9 @@ C-Relay implements a **unique event-based configuration architecture** that fund
## Architectural Decision Analysis
### Configuration System Design
**Traditional Approach vs C-Relay:**
**Traditional Approach vs C-Relay-PG:**
```
Traditional: C-Relay:
Traditional: C-Relay-PG:
config.json → kind 33334 events
ENV variables → cryptographically signed tags
File watching → database polling/restart
@@ -119,18 +119,18 @@ File watching → database polling/restart
```
Developer Machine:
├── ./make_and_restart_relay.sh
├── build/c_relay_x86
├── build/c_relay_pg_x86
├── build/<relay_pubkey>.db
└── relay.log
```
### Production SystemD Deployment
```
/opt/c-relay/:
├── c_relay_x86
/opt/c-relay-pg/:
├── c_relay_pg_x86
├── <relay_pubkey>.db
├── systemd service (c-relay.service)
└── c-relay user isolation
├── systemd service (c-relay-pg.service)
└── c-relay-pg user isolation
```
### Container Deployment Architecture
@@ -144,7 +144,7 @@ Container:
### Reverse Proxy Architecture
```
Internet → Nginx/HAProxy → C-Relay
Internet → Nginx/HAProxy → C-Relay-PG
├── WebSocket upgrade handling
├── SSL termination
└── Rate limiting
@@ -292,7 +292,7 @@ Admin Signs Event → WebSocket Submit → Validate → Store → Restart Requir
**Decision**: Same port serves both protocols
**Consequences**: Simplified deployment, protocol detection overhead, libwebsockets dependency
These architectural decisions form the foundation of C-Relay's unique approach to Nostr relay implementation and should be carefully considered when planning extensions or modifications.
These architectural decisions form the foundation of C-Relay-PG's unique approach to Nostr relay implementation and should be carefully considered when planning extensions or modifications.
**
[Response interrupted by a tool use result. Only one tool may be used at a time and should be placed at the end of the message.]
View File
+8 -8
View File
@@ -1,6 +1,6 @@
# AGENTS.md - AI Agent Integration Guide
**Project-Specific Information for AI Agents Working with C-Relay**
**Project-Specific Information for AI Agents Working with C-Relay-PG**
## Critical Build Commands
@@ -15,9 +15,9 @@
- Starts relay in background with proper logging
### Architecture-Specific Binary Outputs
- **x86_64**: `./build/c_relay_x86`
- **ARM64**: `./build/c_relay_arm64`
- **Other**: `./build/c_relay_$(ARCH)`
- **x86_64**: `./build/c_relay_pg_x86`
- **ARM64**: `./build/c_relay_pg_arm64`
- **Other**: `./build/c_relay_pg_$(ARCH)`
### Database File Naming Convention
- **Format**: `<relay_pubkey>.db` (NOT `.nrdb` as shown in docs)
@@ -75,10 +75,10 @@
### Process Management
```bash
# Kill existing relay processes
pkill -f "c_relay_"
pkill -f "c_relay_pg_"
# Check running processes
ps aux | grep c_relay_
ps aux | grep c_relay_pg_
# Force kill port binding
fuser -k 8888/tcp
@@ -95,7 +95,7 @@ fuser -k 8888/tcp
- Event configuration tests: `tests/event_config_tests.sh`
### SystemD Integration Considerations
- Service runs as `c-relay` user in `/opt/c-relay`
- Service runs as `c-relay-pg` user in `/opt/c-relay-pg`
- Database files created in WorkingDirectory automatically
- No environment variables needed (event-based config)
- Resource limits: 65536 file descriptors, 4096 processes
@@ -137,7 +137,7 @@ fuser -k 8888/tcp
## Quick Debugging Commands
```bash
# Check relay status
ps aux | grep c_relay_ && netstat -tln | grep 8888
ps aux | grep c_relay_pg_ && netstat -tln | grep 8888
# View logs
tail -f relay.log
+9 -9
View File
@@ -1,6 +1,6 @@
# C-Relay API Documentation
# C-Relay-PG API Documentation
Complete API reference for the C-Relay event-based administration system and advanced features.
Complete API reference for the C-Relay-PG event-based administration system and advanced features.
## Table of Contents
@@ -21,7 +21,7 @@ Complete API reference for the C-Relay event-based administration system and adv
## Overview
C-Relay uses an innovative **event-based administration system** where all configuration and management commands are sent as cryptographically signed Nostr events. This provides:
C-Relay-PG uses an innovative **event-based administration system** where all configuration and management commands are sent as cryptographically signed Nostr events. This provides:
- **Cryptographic security**: All commands must be signed with the admin private key
- **Audit trail**: Complete history of all administrative actions
@@ -63,8 +63,8 @@ Store the admin private key securely:
export C_RELAY_ADMIN_KEY="nsec1abc123..."
# Secure file
echo "nsec1abc123..." > ~/.c-relay-admin
chmod 600 ~/.c-relay-admin
echo "nsec1abc123..." > ~/.c-relay-pg-admin
chmod 600 ~/.c-relay-pg-admin
# Password manager (recommended)
# Store in 1Password, Bitwarden, etc.
@@ -167,7 +167,7 @@ Examples:
"data": [
{
"key": "relay_name",
"value": "C-Relay",
"value": "C-Relay-PG",
"data_type": "string",
"category": "relay",
"description": "Relay name displayed in NIP-11"
@@ -506,10 +506,10 @@ ORDER BY count DESC
| Key | Type | Default | Description |
|-----|------|---------|-------------|
| `relay_name` | string | "C-Relay" | Relay name (NIP-11) |
| `relay_name` | string | "C-Relay-PG" | Relay name (NIP-11) |
| `relay_description` | string | "C Nostr Relay" | Relay description |
| `relay_contact` | string | "" | Admin contact info |
| `relay_software` | string | "c-relay" | Software identifier |
| `relay_software` | string | "c-relay-pg" | Software identifier |
| `relay_version` | string | auto | Software version |
| `supported_nips` | string | "1,9,11,13,15,20,33,40,42,45,50,70" | Supported NIPs |
| `language_tags` | string | "*" | Supported languages |
@@ -576,7 +576,7 @@ ORDER BY count DESC
## Real-time Monitoring
C-Relay provides subscription-based real-time monitoring using ephemeral events (kind 24567).
C-Relay-PG provides subscription-based real-time monitoring using ephemeral events (kind 24567).
### Activation
+9 -9
View File
@@ -1,4 +1,4 @@
# Alpine-based MUSL static binary builder for C-Relay
# Alpine-based MUSL static binary builder for C-Relay-PG
# Produces truly portable binaries with zero runtime dependencies
ARG DEBUG_BUILD=false
@@ -95,11 +95,11 @@ RUN cd nostr_core_lib && \
rm -f *.o *.a 2>/dev/null || true && \
./build.sh --nips=1,6,13,17,19,44,59
# Copy c-relay source files LAST (only this layer rebuilds on source changes)
# Copy c-relay-pg source files LAST (only this layer rebuilds on source changes)
COPY src/ /build/src/
COPY Makefile /build/Makefile
# Build c-relay with full static linking (only rebuilds when src/ changes)
# Build c-relay-pg with full static linking (only rebuilds when src/ changes)
# Disable fortification to avoid __*_chk symbols that don't exist in MUSL
# Use conditional compilation flags based on DEBUG_BUILD argument
RUN if [ "$DEBUG_BUILD" = "true" ]; then \
@@ -108,7 +108,7 @@ RUN if [ "$DEBUG_BUILD" = "true" ]; then \
echo "Building with DEBUG symbols enabled (optimized with -O2)"; \
else \
CFLAGS="-O2"; \
STRIP_CMD="strip /build/c_relay_static"; \
STRIP_CMD="strip /build/c_relay_pg_static"; \
echo "Building optimized production binary (symbols stripped)"; \
fi && \
gcc -static $CFLAGS -Wall -Wextra -std=c99 \
@@ -119,7 +119,7 @@ RUN if [ "$DEBUG_BUILD" = "true" ]; then \
src/nip009.c src/nip011.c src/nip013.c src/nip040.c src/nip042.c \
src/websockets.c src/subscriptions.c src/api.c src/embedded_web_content.c src/ip_ban.c \
src/db_ops.c src/thread_pool.c \
-o /build/c_relay_static \
-o /build/c_relay_pg_static \
c_utils_lib/libc_utils.a \
nostr_core_lib/libnostr_core_x64.a \
-lwebsockets -lssl -lcrypto -lsqlite3 -lsecp256k1 \
@@ -128,12 +128,12 @@ RUN if [ "$DEBUG_BUILD" = "true" ]; then \
# Verify it's truly static
RUN echo "=== Binary Information ===" && \
file /build/c_relay_static && \
ls -lh /build/c_relay_static && \
file /build/c_relay_pg_static && \
ls -lh /build/c_relay_pg_static && \
echo "=== Checking for dynamic dependencies ===" && \
(ldd /build/c_relay_static 2>&1 || echo "Binary is static") && \
(ldd /build/c_relay_pg_static 2>&1 || echo "Binary is static") && \
echo "=== Build complete ==="
# Output stage - just the binary
FROM scratch AS output
COPY --from=builder /build/c_relay_static /c_relay_static
COPY --from=builder /build/c_relay_pg_static /c_relay_pg_static
+20 -20
View File
@@ -1,4 +1,4 @@
# C-Relay Makefile
# C-Relay-PG Makefile
CC = gcc
CFLAGS = -Wall -Wextra -std=c99 -g -O2
@@ -16,13 +16,13 @@ C_UTILS_LIB = c_utils_lib/libc_utils.a
# Architecture detection
ARCH = $(shell uname -m)
ifeq ($(ARCH),x86_64)
TARGET = $(BUILD_DIR)/c_relay_x86
TARGET = $(BUILD_DIR)/c_relay_pg_x86
else ifeq ($(ARCH),aarch64)
TARGET = $(BUILD_DIR)/c_relay_arm64
TARGET = $(BUILD_DIR)/c_relay_pg_arm64
else ifeq ($(ARCH),arm64)
TARGET = $(BUILD_DIR)/c_relay_arm64
TARGET = $(BUILD_DIR)/c_relay_pg_arm64
else
TARGET = $(BUILD_DIR)/c_relay_$(ARCH)
TARGET = $(BUILD_DIR)/c_relay_pg_$(ARCH)
endif
# Default target
@@ -82,18 +82,18 @@ force-version:
# Build the relay
$(TARGET): $(BUILD_DIR) src/main.h src/sql_schema.h $(MAIN_SRC) $(NOSTR_CORE_LIB) $(C_UTILS_LIB)
@echo "Compiling C-Relay for architecture: $(ARCH)"
@echo "Compiling C-Relay-PG for architecture: $(ARCH)"
$(CC) $(CFLAGS) $(INCLUDES) $(MAIN_SRC) -o $(TARGET) $(NOSTR_CORE_LIB) $(C_UTILS_LIB) $(LIBS)
@echo "Build complete: $(TARGET)"
# Build for specific architectures
x86: $(BUILD_DIR) src/main.h src/sql_schema.h $(MAIN_SRC) $(NOSTR_CORE_LIB) $(C_UTILS_LIB)
@echo "Building C-Relay for x86_64..."
$(CC) $(CFLAGS) $(INCLUDES) $(MAIN_SRC) -o $(BUILD_DIR)/c_relay_x86 $(NOSTR_CORE_LIB) $(C_UTILS_LIB) $(LIBS)
@echo "Build complete: $(BUILD_DIR)/c_relay_x86"
@echo "Building C-Relay-PG for x86_64..."
$(CC) $(CFLAGS) $(INCLUDES) $(MAIN_SRC) -o $(BUILD_DIR)/c_relay_pg_x86 $(NOSTR_CORE_LIB) $(C_UTILS_LIB) $(LIBS)
@echo "Build complete: $(BUILD_DIR)/c_relay_pg_x86"
arm64: $(BUILD_DIR) src/main.h src/sql_schema.h $(MAIN_SRC) $(NOSTR_CORE_LIB) $(C_UTILS_LIB)
@echo "Cross-compiling C-Relay for ARM64..."
@echo "Cross-compiling C-Relay-PG 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"; \
@@ -116,9 +116,9 @@ arm64: $(BUILD_DIR) src/main.h src/sql_schema.h $(MAIN_SRC) $(NOSTR_CORE_LIB) $(
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) $(C_UTILS_LIB) \
aarch64-linux-gnu-gcc $(CFLAGS) $(INCLUDES) $(MAIN_SRC) -o $(BUILD_DIR)/c_relay_pg_arm64 $(NOSTR_CORE_LIB) $(C_UTILS_LIB) \
-L/usr/lib/aarch64-linux-gnu $(LIBS)
@echo "Build complete: $(BUILD_DIR)/c_relay_arm64"
@echo "Build complete: $(BUILD_DIR)/c_relay_pg_arm64"
# Install ARM64 cross-compilation dependencies
install-arm64-deps:
@@ -160,7 +160,7 @@ test: $(TARGET)
init-db:
@echo "Database initialization is now handled automatically when the server starts."
@echo "The schema is embedded in the binary - no external files needed."
@echo "To manually recreate database: rm -f db/c_nostr_relay.db && ./build/c_relay_x86"
@echo "To manually recreate database: rm -f db/c_nostr_relay.db && ./build/c_relay_pg_x86"
# Clean build artifacts
clean:
@@ -180,7 +180,7 @@ install-deps:
# Help
help:
@echo "C-Relay Build System"
@echo "C-Relay-PG Build System"
@echo ""
@echo "Targets:"
@echo " all Build the relay for current architecture (default)"
@@ -209,15 +209,15 @@ help:
# Build fully static MUSL binaries using Docker
static-musl-x86_64:
@echo "Building fully static MUSL binary for x86_64..."
docker buildx build --platform linux/amd64 -f examples/deployment/static-builder.Dockerfile -t c-relay-static-builder-x86_64 --load .
docker run --rm -v $(PWD)/build:/output c-relay-static-builder-x86_64 sh -c "cp /c_relay_static_musl_x86_64 /output/"
@echo "Static binary created: build/c_relay_static_musl_x86_64"
docker buildx build --platform linux/amd64 -f examples/deployment/static-builder.Dockerfile -t c-relay-pg-static-builder-x86_64 --load .
docker run --rm -v $(PWD)/build:/output c-relay-pg-static-builder-x86_64 sh -c "cp /c_relay_pg_static_musl_x86_64 /output/"
@echo "Static binary created: build/c_relay_pg_static_musl_x86_64"
static-musl-arm64:
@echo "Building fully static MUSL binary for ARM64..."
docker buildx build --platform linux/arm64 -f examples/deployment/static-builder.Dockerfile -t c-relay-static-builder-arm64 --load .
docker run --rm -v $(PWD)/build:/output c-relay-static-builder-arm64 sh -c "cp /c_relay_static_musl_x86_64 /output/c_relay_static_musl_arm64"
@echo "Static binary created: build/c_relay_static_musl_arm64"
docker buildx build --platform linux/arm64 -f examples/deployment/static-builder.Dockerfile -t c-relay-pg-static-builder-arm64 --load .
docker run --rm -v $(PWD)/build:/output c-relay-pg-static-builder-arm64 sh -c "cp /c_relay_pg_static_musl_x86_64 /output/c_relay_pg_static_musl_arm64"
@echo "Static binary created: build/c_relay_pg_static_musl_arm64"
static-musl: static-musl-x86_64 static-musl-arm64
@echo "Built static MUSL binaries for both architectures"
+6 -6
View File
@@ -45,21 +45,21 @@ Also included is a more standard administrative web front end. This front end co
## Screenshots
![](https://git.laantungir.net/laantungir/c-relay/raw/branch/master/screenshots/main.png)
![](https://git.laantungir.net/laantungir/c-relay-pg/raw/branch/master/screenshots/main.png)
Main page with real time updates.
![](https://git.laantungir.net/laantungir/c-relay/raw/branch/master/screenshots/config.png)
![](https://git.laantungir.net/laantungir/c-relay-pg/raw/branch/master/screenshots/config.png)
Set your configuration preferences.
![](https://git.laantungir.net/laantungir/c-relay/raw/branch/master/screenshots/subscriptions.png)
![](https://git.laantungir.net/laantungir/c-relay-pg/raw/branch/master/screenshots/subscriptions.png)
View current subscriptions
![](https://git.laantungir.net/laantungir/c-relay/raw/branch/master/screenshots/white-blacklists.png)
![](https://git.laantungir.net/laantungir/c-relay-pg/raw/branch/master/screenshots/white-blacklists.png)
Add npubs to white or black lists.
![](https://git.laantungir.net/laantungir/c-relay/raw/branch/master/screenshots/sqlQuery.png)
![](https://git.laantungir.net/laantungir/c-relay-pg/raw/branch/master/screenshots/sqlQuery.png)
Run sql queries on the database.
![](https://git.laantungir.net/laantungir/c-relay/raw/branch/master/screenshots/main-light.png)
![](https://git.laantungir.net/laantungir/c-relay-pg/raw/branch/master/screenshots/main-light.png)
Light mode.
+24 -24
View File
@@ -1,11 +1,11 @@
# C-Relay: High-Performance Nostr Relay
# C-Relay-PG: High-Performance Nostr Relay
A blazingly fast, production-ready Nostr relay implemented in C with an innovative event-based configuration system. Built for performance, security, and ease of deployment.
## 🚀 Why C-Relay?
## 🚀 Why C-Relay-PG?
### Event-Based Configuration
Unlike traditional relays that require config files, C-Relay uses **cryptographically signed Nostr events** for all configuration. This means:
Unlike traditional relays that require config files, C-Relay-PG uses **cryptographically signed Nostr events** for all configuration. This means:
- **Zero config files** - Everything stored in the database
- **Real-time updates** - Changes applied instantly without restart
- **Cryptographic security** - All changes must be signed by admin
@@ -36,7 +36,7 @@ Control your relay by sending direct messages from any Nostr client:
## 📋 Supported NIPs
C-Relay implements a comprehensive set of Nostr Improvement Proposals:
C-Relay-PG implements a comprehensive set of Nostr Improvement Proposals:
-**NIP-01**: Basic protocol flow implementation
-**NIP-09**: Event deletion
@@ -89,12 +89,12 @@ Download and run - no dependencies required:
```bash
# Download the latest static release
wget https://git.laantungir.net/laantungir/c-relay/releases/download/v0.6.0/c-relay-v0.6.0-linux-x86_64-static
chmod +x c-relay-v0.6.0-linux-x86_64-static
mv c-relay-v0.6.0-linux-x86_64-static c-relay
wget https://git.laantungir.net/laantungir/c-relay-pg/releases/download/v0.6.0/c-relay-pg-v0.6.0-linux-x86_64-static
chmod +x c-relay-pg-v0.6.0-linux-x86_64-static
mv c-relay-pg-v0.6.0-linux-x86_64-static c-relay-pg
# Run the relay
./c-relay
./c-relay-pg
```
**Important**: On first startup, save the **Admin Private Key** displayed in the console. You'll need it for all administrative operations.
@@ -107,8 +107,8 @@ sudo apt install -y build-essential git sqlite3 libsqlite3-dev \
libwebsockets-dev libssl-dev libsecp256k1-dev libcurl4-openssl-dev zlib1g-dev
# Clone and build
git clone https://github.com/your-org/c-relay.git
cd c-relay
git clone https://github.com/your-org/c-relay-pg.git
cd c-relay-pg
git submodule update --init --recursive
./make_and_restart_relay.sh
```
@@ -136,8 +136,8 @@ The web interface provides:
```bash
# Clone repository
git clone https://github.com/your-org/c-relay.git
cd c-relay
git clone https://github.com/your-org/c-relay-pg.git
cd c-relay-pg
git submodule update --init --recursive
# Build
@@ -147,25 +147,25 @@ make clean && make
sudo systemd/install-service.sh
# Start and enable
sudo systemctl start c-relay
sudo systemctl enable c-relay
sudo systemctl start c-relay-pg
sudo systemctl enable c-relay-pg
# Capture admin keys from logs
sudo journalctl -u c-relay | grep "Admin Private Key"
sudo journalctl -u c-relay-pg | grep "Admin Private Key"
```
### Docker Deployment
```bash
# Build Docker image
docker build -f Dockerfile.alpine-musl -t c-relay .
docker build -f Dockerfile.alpine-musl -t c-relay-pg .
# Run container
docker run -d \
--name c-relay \
--name c-relay-pg \
-p 8888:8888 \
-v /path/to/data:/data \
c-relay
c-relay-pg
```
### Cloud Deployment
@@ -181,7 +181,7 @@ See [`docs/deployment_guide.md`](docs/deployment_guide.md) for detailed deployme
## 🔧 Configuration
C-Relay uses an innovative event-based configuration system. All settings are managed through signed Nostr events.
C-Relay-PG uses an innovative event-based configuration system. All settings are managed through signed Nostr events.
### Basic Configuration
@@ -239,8 +239,8 @@ The admin private key is displayed **only once** during first startup. Store it
```bash
# Save to secure location
echo "ADMIN_PRIVKEY=your_admin_private_key" > ~/.c-relay-admin
chmod 600 ~/.c-relay-admin
echo "ADMIN_PRIVKEY=your_admin_private_key" > ~/.c-relay-pg-admin
chmod 600 ~/.c-relay-pg-admin
```
### Production Security
@@ -269,9 +269,9 @@ Contributions are welcome! Please:
## 🔗 Links
- **Repository**: [https://github.com/your-org/c-relay](https://github.com/your-org/c-relay)
- **Releases**: [https://git.laantungir.net/laantungir/c-relay/releases](https://git.laantungir.net/laantungir/c-relay/releases)
- **Issues**: [https://github.com/your-org/c-relay/issues](https://github.com/your-org/c-relay/issues)
- **Repository**: [https://github.com/your-org/c-relay-pg](https://github.com/your-org/c-relay-pg)
- **Releases**: [https://git.laantungir.net/laantungir/c-relay-pg/releases](https://git.laantungir.net/laantungir/c-relay-pg/releases)
- **Issues**: [https://github.com/your-org/c-relay-pg/issues](https://github.com/your-org/c-relay-pg/issues)
- **Nostr Protocol**: [https://github.com/nostr-protocol/nostr](https://github.com/nostr-protocol/nostr)
## 💬 Support
+7 -7
View File
@@ -41,7 +41,7 @@ This guide is specifically tailored for C programs that use:
```bash
# 1. Copy the Dockerfile template (see below)
cp /path/to/c-relay/Dockerfile.alpine-musl ./Dockerfile.static
cp /path/to/c-relay-pg/Dockerfile.alpine-musl ./Dockerfile.static
# 2. Customize for your project (see Customization section)
vim Dockerfile.static
@@ -466,13 +466,13 @@ docker build -t my-app:latest .
docker run --rm my-app:latest --help
```
## Reusing c-relay Files
## Reusing c-relay-pg Files
You can directly copy these files from c-relay:
You can directly copy these files from c-relay-pg:
### 1. Dockerfile.alpine-musl
```bash
cp /path/to/c-relay/Dockerfile.alpine-musl ./Dockerfile.static
cp /path/to/c-relay-pg/Dockerfile.alpine-musl ./Dockerfile.static
```
Then customize:
@@ -482,7 +482,7 @@ Then customize:
### 2. build_static.sh
```bash
cp /path/to/c-relay/build_static.sh ./
cp /path/to/c-relay-pg/build_static.sh ./
```
Then customize:
@@ -492,7 +492,7 @@ Then customize:
### 3. .dockerignore (Optional)
```bash
cp /path/to/c-relay/.dockerignore ./
cp /path/to/c-relay-pg/.dockerignore ./
```
Helps speed up Docker builds by excluding unnecessary files.
@@ -522,7 +522,7 @@ Helps speed up Docker builds by excluding unnecessary files.
- [Alpine Linux](https://alpinelinux.org/)
- [nostr_core_lib](https://github.com/chebizarro/nostr_core_lib)
- [Static Linking Best Practices](https://www.musl-libc.org/faq.html)
- [c-relay Implementation](./docs/musl_static_build.md)
- [c-relay-pg Implementation](./docs/musl_static_build.md)
## Example: Minimal Nostr Client
+2 -2
View File
@@ -4,7 +4,7 @@
<head>
<meta charset="UTF-8">
<meta name="viewport" content="width=device-width, initial-scale=1.0">
<title>C-Relay Admin</title>
<title>C-Relay-PG Admin</title>
<link rel="stylesheet" href="/api/index.css">
</head>
@@ -42,7 +42,7 @@
<span class="relay-letter" data-letter="Y">Y</span>
</div>
<div class="relay-info">
<div id="relay-name" class="relay-name">C-Relay</div>
<div id="relay-name" class="relay-name">C-Relay-PG</div>
<div id="relay-description" class="relay-description">Loading...</div>
<div id="relay-pubkey-container" class="relay-pubkey-container">
<div id="relay-pubkey" class="relay-pubkey">Loading...</div>
+12 -11
View File
@@ -185,7 +185,7 @@ async function fetchRelayInfo(relayUrl) {
method: 'GET',
headers: {
'Accept': 'application/nostr+json',
'User-Agent': 'C-Relay-Admin-API/1.0'
'User-Agent': 'C-Relay-PG-Admin-API/1.0'
},
timeout: 10000 // 10 second timeout
});
@@ -449,11 +449,12 @@ async function setupAutomaticRelayConnection(showSections = false) {
const relayInfo = await fetchRelayInfo(httpUrl);
const fetchedPubkey = relayInfo && relayInfo.pubkey ? relayInfo.pubkey : null;
relayPubkey = fetchedPubkey || '4f355bdcb7cc0af728ef3cceb9615d90684bb5b2ca5f859ab0f0b704075871aa';
// Fallback must match the actual running relay pubkey when NIP-11 doesn't expose pubkey.
relayPubkey = fetchedPubkey || 'e9aa50decff01f2cec1ec2b2e0b34332cf9c92cafdac5a7cc0881a6d26b59854';
// Keep relay metadata (including version) even when pubkey is missing/fallback
relayInfoData = {
name: relayInfo?.name || 'C-Relay',
name: relayInfo?.name || 'C-Relay-PG',
version: relayInfo?.version || '',
description: relayInfo?.description || 'Nostr Relay',
pubkey: relayPubkey
@@ -466,7 +467,7 @@ async function setupAutomaticRelayConnection(showSections = false) {
}
} catch (error) {
console.log('⚠️ Could not fetch relay info, using fallback pubkey:', error.message);
relayPubkey = '4f355bdcb7cc0af728ef3cceb9615d90684bb5b2ca5f859ab0f0b704075871aa';
relayPubkey = 'e9aa50decff01f2cec1ec2b2e0b34332cf9c92cafdac5a7cc0881a6d26b59854';
}
// Set up subscription to receive admin API responses
@@ -3962,9 +3963,9 @@ async function testPostEvent() {
created_at: Math.floor(Date.now() / 1000),
tags: [
["t", "test"],
["client", "c-relay-admin-api"]
["client", "c-relay-pg-admin-api"]
],
content: `Test event from C-Relay Admin API at ${new Date().toISOString()}`
content: `Test event from C-Relay-PG Admin API at ${new Date().toISOString()}`
};
logTestEvent('SENT', `Test event (before signing): ${JSON.stringify(testEvent)}`, 'EVENT');
@@ -4248,7 +4249,7 @@ function updateRelayInfoInHeader() {
// Get relay info from NIP-11/config data or use defaults
const relayInfo = getRelayInfo();
const relayName = relayInfo.name || 'C-Relay';
const relayName = relayInfo.name || 'C-Relay-PG';
const relayVersion = relayInfo.version || '';
const relayDescription = relayInfo.description || 'Nostr Relay';
@@ -4291,7 +4292,7 @@ function getRelayInfo() {
// Default values
return {
name: 'C-Relay',
name: 'C-Relay-PG',
version: '',
description: 'Nostr Relay',
pubkey: relayPubkey
@@ -4302,7 +4303,7 @@ function getRelayInfo() {
function updateStoredRelayInfo(configData) {
if (configData && configData.data) {
// Extract relay info from config data
const relayName = configData.data.find(item => item.key === 'relay_name')?.value || 'C-Relay';
const relayName = configData.data.find(item => item.key === 'relay_name')?.value || 'C-Relay-PG';
const relayVersion = configData.data.find(item => item.key === 'relay_version')?.value || '';
const relayDescription = configData.data.find(item => item.key === 'relay_description')?.value || 'Nostr Relay';
@@ -5420,7 +5421,7 @@ function switchPage(pageName) {
// Initialize the app
document.addEventListener('DOMContentLoaded', () => {
console.log('C-Relay Admin API interface loaded');
console.log('C-Relay-PG Admin API interface loaded');
// Initialize dark mode
initializeDarkMode();
@@ -5522,7 +5523,7 @@ const SQL_QUERY_TEMPLATES = {
};
// Query history management (localStorage)
const QUERY_HISTORY_KEY = 'c_relay_sql_history';
const QUERY_HISTORY_KEY = 'c_relay_pg_sql_history';
const MAX_HISTORY_ITEMS = 20;
// Load query history from localStorage
+11 -11
View File
@@ -1,6 +1,6 @@
#!/bin/bash
# Build fully static MUSL binaries for C-Relay using Alpine Docker
# Build fully static MUSL binaries for C-Relay-PG using Alpine Docker
# Produces truly portable binaries with zero runtime dependencies
set -e
@@ -14,11 +14,11 @@ DEBUG_BUILD=false
if [[ "$1" == "--debug" ]]; then
DEBUG_BUILD=true
echo "=========================================="
echo "C-Relay MUSL Static Binary Builder (DEBUG MODE)"
echo "C-Relay-PG MUSL Static Binary Builder (DEBUG MODE)"
echo "=========================================="
else
echo "=========================================="
echo "C-Relay MUSL Static Binary Builder (PRODUCTION MODE)"
echo "C-Relay-PG MUSL Static Binary Builder (PRODUCTION MODE)"
echo "=========================================="
fi
echo "Project directory: $SCRIPT_DIR"
@@ -63,17 +63,17 @@ ARCH=$(uname -m)
case "$ARCH" in
x86_64)
PLATFORM="linux/amd64"
OUTPUT_NAME="c_relay_static_x86_64"
OUTPUT_NAME="c_relay_pg_static_x86_64"
;;
aarch64|arm64)
PLATFORM="linux/arm64"
OUTPUT_NAME="c_relay_static_arm64"
OUTPUT_NAME="c_relay_pg_static_arm64"
;;
*)
echo "WARNING: Unknown architecture: $ARCH"
echo "Defaulting to linux/amd64"
PLATFORM="linux/amd64"
OUTPUT_NAME="c_relay_static_${ARCH}"
OUTPUT_NAME="c_relay_pg_static_${ARCH}"
;;
esac
@@ -116,14 +116,14 @@ echo "=========================================="
echo "This will:"
echo " - Use Alpine Linux (native MUSL)"
echo " - Build all dependencies statically"
echo " - Compile c-relay with full static linking"
echo " - Compile c-relay-pg with full static linking"
echo ""
$DOCKER_CMD build \
--platform "$PLATFORM" \
--build-arg DEBUG_BUILD=$DEBUG_BUILD \
-f "$DOCKERFILE" \
-t c-relay-musl-builder:latest \
-t c-relay-pg-musl-builder:latest \
--progress=plain \
. || {
echo ""
@@ -147,14 +147,14 @@ $DOCKER_CMD build \
--build-arg DEBUG_BUILD=$DEBUG_BUILD \
--target builder \
-f "$DOCKERFILE" \
-t c-relay-static-builder-stage:latest \
-t c-relay-pg-static-builder-stage:latest \
. > /dev/null 2>&1
# Create a temporary container to copy the binary
CONTAINER_ID=$($DOCKER_CMD create c-relay-static-builder-stage:latest)
CONTAINER_ID=$($DOCKER_CMD create c-relay-pg-static-builder-stage:latest)
# Copy binary from container
$DOCKER_CMD cp "$CONTAINER_ID:/build/c_relay_static" "$BUILD_DIR/$OUTPUT_NAME" || {
$DOCKER_CMD cp "$CONTAINER_ID:/build/c_relay_pg_static" "$BUILD_DIR/$OUTPUT_NAME" || {
echo "ERROR: Failed to extract binary from container"
$DOCKER_CMD rm "$CONTAINER_ID" 2>/dev/null
exit 1
+4 -4
View File
@@ -1,12 +1,12 @@
#!/bin/bash
# Restart the service
sudo systemctl stop c-relay.service
sudo systemctl stop c-relay-pg.service
# Copy the binary to the deployment location
cp build/c_relay_static_x86_64 ~/Storage/c_relay/crelay
cp build/c_relay_pg_static_x86_64 ~/Storage/c_relay_pg/crelay
# Restart the service
sudo systemctl restart c-relay.service
sudo systemctl restart c-relay-pg.service
# Show service status
sudo systemctl status c-relay.service --no-pager -l
sudo systemctl status c-relay-pg.service --no-pager -l
+8 -8
View File
@@ -1,24 +1,24 @@
#!/bin/bash
# C-Relay Static Binary Deployment Script
# Deploys build/c_relay_static_x86_64 to server via ssh
# C-Relay-PG Static Binary Deployment Script
# Deploys build/c_relay_pg_static_x86_64 to server via ssh
set -e
# Configuration
LOCAL_BINARY="build/c_relay_static_x86_64"
REMOTE_BINARY_PATH="/usr/local/bin/c_relay/c_relay"
SERVICE_NAME="c-relay"
LOCAL_BINARY="build/c_relay_pg_static_x86_64"
REMOTE_BINARY_PATH="/usr/local/bin/c_relay_pg/c_relay_pg"
SERVICE_NAME="c-relay-pg"
# Create backup
ssh ubuntu@laantungir.com "sudo cp '$REMOTE_BINARY_PATH' '${REMOTE_BINARY_PATH}.backup.$(date +%Y%m%d_%H%M%S)'" 2>/dev/null || true
# Upload binary to temp location
scp "$LOCAL_BINARY" "ubuntu@laantungir.com:/tmp/c_relay.tmp"
scp "$LOCAL_BINARY" "ubuntu@laantungir.com:/tmp/c_relay_pg.tmp"
# Install binary
ssh ubuntu@laantungir.com "sudo mv '/tmp/c_relay.tmp' '$REMOTE_BINARY_PATH'"
ssh ubuntu@laantungir.com "sudo chown c-relay:c-relay '$REMOTE_BINARY_PATH'"
ssh ubuntu@laantungir.com "sudo mv '/tmp/c_relay_pg.tmp' '$REMOTE_BINARY_PATH'"
ssh ubuntu@laantungir.com "sudo chown c-relay-pg:c-relay-pg '$REMOTE_BINARY_PATH'"
ssh ubuntu@laantungir.com "sudo chmod +x '$REMOTE_BINARY_PATH'"
# Reload systemd and restart service
+15 -15
View File
@@ -1,14 +1,14 @@
#!/bin/bash
# C-Relay Debug Binary Deployment Script
# Deploys build/c_relay_static_x86_64_debug to server for CPU profiling
# C-Relay-PG Debug Binary Deployment Script
# Deploys build/c_relay_pg_static_x86_64_debug to server for CPU profiling
#
# Usage:
# ./deploy_lt_debug.sh -- deploy debug binary and restart
# ./deploy_lt_debug.sh --profile -- deploy, then run perf and fetch results
#
# After deploying, profile with:
# sudo perf record -g -p $(pgrep c_relay) -- sleep 30
# sudo perf record -g -p $(pgrep c_relay_pg) -- sleep 30
# sudo perf report --stdio --sort=symbol --no-children -n 2>/dev/null | head -80
#
# Restore production binary:
@@ -16,10 +16,10 @@
set -e
LOCAL_DEBUG_BINARY="build/c_relay_static_x86_64_debug"
REMOTE_BINARY_PATH="/usr/local/bin/c_relay/c_relay"
REMOTE_PROD_BACKUP="/usr/local/bin/c_relay/c_relay.production"
SERVICE_NAME="c-relay"
LOCAL_DEBUG_BINARY="build/c_relay_pg_static_x86_64_debug"
REMOTE_BINARY_PATH="/usr/local/bin/c_relay_pg/c_relay_pg"
REMOTE_PROD_BACKUP="/usr/local/bin/c_relay_pg/c_relay_pg.production"
SERVICE_NAME="c-relay-pg"
REMOTE_HOST="ubuntu@laantungir.com"
# Check debug binary exists
@@ -33,7 +33,7 @@ if [ ! -f "$LOCAL_DEBUG_BINARY" ]; then
fi
echo "=========================================="
echo "C-Relay Debug Deployment"
echo "C-Relay-PG Debug Deployment"
echo "=========================================="
echo "Binary: $LOCAL_DEBUG_BINARY ($(du -h "$LOCAL_DEBUG_BINARY" | cut -f1))"
echo "Target: $REMOTE_HOST:$REMOTE_BINARY_PATH"
@@ -55,18 +55,18 @@ ssh "$REMOTE_HOST" "
# Upload debug binary
echo "Uploading debug binary..."
scp "$LOCAL_DEBUG_BINARY" "$REMOTE_HOST:/tmp/c_relay_debug.tmp"
scp "$LOCAL_DEBUG_BINARY" "$REMOTE_HOST:/tmp/c_relay_pg_debug.tmp"
# Install debug binary
echo "Installing debug binary..."
ssh "$REMOTE_HOST" "
sudo mv '/tmp/c_relay_debug.tmp' '$REMOTE_BINARY_PATH'
sudo chown c-relay:c-relay '$REMOTE_BINARY_PATH'
sudo mv '/tmp/c_relay_pg_debug.tmp' '$REMOTE_BINARY_PATH'
sudo chown c-relay-pg:c-relay-pg '$REMOTE_BINARY_PATH'
sudo chmod +x '$REMOTE_BINARY_PATH'
"
# Restart service
echo "Restarting c-relay service..."
echo "Restarting c-relay-pg service..."
ssh "$REMOTE_HOST" "sudo systemctl daemon-reload && sudo systemctl restart '$SERVICE_NAME'"
echo ""
@@ -84,9 +84,9 @@ if [ "$1" = "--profile" ]; then
sleep 3
ssh "$REMOTE_HOST" "
PID=\$(pgrep c_relay)
PID=\$(pgrep c_relay_pg)
if [ -z \"\$PID\" ]; then
echo 'ERROR: c_relay not running'
echo 'ERROR: c_relay_pg not running'
exit 1
fi
echo \"Profiling PID \$PID for 30 seconds...\"
@@ -101,7 +101,7 @@ else
echo "=========================================="
echo ""
echo "SSH to server and run:"
echo " sudo perf record -g -p \$(pgrep c_relay) -- sleep 30"
echo " sudo perf record -g -p \$(pgrep c_relay_pg) -- sleep 30"
echo " sudo perf report --stdio --sort=symbol --no-children -n 2>/dev/null | head -60"
echo ""
echo "Or run with --profile to do it automatically:"
+2 -2
View File
@@ -1,4 +1,4 @@
# C-Relay Administrator API Implementation Plan
# C-Relay-PG Administrator API Implementation Plan
## Problem Analysis
@@ -161,7 +161,7 @@ Would require changing schema, migration scripts, and storage logic.
#### README.md Documentation Format:
```markdown
# C-Relay Administrator API
# C-Relay-PG Administrator API
## Authentication
All admin commands require signing with the admin private key generated during first startup.
+190
View File
@@ -0,0 +1,190 @@
# Agent Browser Testing Guide for C-Relay-PG
This document explains how to use the `agent-browser` CLI tool to test the c-relay-pg admin web UI from an AI agent context (no physical display required).
## Prerequisites
- **agent-browser** installed globally: `npm install -g agent-browser`
- Binary location: `/home/user/.nvm/versions/node/v24.14.1/bin/agent-browser`
- The relay must be running locally (default port 8888)
- Test keys configured in `.test_keys`
## Starting the Relay for Local Testing
```bash
./make_and_restart_relay.sh -t
```
This reads `.test_keys` and starts the relay with:
- `ADMIN_PUBKEY` — the hex public key of the admin account
- `ADMIN_PRIVKEY` — the hex secret key (used for browser login, not by the relay itself)
- `SERVER_PRIVKEY` — the relay's own private key
## Key URLs
| URL | Purpose |
|-----|---------|
| `http://127.0.0.1:8888/api/index.html` | Admin web UI (the correct entry point) |
| `http://127.0.0.1:8888/` | Returns 406 without NIP-11 Accept header — **do not use for browser testing** |
| `http://127.0.0.1:8888/` with `Accept: application/nostr+json` | NIP-11 relay info JSON |
**Important:** The root URL `/` is a WebSocket/NIP-11 endpoint, not an HTML page. Always use `/api/index.html` for browser testing.
## Admin Login Credentials
The admin nsec for the current `.test_keys` configuration:
```
nsec: nsec1zkn0hlt4jvcvn9yt5p7ea4m7f82pf3ph0gj3d4rlcz4s64x5z86satv9qm
hex: 15a6fbfd759330c9948ba07d9ed77e49d414c4377a2516d47fc0ab0d54d411f5
npub: npub1tlyzk6slzt8d989f4pn3synk98fea64ea3jmrdc7dtd0kw8uxlas9a9fwr
hex pubkey: 5fc82b6a1f12ced29ca9a86718127629d39eeab9ec65b1b71e6adafb38fc37fb
```
## Complete Login Flow with agent-browser
### Step 1: Open the admin UI
```bash
agent-browser open http://127.0.0.1:8888/api/index.html
agent-browser wait --load networkidle
```
### Step 2: Take an interactive snapshot to see what is on screen
```bash
agent-browser snapshot -i
```
You will see a login modal with buttons like:
- `"Browser Extension"` — skip this
- `"Local Key"`**use this one**
- `"Seed Phrase"` — skip
- `"Nostr Connect"` — skip
- `"Read Only"` — skip
### Step 3: Click "Local Key"
```bash
agent-browser click @e15
```
(The ref number may vary — use the ref from the snapshot output for the "Local Key" button.)
After clicking, a text input appears asking for the secret key.
### Step 4: Enter the admin nsec
```bash
agent-browser fill @e14 "nsec1zkn0hlt4jvcvn9yt5p7ea4m7f82pf3ph0gj3d4rlcz4s64x5z86satv9qm"
```
(Use the ref from the snapshot for the textbox element.)
### Step 5: Click "Import Key"
```bash
agent-browser snapshot -i
```
Check the snapshot — the "Import Key" button should now be enabled. Click it:
```bash
agent-browser click @e15
```
### Step 6: Click "Continue" on the success screen
After import, a success screen appears with "Continue" button:
```bash
agent-browser wait 800
agent-browser snapshot -i
agent-browser click @e19
```
(Use the ref from the snapshot for the "Continue" button.)
### Step 7: Wait for admin UI to load
```bash
agent-browser wait 5000
agent-browser snapshot -i
```
You should now see the admin dashboard with:
- Statistics table (Database Size, Total Events, PID, etc.)
- Navigation buttons (Statistics, Subscriptions, Configuration, Authorization, etc.)
- Admin profile area showing "admin" label
## Navigating Admin Sections
After login, use the sidebar navigation buttons:
```bash
# View configuration
agent-browser click @e8 # Configuration button ref
# View authorization rules
agent-browser click @e9 # Authorization button ref
# View statistics
agent-browser click @e6 # Statistics button ref
# Always snapshot after navigation to see results
agent-browser wait 2500
agent-browser snapshot -i
```
## Checking for Errors
```bash
# View browser console logs
agent-browser console
# View JavaScript errors
agent-browser errors
# View relay server logs
tail -n 100 relay.log
```
## Chained Command Example (Full Login in One Shot)
```bash
agent-browser open http://127.0.0.1:8888/api/index.html && \
agent-browser wait --load networkidle && \
agent-browser snapshot -i
```
Then use refs from the snapshot to complete login steps.
## Tips for AI Agents
1. **Always use `/api/index.html`** — never the root URL
2. **Use `snapshot -i`** after every action to see the current interactive elements and their refs
3. **Refs change** between snapshots — always re-snapshot before clicking
4. **Wait after clicks** — use `agent-browser wait 2000` (milliseconds) between actions that trigger async operations
5. **The login flow has 3 screens**: method selection → key input → success confirmation
6. **Console logs are cumulative** — they show all logs since page load, which is useful for debugging admin API responses
7. **The relay log** at `relay.log` shows server-side processing of admin commands
8. **Command chaining** with `&&` works — the browser daemon persists between commands
## Verifying Admin API is Working
After login, the statistics page should show populated data:
- Database Size (e.g., "4 KB")
- Process ID (the relay PID)
- WebSocket Connections count
- Memory Usage
If these show "-" or "Loading...", check:
1. `relay.log` for errors
2. `agent-browser console` for JavaScript errors
3. `agent-browser errors` for page-level errors
## Closing the Browser
```bash
agent-browser close --all
```
+11 -11
View File
@@ -324,7 +324,7 @@ int main() {
}
```
## Migration Plan for c-relay
## Migration Plan for c-relay-pg
### Phase 1: Extract Debug System
1. Create `c_utils_lib` repository
@@ -333,16 +333,16 @@ int main() {
4. Add basic tests
### Phase 2: Add Versioning System
1. Extract version generation logic from c-relay
1. Extract version generation logic from c-relay-pg
2. Create reusable version utilities
3. Update c-relay to use new system
3. Update c-relay-pg to use new system
4. Update nostr_core_lib to use new system
### Phase 3: Add as Submodule
1. Add `c_utils_lib` as submodule to c-relay
2. Update c-relay Makefile
3. Update includes in c-relay source files
4. Remove old debug files from c-relay
1. Add `c_utils_lib` as submodule to c-relay-pg
2. Update c-relay-pg Makefile
3. Update includes in c-relay-pg source files
4. Remove old debug files from c-relay-pg
### Phase 4: Documentation & Examples
1. Create comprehensive README
@@ -352,7 +352,7 @@ int main() {
## Benefits
### For c-relay
### For c-relay-pg
- Cleaner separation of concerns
- Reusable utilities across projects
- Easier to maintain and test
@@ -379,7 +379,7 @@ int main() {
- Memory leak detection (valgrind)
### Integration Tests
- Test with real projects (c-relay, nostr_core_lib)
- Test with real projects (c-relay-pg, nostr_core_lib)
- Cross-platform testing
- Performance benchmarks
@@ -433,7 +433,7 @@ MIT License - permissive and suitable for learning and commercial use.
## Success Criteria
1. ✅ Successfully integrated into c-relay
1. ✅ Successfully integrated into c-relay-pg
2. ✅ Successfully integrated into nostr_core_lib
3. ✅ All tests passing
4. ✅ Documentation complete
@@ -449,7 +449,7 @@ MIT License - permissive and suitable for learning and commercial use.
4. Create build system
5. Write tests
6. Create documentation
7. Integrate into c-relay
7. Integrate into c-relay-pg
8. Publish to GitHub
---
+16 -16
View File
@@ -2,13 +2,13 @@
## Overview
This document provides a step-by-step implementation plan for creating the `c_utils_lib` library and integrating it into the c-relay project.
This document provides a step-by-step implementation plan for creating the `c_utils_lib` library and integrating it into the c-relay-pg project.
## Phase 1: Repository Setup & Structure
### Step 1.1: Create Repository Structure
**Location**: Create outside c-relay project (sibling directory)
**Location**: Create outside c-relay-pg project (sibling directory)
```bash
# Create directory structure
@@ -42,7 +42,7 @@ git branch -M main
### Step 2.1: Move Debug Files
**Source files** (from c-relay):
**Source files** (from c-relay-pg):
- `src/debug.c``c_utils_lib/src/debug.c`
- `src/debug.h``c_utils_lib/include/c_utils/debug.h`
@@ -484,20 +484,20 @@ How to integrate into projects:
3. Code examples
4. Migration from standalone utilities
## Phase 7: Integration with c-relay
## Phase 7: Integration with c-relay-pg
### Step 7.1: Add as Submodule
```bash
cd /path/to/c-relay
cd /path/to/c-relay-pg
git submodule add <repo-url> c_utils_lib
git submodule update --init --recursive
```
### Step 7.2: Update c-relay Makefile
### Step 7.2: Update c-relay-pg Makefile
```makefile
# Add to c-relay Makefile
# Add to c-relay-pg Makefile
C_UTILS_LIB = c_utils_lib/libc_utils.a
# Update includes
@@ -514,7 +514,7 @@ $(C_UTILS_LIB):
$(TARGET): $(C_UTILS_LIB) ...
```
### Step 7.3: Update c-relay Source Files
### Step 7.3: Update c-relay-pg Source Files
**Changes needed**:
@@ -543,7 +543,7 @@ $(TARGET): $(C_UTILS_LIB) ...
### Step 7.4: Test Integration
```bash
cd c-relay
cd c-relay-pg
make clean
make
./make_and_restart_relay.sh
@@ -556,7 +556,7 @@ Verify:
## Phase 8: Version System Integration
### Step 8.1: Update c-relay Makefile for Versioning
### Step 8.1: Update c-relay-pg Makefile for Versioning
```makefile
# Add version generation
@@ -567,7 +567,7 @@ src/version.h: .git/refs/tags/*
$(TARGET): src/version.h ...
```
### Step 8.2: Update c-relay to Use Generated Version
### Step 8.2: Update c-relay-pg to Use Generated Version
Replace hardcoded version in `src/main.h` with:
```c
@@ -583,7 +583,7 @@ Replace hardcoded version in `src/main.h` with:
- **Phase 4**: Build System - 2 hours
- **Phase 5**: Examples & Tests - 3 hours
- **Phase 6**: Documentation - 3 hours
- **Phase 7**: c-relay Integration - 2 hours
- **Phase 7**: c-relay-pg Integration - 2 hours
- **Phase 8**: Version Integration - 2 hours
**Total**: ~19 hours
@@ -593,11 +593,11 @@ Replace hardcoded version in `src/main.h` with:
- [ ] c_utils_lib builds successfully
- [ ] All tests pass
- [ ] Examples compile and run
- [ ] c-relay integrates successfully
- [ ] Debug output works in c-relay
- [ ] c-relay-pg integrates successfully
- [ ] Debug output works in c-relay-pg
- [ ] Version generation works
- [ ] Documentation complete
- [ ] No regressions in c-relay functionality
- [ ] No regressions in c-relay-pg functionality
## Next Steps
@@ -608,7 +608,7 @@ Replace hardcoded version in `src/main.h` with:
5. Create build system
6. Write tests and examples
7. Create documentation
8. Integrate into c-relay
8. Integrate into c-relay-pg
9. Test thoroughly
10. Publish to GitHub
+6 -6
View File
@@ -78,9 +78,9 @@ Configuration events follow the standard Nostr event format with kind 33334:
#### `relay_software`
- **Description**: Software identifier for NIP-11
- **Default**: `"c-relay"`
- **Default**: `"c-relay-pg"`
- **Format**: String, max 64 characters
- **Example**: `"c-relay v1.0.0"`
- **Example**: `"c-relay-pg v1.0.0"`
#### `relay_version`
- **Description**: Software version string
@@ -366,7 +366,7 @@ sqlite3 relay.nrdb "SELECT json_pretty(json_object(
#### Invalid Parameter Values
```bash
# Check relay logs for validation errors
journalctl -u c-relay | grep "Configuration.*invalid\|Invalid.*configuration"
journalctl -u c-relay-pg | grep "Configuration.*invalid\|Invalid.*configuration"
# Common issues:
# - Numeric values outside valid ranges
@@ -395,10 +395,10 @@ ORDER BY date DESC;"
#### Resource Usage After Changes
```bash
# Monitor system resources after configuration updates
top -p $(pgrep c_relay)
top -p $(pgrep c_relay_pg)
# Check for memory leaks
ps aux | grep c_relay | awk '{print $6}' # RSS memory
ps aux | grep c_relay_pg | awk '{print $6}' # RSS memory
```
### Emergency Recovery
@@ -425,7 +425,7 @@ nostrtool event \
# If database is corrupted, backup and recreate
cp relay.nrdb relay.nrdb.backup
rm relay.nrdb*
./build/c_relay_x86 # Creates fresh database with new keys
./build/c_relay_pg_x86 # Creates fresh database with new keys
```
---
+14 -14
View File
@@ -21,22 +21,22 @@ typedef enum {
```bash
# Production (default - no debug output)
./c_relay_x86
./c_relay_pg_x86
# Show errors only
./c_relay_x86 --debug-level=1
./c_relay_pg_x86 --debug-level=1
# Show errors and warnings
./c_relay_x86 --debug-level=2
./c_relay_pg_x86 --debug-level=2
# Show errors, warnings, and info (recommended for development)
./c_relay_x86 --debug-level=3
./c_relay_pg_x86 --debug-level=3
# Show all debug messages
./c_relay_x86 --debug-level=4
./c_relay_pg_x86 --debug-level=4
# Show everything including trace with file:line (very verbose)
./c_relay_x86 --debug-level=5
./c_relay_pg_x86 --debug-level=5
```
## Implementation
@@ -475,7 +475,7 @@ When `g_debug_level = 0` (constant), you'll see the compiler has removed all deb
### Level 3 (Errors + Warnings + Info)
```
[2025-01-12 14:30:15] [INFO ] Initializing C-Relay v0.4.6
[2025-01-12 14:30:15] [INFO ] Initializing C-Relay-PG v0.4.6
[2025-01-12 14:30:15] [INFO ] Loading configuration from database
[2025-01-12 14:30:15] [ERROR] Failed to open database: permission denied
[2025-01-12 14:30:16] [WARN ] Port 8888 unavailable, trying 8889
@@ -484,7 +484,7 @@ When `g_debug_level = 0` (constant), you'll see the compiler has removed all deb
### Level 4 (All Debug Messages)
```
[2025-01-12 14:30:15] [INFO ] Initializing C-Relay v0.4.6
[2025-01-12 14:30:15] [INFO ] Initializing C-Relay-PG v0.4.6
[2025-01-12 14:30:15] [DEBUG] Opening database: build/abc123...def.db
[2025-01-12 14:30:15] [DEBUG] Executing schema initialization
[2025-01-12 14:30:15] [INFO ] SQLite WAL mode enabled
@@ -496,7 +496,7 @@ When `g_debug_level = 0` (constant), you'll see the compiler has removed all deb
### Level 5 (Everything Including file:line for ALL messages)
```
[2025-01-12 14:30:15] [INFO ] [main.c:1607] Initializing C-Relay v0.4.6
[2025-01-12 14:30:15] [INFO ] [main.c:1607] Initializing C-Relay-PG v0.4.6
[2025-01-12 14:30:15] [DEBUG] [main.c:348] Opening database: build/abc123...def.db
[2025-01-12 14:30:15] [TRACE] [main.c:330] Entering init_database()
[2025-01-12 14:30:15] [ERROR] [config.c:125] Database locked
@@ -525,11 +525,11 @@ Update the existing `log_*` functions to use the new debug macros
make clean && make
# Test different levels
./build/c_relay_x86 # No output
./build/c_relay_x86 --debug-level=1 # Errors only
./build/c_relay_x86 --debug-level=3 # Info + warnings + errors
./build/c_relay_x86 --debug-level=4 # All debug messages
./build/c_relay_x86 --debug-level=5 # Everything with file:line on TRACE
./build/c_relay_pg_x86 # No output
./build/c_relay_pg_x86 --debug-level=1 # Errors only
./build/c_relay_pg_x86 --debug-level=3 # Info + warnings + errors
./build/c_relay_pg_x86 --debug-level=4 # All debug messages
./build/c_relay_pg_x86 --debug-level=5 # Everything with file:line on TRACE
```
### Step 5: Gradual Migration (Ongoing)
+1 -1
View File
@@ -34,7 +34,7 @@ static const struct {
// NIP-11 Relay Information (relay keys will be populated at runtime)
{"relay_description", "High-performance C Nostr relay with SQLite storage"},
{"relay_contact", ""},
{"relay_software", "https://git.laantungir.net/laantungir/c-relay.git"},
{"relay_software", "https://git.laantungir.net/laantungir/c-relay-pg.git"},
{"relay_version", "v1.0.0"},
// NIP-13 Proof of Work (pow_min_difficulty = 0 means PoW disabled)
+51 -51
View File
@@ -52,11 +52,11 @@ sudo apt install -y build-essential git sqlite3 libsqlite3-dev \
#### User and Directory Setup
```bash
# Create dedicated system user
sudo useradd --system --home-dir /opt/c-relay --shell /bin/false c-relay
sudo useradd --system --home-dir /opt/c-relay-pg --shell /bin/false c-relay-pg
# Create application directory
sudo mkdir -p /opt/c-relay
sudo chown c-relay:c-relay /opt/c-relay
sudo mkdir -p /opt/c-relay-pg
sudo chown c-relay-pg:c-relay-pg /opt/c-relay-pg
```
### Build and Installation
@@ -64,8 +64,8 @@ sudo chown c-relay:c-relay /opt/c-relay
#### Automated Installation (Recommended)
```bash
# Clone repository
git clone https://github.com/your-org/c-relay.git
cd c-relay
git clone https://github.com/your-org/c-relay-pg.git
cd c-relay-pg
git submodule update --init --recursive
# Build
@@ -81,12 +81,12 @@ sudo systemd/install-service.sh
make clean && make
# Install binary
sudo cp build/c_relay_x86 /opt/c-relay/
sudo chown c-relay:c-relay /opt/c-relay/c_relay_x86
sudo chmod +x /opt/c-relay/c_relay_x86
sudo cp build/c_relay_pg_x86 /opt/c-relay-pg/
sudo chown c-relay-pg:c-relay-pg /opt/c-relay-pg/c_relay_pg_x86
sudo chmod +x /opt/c-relay-pg/c_relay_pg_x86
# Install systemd service
sudo cp systemd/c-relay.service /etc/systemd/system/
sudo cp systemd/c-relay-pg.service /etc/systemd/system/
sudo systemctl daemon-reload
```
@@ -95,22 +95,22 @@ sudo systemctl daemon-reload
#### Start and Enable Service
```bash
# Start the service
sudo systemctl start c-relay
sudo systemctl start c-relay-pg
# Enable auto-start on boot
sudo systemctl enable c-relay
sudo systemctl enable c-relay-pg
# Check status
sudo systemctl status c-relay
sudo systemctl status c-relay-pg
```
#### Capture Admin Keys (CRITICAL)
```bash
# View startup logs to get admin keys
sudo journalctl -u c-relay --since "5 minutes ago" | grep -A 10 "IMPORTANT: SAVE THIS ADMIN PRIVATE KEY"
sudo journalctl -u c-relay-pg --since "5 minutes ago" | grep -A 10 "IMPORTANT: SAVE THIS ADMIN PRIVATE KEY"
# Or check the full log
sudo journalctl -u c-relay --no-pager | grep "Admin Private Key"
sudo journalctl -u c-relay-pg --no-pager | grep "Admin Private Key"
```
⚠️ **CRITICAL**: Save the admin private key immediately - it's only shown once and is needed for all configuration updates!
@@ -151,8 +151,8 @@ sudo iptables-save > /etc/iptables/rules.v4
ssh -i your-key.pem ubuntu@your-instance-ip
# Use the simple deployment script
git clone https://github.com/your-org/c-relay.git
cd c-relay
git clone https://github.com/your-org/c-relay-pg.git
cd c-relay-pg
sudo examples/deployment/simple-vps/deploy.sh
```
@@ -168,10 +168,10 @@ sudo examples/deployment/simple-vps/deploy.sh
sudo mkfs.ext4 /dev/xvdf
sudo mkdir /data
sudo mount /dev/xvdf /data
sudo chown c-relay:c-relay /data
sudo chown c-relay-pg:c-relay-pg /data
# Update systemd service to use /data
sudo sed -i 's/WorkingDirectory=\/opt\/c-relay/WorkingDirectory=\/data/' /etc/systemd/system/c-relay.service
sudo sed -i 's/WorkingDirectory=\/opt\/c-relay-pg/WorkingDirectory=\/data/' /etc/systemd/system/c-relay-pg.service
sudo systemctl daemon-reload
```
@@ -180,7 +180,7 @@ sudo systemctl daemon-reload
#### Compute Engine Setup
```bash
# Create VM instance (e2-micro or larger)
gcloud compute instances create c-relay-instance \
gcloud compute instances create c-relay-pg-instance \
--image-family=ubuntu-2204-lts \
--image-project=ubuntu-os-cloud \
--machine-type=e2-micro \
@@ -193,9 +193,9 @@ gcloud compute firewall-rules create allow-nostr-relay \
--target-tags nostr-relay
# SSH and deploy
gcloud compute ssh c-relay-instance
git clone https://github.com/your-org/c-relay.git
cd c-relay
gcloud compute ssh c-relay-pg-instance
git clone https://github.com/your-org/c-relay-pg.git
cd c-relay-pg
sudo examples/deployment/simple-vps/deploy.sh
```
@@ -203,13 +203,13 @@ sudo examples/deployment/simple-vps/deploy.sh
```bash
# Create and attach persistent disk
gcloud compute disks create relay-data --size=50GB
gcloud compute instances attach-disk c-relay-instance --disk=relay-data
gcloud compute instances attach-disk c-relay-pg-instance --disk=relay-data
# Format and mount
sudo mkfs.ext4 /dev/sdb
sudo mkdir /data
sudo mount /dev/sdb /data
sudo chown c-relay:c-relay /data
sudo chown c-relay-pg:c-relay-pg /data
```
### DigitalOcean
@@ -223,8 +223,8 @@ sudo chown c-relay:c-relay /data
ssh root@your-droplet-ip
# Deploy relay
git clone https://github.com/your-org/c-relay.git
cd c-relay
git clone https://github.com/your-org/c-relay-pg.git
cd c-relay-pg
examples/deployment/simple-vps/deploy.sh
```
@@ -245,8 +245,8 @@ The `examples/deployment/` directory contains ready-to-use scripts:
### Simple VPS Deployment
```bash
# Clone repository and run automated deployment
git clone https://github.com/your-org/c-relay.git
cd c-relay
git clone https://github.com/your-org/c-relay-pg.git
cd c-relay-pg
sudo examples/deployment/simple-vps/deploy.sh
```
@@ -417,9 +417,9 @@ LOG_FILE="/var/log/relay-monitor.log"
DATE=$(date '+%Y-%m-%d %H:%M:%S')
# Check if relay is running
if ! pgrep -f "c_relay_x86" > /dev/null; then
if ! pgrep -f "c_relay_pg_x86" > /dev/null; then
echo "[$DATE] ERROR: Relay process not running" >> $LOG_FILE
systemctl restart c-relay
systemctl restart c-relay-pg
fi
# Check port availability
@@ -428,14 +428,14 @@ if ! netstat -tln | grep -q ":8888"; then
fi
# Check database file
RELAY_DB=$(find /opt/c-relay -name "*.nrdb" | head -1)
RELAY_DB=$(find /opt/c-relay-pg -name "*.nrdb" | head -1)
if [[ -n "$RELAY_DB" ]]; then
DB_SIZE=$(du -h "$RELAY_DB" | cut -f1)
echo "[$DATE] INFO: Database size: $DB_SIZE" >> $LOG_FILE
fi
# Check memory usage
MEM_USAGE=$(ps aux | grep c_relay_x86 | grep -v grep | awk '{print $6}')
MEM_USAGE=$(ps aux | grep c_relay_pg_x86 | grep -v grep | awk '{print $6}')
if [[ -n "$MEM_USAGE" ]]; then
echo "[$DATE] INFO: Memory usage: ${MEM_USAGE}KB" >> $LOG_FILE
fi
@@ -454,8 +454,8 @@ sudo chmod +x /usr/local/bin/relay-monitor.sh
#### Centralized Logging with rsyslog
```bash
# /etc/rsyslog.d/50-c-relay.conf
if $programname == 'c-relay' then /var/log/c-relay.log
# /etc/rsyslog.d/50-c-relay-pg.conf
if $programname == 'c-relay-pg' then /var/log/c-relay-pg.log
& stop
```
@@ -465,7 +465,7 @@ if $programname == 'c-relay' then /var/log/c-relay.log
```yaml
# /etc/prometheus/prometheus.yml
scrape_configs:
- job_name: 'c-relay'
- job_name: 'c-relay-pg'
static_configs:
- targets: ['localhost:8888']
metrics_path: '/metrics' # If implemented
@@ -479,30 +479,30 @@ scrape_configs:
#### Service User Restrictions
```bash
# Restrict service user
sudo usermod -s /bin/false c-relay
sudo usermod -d /opt/c-relay c-relay
sudo usermod -s /bin/false c-relay-pg
sudo usermod -d /opt/c-relay-pg c-relay-pg
# Set proper permissions
sudo chmod 700 /opt/c-relay
sudo chown -R c-relay:c-relay /opt/c-relay
sudo chmod 700 /opt/c-relay-pg
sudo chown -R c-relay-pg:c-relay-pg /opt/c-relay-pg
```
#### File System Restrictions
```bash
# Mount data directory with appropriate options
echo "/dev/sdb /opt/c-relay ext4 defaults,noexec,nosuid,nodev 0 2" >> /etc/fstab
echo "/dev/sdb /opt/c-relay-pg ext4 defaults,noexec,nosuid,nodev 0 2" >> /etc/fstab
```
### Network Security
#### Fail2Ban Configuration
```ini
# /etc/fail2ban/jail.d/c-relay.conf
[c-relay-dos]
# /etc/fail2ban/jail.d/c-relay-pg.conf
[c-relay-pg-dos]
enabled = true
port = 8888
filter = c-relay-dos
logpath = /var/log/c-relay.log
filter = c-relay-pg-dos
logpath = /var/log/c-relay-pg.log
maxretry = 10
findtime = 60
bantime = 300
@@ -534,9 +534,9 @@ sudo mkfs.ext4 /dev/mapper/relay-data
#!/bin/bash
# /usr/local/bin/backup-relay.sh
BACKUP_DIR="/backup/c-relay"
BACKUP_DIR="/backup/c-relay-pg"
DATE=$(date +%Y%m%d_%H%M%S)
RELAY_DB=$(find /opt/c-relay -name "*.nrdb" | head -1)
RELAY_DB=$(find /opt/c-relay-pg -name "*.nrdb" | head -1)
mkdir -p "$BACKUP_DIR"
@@ -574,7 +574,7 @@ sudo apt install -y awscli
aws configure
# Sync backups to S3
aws s3 sync /backup/c-relay/ s3://your-backup-bucket/c-relay/ --delete
aws s3 sync /backup/c-relay-pg/ s3://your-backup-bucket/c-relay-pg/ --delete
```
### Disaster Recovery
@@ -583,16 +583,16 @@ aws s3 sync /backup/c-relay/ s3://your-backup-bucket/c-relay/ --delete
```bash
# 1. Restore from backup
gunzip backup/relay_backup_20231201_020000.nrdb.gz
cp backup/relay_backup_20231201_020000.nrdb /opt/c-relay/
cp backup/relay_backup_20231201_020000.nrdb /opt/c-relay-pg/
# 2. Fix permissions
sudo chown c-relay:c-relay /opt/c-relay/*.nrdb
sudo chown c-relay-pg:c-relay-pg /opt/c-relay-pg/*.nrdb
# 3. Restart service
sudo systemctl restart c-relay
sudo systemctl restart c-relay-pg
# 4. Verify recovery
sudo journalctl -u c-relay --since "1 minute ago"
sudo journalctl -u c-relay-pg --since "1 minute ago"
```
---
+25 -25
View File
@@ -2,7 +2,7 @@
## Overview
This guide explains how to build truly portable MUSL-based static binaries of c-relay using Alpine Linux Docker containers. These binaries have **zero runtime dependencies** and work on any Linux distribution.
This guide explains how to build truly portable MUSL-based static binaries of c-relay-pg using Alpine Linux Docker containers. These binaries have **zero runtime dependencies** and work on any Linux distribution.
## Why MUSL?
@@ -36,8 +36,8 @@ This guide explains how to build truly portable MUSL-based static binaries of c-
./build_static.sh
# The binary will be created at:
# build/c_relay_static_musl_x86_64 (on x86_64)
# build/c_relay_static_musl_arm64 (on ARM64)
# build/c_relay_pg_static_musl_x86_64 (on x86_64)
# build/c_relay_pg_static_musl_arm64 (on ARM64)
```
### What Happens During Build
@@ -56,7 +56,7 @@ This guide explains how to build truly portable MUSL-based static binaries of c-
- Includes required NIPs: 001, 006, 013, 017, 019, 044, 059
- Produces static library (~316KB)
4. **c-relay Compilation**: Links everything statically:
4. **c-relay-pg Compilation**: Links everything statically:
- All source files compiled with `-static` flag
- Fortification disabled to avoid `__*_chk` symbols
- Results in ~7.6MB stripped binary
@@ -78,7 +78,7 @@ FROM alpine:3.19 AS builder
- Install build tools and static libraries
- Build dependencies from source
- Compile nostr_core_lib with MUSL flags
- Compile c-relay with full static linking
- Compile c-relay-pg with full static linking
- Strip binary to reduce size
# Stage 2: Output (scratch)
@@ -93,7 +93,7 @@ FROM scratch AS output
CFLAGS="-U_FORTIFY_SOURCE -D_FORTIFY_SOURCE=0 -Wall -Wextra -std=c99 -fPIC -O2"
```
**For c-relay:**
**For c-relay-pg:**
```bash
gcc -static -O2 -Wall -Wextra -std=c99 \
-U_FORTIFY_SOURCE -D_FORTIFY_SOURCE=0 \
@@ -110,7 +110,7 @@ gcc -static -O2 -Wall -Wextra -std=c99 \
- `-U_FORTIFY_SOURCE` (undefine any existing definition)
- `-D_FORTIFY_SOURCE=0` (set to 0)
This must be applied to **both** nostr_core_lib and c-relay compilation.
This must be applied to **both** nostr_core_lib and c-relay-pg compilation.
### NIP Dependencies
@@ -129,26 +129,26 @@ The build includes these NIPs in nostr_core_lib:
```bash
# Should show "statically linked"
file build/c_relay_static_musl_x86_64
file build/c_relay_pg_static_musl_x86_64
# Should show "not a dynamic executable"
ldd build/c_relay_static_musl_x86_64
ldd build/c_relay_pg_static_musl_x86_64
# Check size (should be ~7.6MB)
ls -lh build/c_relay_static_musl_x86_64
ls -lh build/c_relay_pg_static_musl_x86_64
```
### Test Execution
```bash
# Show help
./build/c_relay_static_musl_x86_64 --help
./build/c_relay_pg_static_musl_x86_64 --help
# Show version
./build/c_relay_static_musl_x86_64 --version
./build/c_relay_pg_static_musl_x86_64 --version
# Run relay
./build/c_relay_static_musl_x86_64 --port 8888
./build/c_relay_pg_static_musl_x86_64 --port 8888
```
### Cross-Distribution Testing
@@ -157,16 +157,16 @@ Test the binary on different distributions to verify portability:
```bash
# Alpine Linux
docker run --rm -v $(pwd)/build:/app alpine:latest /app/c_relay_static_musl_x86_64 --version
docker run --rm -v $(pwd)/build:/app alpine:latest /app/c_relay_pg_static_musl_x86_64 --version
# Ubuntu
docker run --rm -v $(pwd)/build:/app ubuntu:latest /app/c_relay_static_musl_x86_64 --version
docker run --rm -v $(pwd)/build:/app ubuntu:latest /app/c_relay_pg_static_musl_x86_64 --version
# Debian
docker run --rm -v $(pwd)/build:/app debian:latest /app/c_relay_static_musl_x86_64 --version
docker run --rm -v $(pwd)/build:/app debian:latest /app/c_relay_pg_static_musl_x86_64 --version
# CentOS
docker run --rm -v $(pwd)/build:/app centos:latest /app/c_relay_static_musl_x86_64 --version
docker run --rm -v $(pwd)/build:/app centos:latest /app/c_relay_pg_static_musl_x86_64 --version
```
## Troubleshooting
@@ -187,7 +187,7 @@ newgrp docker # Or logout and login again
**Solution**: Ensure fortification is disabled in both:
1. nostr_core_lib build.sh (line 534)
2. c-relay compilation flags in Dockerfile
2. c-relay-pg compilation flags in Dockerfile
### Binary Won't Execute
@@ -213,26 +213,26 @@ newgrp docker # Or logout and login again
```bash
# Copy binary to server
scp build/c_relay_static_musl_x86_64 user@server:/opt/c-relay/
scp build/c_relay_pg_static_musl_x86_64 user@server:/opt/c-relay-pg/
# Run on server (no dependencies needed!)
ssh user@server
cd /opt/c-relay
./c_relay_static_musl_x86_64 --port 8888
cd /opt/c-relay-pg
./c_relay_pg_static_musl_x86_64 --port 8888
```
### SystemD Service
```ini
[Unit]
Description=C-Relay Nostr Relay (MUSL Static)
Description=C-Relay-PG Nostr Relay (MUSL Static)
After=network.target
[Service]
Type=simple
User=c-relay
WorkingDirectory=/opt/c-relay
ExecStart=/opt/c-relay/c_relay_static_musl_x86_64
User=c-relay-pg
WorkingDirectory=/opt/c-relay-pg
ExecStart=/opt/c-relay-pg/c_relay_pg_static_musl_x86_64
Restart=always
RestartSec=5
+4 -4
View File
@@ -23,7 +23,7 @@ We chose Option B because:
## Detailed Implementation Steps
### Phase 1: Configuration Setup in c-relay
### Phase 1: Configuration Setup in c-relay-pg
#### 1.1 Add Configuration Parameter
**File:** `src/default_config_event.h`
@@ -293,7 +293,7 @@ int nostr_nip17_send_dm(cJSON* dm_event,
---
### Phase 4: Update c-relay Call Sites
### Phase 4: Update c-relay-pg Call Sites
#### 4.1 Update src/api.c
**Location:** Line 1319
@@ -470,7 +470,7 @@ causing compatibility issues.
- [ ] Update `nostr_nip17_send_dm()` signature and implementation
- [ ] Update `nip017.h` function declaration and documentation
### c-relay Changes
### c-relay-pg Changes
- [ ] Add `nip59_timestamp_max_delay_sec` to `default_config_event.h`
- [ ] Add validation in `config.c` for new parameter
- [ ] Update `src/api.c` call site to pass `max_delay_sec`
@@ -494,7 +494,7 @@ causing compatibility issues.
If issues arise:
1. Revert nostr_core_lib changes (git revert in submodule)
2. Revert c-relay changes
2. Revert c-relay-pg changes
3. Configuration parameter will be ignored if not used
4. Default behavior (0) provides maximum compatibility
+2 -2
View File
@@ -2,7 +2,7 @@
## Overview
This document describes the design for a general-purpose SQL query interface for the C-Relay admin API. This allows administrators to execute read-only SQL queries against the relay database through cryptographically signed kind 23456 events with NIP-44 encrypted command arrays.
This document describes the design for a general-purpose SQL query interface for the C-Relay-PG admin API. This allows administrators to execute read-only SQL queries against the relay database through cryptographically signed kind 23456 events with NIP-44 encrypted command arrays.
## Security Model
@@ -271,7 +271,7 @@ const SQL_QUERY_TEMPLATES = {
};
// Query history management (localStorage)
const QUERY_HISTORY_KEY = 'c_relay_sql_history';
const QUERY_HISTORY_KEY = 'c_relay_pg_sql_history';
const MAX_HISTORY_ITEMS = 20;
// Load query history from localStorage
+1 -1
View File
@@ -224,7 +224,7 @@ The script should:
- `curl` or `websocat` for WebSocket communication
- `jq` for JSON parsing
- Nostr CLI tools (optional, for event signing)
- Running c-relay instance
- Running c-relay-pg instance
## Example Output
+6 -6
View File
@@ -1,8 +1,8 @@
# C-Relay Complete Startup Flow Documentation
# C-Relay-PG Complete Startup Flow Documentation
## Overview
C-Relay has two distinct startup paths:
C-Relay-PG has two distinct startup paths:
1. **First-Time Startup**: No database exists, generates keys and initializes system
2. **Existing Relay Startup**: Database exists, loads configuration and resumes operation
@@ -220,7 +220,7 @@ int populate_default_config_values(void) {
struct config_default defaults[] = {
{"relay_port", "8888", "integer", "WebSocket port", "network", 1},
{"relay_name", "C-Relay", "string", "Relay name", "info", 0},
{"relay_name", "C-Relay-PG", "string", "Relay name", "info", 0},
{"relay_description", "High-performance C Nostr relay", "string", "Description", "info", 0},
{"max_subscriptions_per_client", "25", "integer", "Max subs per client", "limits", 0},
{"pow_min_difficulty", "0", "integer", "Minimum PoW difficulty", "security", 0},
@@ -983,7 +983,7 @@ if (current_version < LATEST_VERSION) {
**Solutions**:
- Use `--port <number>` to specify different port
- Kill existing process: `pkill -f c_relay_`
- Kill existing process: `pkill -f c_relay_pg_`
- Force kill port: `fuser -k 8888/tcp`
- Use `--strict-port` to fail fast instead of trying fallback ports
@@ -994,7 +994,7 @@ if (current_version < LATEST_VERSION) {
**Solutions**:
- Kill existing relay processes
- Remove WAL files: `rm build/*.db-wal build/*.db-shm`
- Check for stale processes: `ps aux | grep c_relay_`
- Check for stale processes: `ps aux | grep c_relay_pg_`
#### 3. Missing Admin Private Key
@@ -1071,7 +1071,7 @@ Typical startup times (on modern hardware):
## Summary
The c-relay startup system is designed for:
The c-relay-pg startup system is designed for:
1. **Security**: Admin keys never stored, relay keys encrypted
2. **Reliability**: Automatic port fallback, schema migrations
+9 -9
View File
@@ -18,10 +18,10 @@ The script now attempts to build with `musl-gcc` for truly portable static binar
SQLite is now built once and cached for future builds:
- **Cache location**: `~/.cache/c-relay-sqlite/`
- **Cache location**: `~/.cache/c-relay-pg-sqlite/`
- **Version-specific**: Each SQLite version gets its own cache directory
- **Significant speedup**: Subsequent builds skip the SQLite compilation step
- **Manual cleanup**: `rm -rf ~/.cache/c-relay-sqlite` to clear cache
- **Manual cleanup**: `rm -rf ~/.cache/c-relay-pg-sqlite` to clear cache
### 3. Smart Package Installation
@@ -51,13 +51,13 @@ The script will:
## Binary Types
### MUSL Static Binary (Ideal - Currently Not Achievable)
- **Filename**: `build/c_relay_static_musl_x86_64`
- **Filename**: `build/c_relay_pg_static_musl_x86_64`
- **Dependencies**: None (truly static)
- **Portability**: Works on any Linux distribution
- **Status**: Requires MUSL-compiled libwebsockets and other dependencies (not available by default)
### Glibc Static Binary (Current Output)
- **Filename**: `build/c_relay_static_x86_64` or `build/c_relay_static_glibc_x86_64`
- **Filename**: `build/c_relay_pg_static_x86_64` or `build/c_relay_pg_static_glibc_x86_64`
- **Dependencies**: None - fully statically linked with glibc
- **Portability**: Works on most Linux distributions (glibc is statically included)
- **Note**: Despite using glibc, this is a **fully static binary** with no runtime dependencies
@@ -68,11 +68,11 @@ The script automatically verifies binaries using `ldd` and `file`:
```bash
# For MUSL binary
ldd build/c_relay_static_musl_x86_64
ldd build/c_relay_pg_static_musl_x86_64
# Output: "not a dynamic executable" (good!)
# For glibc binary
ldd build/c_relay_static_glibc_x86_64
ldd build/c_relay_pg_static_glibc_x86_64
# Output: Shows glibc dependencies
```
@@ -112,7 +112,7 @@ The script attempts MUSL compilation but falls back to glibc:
### Clear SQLite Cache
```bash
rm -rf ~/.cache/c-relay-sqlite
rm -rf ~/.cache/c-relay-pg-sqlite
```
### Force Package Reinstall
@@ -127,8 +127,8 @@ cat /tmp/musl_build.log
### Verify Binary Type
```bash
file build/c_relay_static_*
ldd build/c_relay_static_* 2>&1
file build/c_relay_pg_static_*
ldd build/c_relay_pg_static_* 2>&1
```
## Performance Impact
+1 -1
View File
@@ -2,7 +2,7 @@
## Problem Summary
The c-relay Nostr relay experienced severe performance degradation (90-100% CPU) due to subscription accumulation in the database. Investigation revealed **323,644 orphaned subscriptions** that were never properly closed when WebSocket connections dropped.
The c-relay-pg Nostr relay experienced severe performance degradation (90-100% CPU) due to subscription accumulation in the database. Investigation revealed **323,644 orphaned subscriptions** that were never properly closed when WebSocket connections dropped.
## Solution: Two-Component Approach
+8 -8
View File
@@ -586,7 +586,7 @@ int add_pubkeys_to_config_table(void) {
rm -f *.db
# Start relay with defaults
./build/c_relay_x86
./build/c_relay_pg_x86
# Verify config table complete
sqlite3 <relay_pubkey>.db "SELECT COUNT(*) FROM config;"
@@ -602,7 +602,7 @@ int add_pubkeys_to_config_table(void) {
rm -f *.db
# Start relay with port override
./build/c_relay_x86 --port 9999
./build/c_relay_pg_x86 --port 9999
# Verify port override applied
sqlite3 <relay_pubkey>.db "SELECT value FROM config WHERE key='relay_port';"
@@ -612,13 +612,13 @@ int add_pubkeys_to_config_table(void) {
3. **Restart with Existing Database**
```bash
# Start relay (creates database)
./build/c_relay_x86
./build/c_relay_pg_x86
# Stop relay
pkill -f c_relay_
pkill -f c_relay_pg_
# Restart relay
./build/c_relay_x86
./build/c_relay_pg_x86
# Verify config unchanged
# Check relay.log for validation message
@@ -627,13 +627,13 @@ int add_pubkeys_to_config_table(void) {
4. **Restart with CLI Overrides**
```bash
# Start relay (creates database)
./build/c_relay_x86
./build/c_relay_pg_x86
# Stop relay
pkill -f c_relay_
pkill -f c_relay_pg_
# Restart with port override
./build/c_relay_x86 --port 9999
./build/c_relay_pg_x86 --port 9999
# Verify port override applied
sqlite3 <relay_pubkey>.db "SELECT value FROM config WHERE key='relay_port';"
+32 -32
View File
@@ -19,12 +19,12 @@ Complete guide for deploying, configuring, and managing the C Nostr Relay with e
```bash
# Clone and build
git clone <repository-url>
cd c-relay
cd c-relay-pg
git submodule update --init --recursive
make
# Start relay (zero configuration needed)
./build/c_relay_x86
./build/c_relay_pg_x86
```
### 2. First Startup - Save Keys
@@ -78,7 +78,7 @@ brew install git sqlite libwebsockets openssl libsecp256k1 curl zlib
```bash
# Clone repository
git clone <repository-url>
cd c-relay
cd c-relay-pg
# Initialize submodules
git submodule update --init --recursive
@@ -87,7 +87,7 @@ git submodule update --init --recursive
make clean && make
# Verify build
ls -la build/c_relay_x86
ls -la build/c_relay_pg_x86
```
### Production Deployment
@@ -98,27 +98,27 @@ ls -la build/c_relay_x86
sudo systemd/install-service.sh
# Start service
sudo systemctl start c-relay
sudo systemctl start c-relay-pg
# Enable auto-start
sudo systemctl enable c-relay
sudo systemctl enable c-relay-pg
# Check status
sudo systemctl status c-relay
sudo systemctl status c-relay-pg
```
#### Manual Deployment
```bash
# Create dedicated user
sudo useradd --system --home-dir /opt/c-relay --shell /bin/false c-relay
sudo useradd --system --home-dir /opt/c-relay-pg --shell /bin/false c-relay-pg
# Install binary
sudo mkdir -p /opt/c-relay
sudo cp build/c_relay_x86 /opt/c-relay/
sudo chown -R c-relay:c-relay /opt/c-relay
sudo mkdir -p /opt/c-relay-pg
sudo cp build/c_relay_pg_x86 /opt/c-relay-pg/
sudo chown -R c-relay-pg:c-relay-pg /opt/c-relay-pg
# Run as service user
sudo -u c-relay /opt/c-relay/c_relay_x86
sudo -u c-relay-pg /opt/c-relay-pg/c_relay_pg_x86
```
## Configuration Management
@@ -188,7 +188,7 @@ Send this to your relay via WebSocket, and changes are applied immediately.
|-----------|-------------|---------|---------|
| `relay_description` | Relay description for NIP-11 | "C Nostr Relay" | "My awesome relay" |
| `relay_contact` | Admin contact information | "" | "admin@example.com" |
| `relay_software` | Software identifier | "c-relay" | "c-relay v1.0" |
| `relay_software` | Software identifier | "c-relay-pg" | "c-relay-pg v1.0" |
#### Client Limits
| Parameter | Description | Default | Range |
@@ -273,7 +273,7 @@ chmod 600 admin_keys_backup_*.txt
#### Key Recovery
If you lose your admin private key:
1. **Stop the relay**: `pkill c_relay` or `sudo systemctl stop c-relay`
1. **Stop the relay**: `pkill c_relay_pg` or `sudo systemctl stop c-relay-pg`
2. **Backup events**: `cp <relay_pubkey>.nrdb backup_$(date +%Y%m%d).nrdb`
3. **Remove database**: `rm <relay_pubkey>.nrdb*`
4. **Restart relay**: This creates new database with new keys
@@ -300,7 +300,7 @@ sudo ufw allow 8888/tcp
```bash
# Secure database file permissions
chmod 600 <relay_pubkey>.nrdb
chown c-relay:c-relay <relay_pubkey>.nrdb
chown c-relay-pg:c-relay-pg <relay_pubkey>.nrdb
# Regular backups
cp <relay_pubkey>.nrdb backup/relay_backup_$(date +%Y%m%d_%H%M%S).nrdb
@@ -311,10 +311,10 @@ cp <relay_pubkey>.nrdb backup/relay_backup_$(date +%Y%m%d_%H%M%S).nrdb
### Service Status
```bash
# Check if relay is running
ps aux | grep c_relay
ps aux | grep c_relay_pg
# SystemD status
sudo systemctl status c-relay
sudo systemctl status c-relay-pg
# Network connections
netstat -tln | grep 8888
@@ -324,16 +324,16 @@ sudo ss -tlpn | grep 8888
### Log Monitoring
```bash
# Real-time logs (systemd)
sudo journalctl -u c-relay -f
sudo journalctl -u c-relay-pg -f
# Recent logs
sudo journalctl -u c-relay --since "1 hour ago"
sudo journalctl -u c-relay-pg --since "1 hour ago"
# Error logs only
sudo journalctl -u c-relay -p err
sudo journalctl -u c-relay-pg -p err
# Configuration changes
sudo journalctl -u c-relay | grep "Configuration updated via kind 33334"
sudo journalctl -u c-relay-pg | grep "Configuration updated via kind 33334"
```
### Database Analytics
@@ -365,13 +365,13 @@ ORDER BY created_at DESC;
du -sh <relay_pubkey>.nrdb*
# Memory usage
ps aux | grep c_relay | awk '{print $6}' # RSS memory in KB
ps aux | grep c_relay_pg | awk '{print $6}' # RSS memory in KB
# Connection count (approximate)
netstat -an | grep :8888 | grep ESTABLISHED | wc -l
# System resources
top -p $(pgrep c_relay)
top -p $(pgrep c_relay_pg)
```
## Troubleshooting
@@ -385,11 +385,11 @@ netstat -tln | grep 8888
# If port in use, find process: sudo lsof -i :8888
# Check binary permissions
ls -la build/c_relay_x86
chmod +x build/c_relay_x86
ls -la build/c_relay_pg_x86
chmod +x build/c_relay_pg_x86
# Check dependencies
ldd build/c_relay_x86
ldd build/c_relay_pg_x86
```
#### Configuration Not Updating
@@ -442,7 +442,7 @@ sqlite3 recovered.nrdb < recovered.sql
# If repair fails, start fresh (loses all events)
mv <relay_pubkey>.nrdb <relay_pubkey>.nrdb.corrupted
./build/c_relay_x86 # Creates new database
./build/c_relay_pg_x86 # Creates new database
```
#### Lost Configuration Recovery
@@ -455,12 +455,12 @@ If configuration is lost but database is intact:
#### Emergency Restart
```bash
# Quick restart with clean state
sudo systemctl stop c-relay
sudo systemctl stop c-relay-pg
mv <relay_pubkey>.nrdb <relay_pubkey>.nrdb.backup
sudo systemctl start c-relay
sudo systemctl start c-relay-pg
# Check logs for new admin keys
sudo journalctl -u c-relay --since "5 minutes ago" | grep "Admin Private Key"
sudo journalctl -u c-relay-pg --since "5 minutes ago" | grep "Admin Private Key"
```
## Advanced Usage
@@ -503,7 +503,7 @@ ws.on('open', function() {
# backup-relay.sh
DATE=$(date +%Y%m%d_%H%M%S)
DB_FILE=$(ls *.nrdb | head -1)
BACKUP_DIR="/backup/c-relay"
BACKUP_DIR="/backup/c-relay-pg"
mkdir -p $BACKUP_DIR
cp $DB_FILE $BACKUP_DIR/relay_backup_$DATE.nrdb
@@ -533,7 +533,7 @@ tar czf relay_migration.tar.gz *.nrdb* relay.log
# Target server
tar xzf relay_migration.tar.gz
./build/c_relay_x86 # Will detect existing database and continue
./build/c_relay_pg_x86 # Will detect existing database and continue
```
---
+1 -1
View File
@@ -1,6 +1,6 @@
#!/bin/bash
# Script to embed web files into C headers for the C-Relay admin interface
# Script to embed web files into C headers for the C-Relay-PG admin interface
# Converts HTML, CSS, and JS files from api/ directory into C byte arrays
set -e
+1 -1
View File
@@ -60,7 +60,7 @@ All examples assume the event-based configuration system where:
- **Save Admin Keys**: All deployment examples emphasize capturing the admin private key on first startup
- **Firewall Configuration**: Examples include proper firewall rules
- **SSL/TLS**: Production examples include HTTPS configuration
- **User Isolation**: Service runs as dedicated `c-relay` system user
- **User Isolation**: Service runs as dedicated `c-relay-pg` system user
## Support
+6 -6
View File
@@ -13,8 +13,8 @@ BLUE='\033[0;34m'
NC='\033[0m' # No Color
# Default configuration
RELAY_DIR="/opt/c-relay"
BACKUP_DIR="/backup/c-relay"
RELAY_DIR="/opt/c-relay-pg"
BACKUP_DIR="/backup/c-relay-pg"
RETENTION_DAYS="30"
COMPRESS="true"
REMOTE_BACKUP=""
@@ -47,8 +47,8 @@ show_help() {
echo "Usage: $0 [OPTIONS]"
echo
echo "Options:"
echo " -d, --relay-dir DIR Relay directory (default: /opt/c-relay)"
echo " -b, --backup-dir DIR Backup directory (default: /backup/c-relay)"
echo " -d, --relay-dir DIR Relay directory (default: /opt/c-relay-pg)"
echo " -b, --backup-dir DIR Backup directory (default: /backup/c-relay-pg)"
echo " -r, --retention DAYS Retention period in days (default: 30)"
echo " -n, --no-compress Don't compress backups"
echo " -s, --s3-bucket BUCKET Upload to S3 bucket"
@@ -231,7 +231,7 @@ upload_to_s3() {
print_step "Uploading backup to S3..."
local s3_path="s3://$S3_BUCKET/c-relay/$(date +%Y)/$(date +%m)/"
local s3_path="s3://$S3_BUCKET/c-relay-pg/$(date +%Y)/$(date +%m)/"
if aws s3 cp "$BACKUP_FILE" "$s3_path" --storage-class STANDARD_IA; then
print_success "Backup uploaded to S3: $s3_path"
@@ -264,7 +264,7 @@ cleanup_old_backups() {
print_step "Cleaning S3 backups older than $cutoff_date..."
# Note: This is a simplified approach. In production, use S3 lifecycle policies
aws s3 ls "s3://$S3_BUCKET/c-relay/" --recursive | \
aws s3 ls "s3://$S3_BUCKET/c-relay-pg/" --recursive | \
awk '$1 < "'$cutoff_date'" {print $4}' | \
while read -r key; do
aws s3 rm "s3://$S3_BUCKET/$key"
@@ -13,8 +13,8 @@ BLUE='\033[0;34m'
NC='\033[0m' # No Color
# Configuration
RELAY_DIR="/opt/c-relay"
SERVICE_NAME="c-relay"
RELAY_DIR="/opt/c-relay-pg"
SERVICE_NAME="c-relay-pg"
RELAY_PORT="8888"
LOG_FILE="/var/log/relay-monitor.log"
ALERT_EMAIL=""
@@ -60,7 +60,7 @@ show_help() {
echo "Usage: $0 [OPTIONS]"
echo
echo "Options:"
echo " -d, --relay-dir DIR Relay directory (default: /opt/c-relay)"
echo " -d, --relay-dir DIR Relay directory (default: /opt/c-relay-pg)"
echo " -p, --port PORT Relay port (default: 8888)"
echo " -i, --interval SECONDS Check interval (default: 60)"
echo " -e, --email EMAIL Alert email address"
@@ -134,7 +134,7 @@ parse_args() {
check_process_running() {
print_step "Checking if relay process is running..."
if pgrep -f "c_relay_x86" > /dev/null; then
if pgrep -f "c_relay_pg_x86" > /dev/null; then
print_success "Relay process is running"
return 0
else
@@ -172,7 +172,7 @@ check_service_status() {
check_memory_usage() {
print_step "Checking memory usage..."
local memory_kb=$(ps aux | grep "c_relay_x86" | grep -v grep | awk '{sum+=$6} END {print sum}')
local memory_kb=$(ps aux | grep "c_relay_pg_x86" | grep -v grep | awk '{sum+=$6} END {print sum}')
if [[ -z "$memory_kb" ]]; then
print_warning "Could not determine memory usage"
+4 -4
View File
@@ -56,7 +56,7 @@ http {
}
# Upstream for the relay
upstream c_relay_backend {
upstream c_relay_pg_backend {
server 127.0.0.1:8888;
keepalive 32;
}
@@ -108,7 +108,7 @@ http {
# Main proxy location for WebSocket and HTTP
location / {
# Proxy settings
proxy_pass http://c_relay_backend;
proxy_pass http://c_relay_pg_backend;
proxy_http_version 1.1;
proxy_cache_bypass $http_upgrade;
@@ -144,7 +144,7 @@ http {
# Health check endpoint (if implemented)
location /health {
proxy_pass http://c_relay_backend/health;
proxy_pass http://c_relay_pg_backend/health;
access_log off;
}
@@ -157,7 +157,7 @@ http {
# Optional: Metrics endpoint (if implemented)
location /metrics {
proxy_pass http://c_relay_backend/metrics;
proxy_pass http://c_relay_pg_backend/metrics;
# Restrict access to monitoring systems
allow 10.0.0.0/8;
allow 172.16.0.0/12;
@@ -96,9 +96,9 @@ check_root() {
check_relay_running() {
print_step "Checking if C Nostr Relay is running..."
if ! pgrep -f "c_relay_x86" > /dev/null; then
if ! pgrep -f "c_relay_pg_x86" > /dev/null; then
print_error "C Nostr Relay is not running"
print_error "Please start the relay first with: sudo systemctl start c-relay"
print_error "Please start the relay first with: sudo systemctl start c-relay-pg"
exit 1
fi
+12 -12
View File
@@ -13,9 +13,9 @@ BLUE='\033[0;34m'
NC='\033[0m' # No Color
# Configuration
RELAY_USER="c-relay"
INSTALL_DIR="/opt/c-relay"
SERVICE_NAME="c-relay"
RELAY_USER="c-relay-pg"
INSTALL_DIR="/opt/c-relay-pg"
SERVICE_NAME="c-relay-pg"
RELAY_PORT="8888"
# Functions
@@ -99,7 +99,7 @@ build_relay() {
# Check if we're in the source directory
if [[ ! -f "Makefile" ]]; then
print_error "Makefile not found. Please run this script from the c-relay source directory."
print_error "Makefile not found. Please run this script from the c-relay-pg source directory."
exit 1
fi
@@ -107,7 +107,7 @@ build_relay() {
make clean
make
if [[ ! -f "build/c_relay_x86" ]]; then
if [[ ! -f "build/c_relay_pg_x86" ]]; then
print_error "Build failed - binary not found"
exit 1
fi
@@ -118,9 +118,9 @@ build_relay() {
install_binary() {
print_step "Installing relay binary..."
cp build/c_relay_x86 "$INSTALL_DIR/"
chown "$RELAY_USER:$RELAY_USER" "$INSTALL_DIR/c_relay_x86"
chmod +x "$INSTALL_DIR/c_relay_x86"
cp build/c_relay_pg_x86 "$INSTALL_DIR/"
chown "$RELAY_USER:$RELAY_USER" "$INSTALL_DIR/c_relay_pg_x86"
chmod +x "$INSTALL_DIR/c_relay_pg_x86"
print_success "Binary installed to $INSTALL_DIR"
}
@@ -129,14 +129,14 @@ install_service() {
print_step "Installing systemd service..."
# Use the existing systemd service file
if [[ -f "systemd/c-relay.service" ]]; then
cp systemd/c-relay.service /etc/systemd/system/
if [[ -f "systemd/c-relay-pg.service" ]]; then
cp systemd/c-relay-pg.service /etc/systemd/system/
systemctl daemon-reload
print_success "Systemd service installed"
else
print_warning "Systemd service file not found, creating basic one..."
cat > /etc/systemd/system/c-relay.service << EOF
cat > /etc/systemd/system/c-relay-pg.service << EOF
[Unit]
Description=C Nostr Relay
After=network.target
@@ -146,7 +146,7 @@ Type=simple
User=$RELAY_USER
Group=$RELAY_USER
WorkingDirectory=$INSTALL_DIR
ExecStart=$INSTALL_DIR/c_relay_x86
ExecStart=$INSTALL_DIR/c_relay_pg_x86
Restart=always
RestartSec=5
@@ -1,4 +1,4 @@
# MUSL-based fully static C-Relay builder
# MUSL-based fully static C-Relay-PG builder
# Produces portable binaries with zero runtime dependencies
FROM alpine:latest AS builder
@@ -125,7 +125,7 @@ RUN cd /tmp && \
--prefix=/usr && \
make && make install
# Copy c-relay source
# Copy c-relay-pg source
COPY . /build/
# Initialize submodules
@@ -134,7 +134,7 @@ RUN git submodule update --init --recursive
# Build nostr_core_lib
RUN cd nostr_core_lib && ./build.sh
# Build c-relay static
# Build c-relay-pg static
RUN make clean && \
CC="musl-gcc -static" \
CFLAGS="-O2 -Wall -Wextra -std=c99 -g" \
@@ -143,8 +143,8 @@ RUN make clean && \
make
# Strip binary for size
RUN strip build/c_relay_x86
RUN strip build/c_relay_pg_x86
# Multi-stage build to produce minimal output
FROM scratch AS output
COPY --from=builder /build/build/c_relay_x86 /c_relay_static_musl_x86_64
COPY --from=builder /build/build/c_relay_pg_x86 /c_relay_pg_static_musl_x86_64
+8 -8
View File
@@ -19,7 +19,7 @@ RELEASE_MODE=false
VERSION_INCREMENT_TYPE="patch" # "patch", "minor", or "major"
show_usage() {
echo "C-Relay Increment and Push Script"
echo "C-Relay-PG Increment and Push Script"
echo ""
echo "USAGE:"
echo " $0 [OPTIONS] \"commit message\""
@@ -330,7 +330,7 @@ build_release_binary() {
create_source_tarball() {
print_status "Creating source tarball..."
local tarball_name="c-relay-${NEW_VERSION#v}.tar.gz"
local tarball_name="c-relay-pg-${NEW_VERSION#v}.tar.gz"
# Create tarball excluding build artifacts and git files
if tar -czf "$tarball_name" \
@@ -365,7 +365,7 @@ upload_release_assets() {
fi
local token=$(cat "$HOME/.gitea_token" | tr -d '\n\r')
local api_url="https://git.laantungir.net/api/v1/repos/laantungir/c-relay"
local api_url="https://git.laantungir.net/api/v1/repos/laantungir/c-relay-pg"
local assets_url="$api_url/releases/$release_id/assets"
print_status "Assets URL: $assets_url"
@@ -427,7 +427,7 @@ create_gitea_release() {
fi
local token=$(cat "$HOME/.gitea_token" | tr -d '\n\r')
local api_url="https://git.laantungir.net/api/v1/repos/laantungir/c-relay"
local api_url="https://git.laantungir.net/api/v1/repos/laantungir/c-relay-pg"
# Create release
print_status "Creating release $NEW_VERSION..."
@@ -473,7 +473,7 @@ create_gitea_release() {
# Main execution
main() {
print_status "C-Relay Increment and Push Script"
print_status "C-Relay-PG Increment and Push Script"
# Check prerequisites
check_git_repo
@@ -505,13 +505,13 @@ main() {
# Build release binary
if build_release_binary; then
local binary_path="build/c_relay_static_x86_64"
local binary_path="build/c_relay_pg_static_x86_64"
else
print_warning "Binary build failed, continuing with release creation"
# Check if binary exists from previous build
if [[ -f "build/c_relay_static_x86_64" ]]; then
if [[ -f "build/c_relay_pg_static_x86_64" ]]; then
print_status "Using existing binary from previous build"
binary_path="build/c_relay_static_x86_64"
binary_path="build/c_relay_pg_static_x86_64"
else
binary_path=""
fi
+10 -10
View File
@@ -1,6 +1,6 @@
#!/bin/bash
# C-Relay Build and Restart Script
# C-Relay-PG Build and Restart Script
# Builds the project first, then stops any running relay and starts a new one in the background
echo "=== C Nostr Relay Build and Restart Script ==="
@@ -242,13 +242,13 @@ fi
ARCH=$(uname -m)
case "$ARCH" in
x86_64)
BINARY_PATH="./build/c_relay_static_x86_64"
BINARY_PATH="./build/c_relay_pg_static_x86_64"
;;
aarch64|arm64)
BINARY_PATH="./build/c_relay_static_arm64"
BINARY_PATH="./build/c_relay_pg_static_arm64"
;;
*)
BINARY_PATH="./build/c_relay_static_$ARCH"
BINARY_PATH="./build/c_relay_pg_static_$ARCH"
;;
esac
@@ -270,7 +270,7 @@ echo "Build successful. Proceeding with relay restart..."
echo "Stopping any existing relay servers..."
# Get all relay processes and kill them immediately with -9
RELAY_PIDS=$(pgrep -f "c_relay_" || echo "")
RELAY_PIDS=$(pgrep -f "c_relay_pg_" || echo "")
if [ -n "$RELAY_PIDS" ]; then
echo "Force killing relay processes immediately: $RELAY_PIDS"
kill -9 $RELAY_PIDS 2>/dev/null
@@ -291,7 +291,7 @@ for attempt in {1..15}; do
fuser -k 8888/tcp 2>/dev/null || true
# Double-check for any remaining relay processes
REMAINING_PIDS=$(pgrep -f "c_relay_" || echo "")
REMAINING_PIDS=$(pgrep -f "c_relay_pg_" || echo "")
if [ -n "$REMAINING_PIDS" ]; then
echo "Killing remaining relay processes: $REMAINING_PIDS"
kill -9 $REMAINING_PIDS 2>/dev/null || true
@@ -309,7 +309,7 @@ for attempt in {1..15}; do
done
# Final safety check - ensure no relay processes remain
FINAL_PIDS=$(pgrep -f "c_relay_" || echo "")
FINAL_PIDS=$(pgrep -f "c_relay_pg_" || echo "")
if [ -n "$FINAL_PIDS" ]; then
echo "Final cleanup: killing processes $FINAL_PIDS"
kill -9 $FINAL_PIDS 2>/dev/null || true
@@ -325,7 +325,7 @@ echo "Database will be initialized automatically on startup if needed"
# Start relay in background with output redirection
echo "Starting relay server..."
echo "Debug: Current processes: $(ps aux | grep 'c_relay_' | grep -v grep || echo 'None')"
echo "Debug: Current processes: $(ps aux | grep 'c_relay_pg_' | grep -v grep || echo 'None')"
# Build command line arguments for relay binary
RELAY_ARGS=""
@@ -428,8 +428,8 @@ if ps -p "$RELAY_PID" >/dev/null 2>&1; then
echo "=== Event-Based Relay Server Running ==="
echo "Configuration: Event-based (kind 33334 Nostr events)"
echo "Database: Automatically created with relay pubkey naming"
echo "To kill relay: pkill -f 'c_relay_'"
echo "To check status: ps aux | grep c_relay_"
echo "To kill relay: pkill -f 'c_relay_pg_'"
echo "To check status: ps aux | grep c_relay_pg_"
echo "To view logs: tail -f relay.log"
echo "Binary: $BINARY_PATH (zero configuration needed)"
echo "Ready for Nostr client connections!"
+12 -12
View File
@@ -12,11 +12,11 @@ After the next crash, analyze it:
# List all core dumps (most recent first)
sudo coredumpctl list
# View info about the most recent c-relay crash
sudo coredumpctl info c-relay
# View info about the most recent c-relay-pg crash
sudo coredumpctl info c-relay-pg
# Load the core dump in gdb for detailed analysis
sudo coredumpctl gdb c-relay
sudo coredumpctl gdb c-relay-pg
Inside gdb, run these commands:
(gdb) bt full # Full backtrace with all variables
@@ -36,8 +36,8 @@ DEBUGGING
Even simpler: Use this one-liner
# Start relay and immediately attach gdb
cd /usr/local/bin/c_relay
sudo -u c-relay ./c_relay --debug-level=5 & sleep 2 && sudo gdb -p $(pgrep c_relay)
cd /usr/local/bin/c_relay_pg
sudo -u c-relay-pg ./c_relay_pg --debug-level=5 & sleep 2 && sudo gdb -p $(pgrep c_relay_pg)
Inside gdb, after attaching:
@@ -48,17 +48,17 @@ Or shorter:
How to View the Logs
Check systemd journal:
# View all c-relay logs
sudo journalctl -u c-relay
# View all c-relay-pg logs
sudo journalctl -u c-relay-pg
# View recent logs (last 50 lines)
sudo journalctl -u c-relay -n 50
sudo journalctl -u c-relay-pg -n 50
# Follow logs in real-time
sudo journalctl -u c-relay -f
sudo journalctl -u c-relay-pg -f
# View logs since last boot
sudo journalctl -u c-relay -b
sudo journalctl -u c-relay-pg -b
Check if service is running:
@@ -78,9 +78,9 @@ sudo systemctl start rsyslog
sudo systemctl status rsyslog
sudo -u c-relay ./c_relay --debug-level=5 -r 85d0b37e2ae822966dcadd06b2dc9368cde73865f90ea4d44f8b57d47ef0820a -a 1ec454734dcbf6fe54901ce25c0c7c6bca5edd89443416761fadc321d38df139
sudo -u c-relay-pg ./c_relay_pg --debug-level=5 -r 85d0b37e2ae822966dcadd06b2dc9368cde73865f90ea4d44f8b57d47ef0820a -a 1ec454734dcbf6fe54901ce25c0c7c6bca5edd89443416761fadc321d38df139
./c_relay_static_x86_64 -p 7889 --debug-level=5 -r 85d0b37e2ae822966dcadd06b2dc9368cde73865f90ea4d44f8b57d47ef0820a -a 1ec454734dcbf6fe54901ce25c0c7c6bca5edd89443416761fadc321d38df139
./c_relay_pg_static_x86_64 -p 7889 --debug-level=5 -r 85d0b37e2ae822966dcadd06b2dc9368cde73865f90ea4d44f8b57d47ef0820a -a 1ec454734dcbf6fe54901ce25c0c7c6bca5edd89443416761fadc321d38df139
sudo ufw allow 8888/tcp
+1 -1
View File
@@ -1,5 +1,5 @@
{
"name": "c-relay",
"name": "c-relay-pg",
"lockfileVersion": 3,
"requires": true,
"packages": {
+34 -34
View File
@@ -1,17 +1,17 @@
# c-relay-pg Plan — PostgreSQL Backend + Multi-Instance + Dashboard
# c-relay-pg-pg Plan — PostgreSQL Backend + Multi-Instance + Dashboard
## Overview
**c-relay-pg** is a new project (separate repository) forked from c-relay after the `db_ops` abstraction layer is complete. It replaces the SQLite backend with PostgreSQL, enabling horizontal scaling, external dashboards, and production-grade deployments.
**c-relay-pg-pg** is a new project (separate repository) forked from c-relay-pg after the `db_ops` abstraction layer is complete. It replaces the SQLite backend with PostgreSQL, enabling horizontal scaling, external dashboards, and production-grade deployments.
### Prerequisites
- The `db_ops.h` / `db_ops.c` abstraction layer must be complete in c-relay first — see [c-relay thread pool plan](c_relay_thread_pool_plan.md) Phase 1.
- The `db_ops.h` / `db_ops.c` abstraction layer must be complete in c-relay-pg first — see [c-relay-pg thread pool plan](c_relay_pg_thread_pool_plan.md) Phase 1.
- The fork happens after Phase 1 of that plan is done and tested.
### Why a Separate Project?
| | c-relay | c-relay-pg |
| | c-relay-pg | c-relay-pg-pg |
|---|---------|-----------|
| **Database** | SQLite (embedded) | PostgreSQL (client-server) |
| **Deployment** | Single binary + DB file | Binary + PostgreSQL server |
@@ -26,9 +26,9 @@ For the full analysis of why PostgreSQL was chosen over MySQL/MariaDB and other
```mermaid
flowchart TD
subgraph c-relay-pg - Production Deployment
LB[nginx - TLS + load balancing] -->|WebSocket| R1[c-relay-pg Instance 1]
LB -->|WebSocket| R2[c-relay-pg Instance 2]
subgraph c-relay-pg-pg - Production Deployment
LB[nginx - TLS + load balancing] -->|WebSocket| R1[c-relay-pg-pg Instance 1]
LB -->|WebSocket| R2[c-relay-pg-pg Instance 2]
LB -->|HTTP| DASH[Dashboard]
R1 -->|db_ops API| PGBACK[PostgreSQL Backend - db_ops_postgres.c]
R2 -->|db_ops API| PGBACK
@@ -44,18 +44,18 @@ flowchart TD
### Goal
Fork c-relay into a new repository called **c-relay-pg**. Replace the SQLite `db_ops` implementation with PostgreSQL using `libpq`. The `db_ops.h` interface stays identical — only the backend changes.
Fork c-relay-pg into a new repository called **c-relay-pg-pg**. Replace the SQLite `db_ops` implementation with PostgreSQL using `libpq`. The `db_ops.h` interface stays identical — only the backend changes.
### New Files
- `src/db_ops_sqlite.c` — Renamed from `db_ops.c` (the Phase 1 SQLite implementation from c-relay)
- `src/db_ops_sqlite.c` — Renamed from `db_ops.c` (the Phase 1 SQLite implementation from c-relay-pg)
- `src/db_ops_postgres.c` — New PostgreSQL implementation
- `src/db_ops.c` — Thin dispatcher that calls the active backend
### PostgreSQL Schema
```sql
-- PostgreSQL schema for c-relay-pg
-- PostgreSQL schema for c-relay-pg-pg
-- No event_tags table needed — JSONB + GIN handles everything
CREATE TABLE events (
@@ -289,14 +289,14 @@ psql -d crelay -c "REFRESH MATERIALIZED VIEW event_kinds_mv; REFRESH MATERIALIZE
### Goal
Run multiple c-relay-pg instances behind a load balancer with a separate dashboard web server, all sharing the same PostgreSQL database.
Run multiple c-relay-pg-pg instances behind a load balancer with a separate dashboard web server, all sharing the same PostgreSQL database.
### Components
1. **nginx config** — WebSocket load balancing with `ip_hash` stickiness
2. **systemd template**`c-relay-pg@.service` for multiple instances
2. **systemd template**`c-relay-pg-pg@.service` for multiple instances
3. **LISTEN/NOTIFY integration** — Cross-instance event broadcasting in the `lws_service()` loop
4. **PgBouncer** — Connection pooling between c-relay-pg instances and PostgreSQL
4. **PgBouncer** — Connection pooling between c-relay-pg-pg instances and PostgreSQL
5. **Dashboard** — Separate web server querying PostgreSQL directly
6. **Materialized view refresh** — Background job to refresh analytics views
@@ -306,9 +306,9 @@ Run multiple c-relay-pg instances behind a load balancer with a separate dashboa
flowchart TD
INTERNET[Internet - Nostr Clients] -->|wss://relay.example.com| NGINX[nginx reverse proxy - TLS termination + load balancing]
NGINX -->|ws://localhost:8888| R1[c-relay-pg Instance 1 - port 8888]
NGINX -->|ws://localhost:8889| R2[c-relay-pg Instance 2 - port 8889]
NGINX -->|ws://localhost:8890| R3[c-relay-pg Instance 3 - port 8890]
NGINX -->|ws://localhost:8888| R1[c-relay-pg-pg Instance 1 - port 8888]
NGINX -->|ws://localhost:8889| R2[c-relay-pg-pg Instance 2 - port 8889]
NGINX -->|ws://localhost:8890| R3[c-relay-pg-pg Instance 3 - port 8890]
NGINX -->|http://localhost:3000| DASHWEB[Dashboard Web Server]
R1 -->|libpq connection pool| PGPOOL[PgBouncer - connection pooler]
@@ -325,7 +325,7 @@ flowchart TD
### nginx Load Balancing Config
```nginx
# nginx.conf - WebSocket load balancing for c-relay-pg
# nginx.conf - WebSocket load balancing for c-relay-pg-pg
upstream relay_backends {
# ip_hash ensures a client always hits the same instance
# (important for WebSocket session stickiness)
@@ -366,35 +366,35 @@ Each instance is the same binary, just on a different port, all pointing to the
```bash
# Instance 1
./build/c_relay_x86 --port 8888 --db-host localhost --db-name crelay &
./build/c_relay_pg_x86 --port 8888 --db-host localhost --db-name crelay &
# Instance 2
./build/c_relay_x86 --port 8889 --db-host localhost --db-name crelay &
./build/c_relay_pg_x86 --port 8889 --db-host localhost --db-name crelay &
# Instance 3
./build/c_relay_x86 --port 8890 --db-host localhost --db-name crelay &
./build/c_relay_pg_x86 --port 8890 --db-host localhost --db-name crelay &
```
Or with systemd template units:
```ini
# /etc/systemd/system/c-relay-pg@.service
# /etc/systemd/system/c-relay-pg-pg@.service
[Unit]
Description=C-Relay-PG Nostr Instance %i
Description=C-Relay-PG-PG Nostr Instance %i
After=postgresql.service
[Service]
ExecStart=/opt/c-relay-pg/c_relay_x86 --port %i --db-host localhost --db-name crelay
ExecStart=/opt/c-relay-pg-pg/c_relay_pg_x86 --port %i --db-host localhost --db-name crelay
Restart=always
User=c-relay
User=c-relay-pg
[Install]
WantedBy=multi-user.target
```
```bash
systemctl enable c-relay-pg@8888 c-relay-pg@8889 c-relay-pg@8890
systemctl start c-relay-pg@8888 c-relay-pg@8889 c-relay-pg@8890
systemctl enable c-relay-pg-pg@8888 c-relay-pg-pg@8889 c-relay-pg-pg@8890
systemctl start c-relay-pg-pg@8888 c-relay-pg-pg@8889 c-relay-pg-pg@8890
```
### Cross-Instance Event Broadcasting
@@ -559,13 +559,13 @@ for port in 8888 8889 8890; do
sleep 30
# 3. Stop old instance
systemctl stop c-relay-pg@$port
systemctl stop c-relay-pg-pg@$port
# 4. Deploy new binary
cp ./build/c_relay_x86 /opt/c-relay-pg/c_relay_x86
cp ./build/c_relay_pg_x86 /opt/c-relay-pg-pg/c_relay_pg_x86
# 5. Start new instance
systemctl start c-relay-pg@$port
systemctl start c-relay-pg-pg@$port
# 6. Wait for health check
sleep 5
@@ -584,7 +584,7 @@ With PostgreSQL, the dashboard can be built with any technology:
- **Grafana** — Point directly at PostgreSQL, zero custom code
- **Custom web app** — React/Vue frontend + any backend (Node, Python, Go) querying PostgreSQL
- **Embedded in c-relay-pg** — Keep current approach but queries go through `db_ops` to PostgreSQL (no longer blocks event loop since PostgreSQL handles concurrency)
- **Embedded in c-relay-pg-pg** — Keep current approach but queries go through `db_ops` to PostgreSQL (no longer blocks event loop since PostgreSQL handles concurrency)
### Scaling Scenarios
@@ -622,11 +622,11 @@ Running multiple relay instances with PostgreSQL on a single VPS:
---
## Relationship to c-relay
## Relationship to c-relay-pg
This project depends on the `db_ops.h` abstraction layer built in [c-relay thread pool plan](c_relay_thread_pool_plan.md) Phase 1. The interface is identical — only the backend implementation changes:
This project depends on the `db_ops.h` abstraction layer built in [c-relay-pg thread pool plan](c_relay_pg_thread_pool_plan.md) Phase 1. The interface is identical — only the backend implementation changes:
- **c-relay**: `db_ops.c` → SQLite calls via `sqlite3_*`
- **c-relay-pg**: `db_ops_postgres.c` → PostgreSQL calls via `libpq` (`PQexec`, `PQgetvalue`, etc.)
- **c-relay-pg**: `db_ops.c` → SQLite calls via `sqlite3_*`
- **c-relay-pg-pg**: `db_ops_postgres.c` → PostgreSQL calls via `libpq` (`PQexec`, `PQgetvalue`, etc.)
The `db_ops.h` header file is shared between both projects. Changes to the interface should be coordinated.
+9 -9
View File
@@ -1,13 +1,13 @@
# c-relay Thread Pool Plan — db_ops Abstraction + SQLite Thread Pool
# c-relay-pg Thread Pool Plan — db_ops Abstraction + SQLite Thread Pool
## Overview
This plan covers improvements to **c-relay** (this repo) — the lean, single-binary, embedded-SQLite Nostr relay. Two phases:
This plan covers improvements to **c-relay-pg** (this repo) — the lean, single-binary, embedded-SQLite Nostr relay. Two phases:
1. **Phase 1: Database Abstraction Layer** — Consolidate all scattered `sqlite3_*` calls into a single `db_ops.h` / `db_ops.c` module. Pure refactor, no behavior change.
2. **Phase 2: SQLite Thread Pool** — Add worker threads for concurrent reads, unblocking the `lws_service()` event loop.
This is the **final form of c-relay**: an embedded-SQLite relay with clean architecture and non-blocking database access. The abstraction layer also enables a future fork to PostgreSQL — see [c-relay-pg plan](c_relay_pg_plan.md).
This is the **final form of c-relay-pg**: an embedded-SQLite relay with clean architecture and non-blocking database access. The abstraction layer also enables a future fork to PostgreSQL — see [c-relay-pg-pg plan](c_relay_pg_pg_plan.md).
### Why This First?
@@ -25,15 +25,15 @@ The current architecture has a fundamental bottleneck documented in the [databas
```mermaid
flowchart TD
subgraph c-relay - Final Form
RELAY[c-relay binary] -->|db_ops API| DBOPS[db_ops.c - abstraction layer]
subgraph c-relay-pg - Final Form
RELAY[c-relay-pg binary] -->|db_ops API| DBOPS[db_ops.c - abstraction layer]
DBOPS -->|sqlite3 calls| SQLITE[SQLite WAL database]
end
```
```mermaid
flowchart TD
subgraph c-relay with Thread Pool
subgraph c-relay-pg with Thread Pool
LWS[lws_service event loop] -->|REQ arrives| Q[Thread-safe job queue]
LWS -->|EVENT arrives| WQ[Write queue - single writer]
Q --> T1[Reader Thread 1 - own sqlite3*]
@@ -322,11 +322,11 @@ int thread_pool_submit_write(db_job_t* job);
---
## Relationship to c-relay-pg
## Relationship to c-relay-pg-pg
After Phase 1 (abstraction layer) is complete, the codebase is ready to be forked into a separate **c-relay-pg** project that replaces the SQLite backend with PostgreSQL. See [c-relay-pg plan](c_relay_pg_plan.md) for details.
After Phase 1 (abstraction layer) is complete, the codebase is ready to be forked into a separate **c-relay-pg-pg** project that replaces the SQLite backend with PostgreSQL. See [c-relay-pg-pg plan](c_relay_pg_pg_plan.md) for details.
The `db_ops.h` interface is designed to work with any backend:
- **SQLite** (this plan): `db_result_next_json()` copies from `sqlite3_column_text()`
- **PostgreSQL** (c-relay-pg): `db_result_next_json()` returns from `PQgetvalue()`
- **PostgreSQL** (c-relay-pg-pg): `db_result_next_json()` returns from `PQgetvalue()`
- **LMDB** (future possibility): `db_result_next_json()` returns a zero-copy pointer into mmap'd memory
+19 -19
View File
@@ -2,7 +2,7 @@
## Current Architecture Problem
The c-relay runs a **single-threaded event loop** via `lws_service()` in [`start_websocket_relay()`](src/websockets.c:2637). Every WebSocket callback — including [`handle_req_message()`](src/main.c:1101) and [`store_event()`](src/main.c:787) — executes **synchronously** inside this loop. When a REQ query takes 672ms (as documented in your [query analysis report](query_analysis_report.md:39)), **every other connected client is frozen** for that duration.
The c-relay-pg runs a **single-threaded event loop** via `lws_service()` in [`start_websocket_relay()`](src/websockets.c:2637). Every WebSocket callback — including [`handle_req_message()`](src/main.c:1101) and [`store_event()`](src/main.c:787) — executes **synchronously** inside this loop. When a REQ query takes 672ms (as documented in your [query analysis report](query_analysis_report.md:39)), **every other connected client is frozen** for that duration.
### The Blocking Chain
@@ -183,7 +183,7 @@ LMDB is an embedded key-value store created by Howard Chu for OpenLDAP. It's wha
- **Zero-copy reads**: When you read a value, you get a pointer directly into the mmap'd region. No `malloc`, no `memcpy`.
- **Single-writer, multiple-reader**: Like SQLite WAL, but implemented at a much lower level with near-zero overhead.
### How It Would Work for c-relay
### How It Would Work for c-relay-pg
LMDB is a **key-value store**, not a relational database. There's no SQL. You design your own indexes as separate "databases" (sub-B-trees within the same file):
@@ -216,7 +216,7 @@ strfry doesn't use SQL at all. When a REQ comes in with `kinds: [1], #p: [pubkey
4. For each ID, does a direct pointer lookup in the events B-tree
5. Returns the raw bytes — zero-copy, no JSON parsing, no serialization
Compare this to your current c-relay flow:
Compare this to your current c-relay-pg flow:
1. Build SQL string with parameter binding
2. `sqlite3_prepare_v2()` — parse SQL, build query plan
3. `sqlite3_step()` — traverse B-tree, copy data to SQLite's page cache, then copy to your buffer
@@ -505,9 +505,9 @@ With a client-server database, you unlock something fundamentally impossible wit
flowchart TD
INTERNET[Internet - Nostr Clients] -->|wss://relay.example.com| NGINX[nginx reverse proxy - TLS termination + load balancing]
NGINX -->|ws://localhost:8888| R1[c-relay Instance 1 - port 8888]
NGINX -->|ws://localhost:8889| R2[c-relay Instance 2 - port 8889]
NGINX -->|ws://localhost:8890| R3[c-relay Instance 3 - port 8890]
NGINX -->|ws://localhost:8888| R1[c-relay-pg Instance 1 - port 8888]
NGINX -->|ws://localhost:8889| R2[c-relay-pg Instance 2 - port 8889]
NGINX -->|ws://localhost:8890| R3[c-relay-pg Instance 3 - port 8890]
NGINX -->|http://localhost:3000| DASHWEB[Dashboard Web Server]
R1 -->|libpq connection pool| PGPOOL[PgBouncer - connection pooler]
@@ -526,7 +526,7 @@ flowchart TD
nginx already handles TLS termination for most Nostr relays. Adding WebSocket load balancing is a small configuration change:
```nginx
# nginx.conf - WebSocket load balancing for c-relay
# nginx.conf - WebSocket load balancing for c-relay-pg
upstream relay_backends {
# ip_hash ensures a client always hits the same instance
# (important for WebSocket session stickiness)
@@ -567,35 +567,35 @@ Each instance is the same binary, just on a different port, all pointing to the
```bash
# Instance 1
./build/c_relay_x86 --port 8888 --db-host localhost --db-name crelay &
./build/c_relay_pg_x86 --port 8888 --db-host localhost --db-name crelay &
# Instance 2
./build/c_relay_x86 --port 8889 --db-host localhost --db-name crelay &
./build/c_relay_pg_x86 --port 8889 --db-host localhost --db-name crelay &
# Instance 3
./build/c_relay_x86 --port 8890 --db-host localhost --db-name crelay &
./build/c_relay_pg_x86 --port 8890 --db-host localhost --db-name crelay &
```
Or with systemd, you'd use a template unit:
```ini
# /etc/systemd/system/c-relay@.service
# /etc/systemd/system/c-relay-pg@.service
[Unit]
Description=C-Relay Nostr Instance %i
Description=C-Relay-PG Nostr Instance %i
After=postgresql.service
[Service]
ExecStart=/opt/c-relay/c_relay_x86 --port %i --db-host localhost --db-name crelay
ExecStart=/opt/c-relay-pg/c_relay_pg_x86 --port %i --db-host localhost --db-name crelay
Restart=always
User=c-relay
User=c-relay-pg
[Install]
WantedBy=multi-user.target
```
```bash
systemctl enable c-relay@8888 c-relay@8889 c-relay@8890
systemctl start c-relay@8888 c-relay@8889 c-relay@8890
systemctl enable c-relay-pg@8888 c-relay-pg@8889 c-relay-pg@8890
systemctl start c-relay-pg@8888 c-relay-pg@8889 c-relay-pg@8890
```
### The Subscription Broadcasting Challenge (Detailed)
@@ -741,13 +741,13 @@ for port in 8888 8889 8890; do
sleep 30
# 3. Stop old instance
systemctl stop c-relay@$port
systemctl stop c-relay-pg@$port
# 4. Deploy new binary
cp ./build/c_relay_x86 /opt/c-relay/c_relay_x86
cp ./build/c_relay_pg_x86 /opt/c-relay-pg/c_relay_pg_x86
# 5. Start new instance
systemctl start c-relay@$port
systemctl start c-relay-pg@$port
# 6. Wait for health check
sleep 5
+186
View File
@@ -0,0 +1,186 @@
# Plan: Eliminate g_db from the Main Thread
## Problem Statement
All SQLite calls go through `db_ops.c`, but `db_active_connection()` falls back to `g_db` when `g_thread_db` is NULL. Since the main thread never sets `g_thread_db`, every `db_*` call from lws-main executes synchronous SQLite on the main thread. The thread pool workers have their own connections and work correctly — the problem is the **fallback path**.
Current perf data shows lws-main at **67.6% avg CPU**, dominated by SQLite symbols (`sqlite3BtreeTableMoveto`, `sqlite3VdbeExec`, `pcache1Fetch`).
## Goal
Remove `g_db` as a runtime connection used by the main thread. After this change:
- The main thread has **no SQLite connection** and cannot execute synchronous queries
- All DB work routes through thread pool workers (which have their own connections)
- `g_db` is only used during startup/shutdown (before/after the event loop)
- Any accidental `db_*` call from lws-main returns an error instead of silently blocking
## Architecture After Change
```
lws-main thread:
- WebSocket protocol handling
- Message parsing/routing
- Completion queue draining
- NO SQLite access during event loop
db-read-0 thread:
- REQ queries
- COUNT queries
- Config cache miss reads
- Auth rule checks
- Monitoring/stats queries
db-write thread:
- EVENT inserts
- Subscription logging
- Config updates
- Auth rule modifications
event-worker thread:
- Signature validation
- Duplicate check
- Store via db-write
```
## Categorized g_db Call Sites
### Category A: Startup-only — keep using g_db
These run before the event loop starts. They are fine.
| Function | File | Purpose |
|----------|------|---------|
| `db_init()` | db_ops.c:27 | Open database |
| `db_exec_sql()` for PRAGMAs | main.c:854-882 | WAL, mmap, cache setup |
| `db_table_exists()` | main.c:750 | Schema check |
| `db_get_schema_version_dup()` | main.c:754 | Migration check |
| `db_exec_sql()` for schema | main.c:842 | Create tables |
| `populate_all_config_values_atomic()` | config.c:4290 | First-time config |
| `populate_default_config_values()` | config.c:1657 | Default config |
| `add_pubkeys_to_config_table()` | config.c:1778 | Pubkey storage |
| `apply_cli_overrides_atomic()` | config.c:4211 | CLI overrides |
| `db_store_relay_private_key_hex()` | config.c:495 | Key storage |
| `db_populate_event_tags_from_existing()` | main.c:1156 | Tag migration |
| `cleanup_all_subscriptions_on_startup()` | subscriptions.c:1092 | Orphan cleanup |
### Category B: Shutdown-only — keep using g_db
| Function | File | Purpose |
|----------|------|---------|
| `db_exec_sql()` WAL checkpoint | main.c:900 | Clean shutdown |
| `db_close()` | main.c:904 | Close connection |
### Category C: Runtime hot path — must move off main thread
These are called during the event loop from lws-main context.
| Function | File | Called from | Fix strategy |
|----------|------|------------|--------------|
| `db_get_config_value_dup()` | db_ops.c:777 | config cache miss | Already cached with 5s TTL; pre-warm cache at startup so misses are rare |
| `store_event()` for kind 14/1059 | main.c:983 | websockets.c sync path | Route through async event worker |
| `store_event_post_actions()` | main.c | completion handler | Already runs on main; its DB calls need routing |
| `db_log_subscription_created()` | db_ops.c:145 | subscriptions.c | Fire-and-forget via write queue |
| `db_log_subscription_closed()` | db_ops.c:165 | subscriptions.c | Fire-and-forget via write queue |
| `db_log_subscription_disconnected()` | db_ops.c:193 | subscriptions.c | Fire-and-forget via write queue |
| `db_update_subscription_events_sent()` | db_ops.c:225 | subscriptions.c | Fire-and-forget via write queue |
| `db_cleanup_orphaned_subscriptions()` | db_ops.c:244 | subscriptions.c | Move to startup only |
| `generate_and_post_status_event()` | websockets.c:3408 | periodic timer | Submit to write worker |
| `ip_ban_cleanup()` / `ip_ban_log_stats()` | websockets.c:3196 | periodic timer | Submit to write worker |
| `db_get_total_event_count_ll()` | db_ops.c:589 | api.c monitoring | Submit to read worker |
| `db_get_event_count_since()` | db_ops.c:606 | api.c monitoring | Submit to read worker |
| `db_get_event_kind_distribution_rows()` | db_ops.c:625 | api.c monitoring | Submit to read worker |
| `db_get_top_pubkeys_rows()` | db_ops.c:663 | api.c monitoring | Submit to read worker |
| `db_get_subscription_details_rows()` | db_ops.c:697 | api.c monitoring | Submit to read worker |
| `db_get_all_config_rows()` | db_ops.c:745 | api.c config query | Submit to read worker |
| `db_execute_readonly_query_json()` | db_ops.c:442 | api.c SQL query | Submit to read worker |
| `db_event_id_exists()` | db_ops.c:1041 | main.c event check | Already moved to event worker |
| `db_retrieve_event_by_id()` | db_ops.c:1056 | main.c | Submit to read worker |
| `db_get_event_pubkey()` | db_ops.c:262 | NIP-09 deletion | Submit to read worker |
| `db_delete_event_by_id()` | db_ops.c:285 | NIP-09 deletion | Submit to write worker |
| `db_delete_older_replaceable_events()` | db_ops.c:305 | store_event_core | Already on write worker |
| `db_store_config_event()` | db_ops.c:888 | config.c | Submit to write worker |
| `db_add_auth_rule()` | db_ops.c:1226 | config.c admin | Submit to write worker |
| `db_remove_auth_rule()` | db_ops.c:1242 | config.c admin | Submit to write worker |
| `db_delete_wot_whitelist_rules()` | db_ops.c:1258 | config.c WoT | Submit to write worker |
| `db_count_wot_whitelist_rules()` | db_ops.c:1266 | config.c | Cache or read worker |
| `db_store_event_tags_cjson()` | db_ops.c:1140 | main.c post-actions | Already on write worker path |
| `db_get_config_row_count()` | db_ops.c:1121 | config.c diagnostics | Cache or read worker |
| `db_set_config_value_full()` | db_ops.c:796 | config.c | Submit to write worker |
| `db_update_config_value_only()` | db_ops.c:821 | config.c | Submit to write worker |
| `db_upsert_config_value()` | db_ops.c:838 | api.c | Submit to write worker |
| `db_exec_sql()` for transactions | config.c | admin events | Submit to write worker |
| `db_count_with_sql()` | db_ops.c:415 | config.c admin | Submit to read worker |
| `is_config_table_ready()` | config.c:4595 | config.c hybrid | Cache result at startup |
| `generate_config_event_from_table()` | config.c:4933 | config.c | Cache or read worker |
### Category D: Auth rule checks — already use separate connections
These functions in `db_ops.c` open their own temporary read-only connections:
| Function | Line | Notes |
|----------|------|-------|
| `db_is_pubkey_blacklisted()` | 347 | Opens own connection |
| `db_is_hash_blacklisted()` | 362 | Opens own connection |
| `db_is_pubkey_whitelisted()` | 377 | Opens own connection |
| `db_count_active_whitelist_rules()` | 392 | Opens own connection |
These are already safe — they don't use `g_db`. No change needed.
## Implementation Strategy
### Phase 1: Pre-warm caches and eliminate cache-miss DB reads
1. Pre-warm the config cache at startup by loading all config values into the in-memory cache before the event loop starts
2. Pre-warm the NIP-11 cache at startup
3. Pre-warm the auth rules fast cache at startup
4. This eliminates the most frequent cache-miss `db_get_config_value_dup()` calls
### Phase 2: Route subscription logging through write worker
1. Add a `THREAD_POOL_JOB_FIRE_AND_FORGET` job type to thread_pool
2. Create async wrappers: `db_log_subscription_created_async()`, etc.
3. These submit SQL to the write worker queue and return immediately
4. No completion callback needed — fire and forget
### Phase 3: Route special-kind EVENT store through async worker
1. Remove the `event_is_async_eligible()` exclusion for kinds 14, 1059, 23456
2. For kind 23456: process admin command in completion handler on main thread after store
3. For kind 14/1059: process NIP-17 DM in completion handler after store
### Phase 4: Route periodic timer DB work through workers
1. `generate_and_post_status_event()` — submit event creation to write worker
2. `ip_ban_cleanup()` / `ip_ban_log_stats()` — submit to write worker
3. Monitoring queries — submit to read worker with completion callback
### Phase 5: Null out g_db before event loop
1. After startup is complete and thread pool is initialized, set `g_db = NULL`
2. Change `db_active_connection()` to assert/warn if both `g_thread_db` and `g_db` are NULL
3. Any accidental main-thread DB call will now fail loudly instead of silently blocking
4. Restore `g_db` briefly for shutdown checkpoint
## Implementation Order
1. Phase 1 — lowest risk, immediate benefit from eliminating cache misses
2. Phase 2 — straightforward fire-and-forget pattern
3. Phase 3 — moderate complexity, needs careful completion handler design
4. Phase 4 — moderate complexity, periodic tasks need async patterns
5. Phase 5 — the final enforcement step, only safe after phases 1-4
## Risk Assessment
- **Phase 1**: Very low risk — just pre-warming existing caches
- **Phase 2**: Low risk — subscription logging is non-critical, fire-and-forget is safe
- **Phase 3**: Medium risk — admin/DM event processing has complex state; needs careful testing
- **Phase 4**: Medium risk — periodic tasks have side effects that need to complete
- **Phase 5**: High risk if done prematurely — must verify ALL runtime paths are covered first
## Expected Impact
After all phases:
- lws-main CPU should drop from ~67% to near 0% SQLite overhead
- All SQLite work happens on db-read and db-write threads
- Main thread only does WebSocket I/O, JSON parsing, and completion queue draining
- Thread model becomes: lws-main = pure I/O, workers = all DB
+1 -1
View File
@@ -2,7 +2,7 @@
## Problem Statement
The main `c_relay` thread consumes ~56% CPU while the DB worker threads (`db-read-1..4`, `db-write`) sit near 0%. All protocol handling, JSON parsing, event validation, subscription matching, and message queueing happens synchronously inside `nostr_relay_callback()` on the main libwebsockets event-loop thread.
The main `c_relay_pg` thread consumes ~56% CPU while the DB worker threads (`db-read-1..4`, `db-write`) sit near 0%. All protocol handling, JSON parsing, event validation, subscription matching, and message queueing happens synchronously inside `nostr_relay_callback()` on the main libwebsockets event-loop thread.
## Current Architecture
+3 -3
View File
@@ -1,8 +1,8 @@
# C-Relay Memory Leak Investigation & Fix Plan
# C-Relay-PG Memory Leak Investigation & Fix Plan
## Problem Statement
c-relay reached **1.56 GB** of its 1.5 GB `MemoryHigh` cgroup limit after only **1 hour 37 minutes** of runtime. The kernel is actively throttling the process with swap pressure (1.8M swap used). This strongly indicates one or more memory leaks causing unbounded growth.
c-relay-pg reached **1.56 GB** of its 1.5 GB `MemoryHigh` cgroup limit after only **1 hour 37 minutes** of runtime. The kernel is actively throttling the process with swap pressure (1.8M swap used). This strongly indicates one or more memory leaks causing unbounded growth.
## Root Cause Analysis
@@ -187,7 +187,7 @@ Add `free((char*)result)` after every `get_config_value()` call that doesn't alr
While fixes are being implemented:
1. **Raise `MemoryHigh`** to 2 GB in the systemd unit to prevent throttling
2. **Add a cron job** to restart the service every 12-24 hours
3. **Monitor with**: `watch -n 5 'cat /proc/$(pgrep c_relay)/status | grep -E "VmRSS|VmSwap"'`
3. **Monitor with**: `watch -n 5 'cat /proc/$(pgrep c_relay_pg)/status | grep -E "VmRSS|VmSwap"'`
---
+204
View File
@@ -0,0 +1,204 @@
# Remaining Implementation Plan — Zero SQLite on Main Thread + PG Fork Readiness
## Overview
This plan covers all remaining work to achieve two goals:
1. **Zero direct SQLite calls on lws-main** during the event loop
2. **Clean `db_ops` abstraction boundary** so the PG fork has no raw `sqlite3_*` calls outside `db_ops.c`
## Current State
### Completed (Fixes 1-5, 7 + Phases 1-3, 5)
- Global config cache with 5s TTL
- Dedup check moved to event worker
- Async COUNT queries via thread pool
- WoT auth rules cached per-session
- Special-kind EVENT store through async worker
- Subscription logging via fire-and-forget write queue
- IP ban periodic save via write queue
- g_db nulled before event loop
### Remaining
- Fix 6: Reduce reader threads (trivial)
- Fix 8: Move periodic tasks off main thread (partial — IP ban save done, monitoring/status not)
- Fix 9: Cache NIP-11 response
- Sync `store_event()` calls from admin/DM/NIP-09 paths
- Raw `sqlite3_*` calls in `thread_pool.c` and `websockets.c`
- COUNT query path in `websockets.c` still uses `json_each()` instead of `event_tags`
---
## Phase A: Move Remaining Periodic Tasks Off Main Thread
**Goal:** Eliminate the last regular SQLite calls from the lws-main event loop timer.
### A1: Move `generate_and_post_status_event()` to write worker
Currently called synchronously from the main event loop at `websockets.c:3455`.
**Changes:**
- Add `THREAD_POOL_JOB_STATUS_POST` to `thread_pool.h` job type enum
- Add executor in `thread_pool.c` that calls `generate_and_post_status_event()`
- In `websockets.c` main loop, replace direct call with `thread_pool_submit_write()` job
- The write worker has a DB connection, so `generate_stats_json()``store_event()``broadcast_event_to_subscriptions()` all work naturally
- Note: `broadcast_event_to_subscriptions()` is thread-safe (uses its own mutex)
**Files:** `thread_pool.h`, `thread_pool.c`, `websockets.c`
### A2: Move monitoring queries to read/write worker
`monitoring_on_event_stored()` and `monitoring_on_subscription_change()` in `api.c` call `generate_event_driven_monitoring()` / `generate_subscription_driven_monitoring()` which do DB queries and then broadcast ephemeral events.
**Changes:**
- Add `THREAD_POOL_JOB_MONITORING` job type
- Submit monitoring generation as a read job (it mostly does SELECTs)
- The completion broadcasts the ephemeral event via `lws_cancel_service()` + completion queue
- Alternative simpler approach: since these are throttled (default 5s) and only fire when kind 24567 subscribers exist, they could run on the write worker as fire-and-forget
**Files:** `thread_pool.h`, `thread_pool.c`, `api.c`, `websockets.c`
### A3: Move `ip_ban_cleanup()` and `ip_ban_log_stats()` to write worker
Currently called from the 60-second periodic timer at `websockets.c:3479-3480`.
**Changes:**
- Add `THREAD_POOL_JOB_IP_BAN_CLEANUP` job type (or reuse a generic callback job)
- Submit as fire-and-forget write job from the periodic timer
- `ip_ban_cleanup()` does in-memory cleanup + optional DB writes
- `ip_ban_log_stats()` is purely logging, no DB
**Files:** `thread_pool.h`, `thread_pool.c`, `ip_ban.c`, `websockets.c`
---
## Phase B: Route Remaining Sync `store_event()` Calls Through Async Worker
**Goal:** Eliminate all synchronous `store_event()` calls from lws-main context.
### Current sync `store_event()` call sites on main thread
| Call site | File | Context | Frequency |
|-----------|------|---------|-----------|
| Kind 1059 gift wrap store | `websockets.c:1795,1803,1819` | NIP-17 DM processing | Low |
| Kind 14 DM store | `websockets.c:1843` | NIP-17 DM processing | Low |
| Regular event fallback | `websockets.c:1862,1876` | Non-async-eligible events | Low |
| Kind 1059 (auth path) | `websockets.c:2594,2602,2618` | NIP-42 auth + DM | Low |
| Kind 14 (auth path) | `websockets.c:2642` | NIP-42 auth + DM | Low |
| Regular (auth path) | `websockets.c:2661,2675` | NIP-42 auth fallback | Low |
| Admin response store | `config.c:2585` | Admin API response | Very low |
| DM admin gift wrap | `dm_admin.c:389` | Admin DM response | Very low |
| NIP-09 deletion store | `nip009.c:135` | Deletion events | Low |
| Status event store | `api.c:595` | Periodic status post | Very low (handled by Phase A1) |
| Admin response event | `api.c:908` | Admin API | Very low |
| DM chat response | `api.c:1377` | Admin DM chat | Very low |
| Relay-generated event | `api.c:2350` | Relay metadata | Very low |
| DM stats response | `websockets.c:3648` | DM stats command | Very low |
### Strategy
Rather than converting each call site individually, create a **synchronous-to-async bridge**:
**Changes:**
- Create `store_event_async(cJSON* event, store_event_callback_t cb, void* ctx)` that submits the event to the write worker
- For call sites that need the result (e.g., to broadcast after store), use a completion callback
- For fire-and-forget sites (admin responses, DM responses), use NULL callback
- The existing `store_event()` wrapper becomes: submit to write worker + block on completion (for backward compat during transition)
- Eventually, all callers migrate to the async version
**Alternative simpler approach:** Since these are all low-frequency paths, and the write worker already has a DB connection, we can submit them as generic write jobs with a callback that does the store + broadcast. The main thread just needs to hand off the event JSON.
**Files:** `main.c`, `websockets.c`, `api.c`, `config.c`, `dm_admin.c`, `nip009.c`, `thread_pool.h`, `thread_pool.c`
---
## Phase C: `db_ops` Abstraction Cleanup for PG Fork
**Goal:** Ensure all raw `sqlite3_*` calls are inside `db_ops.c` only. External code uses only `db_ops.h` functions.
### C1: Add `db_open_worker_connection()` / `db_close_worker_connection()` to `db_ops.h`
Currently, `thread_pool.c:208` and `websockets.c:537` call `sqlite3_open_v2()` directly.
**Changes:**
- Add to `db_ops.h`:
```c
void* db_open_worker_connection(void); // Returns opaque connection handle
void db_close_worker_connection(void* conn);
```
- Implementation in `db_ops.c`: opens a new SQLite connection with WAL + busy timeout
- `thread_pool.c` worker init calls `db_open_worker_connection()` instead of raw `sqlite3_open_v2()`
- `websockets.c` event-worker calls `db_open_worker_connection()` instead of raw `sqlite3_open_v2()`
- Remove `#include <sqlite3.h>` from `thread_pool.c` and `websockets.c`
**Files:** `db_ops.h`, `db_ops.c`, `thread_pool.c`, `websockets.c`
### C2: Audit and remove remaining `sqlite3` includes outside `db_ops.c`
After C1, grep for any remaining `sqlite3` references outside `db_ops.c` and eliminate them.
**Files:** All `.c` files
### C3: Unify COUNT query tag path
The COUNT query builder in `websockets.c:3849` still uses `json_each(json(tags))` while the REQ query builder in `main.c:1604` uses the indexed `event_tags` table. Unify to use `event_tags` for consistency.
**Files:** `websockets.c`
---
## Phase D: Quick Wins
### D1: Fix 6 — Reduce reader threads from 4 to 1
Change default in `thread_pool_init()` or make configurable. With all reads going through the pool, a single reader is sufficient unless profiling shows otherwise.
**Files:** `thread_pool.c` or config default
### D2: Fix 9 — Cache NIP-11 response
Cache the serialized NIP-11 JSON string with a 60-second TTL. Invalidate on admin config change.
**Files:** `nip011.c`
---
## Implementation Order
| # | Task | Risk | Dependencies |
|---|------|------|-------------|
| 1 | A3: ip_ban_cleanup to write worker | Very Low | None |
| 2 | A1: status post to write worker | Low | None |
| 3 | A2: monitoring to worker | Low | None |
| 4 | D1: reduce reader threads to 1 | Very Low | None |
| 5 | D2: cache NIP-11 response | Very Low | None |
| 6 | C1: db_open/close_worker_connection | Low | None |
| 7 | C3: unify COUNT tag query path | Low | None |
| 8 | C2: audit/remove sqlite3 includes | Very Low | C1 |
| 9 | B: route sync store_event through async | Medium | A1 (status post already handled) |
## Expected Outcome
After all phases:
- **lws-main has zero SQLite calls** during the event loop
- **All `sqlite3_*` calls are inside `db_ops.c`** — clean abstraction boundary
- **PG fork can replace `db_ops.c`** without touching any other file
- Thread model is fully realized:
- `lws-main` = WebSocket I/O + JSON parse + subscription match + completion drain
- `event-worker` = signature verify + dedup + store submission
- `db-read` = REQ queries + COUNT queries + monitoring reads
- `db-write` = event INSERTs + subscription logging + periodic tasks + admin writes
```mermaid
flowchart LR
CLIENT[Nostr Clients] --> LWS[lws-main<br/>Zero SQLite<br/>WS I/O + JSON + Sub Match]
LWS -->|EVENT| EW[event-worker<br/>Verify + Dedup]
EW -->|store job| DW[db-write<br/>INSERT events<br/>Sub logging<br/>Periodic tasks<br/>Admin writes]
EW -->|completion| LWS
DW -->|completion| LWS
LWS -->|REQ/COUNT| DR[db-read<br/>SELECT queries<br/>Monitoring reads]
DR -->|completion| LWS
LWS -->|OK/EVENT/EOSE/COUNT| CLIENT
```
+107
View File
@@ -0,0 +1,107 @@
# WAL Checkpoint Fix — db-write Thread CPU Plan
## Problem
Fresh profiling of the production server shows the `db-write` thread consuming **77% average CPU** (peaking at 100%) while `lws-main` sits at 0.3%.
The `perf` callgraph confirms 100% of the hot symbols are SQLite **B-tree read operations** running on the `db-write` thread:
| % CPU | Symbol | Role |
|-------|--------|------|
| 13.88 | `sqlite3BtreeTableMoveto` | B-tree seek |
| 10.05 | `sqlite3VdbeExec` | VM execution |
| 7.77 | `rep_movs_alternative` | kernel memcpy in pread |
| 7.76 | `sqlite3GetVarint` | varint decode |
| 6.66 | `pcache1Fetch` | page cache lookup |
| 6.02 | `memcpy` | data copy |
These are **read-path** functions, not write-path. The `event-worker` thread accumulated only 3 CPU ticks over 5 minutes, meaning very few events are actually being stored.
## Root Cause
SQLite WAL auto-checkpoint. By default SQLite triggers a checkpoint every 1000 WAL pages. The checkpoint is executed by the **next writer** — which is always the `db-write` thread since all writes are funnelled through it.
A WAL checkpoint reads every dirty page from the WAL file and writes it back to the main database file. This involves B-tree traversals to locate pages, which explains the `BtreeTableMoveto` dominance.
The current `db_open_worker_connection` in `src/db_ops.c` sets `PRAGMA journal_mode=WAL` and `busy_timeout=5000` but does **not** configure `wal_autocheckpoint`. SQLite's default of 1000 pages applies.
In earlier profiling runs the same CPU burn appeared on `lws-main` at 55-67% — because that thread was doing the writes directly before the thread-pool refactoring. The work simply moved threads; the total cost is unchanged.
## Implementation Plan
### Step 1 — Disable auto-checkpoint on the write connection
In `db_open_worker_connection` add:
```c
sqlite3_exec(db, "PRAGMA wal_autocheckpoint=0;", NULL, NULL, NULL);
```
This prevents the write thread from ever running a checkpoint inline with normal writes.
### Step 2 — Add a periodic checkpoint job type to the thread pool
Add a new job type `THREAD_POOL_JOB_WAL_CHECKPOINT` to `thread_pool.h`.
The handler in `thread_pool.c` calls:
```c
sqlite3_wal_checkpoint_v2(db, NULL, SQLITE_CHECKPOINT_PASSIVE, NULL, NULL);
```
`PASSIVE` mode checkpoints only pages that are not currently being read, so it never blocks readers.
### Step 3 — Submit the checkpoint job on a timer
In the existing 60-second periodic timer in `websockets.c`, add a call to submit a WAL checkpoint job to the write queue:
```c
thread_pool_submit_wal_checkpoint();
```
This runs the checkpoint once per minute on the write thread, but as a **bounded, predictable** operation rather than being triggered unpredictably by every INSERT.
### Step 4 — Also disable auto-checkpoint on reader connections
In `db_open_worker_connection`, the same `wal_autocheckpoint=0` applies to reader connections too. Readers can also trigger checkpoints in SQLite; disabling it on all connections ensures only the explicit periodic job does checkpointing.
### Step 5 — Run a TRUNCATE checkpoint at shutdown
In `thread_pool_shutdown`, before closing the write connection, run:
```c
sqlite3_wal_checkpoint_v2(db, NULL, SQLITE_CHECKPOINT_TRUNCATE, NULL, NULL);
```
This ensures the WAL is fully flushed and truncated on clean shutdown, keeping the database file compact.
## Files Changed
| File | Change |
|------|--------|
| `src/db_ops.c` | Add `PRAGMA wal_autocheckpoint=0` in `db_open_worker_connection` |
| `src/thread_pool.h` | Add `THREAD_POOL_JOB_WAL_CHECKPOINT` enum value |
| `src/thread_pool.c` | Add `execute_wal_checkpoint_job` handler; add checkpoint-at-shutdown in `thread_pool_shutdown`; add `thread_pool_submit_wal_checkpoint` helper |
| `src/websockets.c` | Call `thread_pool_submit_wal_checkpoint()` in the 60-second periodic timer |
## Expected Impact
- `db-write` CPU should drop from ~77% to near 0% when idle (no events to store)
- Periodic checkpoint bursts of a few seconds every 60s instead of continuous burn
- No change to data durability — WAL still protects against crashes; checkpoint just moves data from WAL to main DB file
- Readers are unaffected — PASSIVE checkpoint never blocks them
## Risk
- If the relay crashes between checkpoints, the WAL file may be larger than before (up to 60s of accumulated writes). This is safe — SQLite replays the WAL on next open. The WAL file size is bounded by the write rate, which is low.
- PASSIVE checkpoint may not checkpoint all pages if readers hold them. This is fine — the next checkpoint will catch up.
## Verification
After deploying, re-run the profiler:
```bash
./deploy_lt_debug.sh && DURATION=300 INTERVAL=5 ./tests/thread_cpu_profile.sh
```
Expected: `db-write` avg CPU drops below 5%.
+1 -1
View File
@@ -1 +1 @@
694269
804819
+28 -8
View File
@@ -55,6 +55,14 @@ int generate_monitoring_event_for_type(const char* d_tag_value, cJSON* (*query_f
// Forward declaration for CPU metrics query function
cJSON* query_cpu_metrics(void);
static int api_open_temp_db_connection(void** out_db) {
return db_open_worker_connection(db_get_database_path(), out_db);
}
static void api_close_temp_db_connection(void* db) {
db_close_worker_connection(db);
}
// Monitoring system helper functions
int get_monitoring_throttle_seconds(void) {
return get_config_int("kind_24567_reporting_throttle_sec", 5);
@@ -62,7 +70,8 @@ int get_monitoring_throttle_seconds(void) {
// Query event kind distribution from database
cJSON* query_event_kind_distribution(void) {
if (!db_is_available()) {
void* temp_db = NULL;
if (!db_is_available() && api_open_temp_db_connection(&temp_db) != 0) {
DEBUG_ERROR("Database not available for monitoring query");
return NULL;
}
@@ -95,12 +104,14 @@ cJSON* query_event_kind_distribution(void) {
cJSON_AddNumberToObject(distribution, "total_events", total_events);
cJSON_AddItemToObject(distribution, "kinds", kinds_array);
api_close_temp_db_connection(temp_db);
return distribution;
}
// Query time-based statistics from database
cJSON* query_time_based_statistics(void) {
if (!db_is_available()) {
void* temp_db = NULL;
if (!db_is_available() && api_open_temp_db_connection(&temp_db) != 0) {
DEBUG_ERROR("Database not available for time stats query");
return NULL;
}
@@ -146,12 +157,14 @@ cJSON* query_time_based_statistics(void) {
cJSON_AddItemToObject(time_stats, "periods", periods_array);
cJSON_AddNumberToObject(time_stats, "total_events", total_events);
api_close_temp_db_connection(temp_db);
return time_stats;
}
// Query top pubkeys by event count from database
cJSON* query_top_pubkeys(void) {
if (!db_is_available()) {
void* temp_db = NULL;
if (!db_is_available() && api_open_temp_db_connection(&temp_db) != 0) {
DEBUG_ERROR("Database not available for top pubkeys query");
return NULL;
}
@@ -163,6 +176,7 @@ cJSON* query_top_pubkeys(void) {
cJSON* rows = db_get_top_pubkeys_rows(10);
if (!rows) {
cJSON_Delete(top_pubkeys);
api_close_temp_db_connection(temp_db);
DEBUG_ERROR("Failed to query top pubkeys rows");
return NULL;
}
@@ -188,6 +202,7 @@ cJSON* query_top_pubkeys(void) {
cJSON_AddItemToObject(top_pubkeys, "pubkeys", pubkeys_array);
cJSON_AddNumberToObject(top_pubkeys, "total_events", total_events);
api_close_temp_db_connection(temp_db);
return top_pubkeys;
}
@@ -195,7 +210,8 @@ cJSON* query_top_pubkeys(void) {
// Query detailed subscription information from database log (ADMIN ONLY)
// Uses subscriptions table instead of in-memory iteration to avoid mutex contention
cJSON* query_subscription_details(void) {
if (!db_is_available()) {
void* temp_db = NULL;
if (!db_is_available() && api_open_temp_db_connection(&temp_db) != 0) {
DEBUG_ERROR("Database not available for subscription details query");
return NULL;
}
@@ -226,6 +242,7 @@ cJSON* query_subscription_details(void) {
cJSON_Delete(subscriptions_data);
cJSON_Delete(data);
cJSON_Delete(subscriptions_array);
api_close_temp_db_connection(temp_db);
DEBUG_ERROR("Failed to query subscription details rows");
return NULL;
}
@@ -278,9 +295,9 @@ cJSON* query_subscription_details(void) {
// Parse wsi pointer from hex string
struct lws* wsi = NULL;
if (sscanf(wsi_pointer, "%p", (void**)&wsi) == 1 && wsi != NULL) {
// Get per_session_data from wsi
struct per_session_data* pss = (struct per_session_data*)lws_wsi_user(wsi);
if (pss) {
// Resolve live session data only if this wsi is still tracked.
struct per_session_data* pss = NULL;
if (websocket_get_live_pss(wsi, &pss) && pss) {
db_queries = pss->db_queries_executed;
db_rows = pss->db_rows_returned;
@@ -341,6 +358,7 @@ cJSON* query_subscription_details(void) {
DEBUG_LOG("Total subscriptions found: %d", row_count);
DEBUG_LOG("=== END SUBSCRIPTION_DETAILS QUERY DEBUG ===");
api_close_temp_db_connection(temp_db);
return subscriptions_data;
}
@@ -1165,7 +1183,8 @@ cJSON* query_cpu_metrics(void) {
// Generate stats JSON from database queries
char* generate_stats_json(void) {
if (!db_is_available()) {
void* temp_db = NULL;
if (!db_is_available() && api_open_temp_db_connection(&temp_db) != 0) {
DEBUG_ERROR("Database not available for stats generation");
return NULL;
}
@@ -1274,6 +1293,7 @@ char* generate_stats_json(void) {
DEBUG_ERROR("Failed to generate stats JSON");
}
api_close_temp_db_connection(temp_db);
return json_string;
}
+135 -6
View File
@@ -31,7 +31,6 @@
#include <errno.h>
#include <signal.h>
#include <libwebsockets.h>
// External database connection (from main.c)
// External shutdown flag (from main.c)
@@ -108,6 +107,17 @@ static cJSON* g_pending_config_event = NULL;
// Temporary storage for relay private key during first-time setup
static char g_temp_relay_privkey[65] = {0};
// Runtime cache for relay private key to support strict mode (g_db = NULL)
static char g_cached_relay_privkey[65] = {0};
static int config_open_temp_db_connection(void** out_db) {
return db_open_worker_connection(db_get_database_path(), out_db);
}
static void config_close_temp_db_connection(void* db) {
db_close_worker_connection(db);
}
// ================================
// UTILITY FUNCTIONS
// ================================
@@ -412,6 +422,9 @@ void cleanup_configuration_system(void) {
g_pending_config_event = NULL;
}
// Clear cached sensitive material
memset(g_cached_relay_privkey, 0, sizeof(g_cached_relay_privkey));
// Configuration system now uses direct database queries instead of cache
// No cleanup needed for cache system
}
@@ -493,6 +506,10 @@ int store_relay_private_key(const char* relay_privkey_hex) {
}
if (db_store_relay_private_key_hex(relay_privkey_hex) == 0) {
// Keep an in-memory runtime copy for strict mode where g_db is NULL
memset(g_cached_relay_privkey, 0, sizeof(g_cached_relay_privkey));
strncpy(g_cached_relay_privkey, relay_privkey_hex, sizeof(g_cached_relay_privkey) - 1);
g_cached_relay_privkey[sizeof(g_cached_relay_privkey) - 1] = '\0';
return 0;
}
@@ -501,11 +518,16 @@ int store_relay_private_key(const char* relay_privkey_hex) {
}
char* get_relay_private_key(void) {
// Fast path for strict runtime mode where g_db may be NULL
if (strlen(g_cached_relay_privkey) == 64) {
return strdup(g_cached_relay_privkey);
}
if (!db_is_available()) {
DEBUG_ERROR("Database not available for relay private key retrieval");
return NULL;
}
char* private_key = db_get_relay_private_key_hex_dup();
if (!private_key || strlen(private_key) != 64) {
if (private_key) {
@@ -515,9 +537,35 @@ char* get_relay_private_key(void) {
return NULL;
}
// Prime runtime cache from DB for subsequent strict-mode operations
memset(g_cached_relay_privkey, 0, sizeof(g_cached_relay_privkey));
strncpy(g_cached_relay_privkey, private_key, sizeof(g_cached_relay_privkey) - 1);
g_cached_relay_privkey[sizeof(g_cached_relay_privkey) - 1] = '\0';
return private_key;
}
int preload_relay_private_key_cache(void) {
if (!db_is_available()) {
DEBUG_ERROR("Database not available for relay private key cache preload");
return -1;
}
char* private_key = db_get_relay_private_key_hex_dup();
if (!private_key || strlen(private_key) != 64) {
if (private_key) free(private_key);
DEBUG_ERROR("Relay private key not found for cache preload");
return -1;
}
memset(g_cached_relay_privkey, 0, sizeof(g_cached_relay_privkey));
strncpy(g_cached_relay_privkey, private_key, sizeof(g_cached_relay_privkey) - 1);
g_cached_relay_privkey[sizeof(g_cached_relay_privkey) - 1] = '\0';
free(private_key);
return 0;
}
const char* get_temp_relay_private_key(void) {
if (strlen(g_temp_relay_privkey) == 64) {
return g_temp_relay_privkey;
@@ -2949,7 +2997,8 @@ int handle_kind_23456_unified(cJSON* event, char* error_message, size_t error_si
int handle_auth_query_unified(cJSON* event, const char* query_type, char* error_message, size_t error_size, struct lws* wsi) {
// Suppress unused parameter warning
(void)wsi;
if (!db_is_available()) {
void* temp_db = NULL;
if (!db_is_available() && config_open_temp_db_connection(&temp_db) != 0) {
snprintf(error_message, error_size, "database not available");
return -1;
}
@@ -3041,6 +3090,9 @@ int handle_auth_query_unified(cJSON* event, const char* query_type, char* error_
if (!admin_pubkey) {
cJSON_Delete(response);
cJSON_Delete(results_array);
if (temp_db) {
config_close_temp_db_connection(temp_db);
}
snprintf(error_message, error_size, "missing admin pubkey for response");
return -1;
}
@@ -3049,12 +3101,18 @@ int handle_auth_query_unified(cJSON* event, const char* query_type, char* error_
if (send_admin_response_event(response, admin_pubkey, wsi) == 0) {
cJSON_Delete(response);
cJSON_Delete(results_array);
if (temp_db) {
config_close_temp_db_connection(temp_db);
}
return 0;
}
cJSON_Delete(response);
}
cJSON_Delete(results_array);
if (temp_db) {
config_close_temp_db_connection(temp_db);
}
snprintf(error_message, error_size, "failed to send auth query response");
return -1;
}
@@ -3063,7 +3121,8 @@ int handle_auth_query_unified(cJSON* event, const char* query_type, char* error_
int handle_config_query_unified(cJSON* event, const char* query_type, char* error_message, size_t error_size, struct lws* wsi) {
// Suppress unused parameter warning
(void)wsi;
if (!db_is_available()) {
void* temp_db = NULL;
if (!db_is_available() && config_open_temp_db_connection(&temp_db) != 0) {
snprintf(error_message, error_size, "database not available");
return -1;
}
@@ -3156,6 +3215,9 @@ int handle_config_query_unified(cJSON* event, const char* query_type, char* erro
if (!admin_pubkey) {
cJSON_Delete(response);
cJSON_Delete(results_array);
if (temp_db) {
config_close_temp_db_connection(temp_db);
}
snprintf(error_message, error_size, "missing admin pubkey for response");
return -1;
}
@@ -3164,12 +3226,18 @@ int handle_config_query_unified(cJSON* event, const char* query_type, char* erro
if (send_admin_response_event(response, admin_pubkey, wsi) == 0) {
cJSON_Delete(response);
cJSON_Delete(results_array);
if (temp_db) {
config_close_temp_db_connection(temp_db);
}
return 0;
}
cJSON_Delete(response);
}
cJSON_Delete(results_array);
if (temp_db) {
config_close_temp_db_connection(temp_db);
}
snprintf(error_message, error_size, "failed to send config query response");
return -1;
}
@@ -3672,7 +3740,8 @@ int handle_auth_rule_modification_unified(cJSON* event, char* error_message, siz
int handle_stats_query_unified(cJSON* event, char* error_message, size_t error_size, struct lws* wsi) {
// Suppress unused parameter warning
(void)wsi;
if (!db_is_available()) {
void* temp_db = NULL;
if (!db_is_available() && config_open_temp_db_connection(&temp_db) != 0) {
snprintf(error_message, error_size, "database not available");
return -1;
}
@@ -3778,6 +3847,9 @@ int handle_stats_query_unified(cJSON* event, char* error_message, size_t error_s
if (!admin_pubkey) {
cJSON_Delete(response);
if (temp_db) {
config_close_temp_db_connection(temp_db);
}
snprintf(error_message, error_size, "missing admin pubkey for response");
return -1;
}
@@ -3785,10 +3857,16 @@ int handle_stats_query_unified(cJSON* event, char* error_message, size_t error_s
// Send response as signed kind 23457 event
if (send_admin_response_event(response, admin_pubkey, wsi) == 0) {
cJSON_Delete(response);
if (temp_db) {
config_close_temp_db_connection(temp_db);
}
return 0;
}
cJSON_Delete(response);
if (temp_db) {
config_close_temp_db_connection(temp_db);
}
snprintf(error_message, error_size, "failed to send stats query response");
return -1;
}
@@ -4516,11 +4594,58 @@ void force_config_cache_refresh(void) {
int reload_config_from_table(void) {
force_config_cache_refresh();
if (!db_is_available()) {
return -1;
}
cJSON* rows = db_get_all_config_rows();
if (!rows || !cJSON_IsArray(rows)) {
if (rows) cJSON_Delete(rows);
return -1;
}
pthread_mutex_lock(&g_config_cache_mutex);
cJSON* row = NULL;
cJSON_ArrayForEach(row, rows) {
if (!cJSON_IsObject(row)) {
continue;
}
cJSON* key_obj = cJSON_GetObjectItemCaseSensitive(row, "key");
cJSON* value_obj = cJSON_GetObjectItemCaseSensitive(row, "value");
if (!cJSON_IsString(key_obj) || !cJSON_IsString(value_obj)) {
continue;
}
const char* key = cJSON_GetStringValue(key_obj);
const char* value = cJSON_GetStringValue(value_obj);
if (!key || !value) {
continue;
}
int idx = find_cache_index_by_key(key);
if (idx < 0) {
idx = find_empty_cache_index();
}
if (idx < 0) {
break;
}
g_config_cache[idx].in_use = 1;
strncpy(g_config_cache[idx].key, key, sizeof(g_config_cache[idx].key) - 1);
g_config_cache[idx].key[sizeof(g_config_cache[idx].key) - 1] = '\0';
strncpy(g_config_cache[idx].value, value, sizeof(g_config_cache[idx].value) - 1);
g_config_cache[idx].value[sizeof(g_config_cache[idx].value) - 1] = '\0';
}
pthread_mutex_unlock(&g_config_cache_mutex);
cJSON_Delete(rows);
return 0;
}
const char* get_config_value_from_table(const char* key) {
if (!db_is_available() || !key) {
if (!key) {
return NULL;
}
@@ -4535,6 +4660,10 @@ const char* get_config_value_from_table(const char* key) {
pthread_mutex_unlock(&g_config_cache_mutex);
if (!db_is_available()) {
return NULL;
}
char* db_value = db_get_config_value_dup(key);
if (!db_value) {
return NULL;
+1
View File
@@ -77,6 +77,7 @@ char* extract_pubkey_from_filename(const char* filename);
int store_relay_private_key(const char* relay_privkey_hex);
char* get_relay_private_key(void);
const char* get_temp_relay_private_key(void); // For first-time startup only
int preload_relay_private_key_cache(void); // Preload relay private key before strict DB mode
// NIP-42 authentication configuration functions
int parse_auth_required_kinds(const char* kinds_str, int* kinds_array, int max_kinds);
+164 -85
View File
@@ -57,6 +57,34 @@ void db_clear_thread_connection(void) {
g_thread_db = NULL;
}
int db_open_worker_connection(const char* db_path, void** out_connection) {
if (!out_connection) return -1;
*out_connection = NULL;
const char* effective_path = (db_path && db_path[0] != '\0') ? db_path : g_database_path;
if (!effective_path || effective_path[0] == '\0') return -1;
sqlite3* db = NULL;
int rc = sqlite3_open_v2(effective_path, &db, SQLITE_OPEN_READWRITE, NULL);
if (rc != SQLITE_OK) {
if (db) sqlite3_close(db);
return -1;
}
sqlite3_exec(db, "PRAGMA journal_mode=WAL;", NULL, NULL, NULL);
sqlite3_exec(db, "PRAGMA wal_autocheckpoint=0;", NULL, NULL, NULL);
sqlite3_busy_timeout(db, 5000);
db_set_thread_connection(db);
*out_connection = (void*)db;
return 0;
}
void db_close_worker_connection(void* connection) {
if (!connection) return;
db_clear_thread_connection();
sqlite3_close((sqlite3*)connection);
}
int db_is_available(void) {
return db_active_connection() != NULL;
}
@@ -143,14 +171,15 @@ void db_finalize_stmt(db_stmt_t* stmt) {
int db_log_subscription_created(const char* sub_id, const char* wsi_ptr,
const char* client_ip, const char* filter_json) {
if (!g_db || !sub_id || !wsi_ptr || !client_ip) return -1;
sqlite3* db = db_active_connection();
if (!db || !sub_id || !wsi_ptr || !client_ip) return -1;
const char* sql =
"INSERT OR REPLACE INTO subscriptions (subscription_id, wsi_pointer, client_ip, event_type, filter_json) "
"VALUES (?, ?, ?, 'created', ?)";
sqlite3_stmt* stmt = NULL;
if (sqlite3_prepare_v2(g_db, sql, -1, &stmt, NULL) != SQLITE_OK) return -1;
if (sqlite3_prepare_v2(db, sql, -1, &stmt, NULL) != SQLITE_OK) return -1;
sqlite3_bind_text(stmt, 1, sub_id, -1, SQLITE_TRANSIENT);
sqlite3_bind_text(stmt, 2, wsi_ptr, -1, SQLITE_TRANSIENT);
@@ -163,14 +192,15 @@ int db_log_subscription_created(const char* sub_id, const char* wsi_ptr,
}
int db_log_subscription_closed(const char* sub_id, const char* client_ip) {
if (!g_db || !sub_id) return -1;
sqlite3* db = db_active_connection();
if (!db || !sub_id) return -1;
const char* insert_sql =
"INSERT INTO subscriptions (subscription_id, wsi_pointer, client_ip, event_type) "
"VALUES (?, '', ?, 'closed')";
sqlite3_stmt* stmt = NULL;
if (sqlite3_prepare_v2(g_db, insert_sql, -1, &stmt, NULL) == SQLITE_OK) {
if (sqlite3_prepare_v2(db, insert_sql, -1, &stmt, NULL) == SQLITE_OK) {
sqlite3_bind_text(stmt, 1, sub_id, -1, SQLITE_TRANSIENT);
sqlite3_bind_text(stmt, 2, client_ip ? client_ip : "unknown", -1, SQLITE_TRANSIENT);
sqlite3_step(stmt);
@@ -182,7 +212,7 @@ int db_log_subscription_closed(const char* sub_id, const char* client_ip) {
"SET ended_at = strftime('%s', 'now') "
"WHERE subscription_id = ? AND event_type = 'created' AND ended_at IS NULL";
if (sqlite3_prepare_v2(g_db, update_sql, -1, &stmt, NULL) != SQLITE_OK) return -1;
if (sqlite3_prepare_v2(db, update_sql, -1, &stmt, NULL) != SQLITE_OK) return -1;
sqlite3_bind_text(stmt, 1, sub_id, -1, SQLITE_TRANSIENT);
int rc = sqlite3_step(stmt);
sqlite3_finalize(stmt);
@@ -191,7 +221,8 @@ int db_log_subscription_closed(const char* sub_id, const char* client_ip) {
}
int db_log_subscription_disconnected(const char* client_ip) {
if (!g_db || !client_ip) return -1;
sqlite3* db = db_active_connection();
if (!db || !client_ip) return -1;
const char* update_sql =
"UPDATE subscriptions "
@@ -199,11 +230,11 @@ int db_log_subscription_disconnected(const char* client_ip) {
"WHERE client_ip = ? AND event_type = 'created' AND ended_at IS NULL";
sqlite3_stmt* stmt = NULL;
if (sqlite3_prepare_v2(g_db, update_sql, -1, &stmt, NULL) != SQLITE_OK) return -1;
if (sqlite3_prepare_v2(db, update_sql, -1, &stmt, NULL) != SQLITE_OK) return -1;
sqlite3_bind_text(stmt, 1, client_ip, -1, SQLITE_TRANSIENT);
int rc = sqlite3_step(stmt);
int changes = sqlite3_changes(g_db);
int changes = sqlite3_changes(db);
sqlite3_finalize(stmt);
if (rc != SQLITE_DONE) return -1;
@@ -212,7 +243,7 @@ int db_log_subscription_disconnected(const char* client_ip) {
"INSERT INTO subscriptions (subscription_id, wsi_pointer, client_ip, event_type) "
"VALUES ('disconnect', '', ?, 'disconnected')";
if (sqlite3_prepare_v2(g_db, insert_sql, -1, &stmt, NULL) == SQLITE_OK) {
if (sqlite3_prepare_v2(db, insert_sql, -1, &stmt, NULL) == SQLITE_OK) {
sqlite3_bind_text(stmt, 1, client_ip, -1, SQLITE_TRANSIENT);
sqlite3_step(stmt);
sqlite3_finalize(stmt);
@@ -223,7 +254,8 @@ int db_log_subscription_disconnected(const char* client_ip) {
}
int db_update_subscription_events_sent(const char* sub_id, int events_sent) {
if (!g_db || !sub_id) return -1;
sqlite3* db = db_active_connection();
if (!db || !sub_id) return -1;
const char* sql =
"UPDATE subscriptions "
@@ -231,7 +263,7 @@ int db_update_subscription_events_sent(const char* sub_id, int events_sent) {
"WHERE subscription_id = ? AND event_type = 'created'";
sqlite3_stmt* stmt = NULL;
if (sqlite3_prepare_v2(g_db, sql, -1, &stmt, NULL) != SQLITE_OK) return -1;
if (sqlite3_prepare_v2(db, sql, -1, &stmt, NULL) != SQLITE_OK) return -1;
sqlite3_bind_int(stmt, 1, events_sent);
sqlite3_bind_text(stmt, 2, sub_id, -1, SQLITE_TRANSIENT);
@@ -242,7 +274,8 @@ int db_update_subscription_events_sent(const char* sub_id, int events_sent) {
}
int db_cleanup_orphaned_subscriptions(void) {
if (!g_db) return -1;
sqlite3* db = db_active_connection();
if (!db) return -1;
const char* sql =
"UPDATE subscriptions "
@@ -250,22 +283,23 @@ int db_cleanup_orphaned_subscriptions(void) {
"WHERE event_type = 'created' AND ended_at IS NULL";
sqlite3_stmt* stmt = NULL;
if (sqlite3_prepare_v2(g_db, sql, -1, &stmt, NULL) != SQLITE_OK) return -1;
if (sqlite3_prepare_v2(db, sql, -1, &stmt, NULL) != SQLITE_OK) return -1;
int rc = sqlite3_step(stmt);
int changes = sqlite3_changes(g_db);
int changes = sqlite3_changes(db);
sqlite3_finalize(stmt);
return (rc == SQLITE_DONE) ? changes : -1;
}
int db_get_event_pubkey(const char* event_id, char* pubkey_out, size_t pubkey_out_size) {
if (!g_db || !event_id || !pubkey_out || pubkey_out_size == 0) return -1;
sqlite3* db = db_active_connection();
if (!db || !event_id || !pubkey_out || pubkey_out_size == 0) return -1;
const char* sql = "SELECT pubkey FROM events WHERE id = ?";
sqlite3_stmt* stmt = NULL;
if (sqlite3_prepare_v2(g_db, sql, -1, &stmt, NULL) != SQLITE_OK) return -1;
if (sqlite3_prepare_v2(db, sql, -1, &stmt, NULL) != SQLITE_OK) return -1;
sqlite3_bind_text(stmt, 1, event_id, -1, SQLITE_TRANSIENT);
int rc = sqlite3_step(stmt);
@@ -283,18 +317,19 @@ int db_get_event_pubkey(const char* event_id, char* pubkey_out, size_t pubkey_ou
}
int db_delete_event_by_id(const char* event_id, const char* requester_pubkey) {
if (!g_db || !event_id || !requester_pubkey) return -1;
sqlite3* db = db_active_connection();
if (!db || !event_id || !requester_pubkey) return -1;
const char* sql = "DELETE FROM events WHERE id = ? AND pubkey = ?";
sqlite3_stmt* stmt = NULL;
if (sqlite3_prepare_v2(g_db, sql, -1, &stmt, NULL) != SQLITE_OK) return -1;
if (sqlite3_prepare_v2(db, sql, -1, &stmt, NULL) != SQLITE_OK) return -1;
sqlite3_bind_text(stmt, 1, event_id, -1, SQLITE_TRANSIENT);
sqlite3_bind_text(stmt, 2, requester_pubkey, -1, SQLITE_TRANSIENT);
int rc = sqlite3_step(stmt);
int changes = sqlite3_changes(g_db);
int changes = sqlite3_changes(db);
sqlite3_finalize(stmt);
if (rc != SQLITE_DONE) return -1;
@@ -303,7 +338,8 @@ int db_delete_event_by_id(const char* event_id, const char* requester_pubkey) {
int db_delete_events_by_address(const char* pubkey, int kind,
const char* d_tag, long before_timestamp) {
if (!g_db || !pubkey) return -1;
sqlite3* db = db_active_connection();
if (!db || !pubkey) return -1;
const char* sql_with_d =
"DELETE FROM events WHERE kind = ? AND pubkey = ? AND created_at <= ? "
@@ -314,7 +350,7 @@ int db_delete_events_by_address(const char* pubkey, int kind,
const int has_d = (d_tag && d_tag[0] != '\0');
sqlite3_stmt* stmt = NULL;
if (sqlite3_prepare_v2(g_db, has_d ? sql_with_d : sql_no_d, -1, &stmt, NULL) != SQLITE_OK) {
if (sqlite3_prepare_v2(db, has_d ? sql_with_d : sql_no_d, -1, &stmt, NULL) != SQLITE_OK) {
return -1;
}
@@ -326,7 +362,7 @@ int db_delete_events_by_address(const char* pubkey, int kind,
}
int rc = sqlite3_step(stmt);
int changes = sqlite3_changes(g_db);
int changes = sqlite3_changes(db);
sqlite3_finalize(stmt);
if (rc != SQLITE_DONE) return -1;
@@ -440,14 +476,15 @@ int db_count_with_sql(const char* sql, const char** bind_params, int bind_param_
char* db_execute_readonly_query_json(const char* query, const char* request_id,
char* error_message, size_t error_size,
int max_rows, int timeout_ms) {
if (!g_db || !query || !request_id || !error_message) return NULL;
sqlite3* db = db_active_connection();
if (!db || !query || !request_id || !error_message) return NULL;
sqlite3_busy_timeout(g_db, timeout_ms > 0 ? timeout_ms : 5000);
sqlite3_busy_timeout(db, timeout_ms > 0 ? timeout_ms : 5000);
sqlite3_stmt* stmt = NULL;
int rc = sqlite3_prepare_v2(g_db, query, -1, &stmt, NULL);
int rc = sqlite3_prepare_v2(db, query, -1, &stmt, NULL);
if (rc != SQLITE_OK) {
const char* err_msg = sqlite3_errmsg(g_db);
const char* err_msg = sqlite3_errmsg(db);
snprintf(error_message, error_size, "SQL prepare failed: %s", err_msg ? err_msg : "unknown");
return NULL;
}
@@ -560,7 +597,7 @@ char* db_execute_readonly_query_json(const char* query, const char* request_id,
sqlite3_finalize(stmt);
if (rc != SQLITE_DONE && rc != SQLITE_ROW) {
const char* err_msg = sqlite3_errmsg(g_db);
const char* err_msg = sqlite3_errmsg(db);
snprintf(error_message, error_size, "SQL execution failed: %s", err_msg ? err_msg : "unknown");
cJSON_Delete(rows);
cJSON_Delete(response);
@@ -587,11 +624,12 @@ char* db_execute_readonly_query_json(const char* query, const char* request_id,
}
int db_get_total_event_count_ll(long long* out_count) {
if (!g_db || !out_count) return -1;
sqlite3* db = db_active_connection();
if (!db || !out_count) return -1;
sqlite3_stmt* stmt = NULL;
const char* sql = "SELECT COUNT(*) FROM events";
if (sqlite3_prepare_v2(g_db, sql, -1, &stmt, NULL) != SQLITE_OK) return -1;
if (sqlite3_prepare_v2(db, sql, -1, &stmt, NULL) != SQLITE_OK) return -1;
long long count = 0;
if (sqlite3_step(stmt) == SQLITE_ROW) {
@@ -604,11 +642,12 @@ int db_get_total_event_count_ll(long long* out_count) {
}
int db_get_event_count_since(time_t cutoff, long long* out_count) {
if (!g_db || !out_count) return -1;
sqlite3* db = db_active_connection();
if (!db || !out_count) return -1;
sqlite3_stmt* stmt = NULL;
const char* sql = "SELECT COUNT(*) FROM events WHERE created_at >= ?";
if (sqlite3_prepare_v2(g_db, sql, -1, &stmt, NULL) != SQLITE_OK) return -1;
if (sqlite3_prepare_v2(db, sql, -1, &stmt, NULL) != SQLITE_OK) return -1;
sqlite3_bind_int64(stmt, 1, (sqlite3_int64)cutoff);
@@ -623,11 +662,12 @@ int db_get_event_count_since(time_t cutoff, long long* out_count) {
}
cJSON* db_get_event_kind_distribution_rows(long long* out_total_events) {
if (!g_db) return NULL;
sqlite3* db = db_active_connection();
if (!db) return NULL;
sqlite3_stmt* stmt = NULL;
const char* sql = "SELECT kind, COUNT(*) as count FROM events GROUP BY kind ORDER BY count DESC";
if (sqlite3_prepare_v2(g_db, sql, -1, &stmt, NULL) != SQLITE_OK) return NULL;
if (sqlite3_prepare_v2(db, sql, -1, &stmt, NULL) != SQLITE_OK) return NULL;
cJSON* rows = cJSON_CreateArray();
if (!rows) {
@@ -661,11 +701,12 @@ cJSON* db_get_event_kind_distribution_rows(long long* out_total_events) {
}
cJSON* db_get_top_pubkeys_rows(int limit) {
if (!g_db) return NULL;
sqlite3* db = db_active_connection();
if (!db) return NULL;
sqlite3_stmt* stmt = NULL;
const char* sql = "SELECT pubkey, COUNT(*) as count FROM events GROUP BY pubkey ORDER BY count DESC LIMIT ?";
if (sqlite3_prepare_v2(g_db, sql, -1, &stmt, NULL) != SQLITE_OK) return NULL;
if (sqlite3_prepare_v2(db, sql, -1, &stmt, NULL) != SQLITE_OK) return NULL;
sqlite3_bind_int(stmt, 1, limit > 0 ? limit : 10);
@@ -695,7 +736,8 @@ cJSON* db_get_top_pubkeys_rows(int limit) {
}
cJSON* db_get_subscription_details_rows(void) {
if (!g_db) return NULL;
sqlite3* db = db_active_connection();
if (!db) return NULL;
sqlite3_stmt* stmt = NULL;
const char* sql =
@@ -703,7 +745,7 @@ cJSON* db_get_subscription_details_rows(void) {
"FROM active_subscriptions_log "
"ORDER BY created_at DESC";
if (sqlite3_prepare_v2(g_db, sql, -1, &stmt, NULL) != SQLITE_OK) return NULL;
if (sqlite3_prepare_v2(db, sql, -1, &stmt, NULL) != SQLITE_OK) return NULL;
cJSON* rows = cJSON_CreateArray();
if (!rows) {
@@ -743,11 +785,12 @@ cJSON* db_get_subscription_details_rows(void) {
}
cJSON* db_get_all_config_rows(void) {
if (!g_db) return NULL;
sqlite3* db = db_active_connection();
if (!db) return NULL;
sqlite3_stmt* stmt = NULL;
const char* sql = "SELECT key, value FROM config ORDER BY key";
if (sqlite3_prepare_v2(g_db, sql, -1, &stmt, NULL) != SQLITE_OK) return NULL;
if (sqlite3_prepare_v2(db, sql, -1, &stmt, NULL) != SQLITE_OK) return NULL;
cJSON* rows = cJSON_CreateArray();
if (!rows) {
@@ -775,11 +818,12 @@ cJSON* db_get_all_config_rows(void) {
}
char* db_get_config_value_dup(const char* key) {
if (!g_db || !key) return NULL;
sqlite3* db = db_active_connection();
if (!db || !key) return NULL;
sqlite3_stmt* stmt = NULL;
const char* sql = "SELECT value FROM config WHERE key = ?";
if (sqlite3_prepare_v2(g_db, sql, -1, &stmt, NULL) != SQLITE_OK) return NULL;
if (sqlite3_prepare_v2(db, sql, -1, &stmt, NULL) != SQLITE_OK) return NULL;
sqlite3_bind_text(stmt, 1, key, -1, SQLITE_TRANSIENT);
@@ -795,14 +839,15 @@ char* db_get_config_value_dup(const char* key) {
int db_set_config_value_full(const char* key, const char* value, const char* data_type,
const char* description, const char* category, int requires_restart) {
if (!g_db || !key || !value || !data_type) return -1;
sqlite3* db = db_active_connection();
if (!db || !key || !value || !data_type) return -1;
sqlite3_stmt* stmt = NULL;
const char* sql =
"INSERT OR REPLACE INTO config (key, value, data_type, description, category, requires_restart) "
"VALUES (?, ?, ?, ?, ?, ?)";
if (sqlite3_prepare_v2(g_db, sql, -1, &stmt, NULL) != SQLITE_OK) {
if (sqlite3_prepare_v2(db, sql, -1, &stmt, NULL) != SQLITE_OK) {
return -1;
}
@@ -819,11 +864,12 @@ int db_set_config_value_full(const char* key, const char* value, const char* dat
}
int db_update_config_value_only(const char* key, const char* value) {
if (!g_db || !key || !value) return -1;
sqlite3* db = db_active_connection();
if (!db || !key || !value) return -1;
sqlite3_stmt* stmt = NULL;
const char* sql = "UPDATE config SET value = ?, updated_at = strftime('%s', 'now') WHERE key = ?";
if (sqlite3_prepare_v2(g_db, sql, -1, &stmt, NULL) != SQLITE_OK) {
if (sqlite3_prepare_v2(db, sql, -1, &stmt, NULL) != SQLITE_OK) {
return -1;
}
@@ -836,12 +882,13 @@ int db_update_config_value_only(const char* key, const char* value) {
}
int db_upsert_config_value(const char* key, const char* value, const char* data_type) {
if (!g_db || !key || !value || !data_type) return -1;
sqlite3* db = db_active_connection();
if (!db || !key || !value || !data_type) return -1;
sqlite3_stmt* stmt = NULL;
const char* sql = "INSERT OR REPLACE INTO config (key, value, data_type) VALUES (?, ?, ?)";
if (sqlite3_prepare_v2(g_db, sql, -1, &stmt, NULL) != SQLITE_OK) {
if (sqlite3_prepare_v2(db, sql, -1, &stmt, NULL) != SQLITE_OK) {
return -1;
}
@@ -855,11 +902,12 @@ int db_upsert_config_value(const char* key, const char* value, const char* data_
}
int db_store_relay_private_key_hex(const char* relay_privkey_hex) {
if (!g_db || !relay_privkey_hex) return -1;
sqlite3* db = db_active_connection();
if (!db || !relay_privkey_hex) return -1;
sqlite3_stmt* stmt = NULL;
const char* sql = "INSERT OR REPLACE INTO relay_seckey (private_key_hex) VALUES (?)";
if (sqlite3_prepare_v2(g_db, sql, -1, &stmt, NULL) != SQLITE_OK) return -1;
if (sqlite3_prepare_v2(db, sql, -1, &stmt, NULL) != SQLITE_OK) return -1;
sqlite3_bind_text(stmt, 1, relay_privkey_hex, -1, SQLITE_TRANSIENT);
int rc = sqlite3_step(stmt);
@@ -869,11 +917,12 @@ int db_store_relay_private_key_hex(const char* relay_privkey_hex) {
}
char* db_get_relay_private_key_hex_dup(void) {
if (!g_db) return NULL;
sqlite3* db = db_active_connection();
if (!db) return NULL;
sqlite3_stmt* stmt = NULL;
const char* sql = "SELECT private_key_hex FROM relay_seckey";
if (sqlite3_prepare_v2(g_db, sql, -1, &stmt, NULL) != SQLITE_OK) return NULL;
if (sqlite3_prepare_v2(db, sql, -1, &stmt, NULL) != SQLITE_OK) return NULL;
char* result = NULL;
if (sqlite3_step(stmt) == SQLITE_ROW) {
@@ -886,7 +935,8 @@ char* db_get_relay_private_key_hex_dup(void) {
}
int db_store_config_event(const cJSON* event) {
if (!g_db || !event) return -1;
sqlite3* db = db_active_connection();
if (!db || !event) return -1;
cJSON* id_obj = cJSON_GetObjectItemCaseSensitive((cJSON*)event, "id");
cJSON* pubkey_obj = cJSON_GetObjectItemCaseSensitive((cJSON*)event, "pubkey");
@@ -904,7 +954,7 @@ int db_store_config_event(const cJSON* event) {
sqlite3_stmt* stmt = NULL;
const char* sql = "INSERT OR REPLACE INTO events (id, pubkey, created_at, kind, event_type, content, sig, tags) VALUES (?, ?, ?, ?, ?, ?, ?, ?)";
if (sqlite3_prepare_v2(g_db, sql, -1, &stmt, NULL) != SQLITE_OK) {
if (sqlite3_prepare_v2(db, sql, -1, &stmt, NULL) != SQLITE_OK) {
free(tags_str);
return -1;
}
@@ -1017,11 +1067,12 @@ int db_insert_event_with_json(const char* id, const char* pubkey, long long crea
}
int db_get_event_time_bounds(long long* out_min_created_at, long long* out_max_created_at) {
if (!g_db || !out_min_created_at || !out_max_created_at) return -1;
sqlite3* db = db_active_connection();
if (!db || !out_min_created_at || !out_max_created_at) return -1;
sqlite3_stmt* stmt = NULL;
const char* sql = "SELECT MIN(created_at), MAX(created_at) FROM events";
if (sqlite3_prepare_v2(g_db, sql, -1, &stmt, NULL) != SQLITE_OK) {
if (sqlite3_prepare_v2(db, sql, -1, &stmt, NULL) != SQLITE_OK) {
return -1;
}
@@ -1039,11 +1090,12 @@ int db_get_event_time_bounds(long long* out_min_created_at, long long* out_max_c
}
int db_event_id_exists(const char* event_id, int* out_exists) {
if (!g_db || !event_id || !out_exists) return -1;
sqlite3* db = db_active_connection();
if (!db || !event_id || !out_exists) return -1;
sqlite3_stmt* stmt = NULL;
const char* sql = "SELECT 1 FROM events WHERE id=? LIMIT 1";
if (sqlite3_prepare_v2(g_db, sql, -1, &stmt, NULL) != SQLITE_OK) return -1;
if (sqlite3_prepare_v2(db, sql, -1, &stmt, NULL) != SQLITE_OK) return -1;
sqlite3_bind_text(stmt, 1, event_id, 64, SQLITE_STATIC);
int exists = (sqlite3_step(stmt) == SQLITE_ROW) ? 1 : 0;
@@ -1054,13 +1106,14 @@ int db_event_id_exists(const char* event_id, int* out_exists) {
}
cJSON* db_retrieve_event_by_id(const char* event_id) {
if (!g_db || !event_id) return NULL;
sqlite3* db = db_active_connection();
if (!db || !event_id) return NULL;
const char* sql =
"SELECT id, pubkey, created_at, kind, content, sig, tags FROM events WHERE id = ?";
sqlite3_stmt* stmt = NULL;
if (sqlite3_prepare_v2(g_db, sql, -1, &stmt, NULL) != SQLITE_OK) {
if (sqlite3_prepare_v2(db, sql, -1, &stmt, NULL) != SQLITE_OK) {
return NULL;
}
@@ -1096,11 +1149,12 @@ cJSON* db_retrieve_event_by_id(const char* event_id) {
}
char* db_get_latest_event_pubkey_for_kind_dup(int kind) {
if (!g_db) return NULL;
sqlite3* db = db_active_connection();
if (!db) return NULL;
const char* sql = "SELECT pubkey FROM events WHERE kind = ? ORDER BY created_at DESC LIMIT 1";
sqlite3_stmt* stmt = NULL;
if (sqlite3_prepare_v2(g_db, sql, -1, &stmt, NULL) != SQLITE_OK) {
if (sqlite3_prepare_v2(db, sql, -1, &stmt, NULL) != SQLITE_OK) {
return NULL;
}
@@ -1119,11 +1173,12 @@ char* db_get_latest_event_pubkey_for_kind_dup(int kind) {
}
int db_get_config_row_count(int* out_count) {
if (!g_db || !out_count) return -1;
sqlite3* db = db_active_connection();
if (!db || !out_count) return -1;
sqlite3_stmt* stmt = NULL;
const char* sql = "SELECT COUNT(*) FROM config";
if (sqlite3_prepare_v2(g_db, sql, -1, &stmt, NULL) != SQLITE_OK) {
if (sqlite3_prepare_v2(db, sql, -1, &stmt, NULL) != SQLITE_OK) {
return -1;
}
@@ -1138,15 +1193,16 @@ int db_get_config_row_count(int* out_count) {
}
int db_store_event_tags_cjson(const char* event_id, const cJSON* tags) {
if (!g_db || !event_id || !tags || !cJSON_IsArray(tags)) {
sqlite3* db = db_active_connection();
if (!db || !event_id || !tags || !cJSON_IsArray(tags)) {
return 0; // Not an error if no tags
}
const char* sql = "INSERT INTO event_tags (event_id, tag_name, tag_value, tag_index) VALUES (?, ?, ?, ?)";
sqlite3_stmt* stmt = NULL;
int rc = sqlite3_prepare_v2(g_db, sql, -1, &stmt, NULL);
int rc = sqlite3_prepare_v2(db, sql, -1, &stmt, NULL);
if (rc != SQLITE_OK) {
DEBUG_ERROR("Failed to prepare event_tags insert: %s", sqlite3_errmsg(g_db));
DEBUG_ERROR("Failed to prepare event_tags insert: %s", sqlite3_errmsg(db));
return -1;
}
@@ -1166,7 +1222,7 @@ int db_store_event_tags_cjson(const char* event_id, const cJSON* tags) {
rc = sqlite3_step(stmt);
if (rc != SQLITE_DONE) {
DEBUG_ERROR("Failed to insert event tag: %s", sqlite3_errmsg(g_db));
DEBUG_ERROR("Failed to insert event tag: %s", sqlite3_errmsg(db));
}
}
}
@@ -1178,10 +1234,11 @@ int db_store_event_tags_cjson(const char* event_id, const cJSON* tags) {
}
int db_populate_event_tags_from_existing(void) {
if (!g_db) return -1;
sqlite3* db = db_active_connection();
if (!db) return -1;
sqlite3_stmt* check_stmt = NULL;
if (sqlite3_prepare_v2(g_db, "SELECT COUNT(*) FROM event_tags", -1, &check_stmt, NULL) != SQLITE_OK) {
if (sqlite3_prepare_v2(db, "SELECT COUNT(*) FROM event_tags", -1, &check_stmt, NULL) != SQLITE_OK) {
return -1;
}
@@ -1196,10 +1253,10 @@ int db_populate_event_tags_from_existing(void) {
const char* sql = "SELECT id, tags FROM events WHERE tags != '[]'";
sqlite3_stmt* stmt = NULL;
int rc = sqlite3_prepare_v2(g_db, sql, -1, &stmt, NULL);
int rc = sqlite3_prepare_v2(db, sql, -1, &stmt, NULL);
if (rc != SQLITE_OK) return -1;
sqlite3_exec(g_db, "BEGIN TRANSACTION", NULL, NULL, NULL);
sqlite3_exec(db, "BEGIN TRANSACTION", NULL, NULL, NULL);
int event_count = 0;
while (sqlite3_step(stmt) == SQLITE_ROW) {
@@ -1217,18 +1274,19 @@ int db_populate_event_tags_from_existing(void) {
}
sqlite3_finalize(stmt);
sqlite3_exec(g_db, "COMMIT", NULL, NULL, NULL);
sqlite3_exec(db, "COMMIT", NULL, NULL, NULL);
DEBUG_INFO("Populated event_tags for %d events", event_count);
return 0;
}
int db_add_auth_rule(const char* rule_type, const char* pattern_type, const char* pattern_value) {
if (!g_db || !rule_type || !pattern_type || !pattern_value) return -1;
sqlite3* db = db_active_connection();
if (!db || !rule_type || !pattern_type || !pattern_value) return -1;
sqlite3_stmt* stmt = NULL;
const char* sql = "INSERT INTO auth_rules (rule_type, pattern_type, pattern_value) VALUES (?, ?, ?)";
if (sqlite3_prepare_v2(g_db, sql, -1, &stmt, NULL) != SQLITE_OK) return -1;
if (sqlite3_prepare_v2(db, sql, -1, &stmt, NULL) != SQLITE_OK) return -1;
sqlite3_bind_text(stmt, 1, rule_type, -1, SQLITE_TRANSIENT);
sqlite3_bind_text(stmt, 2, pattern_type, -1, SQLITE_TRANSIENT);
@@ -1240,11 +1298,12 @@ int db_add_auth_rule(const char* rule_type, const char* pattern_type, const char
}
int db_remove_auth_rule(const char* rule_type, const char* pattern_type, const char* pattern_value) {
if (!g_db || !rule_type || !pattern_type || !pattern_value) return -1;
sqlite3* db = db_active_connection();
if (!db || !rule_type || !pattern_type || !pattern_value) return -1;
sqlite3_stmt* stmt = NULL;
const char* sql = "DELETE FROM auth_rules WHERE rule_type = ? AND pattern_type = ? AND pattern_value = ?";
if (sqlite3_prepare_v2(g_db, sql, -1, &stmt, NULL) != SQLITE_OK) return -1;
if (sqlite3_prepare_v2(db, sql, -1, &stmt, NULL) != SQLITE_OK) return -1;
sqlite3_bind_text(stmt, 1, rule_type, -1, SQLITE_TRANSIENT);
sqlite3_bind_text(stmt, 2, pattern_type, -1, SQLITE_TRANSIENT);
@@ -1256,15 +1315,16 @@ int db_remove_auth_rule(const char* rule_type, const char* pattern_type, const c
}
int db_delete_wot_whitelist_rules(void) {
if (!g_db) return -1;
sqlite3* db = db_active_connection();
if (!db) return -1;
const char* sql = "DELETE FROM auth_rules WHERE rule_type = 'wot_whitelist'";
int rc = sqlite3_exec(g_db, sql, NULL, NULL, NULL);
int rc = sqlite3_exec(db, sql, NULL, NULL, NULL);
return (rc == SQLITE_OK) ? 0 : -1;
}
int db_count_wot_whitelist_rules(void) {
if (!g_db) return 0;
if (!db_is_available()) return 0;
const char* sql = "SELECT COUNT(*) FROM auth_rules WHERE rule_type = 'wot_whitelist' AND active = 1";
int count = 0;
@@ -1275,11 +1335,12 @@ int db_count_wot_whitelist_rules(void) {
}
int db_table_exists(const char* table_name, int* out_exists) {
if (!g_db || !table_name || !out_exists) return -1;
sqlite3* db = db_active_connection();
if (!db || !table_name || !out_exists) return -1;
const char* sql = "SELECT 1 FROM sqlite_master WHERE type='table' AND name=? LIMIT 1";
sqlite3_stmt* stmt = NULL;
if (sqlite3_prepare_v2(g_db, sql, -1, &stmt, NULL) != SQLITE_OK) {
if (sqlite3_prepare_v2(db, sql, -1, &stmt, NULL) != SQLITE_OK) {
return -1;
}
@@ -1290,11 +1351,12 @@ int db_table_exists(const char* table_name, int* out_exists) {
}
char* db_get_schema_version_dup(void) {
if (!g_db) return NULL;
sqlite3* db = db_active_connection();
if (!db) return NULL;
const char* sql = "SELECT value FROM schema_info WHERE key = 'version'";
sqlite3_stmt* stmt = NULL;
if (sqlite3_prepare_v2(g_db, sql, -1, &stmt, NULL) != SQLITE_OK) {
if (sqlite3_prepare_v2(db, sql, -1, &stmt, NULL) != SQLITE_OK) {
return NULL;
}
@@ -1311,8 +1373,25 @@ char* db_get_schema_version_dup(void) {
}
int db_exec_sql(const char* sql) {
if (!g_db || !sql) return -1;
sqlite3* db = db_active_connection();
if (!db || !sql) return -1;
int rc = sqlite3_exec(g_db, sql, NULL, NULL, NULL);
int rc = sqlite3_exec(db, sql, NULL, NULL, NULL);
return (rc == SQLITE_OK) ? 0 : -1;
}
int db_wal_checkpoint_passive(void) {
sqlite3* db = db_active_connection();
if (!db) return -1;
int rc = sqlite3_wal_checkpoint_v2(db, NULL, SQLITE_CHECKPOINT_PASSIVE, NULL, NULL);
return (rc == SQLITE_OK) ? 0 : -1;
}
int db_wal_checkpoint_truncate(void) {
sqlite3* db = db_active_connection();
if (!db) return -1;
int rc = sqlite3_wal_checkpoint_v2(db, NULL, SQLITE_CHECKPOINT_TRUNCATE, NULL, NULL);
return (rc == SQLITE_OK) ? 0 : -1;
}
+6
View File
@@ -16,6 +16,10 @@ const char* db_get_database_path(void);
int db_set_thread_connection(void* connection);
void db_clear_thread_connection(void);
// Worker/runtime SQLite connection lifecycle (kept internal to db_ops.c)
int db_open_worker_connection(const char* db_path, void** out_connection);
void db_close_worker_connection(void* connection);
// DB result codes (backend-agnostic)
#define DB_OK 0
#define DB_ERROR 1
@@ -110,5 +114,7 @@ int db_count_wot_whitelist_rules(void);
int db_table_exists(const char* table_name, int* out_exists);
char* db_get_schema_version_dup(void);
int db_exec_sql(const char* sql);
int db_wal_checkpoint_passive(void);
int db_wal_checkpoint_truncate(void);
#endif // DB_OPS_H
File diff suppressed because one or more lines are too long
+23 -1
View File
@@ -3,6 +3,7 @@
#include "debug.h"
#include "config.h"
#include "db_ops.h"
#include "thread_pool.h"
#include <string.h>
#include <stdlib.h>
#include <pthread.h>
@@ -251,6 +252,15 @@ void ip_ban_save_to_db(void) {
// Check if an IP is in the idle_ban_whitelist config (comma-separated list)
static int ip_is_whitelisted(const char* ip) {
if (!ip) {
return 0;
}
// Always trust loopback to avoid local test/dev self-bans.
if (strcmp(ip, "127.0.0.1") == 0 || strcmp(ip, "::1") == 0) {
return 1;
}
const char* whitelist = get_config_value("idle_ban_whitelist");
if (!whitelist || whitelist[0] == '\0') {
if (whitelist) free((char*)whitelist);
@@ -556,7 +566,19 @@ void ip_ban_log_stats(void) {
last_log = now;
// Save to DB every 5 minutes
ip_ban_save_to_db();
if (thread_pool_is_running()) {
thread_pool_job_t job;
memset(&job, 0, sizeof(job));
job.type = THREAD_POOL_JOB_IP_BAN_SAVE;
thread_pool_status_t rc = thread_pool_submit_write(&job, NULL);
if (rc != THREAD_POOL_STATUS_OK) {
DEBUG_WARN("Failed to queue IP_BAN_SAVE job (%d); saving synchronously", rc);
ip_ban_save_to_db();
}
} else {
ip_ban_save_to_db();
}
pthread_mutex_lock(&g_ban_mutex);
int auth_banned_count = 0;
+45 -3
View File
@@ -981,7 +981,7 @@ int store_event_tags(const char* event_id, cJSON* tags) {
// 1 = handled without insert (duplicate or ephemeral)
// -1 = failure
int store_event_core(cJSON* event) {
if (!g_db || !event) {
if (!db_is_available() || !event) {
return -1;
}
@@ -1143,7 +1143,7 @@ int store_event(cJSON* event) {
// Uses the primary key index — single B-tree lookup, ~10μs.
// Call this BEFORE signature verification to skip expensive crypto on duplicates.
int event_id_exists_in_db(const char* event_id) {
if (!g_db || !event_id || strlen(event_id) != 64) return 0;
if (!db_is_available() || !event_id || strlen(event_id) != 64) return 0;
int exists = 0;
if (db_event_id_exists(event_id, &exists) != 0) {
@@ -2231,12 +2231,50 @@ int main(int argc, char* argv[]) {
nostr_cleanup();
return 1;
}
// Pre-warm unified config cache while DB is still available.
// This is critical because runtime strict mode sets g_db = NULL,
// and admin authorization depends on relay_pubkey/admin_pubkey reads.
if (reload_config_from_table() != 0) {
DEBUG_ERROR("Failed to pre-warm configuration cache from config table");
cleanup_configuration_system();
nostr_cleanup();
close_database();
return 1;
}
const char* prewarmed_relay_pubkey = get_config_value("relay_pubkey");
const char* prewarmed_admin_pubkey = get_config_value("admin_pubkey");
if (!prewarmed_relay_pubkey || strlen(prewarmed_relay_pubkey) != 64 ||
!prewarmed_admin_pubkey || strlen(prewarmed_admin_pubkey) != 64) {
if (prewarmed_relay_pubkey) free((char*)prewarmed_relay_pubkey);
if (prewarmed_admin_pubkey) free((char*)prewarmed_admin_pubkey);
DEBUG_ERROR("Critical config pre-warm validation failed (relay_pubkey/admin_pubkey missing)");
cleanup_configuration_system();
nostr_cleanup();
close_database();
return 1;
}
free((char*)prewarmed_relay_pubkey);
free((char*)prewarmed_admin_pubkey);
// Preload relay private key runtime cache while DB is still available.
// Required for admin command decrypt/encrypt after strict mode sets g_db = NULL.
if (preload_relay_private_key_cache() != 0) {
DEBUG_ERROR("Failed to pre-warm relay private key cache");
cleanup_configuration_system();
nostr_cleanup();
close_database();
return 1;
}
DEBUG_INFO("Configuration cache pre-warmed with relay_pubkey/admin_pubkey and relay private key");
// Configuration system is now fully initialized with event-based approach
// All configuration is loaded from database events
// Initialize unified request validator system
if (ginxsom_request_validator_init(g_database_path, "c-relay") != 0) {
if (ginxsom_request_validator_init(g_database_path, "c-relay-pg") != 0) {
DEBUG_ERROR("Failed to initialize unified request validator");
cleanup_configuration_system();
nostr_cleanup();
@@ -2315,6 +2353,10 @@ int main(int argc, char* argv[]) {
}
}
// Phase 5 strict mode: remove main-thread fallback to global DB handle.
// Runtime DB access must go through thread-bound worker connections.
g_db = NULL;
// Start WebSocket Nostr relay server (port from CLI override or configuration)
int result = start_websocket_relay(cli_options.port_override, cli_options.strict_port); // Use CLI port override if specified, otherwise config
+5 -5
View File
@@ -1,5 +1,5 @@
/*
* C-Relay Main Header - Version and Metadata Information
* C-Relay-PG Main Header - Version and Metadata Information
*
* This header contains version information and relay metadata.
* Version macros are auto-updated by the build system.
@@ -13,14 +13,14 @@
// Using CRELAY_ prefix to avoid conflicts with nostr_core_lib VERSION macros
#define CRELAY_VERSION_MAJOR 2
#define CRELAY_VERSION_MINOR 1
#define CRELAY_VERSION_PATCH 5
#define CRELAY_VERSION "v2.1.5"
#define CRELAY_VERSION_PATCH 9
#define CRELAY_VERSION "v2.1.9"
// Relay metadata (authoritative source for NIP-11 information)
#define RELAY_NAME "C-Relay"
#define RELAY_NAME "C-Relay-PG"
#define RELAY_DESCRIPTION "High-performance C Nostr relay with SQLite storage"
#define RELAY_CONTACT ""
#define RELAY_SOFTWARE "https://git.laantungir.net/laantungir/c-relay.git"
#define RELAY_SOFTWARE "https://git.laantungir.net/laantungir/c-relay-pg.git"
#define RELAY_VERSION CRELAY_VERSION // Use the same version as the build
#define SUPPORTED_NIPS "1,2,4,9,11,12,13,15,16,20,22,33,40,42,50,70"
#define LANGUAGE_TAGS ""
+3 -3
View File
@@ -8,7 +8,7 @@
#include <libwebsockets.h>
#include "../nostr_core_lib/cjson/cJSON.h"
#include "config.h"
#include "main.h"
// Forward declarations for configuration functions
const char* get_config_value(const char* key);
@@ -246,14 +246,14 @@ cJSON* generate_relay_info_json() {
cJSON_AddStringToObject(info, "software", relay_software);
free((char*)relay_software);
} else {
cJSON_AddStringToObject(info, "software", "https://git.laantungir.net/laantungir/c-relay.git");
cJSON_AddStringToObject(info, "software", RELAY_SOFTWARE);
}
if (relay_version && strlen(relay_version) > 0) {
cJSON_AddStringToObject(info, "version", relay_version);
free((char*)relay_version);
} else {
cJSON_AddStringToObject(info, "version", "0.2.0");
cJSON_AddStringToObject(info, "version", RELAY_VERSION);
}
// Add policies
+1 -1
View File
@@ -1,5 +1,5 @@
/*
* C-Relay Request Validator - Integrated Authentication System
* C-Relay-PG Request Validator - Integrated Authentication System
*
* Provides complete request validation including:
* - Protocol validation via nostr_core_lib (signatures, pubkey extraction,
+146 -4
View File
@@ -2,6 +2,7 @@
#include <cjson/cJSON.h>
#include "debug.h"
#include "db_ops.h"
#include "thread_pool.h"
#include <string.h>
#include <stdlib.h>
#include <time.h>
@@ -10,6 +11,34 @@
#include <libwebsockets.h>
#include "subscriptions.h"
static void free_sub_log_created_payload_local(thread_pool_sub_log_created_payload_t* payload) {
if (!payload) return;
free(payload->sub_id);
free(payload->wsi_ptr);
free(payload->client_ip);
free(payload->filter_json);
free(payload);
}
static void free_sub_log_closed_payload_local(thread_pool_sub_log_closed_payload_t* payload) {
if (!payload) return;
free(payload->sub_id);
free(payload->client_ip);
free(payload);
}
static void free_sub_log_disconnected_payload_local(thread_pool_sub_log_disconnected_payload_t* payload) {
if (!payload) return;
free(payload->client_ip);
free(payload);
}
static void free_sub_update_events_payload_local(thread_pool_sub_update_events_payload_t* payload) {
if (!payload) return;
free(payload->sub_id);
free(payload);
}
// Forward declarations for logging functions
// Forward declarations for configuration functions
@@ -1040,7 +1069,43 @@ void log_subscription_created(const subscription_t* sub) {
cJSON_Delete(filters_array);
}
db_log_subscription_created(sub->id, wsi_str, sub->client_ip, filter_json ? filter_json : "[]");
if (!thread_pool_is_running()) {
db_log_subscription_created(sub->id, wsi_str, sub->client_ip, filter_json ? filter_json : "[]");
if (filter_json) free(filter_json);
return;
}
thread_pool_sub_log_created_payload_t* payload = calloc(1, sizeof(*payload));
if (!payload) {
if (filter_json) free(filter_json);
return;
}
payload->sub_id = strdup(sub->id);
payload->wsi_ptr = strdup(wsi_str);
payload->client_ip = strdup(sub->client_ip);
payload->filter_json = strdup(filter_json ? filter_json : "[]");
if (!payload->sub_id || !payload->wsi_ptr || !payload->client_ip || !payload->filter_json) {
free(payload->sub_id);
free(payload->wsi_ptr);
free(payload->client_ip);
free(payload->filter_json);
free(payload);
if (filter_json) free(filter_json);
return;
}
thread_pool_job_t job;
memset(&job, 0, sizeof(job));
job.type = THREAD_POOL_JOB_LOG_SUB_CREATED;
job.payload = payload;
job.payload_free = (thread_pool_payload_free_cb)free_sub_log_created_payload_local;
thread_pool_status_t submit_rc = thread_pool_submit_write(&job, NULL);
if (submit_rc != THREAD_POOL_STATUS_OK) {
free_sub_log_created_payload_local(payload);
}
if (filter_json) free(filter_json);
}
@@ -1050,14 +1115,65 @@ void log_subscription_closed(const char* sub_id, const char* client_ip, const ch
(void)reason; // Mark as intentionally unused
if (!sub_id) return;
db_log_subscription_closed(sub_id, client_ip);
if (!thread_pool_is_running()) {
db_log_subscription_closed(sub_id, client_ip);
return;
}
thread_pool_sub_log_closed_payload_t* payload = calloc(1, sizeof(*payload));
if (!payload) {
return;
}
payload->sub_id = strdup(sub_id);
payload->client_ip = strdup(client_ip ? client_ip : "unknown");
if (!payload->sub_id || !payload->client_ip) {
free_sub_log_closed_payload_local(payload);
return;
}
thread_pool_job_t job;
memset(&job, 0, sizeof(job));
job.type = THREAD_POOL_JOB_LOG_SUB_CLOSED;
job.payload = payload;
job.payload_free = (thread_pool_payload_free_cb)free_sub_log_closed_payload_local;
thread_pool_status_t submit_rc = thread_pool_submit_write(&job, NULL);
if (submit_rc != THREAD_POOL_STATUS_OK) {
free_sub_log_closed_payload_local(payload);
}
}
// Log subscription disconnection to database
void log_subscription_disconnected(const char* client_ip) {
if (!client_ip) return;
db_log_subscription_disconnected(client_ip);
if (!thread_pool_is_running()) {
db_log_subscription_disconnected(client_ip);
return;
}
thread_pool_sub_log_disconnected_payload_t* payload = calloc(1, sizeof(*payload));
if (!payload) {
return;
}
payload->client_ip = strdup(client_ip);
if (!payload->client_ip) {
free_sub_log_disconnected_payload_local(payload);
return;
}
thread_pool_job_t job;
memset(&job, 0, sizeof(job));
job.type = THREAD_POOL_JOB_LOG_SUB_DISCONNECTED;
job.payload = payload;
job.payload_free = (thread_pool_payload_free_cb)free_sub_log_disconnected_payload_local;
thread_pool_status_t submit_rc = thread_pool_submit_write(&job, NULL);
if (submit_rc != THREAD_POOL_STATUS_OK) {
free_sub_log_disconnected_payload_local(payload);
}
}
// Log event broadcast to database (optional, can be resource intensive)
@@ -1085,7 +1201,33 @@ void log_subscription_disconnected(const char* client_ip) {
void update_subscription_events_sent(const char* sub_id, int events_sent) {
if (!sub_id) return;
db_update_subscription_events_sent(sub_id, events_sent);
if (!thread_pool_is_running()) {
db_update_subscription_events_sent(sub_id, events_sent);
return;
}
thread_pool_sub_update_events_payload_t* payload = calloc(1, sizeof(*payload));
if (!payload) {
return;
}
payload->sub_id = strdup(sub_id);
payload->events_sent = events_sent;
if (!payload->sub_id) {
free_sub_update_events_payload_local(payload);
return;
}
thread_pool_job_t job;
memset(&job, 0, sizeof(job));
job.type = THREAD_POOL_JOB_UPDATE_SUB_EVENTS_SENT;
job.payload = payload;
job.payload_free = (thread_pool_payload_free_cb)free_sub_update_events_payload_local;
thread_pool_status_t submit_rc = thread_pool_submit_write(&job, NULL);
if (submit_rc != THREAD_POOL_STATUS_OK) {
free_sub_update_events_payload_local(payload);
}
}
// Cleanup all subscriptions on startup
+1 -1
View File
@@ -1,4 +1,4 @@
// Subscription system structures and functions for C-Relay
// Subscription system structures and functions for C-Relay-PG
// This header defines subscription management functionality
#ifndef SUBSCRIPTIONS_H
+136 -27
View File
@@ -3,8 +3,8 @@
#include "thread_pool.h"
#include "debug.h"
#include "db_ops.h"
#include "ip_ban.h"
#include <sqlite3.h>
#include <pthread.h>
#include <stdint.h>
#include <stdio.h>
@@ -198,24 +198,12 @@ static void wake_event_loop(void) {
}
}
static sqlite3* open_worker_connection(void) {
if (g_pool.db_path[0] == '\0') {
static void* open_worker_connection(void) {
void* db_conn = NULL;
if (db_open_worker_connection(g_pool.db_path, &db_conn) != 0) {
return NULL;
}
sqlite3* db = NULL;
int rc = sqlite3_open_v2(g_pool.db_path, &db, SQLITE_OPEN_READWRITE, NULL);
if (rc != SQLITE_OK) {
if (db) {
sqlite3_close(db);
}
return NULL;
}
sqlite3_exec(db, "PRAGMA journal_mode=WAL;", NULL, NULL, NULL);
sqlite3_busy_timeout(db, 5000);
return db;
return db_conn;
}
static void complete_job_with_result(thread_pool_job_node_t* node,
@@ -371,6 +359,98 @@ static void execute_store_event_job(thread_pool_job_node_t* node) {
"STORE_EVENT completed", out, sizeof(*out));
}
static void execute_sub_log_created_job(thread_pool_job_node_t* node) {
thread_pool_sub_log_created_payload_t* payload = (thread_pool_sub_log_created_payload_t*)node->job.payload;
if (!payload || !payload->sub_id || !payload->wsi_ptr || !payload->client_ip) {
complete_job_with_result(node, THREAD_POOL_STATUS_INTERNAL_ERROR,
"SUB_LOG_CREATED payload missing", NULL, 0);
return;
}
if (db_log_subscription_created(payload->sub_id,
payload->wsi_ptr,
payload->client_ip,
payload->filter_json ? payload->filter_json : "[]") != 0) {
complete_job_with_result(node, THREAD_POOL_STATUS_INTERNAL_ERROR,
"SUB_LOG_CREATED failed", NULL, 0);
return;
}
complete_job_with_result(node, THREAD_POOL_STATUS_OK,
"SUB_LOG_CREATED completed", NULL, 0);
}
static void execute_sub_log_closed_job(thread_pool_job_node_t* node) {
thread_pool_sub_log_closed_payload_t* payload = (thread_pool_sub_log_closed_payload_t*)node->job.payload;
if (!payload || !payload->sub_id) {
complete_job_with_result(node, THREAD_POOL_STATUS_INTERNAL_ERROR,
"SUB_LOG_CLOSED payload missing", NULL, 0);
return;
}
if (db_log_subscription_closed(payload->sub_id, payload->client_ip) != 0) {
complete_job_with_result(node, THREAD_POOL_STATUS_INTERNAL_ERROR,
"SUB_LOG_CLOSED failed", NULL, 0);
return;
}
complete_job_with_result(node, THREAD_POOL_STATUS_OK,
"SUB_LOG_CLOSED completed", NULL, 0);
}
static void execute_sub_log_disconnected_job(thread_pool_job_node_t* node) {
thread_pool_sub_log_disconnected_payload_t* payload = (thread_pool_sub_log_disconnected_payload_t*)node->job.payload;
if (!payload || !payload->client_ip) {
complete_job_with_result(node, THREAD_POOL_STATUS_INTERNAL_ERROR,
"SUB_LOG_DISCONNECTED payload missing", NULL, 0);
return;
}
if (db_log_subscription_disconnected(payload->client_ip) < 0) {
complete_job_with_result(node, THREAD_POOL_STATUS_INTERNAL_ERROR,
"SUB_LOG_DISCONNECTED failed", NULL, 0);
return;
}
complete_job_with_result(node, THREAD_POOL_STATUS_OK,
"SUB_LOG_DISCONNECTED completed", NULL, 0);
}
static void execute_sub_update_events_job(thread_pool_job_node_t* node) {
thread_pool_sub_update_events_payload_t* payload = (thread_pool_sub_update_events_payload_t*)node->job.payload;
if (!payload || !payload->sub_id) {
complete_job_with_result(node, THREAD_POOL_STATUS_INTERNAL_ERROR,
"SUB_UPDATE_EVENTS payload missing", NULL, 0);
return;
}
if (db_update_subscription_events_sent(payload->sub_id, payload->events_sent) != 0) {
complete_job_with_result(node, THREAD_POOL_STATUS_INTERNAL_ERROR,
"SUB_UPDATE_EVENTS failed", NULL, 0);
return;
}
complete_job_with_result(node, THREAD_POOL_STATUS_OK,
"SUB_UPDATE_EVENTS completed", NULL, 0);
}
static void execute_ip_ban_save_job(thread_pool_job_node_t* node) {
ip_ban_save_to_db();
complete_job_with_result(node, THREAD_POOL_STATUS_OK,
"IP_BAN_SAVE completed", NULL, 0);
}
static void execute_wal_checkpoint_job(thread_pool_job_node_t* node) {
if (db_wal_checkpoint_passive() != 0) {
complete_job_with_result(node, THREAD_POOL_STATUS_INTERNAL_ERROR,
"WAL_CHECKPOINT failed", NULL, 0);
return;
}
complete_job_with_result(node, THREAD_POOL_STATUS_OK,
"WAL_CHECKPOINT completed", NULL, 0);
}
static void execute_read_job(thread_pool_job_node_t* node) {
switch (node->job.type) {
case THREAD_POOL_JOB_REQ_QUERY:
@@ -391,6 +471,24 @@ static void execute_write_job(thread_pool_job_node_t* node) {
case THREAD_POOL_JOB_STORE_EVENT:
execute_store_event_job(node);
break;
case THREAD_POOL_JOB_LOG_SUB_CREATED:
execute_sub_log_created_job(node);
break;
case THREAD_POOL_JOB_LOG_SUB_CLOSED:
execute_sub_log_closed_job(node);
break;
case THREAD_POOL_JOB_LOG_SUB_DISCONNECTED:
execute_sub_log_disconnected_job(node);
break;
case THREAD_POOL_JOB_UPDATE_SUB_EVENTS_SENT:
execute_sub_update_events_job(node);
break;
case THREAD_POOL_JOB_IP_BAN_SAVE:
execute_ip_ban_save_job(node);
break;
case THREAD_POOL_JOB_WAL_CHECKPOINT:
execute_wal_checkpoint_job(node);
break;
default:
complete_job_with_result(node, THREAD_POOL_STATUS_NOT_IMPLEMENTED,
"Write job type not implemented", NULL, 0);
@@ -405,14 +503,12 @@ static void* reader_worker_main(void* arg) {
snprintf(thread_name, sizeof(thread_name), "db-read-%d", reader_index);
pthread_setname_np(pthread_self(), thread_name);
sqlite3* worker_db = open_worker_connection();
void* worker_db = open_worker_connection();
if (!worker_db) {
DEBUG_ERROR("Reader worker failed to open SQLite connection");
return NULL;
}
db_set_thread_connection(worker_db);
while (g_pool.running) {
thread_pool_job_node_t* node = queue_pop(&g_pool.read_q);
if (!node) break;
@@ -420,8 +516,7 @@ static void* reader_worker_main(void* arg) {
execute_read_job(node);
}
db_clear_thread_connection();
sqlite3_close(worker_db);
db_close_worker_connection(worker_db);
return NULL;
}
@@ -430,14 +525,12 @@ static void* writer_worker_main(void* arg) {
pthread_setname_np(pthread_self(), "db-write");
sqlite3* worker_db = open_worker_connection();
void* worker_db = open_worker_connection();
if (!worker_db) {
DEBUG_ERROR("Writer worker failed to open SQLite connection");
return NULL;
}
db_set_thread_connection(worker_db);
while (g_pool.running) {
thread_pool_job_node_t* node = queue_pop(&g_pool.write_q);
if (!node) break;
@@ -445,8 +538,11 @@ static void* writer_worker_main(void* arg) {
execute_write_job(node);
}
db_clear_thread_connection();
sqlite3_close(worker_db);
if (db_wal_checkpoint_truncate() != 0) {
DEBUG_WARN("Writer shutdown TRUNCATE checkpoint failed");
}
db_close_worker_connection(worker_db);
return NULL;
}
@@ -573,6 +669,19 @@ thread_pool_status_t thread_pool_submit_write(const thread_pool_job_t* job, uint
return submit_to_queue(&g_pool.write_q, job, out_job_id);
}
int thread_pool_submit_wal_checkpoint(void) {
if (!thread_pool_is_running()) {
return -1;
}
thread_pool_job_t job;
memset(&job, 0, sizeof(job));
job.type = THREAD_POOL_JOB_WAL_CHECKPOINT;
thread_pool_status_t rc = thread_pool_submit_write(&job, NULL);
return (rc == THREAD_POOL_STATUS_OK) ? 0 : -1;
}
int thread_pool_execute_count_sync(const char* sql, const char** bind_params, int bind_param_count, int* out_count) {
if (!sql || !out_count) {
return -1;
+29
View File
@@ -13,6 +13,12 @@ typedef enum {
THREAD_POOL_JOB_COUNT_QUERY,
THREAD_POOL_JOB_STORE_EVENT,
THREAD_POOL_JOB_DELETE_EVENT,
THREAD_POOL_JOB_LOG_SUB_CREATED,
THREAD_POOL_JOB_LOG_SUB_CLOSED,
THREAD_POOL_JOB_LOG_SUB_DISCONNECTED,
THREAD_POOL_JOB_UPDATE_SUB_EVENTS_SENT,
THREAD_POOL_JOB_IP_BAN_SAVE,
THREAD_POOL_JOB_WAL_CHECKPOINT,
THREAD_POOL_JOB_CUSTOM
} thread_pool_job_type_t;
@@ -95,6 +101,27 @@ typedef struct {
int extended_errcode;
} thread_pool_store_event_result_t;
typedef struct {
char* sub_id;
char* wsi_ptr;
char* client_ip;
char* filter_json;
} thread_pool_sub_log_created_payload_t;
typedef struct {
char* sub_id;
char* client_ip;
} thread_pool_sub_log_closed_payload_t;
typedef struct {
char* client_ip;
} thread_pool_sub_log_disconnected_payload_t;
typedef struct {
char* sub_id;
int events_sent;
} thread_pool_sub_update_events_payload_t;
int thread_pool_init(const thread_pool_config_t* config);
void thread_pool_shutdown(void);
int thread_pool_is_running(void);
@@ -102,6 +129,8 @@ int thread_pool_is_running(void);
thread_pool_status_t thread_pool_submit_read(const thread_pool_job_t* job, uint64_t* out_job_id);
thread_pool_status_t thread_pool_submit_write(const thread_pool_job_t* job, uint64_t* out_job_id);
int thread_pool_submit_wal_checkpoint(void);
int thread_pool_execute_count_sync(const char* sql, const char** bind_params, int bind_param_count, int* out_count);
int thread_pool_execute_req_sync(const char* sql, const char** bind_params, int bind_param_count,
int row_limit, thread_pool_req_result_t** out_result);
+42 -6
View File
@@ -137,6 +137,26 @@ static pthread_mutex_t g_connections_lock = PTHREAD_MUTEX_INITIALIZER;
int get_active_connection_count(void) { return g_connection_count; }
int websocket_get_live_pss(struct lws* wsi, struct per_session_data** out_pss) {
if (!wsi || !out_pss) {
return 0;
}
*out_pss = NULL;
pthread_mutex_lock(&g_connections_lock);
for (int i = 0; i < MAX_TRACKED_CONNECTIONS; i++) {
if (g_connections[i].wsi == wsi && g_connections[i].pss != NULL) {
*out_pss = g_connections[i].pss;
pthread_mutex_unlock(&g_connections_lock);
return 1;
}
}
pthread_mutex_unlock(&g_connections_lock);
return 0;
}
static void connection_list_add(struct lws* wsi, struct per_session_data* pss) {
pthread_mutex_lock(&g_connections_lock);
for (int i = 0; i < MAX_TRACKED_CONNECTIONS; i++) {
@@ -477,8 +497,8 @@ static int event_is_async_eligible(cJSON* event, int* out_kind, char event_id_ou
int kind = (int)cJSON_GetNumberValue(kind_obj);
// Keep special/admin paths on main thread for existing behavior.
if (kind == 14 || kind == 1059 || kind == 23456) {
// Keep admin command path on main thread for existing behavior.
if (kind == 23456) {
return 0;
}
@@ -509,6 +529,12 @@ static void* async_event_worker_main(void* arg) {
(void)arg;
pthread_setname_np(pthread_self(), "event-worker");
void* worker_db = NULL;
const char* db_path = db_get_database_path();
if (db_open_worker_connection(db_path, &worker_db) != 0) {
DEBUG_WARN("event-worker: failed to open dedicated DB connection; using default connection context");
}
while (g_async_event_worker_running) {
async_event_job_t* job = async_event_job_pop_blocking();
if (!job) {
@@ -579,6 +605,10 @@ static void* async_event_worker_main(void* arg) {
free(job);
}
if (worker_db) {
db_close_worker_connection(worker_db);
}
return NULL;
}
@@ -680,8 +710,9 @@ static int try_submit_async_event(cJSON* event, const char* event_json, struct l
static void process_async_event_completions(void) {
async_event_completion_t* completion = NULL;
while ((completion = async_event_completion_pop()) != NULL) {
struct per_session_data* current_pss = (struct per_session_data*)lws_wsi_user(completion->wsi);
int target_alive = (current_pss && current_pss == completion->pss_token);
struct per_session_data* current_pss = NULL;
int target_alive = websocket_get_live_pss(completion->wsi, &current_pss) &&
current_pss == completion->pss_token;
int needs_event_obj = completion->run_post_actions ||
(target_alive && completion->success && completion->should_broadcast);
@@ -896,8 +927,9 @@ static void process_count_async_completions(void) {
state->pending_jobs--;
if (state->pending_jobs <= 0) {
struct per_session_data* current_pss = (struct per_session_data*)lws_wsi_user(state->wsi);
int target_alive = (current_pss && current_pss == state->pss_token);
struct per_session_data* current_pss = NULL;
int target_alive = websocket_get_live_pss(state->wsi, &current_pss) &&
current_pss == state->pss_token;
if (target_alive) {
if (state->had_error) {
send_notice_message(state->wsi, current_pss, state->error_message[0] ? state->error_message : "error: count query failed");
@@ -3425,6 +3457,10 @@ int start_websocket_relay(int port_override, int strict_port) {
int idle_timeout_sec = hot_cfg_idle_connection_timeout_sec();
check_idle_connections(idle_timeout_sec);
if (thread_pool_submit_wal_checkpoint() != 0) {
DEBUG_TRACE("WAL checkpoint submit skipped or failed");
}
if (max_connection_seconds > 0) {
check_connection_age(max_connection_seconds);
} else {
+5 -1
View File
@@ -1,4 +1,4 @@
// WebSocket protocol structures and constants for C-Relay
// WebSocket protocol structures and constants for C-Relay-PG
// This header defines structures shared between main.c and websockets.c
#ifndef WEBSOCKETS_H
@@ -99,6 +99,10 @@ struct per_session_data {
// Get current active WebSocket connection count
int get_active_connection_count(void);
// Safely resolve per-session data only for currently tracked live WebSocket sessions.
// Returns 1 and sets *out_pss on success, otherwise returns 0.
int websocket_get_live_pss(struct lws* wsi, struct per_session_data** out_pss);
// Actual relay port bound by libwebsockets at runtime (after fallback/retry logic)
extern int g_relay_port;
+34 -34
View File
@@ -8,7 +8,7 @@ The C Nostr Relay now uses a revolutionary **zero-configuration** approach where
## Files
- **`c-relay.service`** - SystemD service unit file
- **`c-relay-pg.service`** - SystemD service unit file
- **`install-service.sh`** - Automated installation script
- **`uninstall-service.sh`** - Automated uninstall script
- **`README.md`** - This documentation
@@ -27,12 +27,12 @@ The C Nostr Relay now uses a revolutionary **zero-configuration** approach where
3. **Start the service:**
```bash
sudo systemctl start c-relay
sudo systemctl start c-relay-pg
```
4. **Check admin keys (IMPORTANT!):**
```bash
sudo journalctl -u c-relay --since="1 hour ago" | grep "Admin Private Key"
sudo journalctl -u c-relay-pg --since="1 hour ago" | grep "Admin Private Key"
```
## Event-Based Configuration System
@@ -61,7 +61,7 @@ On first startup, the relay will:
```bash
# View first startup logs to get admin private key
sudo journalctl -u c-relay --since="1 hour ago" | grep -A 5 "IMPORTANT: SAVE THIS ADMIN PRIVATE KEY"
sudo journalctl -u c-relay-pg --since="1 hour ago" | grep -A 5 "IMPORTANT: SAVE THIS ADMIN PRIVATE KEY"
```
The admin private key is needed to update relay configuration by sending signed kind 33334 events.
@@ -72,10 +72,10 @@ The admin private key is needed to update relay configuration by sending signed
```bash
# Find the database file
ls /opt/c-relay/*.nrdb
ls /opt/c-relay-pg/*.nrdb
# View configuration event
sqlite3 /opt/c-relay/<relay_pubkey>.nrdb "SELECT content, tags FROM events WHERE kind = 33334;"
sqlite3 /opt/c-relay-pg/<relay_pubkey>.nrdb "SELECT content, tags FROM events WHERE kind = 33334;"
```
### Updating Configuration
@@ -93,57 +93,57 @@ Send a new kind 33334 event to the relay via WebSocket:
```bash
# Start service
sudo systemctl start c-relay
sudo systemctl start c-relay-pg
# Stop service
sudo systemctl stop c-relay
sudo systemctl stop c-relay-pg
# Restart service
sudo systemctl restart c-relay
sudo systemctl restart c-relay-pg
# Enable auto-start on boot
sudo systemctl enable c-relay
sudo systemctl enable c-relay-pg
# Check status
sudo systemctl status c-relay
sudo systemctl status c-relay-pg
# View logs (live)
sudo journalctl -u c-relay -f
sudo journalctl -u c-relay-pg -f
# View recent logs
sudo journalctl -u c-relay --since="1 hour ago"
sudo journalctl -u c-relay-pg --since="1 hour ago"
```
### Log Analysis
```bash
# Check for successful startup
sudo journalctl -u c-relay | grep "First-time startup sequence completed"
sudo journalctl -u c-relay-pg | grep "First-time startup sequence completed"
# Find admin keys
sudo journalctl -u c-relay | grep "Admin Private Key"
sudo journalctl -u c-relay-pg | grep "Admin Private Key"
# Check configuration updates
sudo journalctl -u c-relay | grep "Configuration updated via kind 33334"
sudo journalctl -u c-relay-pg | grep "Configuration updated via kind 33334"
# Monitor real-time activity
sudo journalctl -u c-relay -f | grep -E "(INFO|SUCCESS|ERROR)"
sudo journalctl -u c-relay-pg -f | grep -E "(INFO|SUCCESS|ERROR)"
```
## File Locations
After installation:
- **Binary:** `/opt/c-relay/c_relay_x86`
- **Database:** `/opt/c-relay/<relay_pubkey>.nrdb` (created automatically)
- **Service File:** `/etc/systemd/system/c-relay.service`
- **User:** `c-relay` (system user created automatically)
- **Binary:** `/opt/c-relay-pg/c_relay_pg_x86`
- **Database:** `/opt/c-relay-pg/<relay_pubkey>.nrdb` (created automatically)
- **Service File:** `/etc/systemd/system/c-relay-pg.service`
- **User:** `c-relay-pg` (system user created automatically)
## Security Features
The systemd service includes security hardening:
- Runs as dedicated system user `c-relay`
- Runs as dedicated system user `c-relay-pg`
- `NoNewPrivileges=true`
- `ProtectSystem=strict`
- `ProtectHome=true`
@@ -165,7 +165,7 @@ The database file contains everything:
```bash
# Backup database file
sudo cp /opt/c-relay/*.nrdb /backup/location/
sudo cp /opt/c-relay-pg/*.nrdb /backup/location/
# The .nrdb file contains:
# - All Nostr events
@@ -177,7 +177,7 @@ sudo cp /opt/c-relay/*.nrdb /backup/location/
To migrate to new server:
1. Copy `.nrdb` file to new server's `/opt/c-relay/` directory
1. Copy `.nrdb` file to new server's `/opt/c-relay-pg/` directory
2. Install service with `install-service.sh`
3. Start service - it will automatically detect existing configuration
@@ -187,39 +187,39 @@ To migrate to new server:
```bash
# Check service status
sudo systemctl status c-relay
sudo systemctl status c-relay-pg
# Check logs for errors
sudo journalctl -u c-relay --no-pager
sudo journalctl -u c-relay-pg --no-pager
# Check if binary exists and is executable
ls -la /opt/c-relay/c_relay_x86
ls -la /opt/c-relay-pg/c_relay_pg_x86
# Check permissions
sudo -u c-relay ls -la /opt/c-relay/
sudo -u c-relay-pg ls -la /opt/c-relay-pg/
```
### Database Issues
```bash
# Check if database file exists
ls -la /opt/c-relay/*.nrdb*
ls -la /opt/c-relay-pg/*.nrdb*
# Check database integrity
sqlite3 /opt/c-relay/*.nrdb "PRAGMA integrity_check;"
sqlite3 /opt/c-relay-pg/*.nrdb "PRAGMA integrity_check;"
# View database schema
sqlite3 /opt/c-relay/*.nrdb ".schema"
sqlite3 /opt/c-relay-pg/*.nrdb ".schema"
```
### Configuration Issues
```bash
# Check if configuration event exists
sqlite3 /opt/c-relay/*.nrdb "SELECT COUNT(*) FROM events WHERE kind = 33334;"
sqlite3 /opt/c-relay-pg/*.nrdb "SELECT COUNT(*) FROM events WHERE kind = 33334;"
# View configuration event
sqlite3 /opt/c-relay/*.nrdb "SELECT id, created_at, LENGTH(tags) FROM events WHERE kind = 33334;"
sqlite3 /opt/c-relay-pg/*.nrdb "SELECT id, created_at, LENGTH(tags) FROM events WHERE kind = 33334;"
```
## Uninstallation
@@ -238,7 +238,7 @@ The uninstall script will:
For issues with the event-based configuration system:
1. Check service logs: `sudo journalctl -u c-relay -f`
1. Check service logs: `sudo journalctl -u c-relay-pg -f`
2. Verify database integrity
3. Ensure admin private key is saved securely
4. Check WebSocket connectivity on port 8888
+7 -7
View File
@@ -1,27 +1,27 @@
[Unit]
Description=C Nostr Relay Server (Event-Based Configuration)
Documentation=https://github.com/your-repo/c-relay
Documentation=https://github.com/your-repo/c-relay-pg
After=network.target
Wants=network-online.target
[Service]
Type=simple
User=c-relay
Group=c-relay
WorkingDirectory=/opt/c-relay
User=c-relay-pg
Group=c-relay-pg
WorkingDirectory=/opt/c-relay-pg
Environment=DEBUG_LEVEL=0
ExecStart=/opt/c-relay/c_relay_x86 --debug-level=$DEBUG_LEVEL
ExecStart=/opt/c-relay-pg/c_relay_pg_x86 --debug-level=$DEBUG_LEVEL
Restart=always
RestartSec=5
StandardOutput=journal
StandardError=journal
SyslogIdentifier=c-relay
SyslogIdentifier=c-relay-pg
# Security settings
NoNewPrivileges=true
ProtectSystem=strict
ProtectHome=true
ReadWritePaths=/opt/c-relay
ReadWritePaths=/opt/c-relay-pg
PrivateTmp=true
ProtectKernelTunables=true
ProtectKernelModules=true
+4 -4
View File
@@ -6,10 +6,10 @@
set -e
# Configuration
SERVICE_NAME="c-relay"
SERVICE_USER="c-relay"
INSTALL_DIR="/opt/c-relay"
BINARY_NAME="c_relay_x86"
SERVICE_NAME="c-relay-pg"
SERVICE_USER="c-relay-pg"
INSTALL_DIR="/opt/c-relay-pg"
BINARY_NAME="c_relay_pg_x86"
# Colors for output
RED='\033[0;31m'
+13 -13
View File
@@ -1,15 +1,15 @@
#!/bin/bash
# C-Relay Systemd Service Installation Script
# This script installs the C-Relay as a systemd service
# C-Relay-PG Systemd Service Installation Script
# This script installs the C-Relay-PG as a systemd service
set -e
# Configuration
INSTALL_DIR="/opt/c-relay"
SERVICE_NAME="c-relay"
SERVICE_FILE="c-relay.service"
BINARY_NAME="c_relay_x86"
INSTALL_DIR="/opt/c-relay-pg"
SERVICE_NAME="c-relay-pg"
SERVICE_FILE="c-relay-pg.service"
BINARY_NAME="c_relay_pg_x86"
# Colors for output
RED='\033[0;31m'
@@ -17,7 +17,7 @@ GREEN='\033[0;32m'
YELLOW='\033[1;33m'
NC='\033[0m' # No Color
echo -e "${GREEN}=== C-Relay Systemd Service Installation ===${NC}"
echo -e "${GREEN}=== C-Relay-PG Systemd Service Installation ===${NC}"
# Check if running as root
if [[ $EUID -ne 0 ]]; then
@@ -39,12 +39,12 @@ if [ ! -f "$SERVICE_FILE" ]; then
exit 1
fi
# Create c-relay user if it doesn't exist
if ! id "c-relay" &>/dev/null; then
echo -e "${YELLOW}Creating c-relay user...${NC}"
useradd --system --shell /bin/false --home-dir $INSTALL_DIR --create-home c-relay
# Create c-relay-pg user if it doesn't exist
if ! id "c-relay-pg" &>/dev/null; then
echo -e "${YELLOW}Creating c-relay-pg user...${NC}"
useradd --system --shell /bin/false --home-dir $INSTALL_DIR --create-home c-relay-pg
else
echo -e "${GREEN}User c-relay already exists${NC}"
echo -e "${GREEN}User c-relay-pg already exists${NC}"
fi
# Create installation directory
@@ -59,7 +59,7 @@ chmod +x $INSTALL_DIR/$BINARY_NAME
# Set permissions
echo -e "${YELLOW}Setting permissions...${NC}"
chown -R c-relay:c-relay $INSTALL_DIR
chown -R c-relay-pg:c-relay-pg $INSTALL_DIR
# Install systemd service
echo -e "${YELLOW}Installing systemd service...${NC}"
+3 -3
View File
@@ -6,9 +6,9 @@
set -e
# Configuration
SERVICE_NAME="c-relay"
SERVICE_USER="c-relay"
INSTALL_DIR="/opt/c-relay"
SERVICE_NAME="c-relay-pg"
SERVICE_USER="c-relay-pg"
INSTALL_DIR="/opt/c-relay-pg"
# Colors for output
RED='\033[0;31m'
+15 -15
View File
@@ -1,14 +1,14 @@
#!/bin/bash
# C-Relay Systemd Service Uninstallation Script
# This script removes the C-Relay systemd service
# C-Relay-PG Systemd Service Uninstallation Script
# This script removes the C-Relay-PG systemd service
set -e
# Configuration
INSTALL_DIR="/opt/c-relay"
SERVICE_NAME="c-relay"
SERVICE_FILE="c-relay.service"
INSTALL_DIR="/opt/c-relay-pg"
SERVICE_NAME="c-relay-pg"
SERVICE_FILE="c-relay-pg.service"
# Colors for output
RED='\033[0;31m'
@@ -16,7 +16,7 @@ GREEN='\033[0;32m'
YELLOW='\033[1;33m'
NC='\033[0m' # No Color
echo -e "${GREEN}=== C-Relay Systemd Service Uninstallation ===${NC}"
echo -e "${GREEN}=== C-Relay-PG Systemd Service Uninstallation ===${NC}"
# Check if running as root
if [[ $EUID -ne 0 ]]; then
@@ -65,22 +65,22 @@ else
echo -e "${GREEN}Installation directory preserved${NC}"
fi
# Ask about removing c-relay user
# Ask about removing c-relay-pg user
echo
echo -e "${YELLOW}Do you want to remove the c-relay user? (y/N)${NC}"
echo -e "${YELLOW}Do you want to remove the c-relay-pg user? (y/N)${NC}"
read -r response
if [[ "$response" =~ ^([yY][eE][sS]|[yY])$ ]]; then
echo -e "${YELLOW}Removing c-relay user...${NC}"
if id "c-relay" &>/dev/null; then
userdel c-relay
echo -e "${GREEN}User c-relay removed${NC}"
echo -e "${YELLOW}Removing c-relay-pg user...${NC}"
if id "c-relay-pg" &>/dev/null; then
userdel c-relay-pg
echo -e "${GREEN}User c-relay-pg removed${NC}"
else
echo -e "${GREEN}User c-relay was not found${NC}"
echo -e "${GREEN}User c-relay-pg was not found${NC}"
fi
else
echo -e "${GREEN}User c-relay preserved${NC}"
echo -e "${GREEN}User c-relay-pg preserved${NC}"
fi
echo
echo -e "${GREEN}=== Uninstallation Complete ===${NC}"
echo -e "${GREEN}C-Relay systemd service has been removed${NC}"
echo -e "${GREEN}C-Relay-PG systemd service has been removed${NC}"
+1 -1
View File
@@ -421,7 +421,7 @@ 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 "The C-Relay-PG NIP-11 implementation is fully compliant"
print_info "✅ HTTP endpoint, Accept header validation, CORS, and JSON structure all working"
echo
exit 0
+6 -6
View File
@@ -1,6 +1,6 @@
#!/bin/bash
# Comprehensive C-Relay Test - Test event types and subscriptions
# Comprehensive C-Relay-PG Test - Test event types and subscriptions
# Uses nak to generate and publish various event types, then tests subscriptions
set -e # Exit on any error
@@ -284,7 +284,7 @@ test_subscription() {
# Main test function
run_comprehensive_test() {
print_header "C-Relay Comprehensive Test"
print_header "C-Relay-PG Comprehensive Test"
# Check dependencies
print_step "Checking dependencies..."
@@ -317,7 +317,7 @@ run_comprehensive_test() {
# 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 replaceable1=$(nak event --sec "$TEST_PRIVATE_KEY" -c '{"name":"Test User","about":"Testing C-Relay-PG"}' -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)"
@@ -473,20 +473,20 @@ run_comprehensive_test() {
}
# Run the comprehensive test
print_header "Starting C-Relay Comprehensive Test Suite with NIP-01 Validation"
print_header "Starting C-Relay-PG 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 "The C-Relay-PG with full NIP-01 validation is working correctly"
print_info "✅ Event validation, signature verification, and error handling all working"
echo
exit 0
else
echo
print_error "❌ TESTS FAILED: Protocol violations detected!"
print_error "The C-Relay has critical issues that need to be fixed:"
print_error "The C-Relay-PG has critical issues that need to be fixed:"
print_error " - Server-side filtering is not implemented properly"
print_error " - Events are sent to clients regardless of subscription filters"
print_error " - This violates the Nostr protocol specification"
+1 -1
View File
@@ -461,7 +461,7 @@ echo
if run_count_test; then
echo
print_success "All NIP-45 COUNT tests completed successfully!"
print_info "The C-Relay COUNT functionality is working correctly"
print_info "The C-Relay-PG COUNT functionality is working correctly"
print_info "✅ COUNT messages are processed and return correct event counts"
echo
exit 0
+1 -1
View File
@@ -405,7 +405,7 @@ echo
if run_search_test; then
echo
print_success "All NIP-50 SEARCH tests completed successfully!"
print_info "The C-Relay SEARCH functionality is working correctly"
print_info "The C-Relay-PG SEARCH functionality is working correctly"
print_info "✅ Search field in filter objects works for both REQ and COUNT messages"
print_info "✅ Search works across event content and tag values"
print_info "✅ Search is case-insensitive and supports partial matches"
+1 -1
View File
@@ -220,7 +220,7 @@ echo
if run_protected_events_test; then
echo
print_success "All NIP-70 PROTECTED EVENTS tests completed successfully!"
print_info "The C-Relay PROTECTED EVENTS functionality is working correctly"
print_info "The C-Relay-PG PROTECTED EVENTS functionality is working correctly"
print_info "✅ Protected events are rejected when feature is disabled"
print_info "✅ Protected events are rejected when enabled but not authenticated"
print_info "✅ Normal events work regardless of protected events setting"
+2 -2
View File
@@ -1,6 +1,6 @@
#!/bin/bash
# NIP-09 Event Deletion Request Test for C-Relay
# NIP-09 Event Deletion Request Test for C-Relay-PG
# Tests deletion request functionality - assumes relay is already running
# Based on the pattern from 1_nip_test.sh
@@ -372,7 +372,7 @@ 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 "The C-Relay-PG NIP-09 implementation is working correctly"
print_info "✅ Event deletion by ID working"
print_info "✅ Address-based deletion working"
print_info "✅ Authorization validation working"
+6 -6
View File
@@ -1,6 +1,6 @@
# C-Relay Comprehensive Testing Framework
# C-Relay-PG Comprehensive Testing Framework
This directory contains a comprehensive testing framework for the C-Relay Nostr relay implementation. The framework provides automated testing for security vulnerabilities, performance validation, and stability assurance.
This directory contains a comprehensive testing framework for the C-Relay-PG Nostr relay implementation. The framework provides automated testing for security vulnerabilities, performance validation, and stability assurance.
## Overview
@@ -194,14 +194,14 @@ brew install websocat curl jq
Download `websocat` from: https://github.com/vi/websocat/releases
### Relay Setup
Before running tests, ensure the C-Relay is running:
Before running tests, ensure the C-Relay-PG is running:
```bash
# Build and start the relay
./make_and_restart_relay.sh
# Verify it's running
ps aux | grep c_relay
ps aux | grep c_relay_pg
curl -H "Accept: application/nostr+json" http://localhost:8888
```
@@ -308,7 +308,7 @@ Modify test parameters within individual test scripts:
#### "Could not connect to relay"
- Ensure relay is running: `./make_and_restart_relay.sh`
- Check port availability: `netstat -tln | grep 8888`
- Verify relay process: `ps aux | grep c_relay`
- Verify relay process: `ps aux | grep c_relay_pg`
#### "websocat: command not found"
- Install websocat: `sudo apt-get install websocat`
@@ -377,7 +377,7 @@ Integrate with CI/CD pipelines:
```yaml
# Example GitHub Actions workflow
- name: Run C-Relay Tests
- name: Run C-Relay-PG Tests
run: |
./make_and_restart_relay.sh
./tests/run_all_tests.sh
+1 -1
View File
@@ -1,7 +1,7 @@
#!/bin/bash
# Authentication Testing Suite for C-Relay
# Authentication Testing Suite for C-Relay-PG
# Tests NIP-42 authentication mechanisms
set -e
+1 -1
View File
@@ -262,7 +262,7 @@ if [ -n "$DB_FILE" ]; then
fi
echo "Check relay logs for [QUERY] entries to see actual query times:"
echo " journalctl -u c-relay -n 100 | grep QUERY"
echo " journalctl -u c-relay-pg -n 100 | grep QUERY"
echo ""
echo "=========================================="
+2 -2
View File
@@ -1,6 +1,6 @@
#!/bin/bash
# Configuration Testing Suite for C-Relay
# Configuration Testing Suite for C-Relay-PG
# Tests configuration management and persistence
set -e
@@ -138,7 +138,7 @@ test_nip11_info() {
}
echo "=========================================="
echo "C-Relay Configuration Testing Suite"
echo "C-Relay-PG Configuration Testing Suite"
echo "=========================================="
echo "Testing configuration management at ws://$RELAY_HOST:$RELAY_PORT"
echo ""
+2 -2
View File
@@ -1,6 +1,6 @@
#!/bin/bash
# Filter Validation Test Script for C-Relay
# Filter Validation Test Script for C-Relay-PG
# Tests comprehensive input validation for REQ and COUNT messages
set -e
@@ -91,7 +91,7 @@ test_valid_message() {
fi
}
echo "=== C-Relay Filter Validation Tests ==="
echo "=== C-Relay-PG Filter Validation Tests ==="
echo "Testing against relay at ws://$RELAY_HOST:$RELAY_PORT"
echo
+2 -2
View File
@@ -1,6 +1,6 @@
#!/bin/bash
# Input Validation Test Suite for C-Relay
# Input Validation Test Suite for C-Relay-PG
# Comprehensive testing of input boundary conditions and malformed data
set -e
@@ -69,7 +69,7 @@ test_input_validation() {
}
echo "=========================================="
echo "C-Relay Input Validation Test Suite"
echo "C-Relay-PG Input Validation Test Suite"
echo "=========================================="
echo "Testing against relay at ws://$RELAY_HOST:$RELAY_PORT"
echo
+2 -2
View File
@@ -1,6 +1,6 @@
#!/bin/bash
# Load Testing Suite for C-Relay
# Load Testing Suite for C-Relay-PG
# Tests high concurrent connection scenarios and performance under load
set -e
@@ -203,7 +203,7 @@ run_load_test() {
# Only run main code if script is executed directly (not sourced)
if [[ "${BASH_SOURCE[0]}" == "${0}" ]]; then
echo "=========================================="
echo "C-Relay Load Testing Suite"
echo "C-Relay-PG Load Testing Suite"
echo "=========================================="
echo "Testing against relay at ws://$RELAY_HOST:$RELAY_PORT"
echo ""
+2 -2
View File
@@ -1,6 +1,6 @@
#!/bin/bash
# Memory Corruption Detection Test Suite for C-Relay
# Memory Corruption Detection Test Suite for C-Relay-PG
# Tests for buffer overflows, use-after-free, and memory safety issues
set -e
@@ -135,7 +135,7 @@ test_concurrent_access() {
}
echo "=========================================="
echo "C-Relay Memory Corruption Test Suite"
echo "C-Relay-PG Memory Corruption Test Suite"
echo "=========================================="
echo "Testing against relay at ws://$RELAY_HOST:$RELAY_PORT"
echo "Note: These tests may cause the relay to crash if vulnerabilities exist"
+5 -5
View File
@@ -1,6 +1,6 @@
#!/bin/bash
# Performance Benchmarking Suite for C-Relay
# Performance Benchmarking Suite for C-Relay-PG
# Measures performance metrics and throughput
set -e
@@ -202,7 +202,7 @@ benchmark_memory_usage() {
echo "=========================================="
local initial_memory
initial_memory=$(ps aux | grep c_relay | grep -v grep | awk '{print $6}' | head -1)
initial_memory=$(ps aux | grep c_relay_pg | grep -v grep | awk '{print $6}' | head -1)
echo "Initial memory usage: ${initial_memory}KB"
@@ -218,7 +218,7 @@ benchmark_memory_usage() {
sleep 2
local current_memory
current_memory=$(ps aux | grep c_relay | grep -v grep | awk '{print $6}' | head -1)
current_memory=$(ps aux | grep c_relay_pg | grep -v grep | awk '{print $6}' | head -1)
local memory_increase=$((current_memory - initial_memory))
echo "${current_memory}KB (+${memory_increase}KB)"
@@ -232,14 +232,14 @@ benchmark_memory_usage() {
done
local final_memory
final_memory=$(ps aux | grep c_relay | grep -v grep | awk '{print $6}' | head -1)
final_memory=$(ps aux | grep c_relay_pg | grep -v grep | awk '{print $6}' | head -1)
echo "Final memory usage: ${final_memory}KB"
}
# Only run main code if script is executed directly (not sourced)
if [[ "${BASH_SOURCE[0]}" == "${0}" ]]; then
echo "=========================================="
echo "C-Relay Performance Benchmarking Suite"
echo "C-Relay-PG Performance Benchmarking Suite"
echo "=========================================="
echo "Benchmarking relay at ws://$RELAY_HOST:$RELAY_PORT"
echo ""
+4 -4
View File
@@ -1,6 +1,6 @@
#!/bin/bash
# Resource Monitoring Suite for C-Relay
# Resource Monitoring Suite for C-Relay-PG
# Monitors memory and CPU usage during testing
set -e
@@ -27,7 +27,7 @@ TIMESTAMP_SAMPLES=()
# Function to get relay process info
get_relay_info() {
local pid
pid=$(pgrep -f "c_relay" | head -1)
pid=$(pgrep -f "c_relay_pg" | head -1)
if [[ -z "$pid" ]]; then
echo "0:0:0:0"
@@ -232,13 +232,13 @@ run_monitored_load_test() {
}
echo "=========================================="
echo "C-Relay Resource Monitoring Suite"
echo "C-Relay-PG Resource Monitoring Suite"
echo "=========================================="
echo "Monitoring relay at ws://$RELAY_HOST:$RELAY_PORT"
echo ""
# Check if relay is running
if ! pgrep -f "c_relay" >/dev/null 2>&1; then
if ! pgrep -f "c_relay_pg" >/dev/null 2>&1; then
echo -e "${RED}Relay process not found. Please start the relay first.${NC}"
echo "Use: ./make_and_restart_relay.sh"
exit 1
+4 -4
View File
@@ -1,6 +1,6 @@
#!/bin/bash
# C-Relay Comprehensive Test Suite Runner
# C-Relay-PG Comprehensive Test Suite Runner
# This script runs all security and stability tests for the Nostr relay
set -e
@@ -132,7 +132,7 @@ generate_html_report() {
<head>
<meta charset="UTF-8">
<meta name="viewport" content="width=device-width, initial-scale=1.0">
<title>C-Relay Test Report - $(date)</title>
<title>C-Relay-PG Test Report - $(date)</title>
<style>
body { font-family: Arial, sans-serif; margin: 40px; background-color: #f5f5f5; }
.header { background: linear-gradient(135deg, #667eea 0%, #764ba2 100%); color: white; padding: 20px; border-radius: 8px; margin-bottom: 30px; }
@@ -152,7 +152,7 @@ generate_html_report() {
</head>
<body>
<div class="header">
<h1>C-Relay Comprehensive Test Report</h1>
<h1>C-Relay-PG Comprehensive Test Report</h1>
<p>Generated on: $(date)</p>
<p>Test Environment: $RELAY_URL</p>
</div>
@@ -211,7 +211,7 @@ EOF
# Main execution
log "=========================================="
log "C-Relay Comprehensive Test Suite Runner"
log "C-Relay-PG Comprehensive Test Suite Runner"
log "=========================================="
log "Relay URL: $RELAY_URL"
log "Log file: $LOG_FILE"
+2 -2
View File
@@ -1,6 +1,6 @@
#!/bin/bash
# NIP Protocol Test Runner for C-Relay
# NIP Protocol Test Runner for C-Relay-PG
# Runs all NIP compliance tests
set -e
@@ -84,7 +84,7 @@ check_relay() {
}
echo "=========================================="
echo "C-Relay NIP Protocol Test Suite"
echo "C-Relay-PG NIP Protocol Test Suite"
echo "=========================================="
echo "Testing NIP compliance against relay at ws://$RELAY_HOST:$RELAY_PORT"
echo ""
+2 -2
View File
@@ -1,6 +1,6 @@
#!/bin/bash
# SQL Injection Test Suite for C-Relay
# SQL Injection Test Suite for C-Relay-PG
# Comprehensive testing of SQL injection vulnerabilities across all filter types
set -e
@@ -93,7 +93,7 @@ test_valid_query() {
}
echo "=========================================="
echo "C-Relay SQL Injection Test Suite"
echo "C-Relay-PG SQL Injection Test Suite"
echo "=========================================="
echo "Testing against relay at ws://$RELAY_HOST:$RELAY_PORT"
echo
+1 -1
View File
@@ -380,7 +380,7 @@ test_empty_result() {
}
echo "=========================================="
echo "C-Relay SQL Query Admin API Testing Suite"
echo "C-Relay-PG SQL Query Admin API Testing Suite"
echo "=========================================="
echo "Testing SQL query functionality at $RELAY_URL"
echo ""
+1 -1
View File
@@ -2,7 +2,7 @@
# 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
# This tests the persistent subscription functionality of the C-Relay-PG
set -e # Exit on any error
+1 -1
View File
@@ -1,6 +1,6 @@
#!/bin/bash
# Subscription Cleanup Testing Suite for C-Relay
# Subscription Cleanup Testing Suite for C-Relay-PG
# Tests startup cleanup and connection age limit features
set -e
+5 -5
View File
@@ -1,6 +1,6 @@
#!/bin/bash
# Thread-level CPU profiler for c-relay on remote server
# Thread-level CPU profiler for c-relay-pg on remote server
# - Samples per-thread CPU every N seconds using /proc/<pid>/task/*/stat deltas
# - Optionally runs perf record in parallel for callgraph data
# - Pulls artifacts locally and generates a summary report
@@ -15,13 +15,13 @@ RUN_TS="$(date -u +%Y%m%d_%H%M%S)"
RUN_DIR="${OUTDIR_BASE}/${RUN_TS}"
LOCAL_CSV="${RUN_DIR}/thread_samples.csv"
LOCAL_REPORT="${RUN_DIR}/thread_summary.txt"
REMOTE_DIR="/tmp/c_relay_thread_profile_${RUN_TS}"
REMOTE_DIR="/tmp/c_relay_pg_thread_profile_${RUN_TS}"
ENABLE_PERF="${ENABLE_PERF:-1}" # 1=run perf, 0=skip
mkdir -p "${RUN_DIR}"
echo "=========================================="
echo "C-Relay Thread CPU Profiler"
echo "C-Relay-PG Thread CPU Profiler"
echo "=========================================="
echo "Remote host : ${REMOTE_HOST}"
echo "Duration : ${DURATION}s"
@@ -43,9 +43,9 @@ CSV_FILE="\${REMOTE_DIR}/thread_samples.csv"
PERF_FILE="\${REMOTE_DIR}/perf.data"
LOG_FILE="\${REMOTE_DIR}/run.log"
PID=\$(pgrep -f '/usr/local/bin/c_relay/c_relay|c_relay' | head -1 || true)
PID=\$(pgrep -f '/usr/local/bin/c_relay_pg/c_relay_pg|c_relay_pg' | head -1 || true)
if [ -z "\${PID}" ]; then
echo "ERROR: c_relay process not found (checked by cmdline pattern)" | tee -a "\${LOG_FILE}"
echo "ERROR: c_relay_pg process not found (checked by cmdline pattern)" | tee -a "\${LOG_FILE}"
exit 1
fi

Some files were not shown because too many files have changed in this diff Show More