Compare commits

..
11 Commits
Author SHA1 Message Date
Laan Tungir 2d2ca79dfd v2.1.9 - Add Raspberry Pi ARM64 static build support 2026-04-23 06:44:45 -04:00
Laan Tungir 22c943d495 v2.1.8 - Implement explicit WAL checkpoint scheduling and disable SQLite auto-checkpoint 2026-04-01 21:03:24 -04:00
Laan Tungir 7e3d9b6825 v2.1.7 - Refactor SQLite connection ownership into db_ops, remove direct sqlite usage from runtime modules, and document agent-browser admin testing flow 2026-04-01 19:48:29 -04:00
Laan Tungir 547d22d09f v2.1.6 - Fix strict-mode admin API key access by prewarming config and relay private key caches 2026-04-01 19:08:42 -04:00
Laan Tungir ce9ae5a488 v2.1.5 - Offload duplicate and COUNT paths, add config/NIP-11 caching, and reduce DB reader threads 2026-04-01 16:15:46 -04:00
Laan Tungir 9b248b740f v2.1.4 - Harden async EVENT fallback handling and verify relay/nginx 503 behavior 2026-04-01 15:19:28 -04:00
Laan Tungir 0f77aeb72b v2.1.3 - Fix async EVENT thread safety by splitting store core/post-actions and naming lws main thread 2026-04-01 11:29:14 -04:00
Laan Tungir bb64651a0c v2.1.2 - Fix async REQ callback leak and validate Phase 2+3 subscription offload path 2026-04-01 11:01:38 -04:00
Laan Tungir 17be9c2b03 v2.1.1 - Wire REQ/COUNT/EVENT paths through thread pool sync helpers and stabilize filter limit tests 2026-04-01 09:38:25 -04:00
Laan Tungir f396c622b6 v2.1.0 - Bump to v2.1.0 as SQLite baseline for PostgreSQL fork handoff 2026-04-01 09:14:23 -04:00
Laan Tungir cbd260c1ae v2.0.4 - Complete Phase 1 DB abstraction: opaque db_stmt API, ip_ban migration, and SQLite leakage cleanup 2026-04-01 09:10:42 -04:00
54 changed files with 12374 additions and 903 deletions
View File
+16 -4
View File
@@ -92,8 +92,20 @@ RUN cd c_utils_lib && \
RUN cd nostr_core_lib && \
chmod +x build.sh && \
sed -i 's/CFLAGS="-Wall -Wextra -std=c99 -fPIC -O2"/CFLAGS="-U_FORTIFY_SOURCE -D_FORTIFY_SOURCE=0 -Wall -Wextra -std=c99 -fPIC -O2"/' build.sh && \
if [ "$(uname -m)" = "aarch64" ] && ! command -v aarch64-linux-gnu-gcc >/dev/null 2>&1; then \
ln -sf /usr/bin/gcc /usr/local/bin/aarch64-linux-gnu-gcc; \
fi && \
rm -f *.o *.a 2>/dev/null || true && \
./build.sh --nips=1,6,13,17,19,44,59
./build.sh --nips=1,6,13,17,19,44,59 && \
if [ -f libnostr_core_arm64.a ]; then \
cp libnostr_core_arm64.a libnostr_core.a; \
elif [ -f libnostr_core_x64.a ]; then \
cp libnostr_core_x64.a libnostr_core.a; \
else \
echo "ERROR: No supported nostr_core static library produced"; \
ls -la *.a 2>/dev/null || true; \
exit 1; \
fi
# Copy c-relay source files LAST (only this layer rebuilds on source changes)
COPY src/ /build/src/
@@ -103,9 +115,9 @@ COPY Makefile /build/Makefile
# 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 \
CFLAGS="-g -O2 -DDEBUG"; \
CFLAGS="-g -O2 -DDEBUG -fno-omit-frame-pointer"; \
STRIP_CMD="echo 'Keeping debug symbols'"; \
echo "Building with DEBUG symbols enabled (optimized with -O2)"; \
echo "Building with DEBUG symbols enabled (optimized with -O2, frame pointers kept for perf)"; \
else \
CFLAGS="-O2"; \
STRIP_CMD="strip /build/c_relay_static"; \
@@ -121,7 +133,7 @@ RUN if [ "$DEBUG_BUILD" = "true" ]; then \
src/db_ops.c src/thread_pool.c \
-o /build/c_relay_static \
c_utils_lib/libc_utils.a \
nostr_core_lib/libnostr_core_x64.a \
nostr_core_lib/libnostr_core.a \
-lwebsockets -lssl -lcrypto -lsqlite3 -lsecp256k1 \
-lcurl -lz -lpthread -lm -ldl && \
eval "$STRIP_CMD"
+23 -67
View File
@@ -92,63 +92,20 @@ x86: $(BUILD_DIR) src/main.h src/sql_schema.h $(MAIN_SRC) $(NOSTR_CORE_LIB) $(C_
$(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"
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..."
@if ! command -v aarch64-linux-gnu-gcc >/dev/null 2>&1; then \
echo "ERROR: ARM64 cross-compiler not found."; \
echo "Install with: make install-cross-tools"; \
echo "Or install manually: sudo apt install gcc-aarch64-linux-gnu"; \
exit 1; \
fi
@echo "Checking for ARM64 development libraries..."
@if ! dpkg -l | grep -q "libssl-dev:arm64\|libsqlite3-dev:arm64"; then \
echo "ERROR: ARM64 libraries not found. Cross-compilation requires ARM64 versions of:"; \
echo " - libssl-dev:arm64"; \
echo " - libsqlite3-dev:arm64"; \
echo " - libwebsockets-dev:arm64"; \
echo " - libsecp256k1-dev:arm64"; \
echo " - zlib1g-dev:arm64"; \
echo " - libcurl4-openssl-dev:arm64"; \
echo ""; \
echo "Install ARM64 libraries with: make install-arm64-deps"; \
echo "Or use Docker for cross-platform builds."; \
exit 1; \
fi
@echo "Using aarch64-linux-gnu-gcc with ARM64 libraries..."
PKG_CONFIG_PATH=/usr/lib/aarch64-linux-gnu/pkgconfig:/usr/share/pkgconfig \
aarch64-linux-gnu-gcc $(CFLAGS) $(INCLUDES) $(MAIN_SRC) -o $(BUILD_DIR)/c_relay_arm64 $(NOSTR_CORE_LIB) $(C_UTILS_LIB) \
-L/usr/lib/aarch64-linux-gnu $(LIBS)
@echo "Build complete: $(BUILD_DIR)/c_relay_arm64"
# Install ARM64 cross-compilation dependencies
install-arm64-deps:
@echo "Installing ARM64 cross-compilation dependencies..."
@echo "This requires adding ARM64 architecture and installing cross-libraries..."
sudo dpkg --add-architecture arm64
sudo apt update
sudo apt install -y \
gcc-aarch64-linux-gnu \
libc6-dev-arm64-cross \
libssl-dev:arm64 \
libsqlite3-dev:arm64 \
zlib1g-dev:arm64 \
libcurl4-openssl-dev:arm64
@echo "Note: libwebsockets-dev:arm64 and libsecp256k1-dev:arm64 may need manual building"
# Install cross-compilation tools
install-cross-tools:
@echo "Installing cross-compilation tools..."
sudo apt update
sudo apt install -y gcc-aarch64-linux-gnu libc6-dev-arm64-cross
# Check what architectures we can actually build
# Check local build tooling used by static Docker builder
check-toolchain:
@echo "Checking available toolchains:"
@echo "Native compiler: $(shell $(CC) --version | head -1)"
@if command -v aarch64-linux-gnu-gcc >/dev/null 2>&1; then \
echo "ARM64 cross-compiler: $(shell aarch64-linux-gnu-gcc --version | head -1)"; \
@if command -v docker >/dev/null 2>&1; then \
echo "Docker: $$(docker --version)"; \
else \
echo "ARM64 cross-compiler: NOT INSTALLED (install with 'make install-cross-tools')"; \
echo "Docker: NOT INSTALLED"; \
fi
@if docker buildx version >/dev/null 2>&1; then \
echo "Docker buildx: AVAILABLE"; \
else \
echo "Docker buildx: NOT AVAILABLE (required for cross-architecture static builds)"; \
fi
# Run tests
@@ -185,42 +142,41 @@ help:
@echo "Targets:"
@echo " all Build the relay for current architecture (default)"
@echo " x86 Build specifically for x86_64"
@echo " arm64 Build for ARM64 (requires cross-compilation setup)"
@echo " static-musl-x86_64 Build static MUSL binary using Docker (host arch default)"
@echo " static-musl-arm64 Build static MUSL ARM64 binary using Docker buildx"
@echo " static-musl Build both static MUSL binaries"
@echo " test Build and run tests"
@echo " init-db Initialize the database"
@echo " clean Clean build artifacts"
@echo " clean-all Clean everything including dependencies"
@echo " install-deps Install system dependencies"
@echo " install-cross-tools Install basic ARM64 cross-compiler"
@echo " install-arm64-deps Install ARM64 cross-compilation libraries"
@echo " check-toolchain Check available compilers"
@echo " check-toolchain Check available local tooling"
@echo " help Show this help"
@echo ""
@echo "Usage:"
@echo " make # Build the relay for current arch"
@echo " make x86 # Build for x86_64"
@echo " make arm64 # Build for ARM64 (fails if cross-compilation not set up)"
@echo " make install-arm64-deps # Install full ARM64 cross-compilation setup"
@echo " make check-toolchain # Check what compilers are available"
@echo " make static-musl-x86_64 # Build static binary via ./build_static.sh"
@echo " make static-musl-arm64 # Build ARM64 static binary via ./build_static.sh --arch arm64"
@echo " make static-musl # Build both static binaries"
@echo " make check-toolchain # Check docker/buildx availability"
@echo " make test # Run tests"
@echo " make init-db # Set up database"
@echo " make force-version # Force regenerate main.h from git"
# 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"
@echo "Building fully static MUSL binary using host architecture default..."
./build_static.sh
@echo "Static binary created in build/ (host architecture naming)"
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"
./build_static.sh --arch arm64
@echo "Static ARM64 binary created in build/"
static-musl: static-musl-x86_64 static-musl-arm64
@echo "Built static MUSL binaries for both architectures"
.PHONY: static-musl-x86_64 static-musl-arm64 static-musl
.PHONY: all x86 arm64 test init-db clean clean-all install-deps install-cross-tools install-arm64-deps check-toolchain help force-version
.PHONY: all x86 test init-db clean clean-all install-deps check-toolchain help force-version
+3 -2
View File
@@ -449,7 +449,8 @@ 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 = {
@@ -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
+78 -3
View File
@@ -11,8 +11,41 @@ DOCKERFILE="$SCRIPT_DIR/Dockerfile.alpine-musl"
# Parse command line arguments
DEBUG_BUILD=false
if [[ "$1" == "--debug" ]]; then
DEBUG_BUILD=true
TARGET_ARCH=""
while [[ $# -gt 0 ]]; do
case "$1" in
--debug)
DEBUG_BUILD=true
shift
;;
--arch)
if [[ -z "$2" ]]; then
echo "ERROR: --arch requires a value"
echo "Usage: $0 [--debug] [--arch <arm64|armv7|x86_64>]"
exit 1
fi
case "$2" in
arm64|armv7|x86_64)
TARGET_ARCH="$2"
;;
*)
echo "ERROR: Unsupported architecture '$2'"
echo "Supported values: arm64, armv7, x86_64"
exit 1
;;
esac
shift 2
;;
*)
echo "ERROR: Unknown argument '$1'"
echo "Usage: $0 [--debug] [--arch <arm64|armv7|x86_64>]"
exit 1
;;
esac
done
if [ "$DEBUG_BUILD" = true ]; then
echo "=========================================="
echo "C-Relay MUSL Static Binary Builder (DEBUG MODE)"
echo "=========================================="
@@ -24,6 +57,9 @@ fi
echo "Project directory: $SCRIPT_DIR"
echo "Build directory: $BUILD_DIR"
echo "Debug build: $DEBUG_BUILD"
if [[ -n "$TARGET_ARCH" ]]; then
echo "Requested target architecture: $TARGET_ARCH"
fi
echo ""
# Create build directory
@@ -58,17 +94,43 @@ DOCKER_CMD="docker"
echo "✓ Docker is available and running"
echo ""
# Detect architecture
# Detect host architecture
ARCH=$(uname -m)
case "$ARCH" in
x86_64)
HOST_ARCH="x86_64"
;;
aarch64|arm64)
HOST_ARCH="arm64"
;;
armv7l|armv7)
HOST_ARCH="armv7"
;;
*)
HOST_ARCH="unknown"
;;
esac
# Resolve target architecture
if [[ -n "$TARGET_ARCH" ]]; then
ARCH="$TARGET_ARCH"
fi
case "$ARCH" in
x86_64)
PLATFORM="linux/amd64"
OUTPUT_NAME="c_relay_static_x86_64"
;;
aarch64|arm64)
ARCH="arm64"
PLATFORM="linux/arm64"
OUTPUT_NAME="c_relay_static_arm64"
;;
armv7l|armv7)
ARCH="armv7"
PLATFORM="linux/arm/v7"
OUTPUT_NAME="c_relay_static_armv7"
;;
*)
echo "WARNING: Unknown architecture: $ARCH"
echo "Defaulting to linux/amd64"
@@ -77,6 +139,19 @@ case "$ARCH" in
;;
esac
# When cross-building, ensure buildx exists and warn about QEMU/binfmt setup
if [[ "$HOST_ARCH" != "unknown" && "$ARCH" != "$HOST_ARCH" ]]; then
echo "NOTE: Cross-building from host '$HOST_ARCH' to target '$ARCH'."
echo " Docker QEMU/binfmt support is required for this to work."
if ! docker buildx version >/dev/null 2>&1; then
echo "ERROR: docker buildx is required for cross-architecture builds but is not available."
echo "Install/enable buildx and binfmt support, then retry."
exit 1
fi
echo "✓ docker buildx is available for cross-architecture build"
echo ""
fi
# Append _debug suffix to output name for debug builds so production binary is never overwritten
if [ "$DEBUG_BUILD" = true ]; then
OUTPUT_NAME="${OUTPUT_NAME}_debug"
+190
View File
@@ -0,0 +1,190 @@
# Agent Browser Testing Guide for C-Relay
This document explains how to use the `agent-browser` CLI tool to test the c-relay 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
```
+81 -26
View File
@@ -18,6 +18,8 @@ COMMIT_MESSAGE=""
RELEASE_MODE=false
VERSION_INCREMENT_TYPE="patch" # "patch", "minor", or "major"
VERSION_INCREMENT_EXPLICIT=false
show_usage() {
echo "C-Relay Increment and Push Script"
echo ""
@@ -30,6 +32,7 @@ show_usage() {
echo " $0 -m \"commit message\" Increment minor version"
echo " $0 -M \"commit message\" Increment major version"
echo " $0 -r \"commit message\" Create release with assets (no version increment)"
echo " $0 -r -p \"commit message\" Create release with patch version increment"
echo " $0 -r -m \"commit message\" Create release with minor version increment"
echo " $0 -h Show this help message"
echo ""
@@ -45,6 +48,7 @@ show_usage() {
echo " $0 -m \"Added new features\""
echo " $0 -M \"Breaking API changes\""
echo " $0 -r \"Release current version\""
echo " $0 -r -p \"Release with patch increment\""
echo " $0 -r -m \"Release with minor increment\""
echo ""
echo "VERSION INCREMENT MODES:"
@@ -53,7 +57,7 @@ show_usage() {
echo " -M, --major: Increment major version, zero minor+patch (v1.2.3 → v2.0.0)"
echo ""
echo "RELEASE MODE (-r flag):"
echo " - Build static binary using build_static.sh"
echo " - Build static binaries for x86_64 and ARM64 (Raspberry Pi) using build_static.sh"
echo " - Create source tarball"
echo " - Git add, commit, push, and create Gitea release with assets"
echo " - Can be combined with version increment flags"
@@ -72,14 +76,17 @@ while [[ $# -gt 0 ]]; do
;;
-p|--patch)
VERSION_INCREMENT_TYPE="patch"
VERSION_INCREMENT_EXPLICIT=true
shift
;;
-m|--minor)
VERSION_INCREMENT_TYPE="minor"
VERSION_INCREMENT_EXPLICIT=true
shift
;;
-M|--major)
VERSION_INCREMENT_TYPE="major"
VERSION_INCREMENT_EXPLICIT=true
shift
;;
-h|--help)
@@ -306,9 +313,9 @@ git_commit_and_push_no_tag() {
fi
}
# Function to build release binary
# Function to build release binaries
build_release_binary() {
print_status "Building release binary..."
print_status "Building release binaries..."
# Check if build_static.sh exists
if [[ ! -f "build_static.sh" ]]; then
@@ -316,14 +323,22 @@ build_release_binary() {
return 1
fi
# Run the static build script
# Build x86_64 first (required for success)
if ./build_static.sh > /dev/null 2>&1; then
print_success "Built static binary successfully"
return 0
print_success "Built x86_64 static binary successfully"
else
print_error "Failed to build static binary"
print_error "Failed to build x86_64 static binary"
return 1
fi
# Build arm64 second (warning-only on failure)
if ./build_static.sh --arch arm64 > /dev/null 2>&1; then
print_success "Built ARM64 static binary successfully"
else
print_warning "Failed to build ARM64 static binary (continuing with release)"
fi
return 0
}
# Function to create source tarball
@@ -353,8 +368,9 @@ create_source_tarball() {
# Function to upload release assets to Gitea
upload_release_assets() {
local release_id="$1"
local binary_path="$2"
local binary_path_x86="$2"
local tarball_path="$3"
local binary_path_arm64="$4"
print_status "Uploading release assets..."
@@ -369,9 +385,9 @@ upload_release_assets() {
local assets_url="$api_url/releases/$release_id/assets"
print_status "Assets URL: $assets_url"
# Upload binary
if [[ -f "$binary_path" ]]; then
print_status "Uploading binary: $(basename "$binary_path")"
# Upload x86_64 binary
if [[ -f "$binary_path_x86" ]]; then
print_status "Uploading binary: $(basename "$binary_path_x86")"
# Retry loop for eventual consistency
local max_attempts=3
@@ -380,11 +396,11 @@ upload_release_assets() {
print_status "Upload attempt $attempt/$max_attempts"
local binary_response=$(curl -fS -X POST "$assets_url" \
-H "Authorization: token $token" \
-F "attachment=@$binary_path;filename=$(basename "$binary_path")" \
-F "name=$(basename "$binary_path")")
-F "attachment=@$binary_path_x86;filename=$(basename "$binary_path_x86")" \
-F "name=$(basename "$binary_path_x86")")
if echo "$binary_response" | grep -q '"id"'; then
print_success "Uploaded binary successfully"
print_success "Uploaded x86_64 binary successfully"
break
else
print_warning "Upload attempt $attempt failed"
@@ -392,7 +408,37 @@ upload_release_assets() {
print_status "Retrying in 2 seconds..."
sleep 2
else
print_error "Failed to upload binary after $max_attempts attempts"
print_error "Failed to upload x86_64 binary after $max_attempts attempts"
print_error "Response: $binary_response"
fi
fi
((attempt++))
done
fi
# Upload ARM64 binary
if [[ -f "$binary_path_arm64" ]]; then
print_status "Uploading binary: $(basename "$binary_path_arm64")"
local max_attempts=3
local attempt=1
while [[ $attempt -le $max_attempts ]]; do
print_status "Upload attempt $attempt/$max_attempts"
local binary_response=$(curl -fS -X POST "$assets_url" \
-H "Authorization: token $token" \
-F "attachment=@$binary_path_arm64;filename=$(basename "$binary_path_arm64")" \
-F "name=$(basename "$binary_path_arm64")")
if echo "$binary_response" | grep -q '"id"'; then
print_success "Uploaded ARM64 binary successfully"
break
else
print_warning "Upload attempt $attempt failed"
if [[ $attempt -lt $max_attempts ]]; then
print_status "Retrying in 2 seconds..."
sleep 2
else
print_error "Failed to upload ARM64 binary after $max_attempts attempts"
print_error "Response: $binary_response"
fi
fi
@@ -482,7 +528,7 @@ main() {
print_status "=== RELEASE MODE ==="
# Only increment version if explicitly requested (not just because of -r flag)
if [[ "$VERSION_INCREMENT_TYPE" != "patch" ]]; then
if [[ "$VERSION_INCREMENT_EXPLICIT" == true ]]; then
increment_version "$VERSION_INCREMENT_TYPE"
else
# In release mode without version increment, get current version
@@ -504,16 +550,25 @@ main() {
git_commit_and_push_no_tag
# Build release binary
local binary_path_x86=""
local binary_path_arm64=""
if build_release_binary; then
local binary_path="build/c_relay_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
print_status "Using existing binary from previous build"
binary_path="build/c_relay_static_x86_64"
else
binary_path=""
binary_path_x86="build/c_relay_static_x86_64"
fi
if [[ -f "build/c_relay_static_arm64" ]]; then
binary_path_arm64="build/c_relay_static_arm64"
fi
else
print_warning "x86_64 binary build failed, continuing with release creation"
# Check if binaries exist from previous build
if [[ -f "build/c_relay_static_x86_64" ]]; then
print_status "Using existing x86_64 binary from previous build"
binary_path_x86="build/c_relay_static_x86_64"
fi
if [[ -f "build/c_relay_static_arm64" ]]; then
print_status "Using existing ARM64 binary from previous build"
binary_path_arm64="build/c_relay_static_arm64"
fi
fi
@@ -531,8 +586,8 @@ main() {
# Validate release_id is numeric
if [[ "$release_id" =~ ^[0-9]+$ ]]; then
# Upload assets if we have a release ID and assets
if [[ -n "$release_id" && (-n "$binary_path" || -n "$tarball_path") ]]; then
upload_release_assets "$release_id" "$binary_path" "$tarball_path"
if [[ -n "$release_id" && (-n "$binary_path_x86" || -n "$binary_path_arm64" || -n "$tarball_path") ]]; then
upload_release_assets "$release_id" "$binary_path_x86" "$tarball_path" "$binary_path_arm64"
fi
print_success "Release $NEW_VERSION completed successfully!"
else
+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
+194
View File
@@ -0,0 +1,194 @@
# Main Thread CPU Offload Plan
## 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.
## Current Architecture
```mermaid
flowchart TD
LWS[lws_service - main thread] --> CB[nostr_relay_callback]
CB --> PARSE[JSON parse - cJSON_Parse]
CB --> VALIDATE[Signature verify - nostr_validate_unified_request]
CB --> STORE[store_event - builds payload]
STORE --> SYNC_WRITE[thread_pool_execute_store_event_sync]
SYNC_WRITE --> |blocks main thread| DB_WRITE[db-write thread]
DB_WRITE --> |signal| SYNC_WRITE
CB --> REQ[handle_req_message - builds SQL]
REQ --> SYNC_READ[thread_pool_execute_req_sync]
SYNC_READ --> |blocks main thread| DB_READ[db-read-N thread]
DB_READ --> |signal| SYNC_READ
CB --> BROADCAST[broadcast_event_to_subscriptions]
BROADCAST --> QUEUE[queue_message per subscriber]
CB --> WRITEABLE[LWS_CALLBACK_SERVER_WRITEABLE]
WRITEABLE --> DRAIN[process_message_queue - lws_write]
```
### Why the main thread is hot
| Work item | Where | Cost |
|-----------|-------|------|
| JSON parsing | `cJSON_Parse` in `LWS_CALLBACK_RECEIVE` | Medium - per message |
| Signature verification | `nostr_validate_unified_request` - ed25519 crypto | **High** - per EVENT |
| SQL query building | `handle_req_message` filter-to-SQL loop | Low-Medium |
| Sync DB wait | `thread_pool_execute_*_sync` - pthread_cond_wait | Blocks but yields CPU |
| Result iteration + expiration check | Row loop in `handle_req_message` with `cJSON_Parse` per row | Medium-High for large result sets |
| Subscription matching | `broadcast_event_to_subscriptions` - filter matching | Medium - scales with subscriber count |
| Message serialization + queueing | `snprintf` + `queue_message_take_ownership` per subscriber | Medium |
| Config lookups | `get_config_int/bool` called repeatedly in hot paths | Low but frequent |
### Key constraint: libwebsockets is single-threaded
libwebsockets requires that **all `lws_write`, `lws_callback_on_writable`, and `lws_close_reason` calls happen from the service thread** (the thread running `lws_service`). This means we cannot directly write to WebSocket connections from worker threads. However, we **can** do computation on worker threads and post results back to the main thread for I/O.
## Offload Strategy
### Phase 1: Async EVENT Processing (Highest Impact)
Convert EVENT handling from synchronous to async. Currently the main thread does: parse → validate → store → broadcast, all blocking. Instead:
1. **Main thread**: Parse JSON (fast), extract event ID for dedup check, then submit a job to a new "event processing" worker thread
2. **Worker thread**: Signature verification (expensive crypto), store_event (already goes to DB thread), prepare broadcast payload
3. **Main thread callback**: Receive result via `wake_loop_cb` + `lws_cancel_service`, send OK response and broadcast to subscribers
```mermaid
flowchart TD
LWS[lws_service - main thread] --> RECEIVE[LWS_CALLBACK_RECEIVE]
RECEIVE --> PARSE[JSON parse + dedup check]
PARSE --> SUBMIT[Submit to event-worker queue]
SUBMIT --> LWS
WORKER[event-worker thread] --> VERIFY[Signature verification]
VERIFY --> STORE_DB[store_event via DB pool]
STORE_DB --> PREP[Prepare broadcast payload]
PREP --> RESULT_Q[Push result to completion queue]
RESULT_Q --> WAKE[lws_cancel_service]
LWS2[lws_service wakes] --> POLL[Poll completion queue]
POLL --> OK[Send OK response via queue_message]
POLL --> BCAST[broadcast_event_to_subscriptions]
```
**What this offloads**: ed25519 signature verification (~the most expensive per-event operation), event classification, tag serialization, and the synchronous DB store wait.
**What stays on main thread**: JSON parse (needed to extract event ID for dedup), OK response writing, broadcast fan-out (requires lws access).
### Phase 2: Async REQ Query Execution (Medium Impact)
Convert REQ handling from sync to async:
1. **Main thread**: Parse filters, build SQL, create subscription, submit query job
2. **DB reader thread**: Execute query (already happens, but currently blocks main thread via `_sync`)
3. **Main thread callback**: Iterate results, queue EVENT messages, send EOSE
This is simpler than Phase 1 because the thread pool already supports async submission via `thread_pool_submit_read` with a `result_cb`. The `_sync` wrappers just add a condvar wait on top. We need to:
- Use `thread_pool_submit_read` directly instead of `thread_pool_execute_req_sync`
- Store pending REQ context (sub_id, wsi, pss) so the callback can complete the work
- In the result callback, push results to a completion queue and call `lws_cancel_service`
- On the main thread, drain the completion queue and send EVENT + EOSE messages
### Phase 3: Reduce Per-Row Overhead in REQ Results (Low-Medium Impact)
Currently each row from a REQ query gets `cJSON_Parse` just to check NIP-40 expiration. This is wasteful:
- Option A: Add an `expiration` column to the events table so expiration filtering can be done in SQL
- Option B: Store expiration timestamp in a fast-parse format (extract during INSERT, store as integer column)
- Option C: Use string search on the raw JSON for the expiration tag instead of full parse
### Phase 4: Config Value Caching (Low Impact, Easy Win)
`get_config_int` and `get_config_bool` are called on every message in hot paths. These do SQLite queries. Cache config values in memory with a TTL or invalidation signal, so the main loop only refreshes them periodically (already done for `debug_level` every 60s — extend to all hot-path config values).
## Implementation Priority
| Phase | Impact | Risk | Complexity |
|-------|--------|------|------------|
| Phase 1: Async EVENT | **High** - removes crypto from main thread | Medium - async state management | Medium-High |
| Phase 2: Async REQ | **Medium** - unblocks main thread during queries | Low - infrastructure exists | Medium |
| Phase 3: Expiration optimization | **Low-Medium** - reduces per-row parse cost | Low | Low |
| Phase 4: Config caching | **Low** - reduces DB round-trips | Very Low | Low |
## Detailed Design: Phase 1 (Async EVENT Processing)
### New Components
#### Completion Queue (`src/completion_queue.h/.c`)
A thread-safe FIFO queue for posting results from worker threads back to the main thread:
```c
typedef struct {
int type; // COMPLETION_TYPE_EVENT_RESULT, COMPLETION_TYPE_REQ_RESULT, etc.
void* data; // Type-specific result data
struct lws* wsi; // Target WebSocket connection
void* pss; // Per-session data
} completion_item_t;
int completion_queue_init(void);
int completion_queue_push(completion_item_t* item);
completion_item_t* completion_queue_pop(void); // Non-blocking
void completion_queue_shutdown(void);
```
#### Event Worker Thread
A dedicated pthread that processes EVENT validation/storage:
```c
typedef struct {
cJSON* event; // Parsed event JSON - ownership transferred
cJSON* full_message; // Full message JSON for context
struct lws* wsi;
void* pss;
char sub_id[64]; // For response routing
} event_work_item_t;
```
#### Main Loop Integration
Add a completion queue drain step to the main event loop. After `lws_service` returns (either from timeout or `lws_cancel_service` wake), check the completion queue:
```c
while (g_server_running && !g_shutdown_flag) {
int result = lws_service(ws_context, 1000);
// NEW: Drain completion queue
completion_item_t* item;
while ((item = completion_queue_pop()) != NULL) {
process_completion(item); // Send OK, broadcast, etc.
free(item);
}
// ... existing periodic checks ...
}
```
### Changes to Existing Code
1. **`nostr_relay_callback` EVENT path**: After JSON parse and dedup check, instead of calling `nostr_validate_unified_request` + `store_event` + `broadcast_event_to_subscriptions` synchronously, submit an `event_work_item_t` to the event worker queue and return 0 immediately.
2. **`store_event`**: No changes needed — it already uses `thread_pool_execute_store_event_sync` which will run on the DB writer thread. The event worker thread will call it.
3. **`broadcast_event_to_subscriptions`**: No changes needed — it will be called from the main thread when processing the completion item, which is correct since it calls `queue_message_take_ownership` (requires lws thread).
### Thread Safety Considerations
- The `cJSON* event` object must be fully owned by the worker thread during processing. The main thread must not access it after submission.
- The `wsi` and `pss` pointers could become invalid if the client disconnects while the event is being processed. The completion handler must validate that the connection is still alive before sending the OK response.
- A generation counter or epoch on `pss` can detect stale references.
## What Cannot Be Offloaded
- **`lws_write` / `queue_message`**: Must happen on the lws service thread
- **`lws_callback_on_writable`**: Must happen on the lws service thread
- **`lws_close_reason`**: Must happen on the lws service thread
- **Subscription list iteration for broadcast**: Accesses `lws_wsi_user` which is lws-internal
These are fundamental libwebsockets constraints. The pattern is always: do computation off-thread, post result to completion queue, wake main thread, do I/O on main thread.
## Expected Impact
With Phase 1 alone, the main thread would no longer perform:
- ed25519 signature verification (~100-500μs per event depending on CPU)
- Synchronous DB store wait (~50-200μs per event)
- Event classification, tag serialization, JSON serialization for storage
This should reduce main-thread CPU by roughly 30-50% for EVENT-heavy workloads, shifting that work to the event worker thread and DB threads.
+257
View File
@@ -0,0 +1,257 @@
# PostgreSQL Nostr Relay Architecture Analysis
This document summarizes common approaches and best practices for building Nostr relays backed by PostgreSQL, based on industry-standard designs and existing open-source relay implementations.
## Identified Projects
* **Nostrss**: High-performance Rust-based relay.
* **Nostrgres**: Focused on PostgreSQL integration with complex event filtering.
* **Relay-rs (Postgres branch)**: General purpose high-throughput relay.
* **Nostream** ([github.com/Cameri/nostream](https://github.com/Cameri/nostream)): Production-ready TypeScript relay backed by PostgreSQL and Redis.
* **Nostr-Relay-NestJS** ([github.com/CodyTseng/nostr-relay-nestjs](https://github.com/CodyTseng/nostr-relay-nestjs)): High-performance relay built with NestJS and PostgreSQL, utilizing Kysely for type-safe database interactions.
## Detailed Analysis
### 1. Common Schema Patterns
* **Event Storage (JSONB)**: Most PostgreSQL-backed relays store the raw event as a JSONB object in a central `events` table. This provides flexibility for the evolving Nostr spec while allowing efficient extraction of fields.
* **Tag Denormalization**: While the raw event is in JSONB, performant relays extract tags (like `p`, `e`, `t`, `d`) into a separate `tags` table with foreign key relationships to the `events` table. This avoids slow JSONB traversal during complex filter queries.
* **Author/Publisher Tracking**: A dedicated `authors` or `pubkeys` table is typically used to maintain indexing on the `pubkey` field, enabling quick lookups of user activity.
### 2. Performance Optimization
* **GIN Indexes on JSONB**: Crucial for filtering on specific event tags or custom properties stored within the event object. GIN indexes with `jsonb_path_ops` are generally preferred for equality checks.
* **Time-Series Partitioning**: Given the append-only nature of Nostr, partitioning the `events` table by time (e.g., daily or weekly chunks) is highly recommended. This significantly improves query performance for recent data and simplifies data expiration/deletion.
* **Clustering**: Clustering the `events` table by the timestamp index can reduce disk I/O, as it keeps temporally related events physically adjacent on the disk.
### 3. Schema Management Approaches
* **Migrations**: Most mature relays utilize migration tools (like `Flyway`, `Diesel migrations`, or `Golang-migrate`) to version control database schema changes. This is critical for production stability.
* **Auto-generation**: Some lightweight prototypes auto-generate schemas at startup. This is generally discouraged for production due to the risk of destructive changes, data loss, or blocking DDL operations on large tables.
## Recommendations for Building a New Relay
1. **Prioritize Denormalization**: Do not rely solely on JSONB queries for high-traffic filters. Extract indexed tags into dedicated columns or tables.
2. **Use Partitioning from Day One**: Implementing native PostgreSQL partitioning (e.g., using `pg_partman`) is much easier before the dataset grows to millions of rows.
3. **Connection Pooling**: PostgreSQL requires aggressive connection management. Use a high-performance pooler like `PgBouncer` to handle the large number of concurrent, short-lived connections common in WebSocket-based relay traffic.
4. **Asynchronous Writes**: Decouple the WebSocket ingestion thread from the database writer thread to ensure that slow database writes do not impact the relay's responsiveness.
## Nostream ([github.com/Cameri/nostream](https://github.com/Cameri/nostream))
A production-ready Nostr relay written in TypeScript (Node.js), backed by PostgreSQL 14+ and Redis. Uses Knex.js for versioned migrations.
### PostgreSQL Schema
The schema is fully normalized — no JSONB blob for the whole event. Tags are stored separately and populated by a trigger.
**`events` table** — one row per event, tags kept as a JSONB column for the trigger to process:
```sql
CREATE TABLE events (
id UUID PRIMARY KEY DEFAULT uuid_generate_v4(),
event_id BYTEA UNIQUE NOT NULL, -- 32-byte hash
event_pubkey BYTEA NOT NULL,
event_kind INTEGER UNSIGNED NOT NULL,
event_created_at INTEGER UNSIGNED NOT NULL,
event_content TEXT NOT NULL,
event_tags JSONB, -- raw tags array, source of truth for trigger
event_signature BYTEA NOT NULL,
remote_address TEXT,
expires_at INTEGER,
deleted_at TIMESTAMP,
event_deduplication JSONB, -- used for replaceable event conflict key
first_seen TIMESTAMP DEFAULT now()
);
-- Indexes
CREATE INDEX ON events (event_pubkey);
CREATE INDEX ON events (event_kind);
CREATE INDEX ON events (event_created_at);
CREATE INDEX ON events (expires_at);
CREATE INDEX event_tags_idx ON events USING GIN (event_tags); -- GIN on raw JSONB
-- Unique index enforcing NIP-16 replaceable event semantics
CREATE UNIQUE INDEX replaceable_events_idx ON events (event_pubkey, event_kind)
WHERE event_kind = 0 OR event_kind = 3
OR (event_kind >= 10000 AND event_kind < 20000);
```
**`event_tags` table** — denormalized single-letter tags, populated by a PostgreSQL trigger on `events`:
```sql
CREATE TABLE event_tags (
id UUID PRIMARY KEY DEFAULT uuid_generate_v4(),
event_id BYTEA NOT NULL,
tag_name TEXT NOT NULL, -- single-letter only (e.g. 'e', 'p', 't')
tag_value TEXT NOT NULL
);
CREATE INDEX ON event_tags (event_id);
CREATE INDEX ON event_tags (tag_name, tag_value);
-- Trigger: on INSERT/UPDATE/DELETE to events, repopulate event_tags
CREATE OR REPLACE FUNCTION process_event_tags() RETURNS TRIGGER AS $$
DECLARE
tag_element jsonb;
BEGIN
DELETE FROM event_tags WHERE event_id = OLD.event_id;
IF TG_OP = 'INSERT' OR TG_OP = 'UPDATE' THEN
FOR tag_element IN SELECT jsonb_array_elements(NEW.event_tags) LOOP
IF length(trim('"' FROM (tag_element->0)::text)) = 1
AND (tag_element->1)::text IS NOT NULL THEN
INSERT INTO event_tags (event_id, tag_name, tag_value)
VALUES (NEW.event_id,
trim('"' FROM (tag_element->0)::text),
trim('"' FROM (tag_element->1)::text));
END IF;
END LOOP;
END IF;
RETURN NEW;
END;
$$ LANGUAGE plpgsql;
CREATE TRIGGER insert_event_tags
AFTER INSERT OR UPDATE OR DELETE ON events
FOR EACH ROW EXECUTE FUNCTION process_event_tags();
```
Tag queries (`#e`, `#p`, etc.) join `event_tags` rather than traversing the JSONB array.
**`users` and `invoices` tables** — for paid relay admission (Lightning payments):
```sql
CREATE TABLE users (
pubkey BYTEA PRIMARY KEY,
is_admitted BOOLEAN DEFAULT FALSE,
balance BIGINT DEFAULT 0, -- in msats
tos_accepted_at DATETIME
);
CREATE TABLE invoices (
id UUID PRIMARY KEY DEFAULT uuid_generate_v4(),
pubkey BYTEA NOT NULL,
bolt11 TEXT NOT NULL,
amount_requested BIGINT UNSIGNED NOT NULL,
amount_paid BIGINT UNSIGNED,
unit ENUM('msats','sats','btc'),
status ENUM('pending','completed','expired'),
description TEXT,
confirmed_at DATETIME,
expires_at DATETIME
);
```
### How Redis Is Used
Redis is **not** used for subscription fan-out. Subscription delivery is handled entirely in-process: each `WebSocketAdapter` holds a `Map<subscriptionId, filters[]>`, and a broadcast event walks all connected clients and filters locally.
Redis has two actual roles:
1. **Sliding-window rate limiting** — The `SlidingWindowRateLimiter` uses Redis sorted sets to track request timestamps per key (pubkey, IP, etc.) within a rolling time window:
- `ZREMRANGEBYSCORE key 0 (now - period)` — evict old entries
- `ZADD key timestamp member` — record this hit
- `ZRANGE key 0 -1` — count hits in window
- `EXPIRE key period` — auto-cleanup
- Keys follow the pattern `<pubkey>:events:<period>` or `<ip>:message:<period>`
2. **General-purpose cache** — A `RedisAdapter` wraps the client with `get`/`set`/`hasKey` helpers, used for caching admission checks (e.g. `<pubkey>:is-admitted`). The event handler has a `TODO` comment noting that the `userRepository.findByPubkey()` call should be replaced with a cache lookup — meaning this is partially implemented.
---
## Schema Definitions
### Relay-rs (Postgres)
```sql
-- Events table
CREATE TABLE "event" (
id bytea NOT NULL,
pub_key bytea NOT NULL,
created_at timestamp with time zone NOT NULL,
kind integer NOT NULL,
"content" bytea NOT NULL,
hidden bit(1) NOT NULL DEFAULT 0::bit(1),
delegated_by bytea NULL,
first_seen timestamp with time zone NOT NULL DEFAULT now(),
expires_at timestamp(0) with time zone,
CONSTRAINT event_pkey PRIMARY KEY (id)
);
CREATE INDEX event_created_at_idx ON "event" (created_at,kind);
CREATE INDEX event_pub_key_idx ON "event" (pub_key);
CREATE INDEX event_delegated_by_idx ON "event" (delegated_by);
CREATE INDEX event_expires_at_idx ON "event" (expires_at);
-- Tags table
CREATE TABLE "tag" (
id int8 NOT NULL GENERATED BY DEFAULT AS IDENTITY,
event_id bytea NOT NULL,
"name" varchar NOT NULL,
value bytea NULL,
value_hex bytea NULL,
CONSTRAINT tag_fk FOREIGN KEY (event_id) REFERENCES "event"(id) ON DELETE CASCADE,
CONSTRAINT unique_constraint_name UNIQUE (event_id, "name", value, value_hex)
);
CREATE INDEX tag_event_id_idx ON tag USING btree (event_id, name);
CREATE INDEX tag_value_idx ON tag USING btree (value);
CREATE INDEX tag_value_hex_idx ON tag USING btree (value_hex);
-- Account table
CREATE TABLE "account" (
pubkey varchar NOT NULL,
is_admitted BOOLEAN NOT NULL DEFAULT FALSE,
balance BIGINT NOT NULL DEFAULT 0,
tos_accepted_at TIMESTAMP,
CONSTRAINT account_pkey PRIMARY KEY (pubkey)
);
-- Invoice table
CREATE TYPE status AS ENUM ('Paid', 'Unpaid', 'Expired');
CREATE TABLE "invoice" (
payment_hash varchar NOT NULL,
pubkey varchar NOT NULL,
invoice varchar NOT NULL,
amount BIGINT NOT NULL,
status status NOT NULL DEFAULT 'Unpaid',
description varchar,
created_at timestamp,
confirmed_at timestamp,
CONSTRAINT invoice_payment_hash PRIMARY KEY (payment_hash),
CONSTRAINT invoice_pubkey_fkey FOREIGN KEY (pubkey) REFERENCES account (pubkey) ON DELETE CASCADE
);
```
### Nostr-Relay-NestJS (PostgreSQL via Kysely)
```sql
-- Events table
CREATE TABLE events (
id CHAR(64) PRIMARY KEY,
pubkey CHAR(64) NOT NULL,
created_at BIGINT NOT NULL,
kind INTEGER NOT NULL,
tags JSONB NOT NULL DEFAULT '[]',
generic_tags TEXT[] NOT NULL DEFAULT '{}',
content TEXT NOT NULL DEFAULT '',
sig CHAR(128) NOT NULL,
expired_at BIGINT,
d_tag_value TEXT,
delegator CHAR(64),
create_date TIMESTAMP NOT NULL DEFAULT now(),
update_date TIMESTAMP NOT NULL DEFAULT now(),
delete_date TIMESTAMP
);
-- Indices
CREATE EXTENSION IF NOT EXISTS btree_gin;
CREATE INDEX generic_tags_kind_idx ON events USING gin (generic_tags, kind);
CREATE INDEX pubkey_kind_created_at_idx ON events (pubkey, kind, created_at);
CREATE INDEX delegator_kind_created_at_idx ON events (delegator, kind, created_at);
CREATE INDEX created_at_kind_idx ON events (created_at, kind);
-- Generic tags table (for optimized tag queries)
CREATE TABLE generic_tags (
id SERIAL PRIMARY KEY,
tag TEXT NOT NULL,
author CHAR(64) NOT NULL,
kind INTEGER NOT NULL,
event_id CHAR(64) NOT NULL REFERENCES events(id),
created_at BIGINT NOT NULL
);
-- Indices for tag filtering
CREATE UNIQUE INDEX g_tag_event_id_idx ON generic_tags (tag, event_id);
CREATE INDEX g_tag_kind_created_at_idx ON generic_tags (tag, kind, created_at);
CREATE INDEX g_tag_created_at_idx ON generic_tags (tag, created_at);
```
Nostr-Relay-NestJS features a hybrid storage model where tags are stored both in a JSONB field (for general lookup) and a normalized `generic_tags` table (for optimized tag query performance). The schema uses Kysely for type-safe interaction.
+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
```
+379
View File
@@ -0,0 +1,379 @@
# Plan: Remove All SQLite from lws-main Thread
## Problem Statement
After implementing the 4 offload fixes (v2.1.4), `lws-main` still consumes **53.87% avg CPU** (down from 68.71%). Perf profiling shows **100% of the top symbols are SQLite** — B-tree traversal, page cache, VFS reads. The worker threads (`db-read`, `db-write`, `event-worker`) are nearly idle at 0.00.1%.
The goal is to eliminate all synchronous SQLite access from `lws-main` so it only does: JSON parsing, WebSocket I/O, subscription matching (in-memory), and message queuing.
## Thread Architecture Change
Reduce from 7 threads to 4:
```
Current: lws-main, db-read-1, db-read-2, db-read-3, db-read-4, db-write, event-worker
Proposed: lws-main, db-read, db-write, event-worker
```
The 4 reader threads are overkill — they average 0.070.10% CPU each. A single `db-read` thread is sufficient for the current workload. This can be made configurable later if needed.
## Remaining SQLite Calls on lws-main
Every synchronous SQLite call that currently runs on `lws-main`:
### Hot Path — Per-Event (every incoming EVENT message)
| Call | Location | Frequency | Cost |
|------|----------|-----------|------|
| `event_id_exists_in_db()` | websockets.c:1324, 2134 | Every EVENT | ~10-50us per B-tree lookup |
| `get_config_value("relay_pubkey")` | websockets.c:2050 | Every kind-14 EVENT | SQLite SELECT |
| `get_config_value("admin_pubkey")` | websockets.c:2073 | Every kind-23456 EVENT | SQLite SELECT |
### Hot Path — Per-Connection (every new WebSocket connection)
| Call | Location | Frequency | Cost |
|------|----------|-----------|------|
| `get_config_bool("trust_proxy_headers")` | websockets.c:1071 | Every connection | SQLite SELECT |
| `get_config_bool("nip42_auth_required_events")` | websockets.c:1134 | Every connection | SQLite SELECT |
| `get_config_bool("nip42_auth_required_subscriptions")` | websockets.c:1135 | Every connection | SQLite SELECT |
### Hot Path — Per-REQ (every subscription request)
| Call | Location | Frequency | Cost |
|------|----------|-----------|------|
| `get_config_bool("expiration_enabled")` | main.c:1424 | Every REQ | SQLite SELECT |
| `get_config_bool("expiration_filter")` | main.c:1425 | Every REQ | SQLite SELECT |
| `check_database_auth_rules()` | websockets.c:1706 | Every REQ when WoT enabled | SQLite SELECT |
### Medium Path — Per-COUNT (NIP-45)
| Call | Location | Frequency | Cost |
|------|----------|-----------|------|
| `thread_pool_execute_count_sync()` | websockets.c:3689 | Every COUNT message | Blocks main thread waiting for db-read |
### Low Path — Periodic (every 60s or on timer)
| Call | Location | Frequency | Cost |
|------|----------|-----------|------|
| `refresh_hot_config_if_needed()` | websockets.c:228-247 | Every 5s | 8 SQLite SELECTs |
| `generate_and_post_status_event()` | websockets.c:3213 | Every N hours | store_event + broadcast |
| `ip_ban_cleanup/log_stats` | websockets.c:3004-3005 | Every 60s | SQLite reads/writes |
### Per-EVENT — Validator Path (called from event-worker AND sync path)
| Call | Location | Frequency | Cost |
|------|----------|-----------|------|
| `get_config_value("admin_pubkey")` | request_validator.c:308 | Every kind-23456 EVENT | SQLite SELECT |
| `get_config_value("nip42_auth_enabled")` | request_validator.c:326 | Every EVENT | SQLite SELECT |
| `get_config_bool("pow_enabled")` | request_validator.c:370 | Every EVENT | SQLite SELECT |
| `get_config_int("pow_min_difficulty")` | request_validator.c:371 | Every EVENT | SQLite SELECT |
| `get_config_int("pow_validation_flags")` | request_validator.c:372 | Every EVENT | SQLite SELECT |
| `get_config_int("nip40_expiration_grace_period")` | request_validator.c:432 | Every EVENT with expiration | SQLite SELECT |
| `get_config_value("auth_enabled")` | request_validator.c:212 | Every EVENT (reload_auth_config) | SQLite SELECT |
| `get_config_value("auth_rules_enabled")` | request_validator.c:220 | Every EVENT (reload_auth_config) | SQLite SELECT |
| `db_count_active_whitelist_rules()` | request_validator.c:536 | Every 5s (cached) | Opens separate SQLite connection |
**Note:** These run on the **event-worker** thread for async-eligible events, but on **lws-main** for special kinds (14, 1059, 23456) that go through the sync validation path. They also run on lws-main for the `nostr_validate_unified_request()` call in the sync fallback.
### Per-EVENT — NIP-13 PoW Validation
| Call | Location | Frequency | Cost |
|------|----------|-----------|------|
| `get_config_bool("pow_enabled")` | nip013.c:27 | Every EVENT | SQLite SELECT |
| `get_config_int("pow_min_difficulty")` | nip013.c:28 | Every EVENT | SQLite SELECT |
| `get_config_value("pow_mode")` | nip013.c:29 | Every EVENT | SQLite SELECT |
### Per-Broadcast — NIP-40 Expiration Check
| Call | Location | Frequency | Cost |
|------|----------|-----------|------|
| `get_config_bool("expiration_enabled")` | subscriptions.c:764 | Every broadcast | SQLite SELECT |
| `get_config_bool("expiration_filter")` | subscriptions.c:765 | Every broadcast | SQLite SELECT |
### Per-NIP-11 Request (HTTP, not WebSocket)
| Call | Location | Frequency | Cost |
|------|----------|-----------|------|
| 15+ `get_config_value()` calls | nip011.c:96-110 | Every NIP-11 HTTP request | 15 SQLite SELECTs |
| 10+ `get_config_int()`/`get_config_bool()` calls | nip011.c:113-123 | Every NIP-11 HTTP request | 10 SQLite SELECTs |
### Per-NIP-42 Auth Event
| Call | Location | Frequency | Cost |
|------|----------|-----------|------|
| `get_config_value("relay_url")` | nip042.c:99 | Every AUTH event | SQLite SELECT |
| `get_config_int("relay_port")` | nip042.c:103 | Every AUTH event (fallback) | SQLite SELECT |
### Per-NIP-40 Expiration Check (EVENT submission)
| Call | Location | Frequency | Cost |
|------|----------|-----------|------|
| 5 `get_config_bool()`/`get_config_int()` calls | nip040.c:37-41 | Every EVENT with expiration tag | 5 SQLite SELECTs |
### Subscription Lifecycle (per sub create/close/disconnect)
| Call | Location | Frequency | Cost |
|------|----------|-----------|------|
| `db_log_subscription_created()` | subscriptions.c:1043 | Every REQ | SQLite INSERT |
| `db_log_subscription_closed()` | subscriptions.c:1053 | Every CLOSE | SQLite INSERT + UPDATE |
| `db_log_subscription_disconnected()` | subscriptions.c:1060 | Every disconnect | SQLite UPDATE + INSERT |
| `db_update_subscription_events_sent()` | subscriptions.c:1088 | Every broadcast match | SQLite UPDATE |
| `db_cleanup_orphaned_subscriptions()` | subscriptions.c:1100 | Startup only | SQLite DELETE |
### IP Ban System (per-connection and periodic)
| Call | Location | Frequency | Cost |
|------|----------|-----------|------|
| `get_config_value("idle_ban_whitelist")` | ip_ban.c:254 | Every ban check | SQLite SELECT |
| `get_config_bool("auth_fail_ban_enabled")` | ip_ban.c:301, 343 | Every ban check + failure | SQLite SELECT |
| `get_config_bool("idle_ban_enabled")` | ip_ban.c:307, 394 | Every ban check + idle failure | SQLite SELECT |
| 6+ `get_config_int()` calls | ip_ban.c:345-399, 464-465 | Every failure recording | SQLite SELECTs |
| `db_prepare()`/`db_step_stmt()` for ip_bans | ip_ban.c:141-238 | Load at startup + periodic save | SQLite reads/writes |
### Special Kind Sync Path (kinds 14, 1059, 23456)
| Call | Location | Frequency | Cost |
|------|----------|-----------|------|
| `store_event()` | websockets.c:1548-1629 | Every special-kind EVENT | Full sync store |
| `process_admin_event_in_config()` | websockets.c:1468 | Every kind-23456 | Config DB writes |
| `process_nip17_admin_message()` | websockets.c:1523 | Every kind-1059 when enabled | Decrypt + DB |
| `process_dm_stats_command()` | websockets.c:3240 | Admin DM stats | Decrypt + query + encrypt |
| `generate_stats_json()` | api.c:1167 (via dm_admin.c:805) | Admin stats command | Multiple SQLite queries |
### DM Admin Path (called from lws-main for kind 1059/14)
| Call | Location | Frequency | Cost |
|------|----------|-----------|------|
| `get_config_value("relay_pubkey")` | dm_admin.c:358, 451 | Every admin DM | SQLite SELECT |
| `get_config_value("admin_pubkey")` | dm_admin.c:504 | Every admin DM | SQLite SELECT |
| `get_config_int("nip59_timestamp_max_delay_sec")` | dm_admin.c:373 | Every admin DM | SQLite SELECT |
| `get_config_int("wot_enabled")` | dm_admin.c:613, 640, 660 | WoT commands | SQLite SELECT |
### API/Monitoring (called from lws-main periodic timer)
| Call | Location | Frequency | Cost |
|------|----------|-----------|------|
| `generate_stats_json()` | api.c:1167 | Status post + admin stats | ~10 SQLite queries |
| `query_time_based_statistics()` | api.c:102 | Monitoring events | SQLite queries |
| `query_subscription_details()` | api.c:197 | Monitoring events | SQLite queries |
| `get_config_value("relay_pubkey")` | api.c:419, 821, 1291 | Every monitoring event | SQLite SELECT |
| `get_config_int("kind_1_status_posts_hours")` | api.c:547 | Status post check | SQLite SELECT |
| `get_config_int("kind_24567_reporting_throttle_sec")` | api.c:60 | Monitoring throttle | SQLite SELECT |
## Implementation Plan
### Fix 1: Create Global Config Cache — Eliminate ALL get_config_* SQLite Calls
**Impact: Very High** — eliminates 50+ distinct `get_config_value/int/bool` call sites across 8 source files
The audit found `get_config_value/int/bool` calls in: `websockets.c`, `main.c`, `subscriptions.c`, `request_validator.c`, `nip011.c`, `nip013.c`, `nip040.c`, `nip042.c`, `ip_ban.c`, `dm_admin.c`, `api.c`, `config.c`. Every single one does a `SELECT value FROM config WHERE key=?` SQLite query.
**Approach:** Replace `get_config_value_from_table()` with an in-memory hash map that is loaded once at startup and refreshed periodically (every 5s) or on admin config change events.
**Changes:**
1. **New: `config_cache` in config.c** — A simple hash map or fixed array of key-value pairs loaded from the config table:
```c
typedef struct {
char key[64];
char value[512];
} config_cache_entry_t;
static config_cache_entry_t g_config_cache[256];
static int g_config_cache_count = 0;
static time_t g_config_cache_last_refresh = 0;
static pthread_mutex_t g_config_cache_mutex;
#define CONFIG_CACHE_TTL_SEC 5
```
2. **Modify `get_config_value_from_table()`** — Instead of querying SQLite, look up in the in-memory cache. If cache is stale, refresh from SQLite (but only once per TTL period, not per call).
3. **Add `config_cache_refresh()`** — Loads all rows from `SELECT key, value FROM config` into the hash map. Called:
- Once at startup after config table is populated
- Every 5 seconds from the existing `refresh_hot_config_if_needed()` timer
- Immediately after `process_admin_event_in_config()` modifies config
4. **Thread safety:** The cache is read from lws-main and event-worker threads. Use a read-write lock or double-buffer pattern. Since config changes are rare, a simple mutex with short hold time is fine.
5. **Remove the existing `hot_config_cache_t`** in websockets.c — it becomes redundant since the global config cache serves the same purpose with broader coverage.
This single change eliminates SQLite calls from:
- Per-connection: `trust_proxy_headers`, `nip42_auth_required_*` (websockets.c)
- Per-REQ: `expiration_enabled`, `expiration_filter` (main.c)
- Per-EVENT: `admin_pubkey`, `relay_pubkey`, `nip42_auth_enabled`, `pow_*` (request_validator.c, nip013.c)
- Per-broadcast: `expiration_enabled`, `expiration_filter` (subscriptions.c)
- Per-NIP-11: 25+ config values (nip011.c)
- Per-NIP-42: `relay_url`, `relay_port` (nip042.c)
- Per-NIP-40: 5 expiration config values (nip040.c)
- Per-IP-ban: `idle_ban_*`, `auth_fail_*` config values (ip_ban.c)
- Per-admin-DM: `relay_pubkey`, `admin_pubkey`, `wot_enabled` (dm_admin.c)
- Per-monitoring: `relay_pubkey`, throttle values (api.c)
### Fix 2: Move Duplicate Check to Event Worker Thread
**Impact: High** — eliminates the most frequent per-EVENT SQLite call from lws-main
Currently `event_id_exists_in_db()` runs on lws-main before async submission. Move it into the `async_event_worker_main()` function, which already has its own SQLite connection.
**Changes:**
- Remove `event_id_exists_in_db()` calls from both EVENT paths in websockets.c (lines 1324 and 2134)
- Add the duplicate check at the start of `async_event_worker_main()` before `nostr_validate_unified_request()`
- If duplicate found, set `completion->success = 1` and `completion->error_message = "duplicate: already have this event"` and skip crypto
- The completion handler already sends OK responses, so this works seamlessly
**Trade-off:** Without the early dedup check on lws-main, duplicate events will be submitted to the event worker queue and consume a queue slot + thread wakeup before being detected. This is acceptable because:
- The worker thread dedup check is still fast (B-tree index lookup)
- It avoids the much larger cost of running SQLite on lws-main for every event
- Queue depth is 4096, so even under heavy duplicate traffic this won't overflow
### Fix 3: Make COUNT Queries Async
**Impact: Medium** — eliminates blocking wait on lws-main for NIP-45 COUNT
Currently `handle_count_message()` calls `thread_pool_execute_count_sync()` which blocks lws-main waiting for the db-read worker. Convert to the same async pattern used for REQ queries.
**Changes:**
- Create `count_async_state_t` similar to `req_async_state_t`
- Create `count_async_completion_t` with count result
- Add `submit_count_query_async()` that uses `thread_pool_submit_read()` with a callback
- The callback pushes to a count completion queue
- Add `process_count_async_completions()` to drain the queue on lws-main and send COUNT responses
- Call it from the main event loop alongside `process_req_async_completions()`
### Fix 4: Cache Auth Rules Check Result
**Impact: Low-Medium** — eliminates per-REQ SQLite when WoT is enabled
`check_database_auth_rules()` at websockets.c:1706 runs a SQLite query for every REQ when `wot_enabled == 2`. Cache the result per-session.
**Changes:**
- Add `wot_checked` and `wot_allowed` fields to `per_session_data`
- On first REQ, call `check_database_auth_rules()` and cache the result
- On subsequent REQs from the same session, use the cached value
- Reset cache when auth state changes (e.g., after NIP-42 AUTH)
### Fix 5: Move Special-Kind EVENT Store to Event Worker
**Impact: Low** — special kinds are rare but currently do full sync store on lws-main
Kinds 14, 1059, and 23456 are currently excluded from async processing because they have special handling. However, the `store_event()` call within those paths still blocks lws-main.
**Changes:**
- For kind 1059 and kind 14: after special processing completes, submit the store to the event worker instead of calling `store_event()` synchronously
- For kind 23456: admin events are not stored (processed by admin API), so no change needed
- This requires the special processing (decryption, admin check) to still happen on lws-main (it needs `pss` context), but the final DB write goes async
### Fix 6: Reduce Reader Threads from 4 to 1
**Impact: Resource savings** — 3 fewer threads, 3 fewer SQLite connections
**Changes:**
- Change default `reader_threads` in `thread_pool_init()` from 4 to 1
- Or make it configurable via config with default 1
- The single `db-read` thread handles all REQ and COUNT queries sequentially
- If future profiling shows the read thread becoming a bottleneck, increase back to 2+
### Fix 7: Move Subscription Logging Off Main Thread
**Impact: Medium** — eliminates per-REQ INSERT, per-CLOSE UPDATE, per-broadcast UPDATE
The subscription lifecycle functions in subscriptions.c do synchronous SQLite writes on lws-main:
- `db_log_subscription_created()` — INSERT on every REQ
- `db_log_subscription_closed()` — INSERT + UPDATE on every CLOSE
- `db_log_subscription_disconnected()` — UPDATE + INSERT on every disconnect
- `db_update_subscription_events_sent()` — UPDATE on every broadcast match
**Changes:**
- Submit these as fire-and-forget write jobs to the db-write thread via `thread_pool_submit_write()`
- Create a new job type `THREAD_POOL_JOB_SUBSCRIPTION_LOG` or simply use a generic callback job
- The main thread doesn't need to wait for these to complete — they're purely observational logging
### Fix 8: Move Periodic Tasks Off Main Thread
**Impact: Low** — these run every 60s but can still cause latency spikes
- `generate_and_post_status_event()`: Submit as an async job to the event worker
- `ip_ban_cleanup()`/`ip_ban_log_stats()`/`ip_ban_save()`: These do SQLite reads/writes. Move to a periodic job on the db-write thread
- Config cache refresh (Fix 1) replaces the old `refresh_hot_config_if_needed()` SQLite queries
### Fix 9: Cache NIP-11 Response
**Impact: Low-Medium** — eliminates 25+ SQLite SELECTs per NIP-11 HTTP request
`generate_relay_info_json()` in nip011.c calls `get_config_value()` 15 times and `get_config_int()`/`get_config_bool()` 10 times. After Fix 1 (global config cache), these become in-memory lookups. But we can go further:
**Changes:**
- Cache the serialized NIP-11 JSON response string with a TTL (e.g., 60 seconds)
- On NIP-11 request, return the cached string directly without rebuilding
- Invalidate on admin config change events
## Implementation Order
| # | Fix | Risk | Complexity |
|---|-----|------|------------|
| 1 | Global config cache (replaces all get_config_* SQLite) | Low | Medium |
| 2 | Move dedup check to event worker | Low | Low |
| 3 | Async COUNT queries | Low | Medium |
| 4 | Cache WoT auth rules per-session | Very Low | Low |
| 5 | Async store for special kinds | Medium | Medium |
| 6 | Reduce reader threads to 1 | Very Low | Trivial |
| 7 | Move subscription logging off main thread | Low | Low-Medium |
| 8 | Move periodic tasks off main thread | Low | Medium |
| 9 | Cache NIP-11 response | Very Low | Low |
## Expected Impact
After all 9 fixes, `lws-main` should have **zero direct SQLite calls**. Its work becomes:
- JSON parse incoming messages (~fast)
- In-memory subscription matching (~fast)
- In-memory config lookups (~fast, hash map)
- Message formatting and queuing (~fast)
- `lws_service()` I/O (~fast)
- Draining completion queues (~fast)
### Impact by fix
| Fix | SQLite calls eliminated | Frequency |
|-----|------------------------|-----------|
| Fix 1: Global config cache | ~50+ `get_config_*` call sites across 8 files | Per-event, per-connection, per-REQ, per-broadcast, per-NIP-11 |
| Fix 2: Dedup to event worker | 2 `event_id_exists_in_db()` calls | Per-EVENT |
| Fix 3: Async COUNT | 1 `thread_pool_execute_count_sync()` | Per-COUNT message |
| Fix 4: WoT cache per-session | 1 `check_database_auth_rules()` | Per-REQ when WoT enabled |
| Fix 5: Async special-kind store | ~6 `store_event()` calls | Per special-kind EVENT |
| Fix 6: Reduce readers | N/A (resource savings) | N/A |
| Fix 7: Async subscription logging | 4 `db_log_*`/`db_update_*` calls | Per-REQ, per-CLOSE, per-broadcast |
| Fix 8: Async periodic tasks | `generate_stats_json()`, `ip_ban_*` | Every 60s |
| Fix 9: Cache NIP-11 | 25+ config lookups per request | Per NIP-11 HTTP request |
Estimated main-thread CPU reduction: from **54% to ~5-10%** (mostly WebSocket I/O, subscription matching, and JSON parsing).
## Thread Model After Changes
```
lws-main — WebSocket I/O, JSON parse, subscription match, message queue
NO SQLite. Pure compute + I/O.
db-read — All SELECT queries: REQ results, COUNT results, config refresh,
auth rule checks, event dedup checks via completion queue
db-write — All INSERT/UPDATE: event storage, tag inserts, IP ban persistence,
status event generation
event-worker — EVENT validation: signature verification, then submits store to
db-write via completion queue, results flow back to lws-main
```
```mermaid
flowchart LR
CLIENT[Nostr Clients] --> LWS[lws-main<br/>WebSocket I/O<br/>JSON parse<br/>Sub matching<br/>NO SQLite]
LWS -->|EVENT submit| EW[event-worker<br/>Sig verify<br/>Dedup check]
EW -->|store job| DW[db-write<br/>INSERT events<br/>INSERT tags<br/>IP ban save]
EW -->|completion| LWS
DW -->|completion| LWS
LWS -->|REQ/COUNT submit| DR[db-read<br/>SELECT queries<br/>Config refresh<br/>Auth checks]
DR -->|completion| LWS
LWS -->|OK/EVENT/EOSE| 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%.
-91
View File
@@ -1,91 +0,0 @@
# PostgreSQL Nostr Relay Architecture Analysis
This document summarizes common approaches and best practices for building Nostr relays backed by PostgreSQL, based on industry-standard designs and existing open-source relay implementations.
## Identified Projects
* **Nostrss**: High-performance Rust-based relay.
* **Nostrgres**: Focused on PostgreSQL integration with complex event filtering.
* **Relay-rs (Postgres branch)**: General purpose high-throughput relay.
## Detailed Analysis
### 1. Common Schema Patterns
* **Event Storage (JSONB)**: Most PostgreSQL-backed relays store the raw event as a JSONB object in a central `events` table. This provides flexibility for the evolving Nostr spec while allowing efficient extraction of fields.
* **Tag Denormalization**: While the raw event is in JSONB, performant relays extract tags (like `p`, `e`, `t`, `d`) into a separate `tags` table with foreign key relationships to the `events` table. This avoids slow JSONB traversal during complex filter queries.
* **Author/Publisher Tracking**: A dedicated `authors` or `pubkeys` table is typically used to maintain indexing on the `pubkey` field, enabling quick lookups of user activity.
### 2. Performance Optimization
* **GIN Indexes on JSONB**: Crucial for filtering on specific event tags or custom properties stored within the event object. GIN indexes with `jsonb_path_ops` are generally preferred for equality checks.
* **Time-Series Partitioning**: Given the append-only nature of Nostr, partitioning the `events` table by time (e.g., daily or weekly chunks) is highly recommended. This significantly improves query performance for recent data and simplifies data expiration/deletion.
* **Clustering**: Clustering the `events` table by the timestamp index can reduce disk I/O, as it keeps temporally related events physically adjacent on the disk.
### 3. Schema Management Approaches
* **Migrations**: Most mature relays utilize migration tools (like `Flyway`, `Diesel migrations`, or `Golang-migrate`) to version control database schema changes. This is critical for production stability.
* **Auto-generation**: Some lightweight prototypes auto-generate schemas at startup. This is generally discouraged for production due to the risk of destructive changes, data loss, or blocking DDL operations on large tables.
## Recommendations for Building a New Relay
1. **Prioritize Denormalization**: Do not rely solely on JSONB queries for high-traffic filters. Extract indexed tags into dedicated columns or tables.
2. **Use Partitioning from Day One**: Implementing native PostgreSQL partitioning (e.g., using `pg_partman`) is much easier before the dataset grows to millions of rows.
3. **Connection Pooling**: PostgreSQL requires aggressive connection management. Use a high-performance pooler like `PgBouncer` to handle the large number of concurrent, short-lived connections common in WebSocket-based relay traffic.
4. **Asynchronous Writes**: Decouple the WebSocket ingestion thread from the database writer thread to ensure that slow database writes do not impact the relay's responsiveness.
## Schema Definitions
### Relay-rs (Postgres)
```sql
-- Events table
CREATE TABLE "event" (
id bytea NOT NULL,
pub_key bytea NOT NULL,
created_at timestamp with time zone NOT NULL,
kind integer NOT NULL,
"content" bytea NOT NULL,
hidden bit(1) NOT NULL DEFAULT 0::bit(1),
delegated_by bytea NULL,
first_seen timestamp with time zone NOT NULL DEFAULT now(),
expires_at timestamp(0) with time zone,
CONSTRAINT event_pkey PRIMARY KEY (id)
);
CREATE INDEX event_created_at_idx ON "event" (created_at,kind);
CREATE INDEX event_pub_key_idx ON "event" (pub_key);
CREATE INDEX event_delegated_by_idx ON "event" (delegated_by);
CREATE INDEX event_expires_at_idx ON "event" (expires_at);
-- Tags table
CREATE TABLE "tag" (
id int8 NOT NULL GENERATED BY DEFAULT AS IDENTITY,
event_id bytea NOT NULL,
"name" varchar NOT NULL,
value bytea NULL,
value_hex bytea NULL,
CONSTRAINT tag_fk FOREIGN KEY (event_id) REFERENCES "event"(id) ON DELETE CASCADE,
CONSTRAINT unique_constraint_name UNIQUE (event_id, "name", value, value_hex)
);
CREATE INDEX tag_event_id_idx ON tag USING btree (event_id, name);
CREATE INDEX tag_value_idx ON tag USING btree (value);
CREATE INDEX tag_value_hex_idx ON tag USING btree (value_hex);
-- Account table
CREATE TABLE "account" (
pubkey varchar NOT NULL,
is_admitted BOOLEAN NOT NULL DEFAULT FALSE,
balance BIGINT NOT NULL DEFAULT 0,
tos_accepted_at TIMESTAMP,
CONSTRAINT account_pkey PRIMARY KEY (pubkey)
);
-- Invoice table
CREATE TYPE status AS ENUM ('Paid', 'Unpaid', 'Expired');
CREATE TABLE "invoice" (
payment_hash varchar NOT NULL,
pubkey varchar NOT NULL,
invoice varchar NOT NULL,
amount BIGINT NOT NULL,
status status NOT NULL DEFAULT 'Unpaid',
description varchar,
created_at timestamp,
confirmed_at timestamp,
CONSTRAINT invoice_payment_hash PRIMARY KEY (payment_hash),
CONSTRAINT invoice_pubkey_fkey FOREIGN KEY (pubkey) REFERENCES account (pubkey) ON DELETE CASCADE
);
```
+1 -1
View File
@@ -1 +1 @@
484178
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;
}
+314 -72
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
// ================================
@@ -251,14 +261,21 @@ const char* get_config_value(const char* key) {
if (!key) {
return NULL;
}
return get_config_value_from_table(key);
const char* cached = get_config_value_from_table(key);
if (!cached) {
return NULL;
}
// Public API returns an owned string; callers may free it.
return cached;
}
int get_config_int(const char* key, int default_value) {
if (!key) {
return default_value;
}
const char* value = get_config_value_from_table(key);
const char* value = get_config_value(key);
if (!value) {
return default_value;
}
@@ -271,7 +288,7 @@ int get_config_bool(const char* key, int default_value) {
if (!key) {
return default_value;
}
const char* value = get_config_value_from_table(key);
const char* value = get_config_value(key);
if (!value) {
return default_value;
}
@@ -405,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
}
@@ -486,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;
}
@@ -494,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) {
@@ -508,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;
@@ -1606,13 +1661,7 @@ int handle_configuration_event(cJSON* event, char* error_message, size_t error_s
// Note: Config table is now created via embedded schema in sql_schema.h
// Get value from config table
const char* get_config_value_from_table(const char* key) {
if (!db_is_available() || !key) {
return NULL;
}
return db_get_config_value_dup(key);
}
const char* get_config_value_from_table(const char* key);
// Set value in config table
int set_config_value_in_table(const char* key, const char* value, const char* data_type,
@@ -1621,7 +1670,12 @@ int set_config_value_in_table(const char* key, const char* value, const char* da
return -1;
}
return db_set_config_value_full(key, value, data_type, description, category, requires_restart);
int rc = db_set_config_value_full(key, value, data_type, description, category, requires_restart);
if (rc == 0) {
invalidate_config_cache();
}
return rc;
}
// Update config in table (simpler version of set_config_value_in_table)
@@ -1637,7 +1691,12 @@ int update_config_in_table(const char* key, const char* value) {
return -1;
}
return db_update_config_value_only(key, value);
int rc = db_update_config_value_only(key, value);
if (rc == 0) {
invalidate_config_cache();
}
return rc;
}
// DEPRECATED: This function is no longer used in the unified startup flow.
@@ -2067,7 +2126,7 @@ int wot_sync_from_admin_kind3(void) {
DEBUG_INFO("WoT sync: Starting sync from admin's kind 3 event (level %d)", wot_level);
sqlite3_stmt* stmt = NULL;
db_stmt_t* stmt = NULL;
int rc;
int whitelisted_count = 0;
@@ -2079,7 +2138,7 @@ int wot_sync_from_admin_kind3(void) {
"AND e.created_at = (SELECT MAX(created_at) FROM events WHERE kind = 3 AND pubkey = ?)";
rc = db_prepare(query_sql, &stmt);
if (rc != SQLITE_OK) {
if (rc != DB_OK) {
DEBUG_ERROR("WoT sync: Failed to prepare p-tag query: %s", db_last_error());
free((char*)admin_pubkey);
free((char*)relay_pubkey);
@@ -2103,7 +2162,7 @@ int wot_sync_from_admin_kind3(void) {
return -1;
}
while ((rc = db_step_stmt(stmt)) == SQLITE_ROW) {
while ((rc = db_step_stmt(stmt)) == DB_ROW) {
const char* p_tag = (const char*)db_column_text_value(stmt, 0);
if (p_tag) {
// Expand array if needed
@@ -2139,7 +2198,7 @@ int wot_sync_from_admin_kind3(void) {
// Step 2: Begin transaction
rc = db_exec_sql("BEGIN TRANSACTION");
if (rc != SQLITE_OK) {
if (rc != DB_OK) {
DEBUG_ERROR("WoT sync: Failed to begin transaction: %s", db_last_error());
for (int i = 0; i < p_tag_count; i++) {
free(p_tags[i]);
@@ -2170,7 +2229,7 @@ int wot_sync_from_admin_kind3(void) {
"VALUES ('wot_whitelist', 'pubkey', ?, 1)";
rc = db_prepare(insert_sql, &stmt);
if (rc != SQLITE_OK) {
if (rc != DB_OK) {
DEBUG_ERROR("WoT sync: Failed to prepare insert statement: %s", db_last_error());
db_exec_sql("ROLLBACK");
for (int i = 0; i < p_tag_count; i++) {
@@ -2186,7 +2245,7 @@ int wot_sync_from_admin_kind3(void) {
rc = db_step_stmt(stmt);
db_finalize_stmt(stmt);
if (rc != SQLITE_DONE) {
if (rc != DB_DONE) {
DEBUG_ERROR("WoT sync: Failed to insert admin pubkey whitelist: %s", db_last_error());
db_exec_sql("ROLLBACK");
for (int i = 0; i < p_tag_count; i++) {
@@ -2202,7 +2261,7 @@ int wot_sync_from_admin_kind3(void) {
// Step 5: Insert relay pubkey as wot_whitelist (needed for DM responses)
rc = db_prepare(insert_sql, &stmt);
if (rc != SQLITE_OK) {
if (rc != DB_OK) {
DEBUG_ERROR("WoT sync: Failed to prepare insert statement for relay: %s", db_last_error());
db_exec_sql("ROLLBACK");
for (int i = 0; i < p_tag_count; i++) {
@@ -2218,7 +2277,7 @@ int wot_sync_from_admin_kind3(void) {
rc = db_step_stmt(stmt);
db_finalize_stmt(stmt);
if (rc != SQLITE_DONE) {
if (rc != DB_DONE) {
DEBUG_ERROR("WoT sync: Failed to insert relay pubkey whitelist: %s", db_last_error());
db_exec_sql("ROLLBACK");
for (int i = 0; i < p_tag_count; i++) {
@@ -2235,7 +2294,7 @@ int wot_sync_from_admin_kind3(void) {
// Step 6: Insert each p tag from kind 3 event
for (int i = 0; i < p_tag_count; i++) {
rc = db_prepare(insert_sql, &stmt);
if (rc != SQLITE_OK) {
if (rc != DB_OK) {
DEBUG_ERROR("WoT sync: Failed to prepare insert for p_tag %d: %s", i, db_last_error());
db_exec_sql("ROLLBACK");
// Free remaining tags
@@ -2252,7 +2311,7 @@ int wot_sync_from_admin_kind3(void) {
rc = db_step_stmt(stmt);
db_finalize_stmt(stmt);
if (rc != SQLITE_DONE) {
if (rc != DB_DONE) {
DEBUG_ERROR("WoT sync: Failed to insert p_tag %d: %s", i, db_last_error());
db_exec_sql("ROLLBACK");
// Free remaining tags
@@ -2269,7 +2328,7 @@ int wot_sync_from_admin_kind3(void) {
// Step 7: Commit transaction
rc = db_exec_sql("COMMIT");
if (rc != SQLITE_OK) {
if (rc != DB_OK) {
DEBUG_ERROR("WoT sync: Failed to commit transaction: %s", db_last_error());
db_exec_sql("ROLLBACK");
for (int i = 0; i < p_tag_count; i++) {
@@ -2938,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;
}
@@ -2973,9 +3033,9 @@ int handle_auth_query_unified(cJSON* event, const char* query_type, char* error_
}
// Execute query
sqlite3_stmt* stmt;
db_stmt_t* stmt;
int rc = db_prepare(sql, &stmt);
if (rc != SQLITE_OK) {
if (rc != DB_OK) {
snprintf(error_message, error_size, "failed to prepare auth query");
return -1;
}
@@ -2995,7 +3055,7 @@ int handle_auth_query_unified(cJSON* event, const char* query_type, char* error_
int rule_count = 0;
// printf("=== Auth Query Results (%s) ===\n", query_type);
while (db_step_stmt(stmt) == SQLITE_ROW) {
while (db_step_stmt(stmt) == DB_ROW) {
const char* rule_type = (const char*)db_column_text_value(stmt, 0);
const char* pattern_type = (const char*)db_column_text_value(stmt, 1);
const char* pattern_value_result = (const char*)db_column_text_value(stmt, 2);
@@ -3030,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;
}
@@ -3038,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;
}
@@ -3052,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;
}
@@ -3091,9 +3161,9 @@ int handle_config_query_unified(cJSON* event, const char* query_type, char* erro
}
// Execute query
sqlite3_stmt* stmt;
db_stmt_t* stmt;
int rc = db_prepare(sql, &stmt);
if (rc != SQLITE_OK) {
if (rc != DB_OK) {
snprintf(error_message, error_size, "failed to prepare config query");
return -1;
}
@@ -3112,7 +3182,7 @@ int handle_config_query_unified(cJSON* event, const char* query_type, char* erro
int config_count = 0;
while (db_step_stmt(stmt) == SQLITE_ROW) {
while (db_step_stmt(stmt) == DB_ROW) {
const char* key = (const char*)db_column_text_value(stmt, 0);
const char* value = (const char*)db_column_text_value(stmt, 1);
const char* data_type = (const char*)db_column_text_value(stmt, 2);
@@ -3145,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;
}
@@ -3153,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;
}
@@ -3661,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;
}
@@ -3680,9 +3760,9 @@ int handle_stats_query_unified(cJSON* event, char* error_message, size_t error_s
cJSON_AddNumberToObject(response, "database_size_bytes", db_size);
// Query total events count
sqlite3_stmt* stmt;
if (db_prepare("SELECT COUNT(*) FROM events", &stmt) == SQLITE_OK) {
if (db_step_stmt(stmt) == SQLITE_ROW) {
db_stmt_t* stmt;
if (db_prepare("SELECT COUNT(*) FROM events", &stmt) == DB_OK) {
if (db_step_stmt(stmt) == DB_ROW) {
cJSON_AddNumberToObject(response, "total_events", db_column_int64_value(stmt, 0));
}
db_finalize_stmt(stmt);
@@ -3690,8 +3770,8 @@ int handle_stats_query_unified(cJSON* event, char* error_message, size_t error_s
// Query event kinds distribution
cJSON* event_kinds = cJSON_CreateArray();
if (db_prepare("SELECT kind, count, percentage FROM event_kinds_view ORDER BY count DESC", &stmt) == SQLITE_OK) {
while (db_step_stmt(stmt) == SQLITE_ROW) {
if (db_prepare("SELECT kind, count, percentage FROM event_kinds_view ORDER BY count DESC", &stmt) == DB_OK) {
while (db_step_stmt(stmt) == DB_ROW) {
cJSON* kind_obj = cJSON_CreateObject();
cJSON_AddNumberToObject(kind_obj, "kind", db_column_int_value(stmt, 0));
cJSON_AddNumberToObject(kind_obj, "count", db_column_int64_value(stmt, 1));
@@ -3704,10 +3784,10 @@ int handle_stats_query_unified(cJSON* event, char* error_message, size_t error_s
// Query time-based statistics
cJSON* time_stats = cJSON_CreateObject();
if (db_prepare("SELECT period, total_events FROM time_stats_view", &stmt) == SQLITE_OK) {
while (db_step_stmt(stmt) == SQLITE_ROW) {
if (db_prepare("SELECT period, total_events FROM time_stats_view", &stmt) == DB_OK) {
while (db_step_stmt(stmt) == DB_ROW) {
const char* period = (const char*)db_column_text_value(stmt, 0);
sqlite3_int64 count = db_column_int64_value(stmt, 1);
long long count = db_column_int64_value(stmt, 1);
if (strcmp(period, "total") == 0) {
cJSON_AddNumberToObject(time_stats, "total", count);
@@ -3725,8 +3805,8 @@ int handle_stats_query_unified(cJSON* event, char* error_message, size_t error_s
// Query top pubkeys
cJSON* top_pubkeys = cJSON_CreateArray();
if (db_prepare("SELECT pubkey, event_count, percentage FROM top_pubkeys_view ORDER BY event_count DESC LIMIT 10", &stmt) == SQLITE_OK) {
while (db_step_stmt(stmt) == SQLITE_ROW) {
if (db_prepare("SELECT pubkey, event_count, percentage FROM top_pubkeys_view ORDER BY event_count DESC LIMIT 10", &stmt) == DB_OK) {
while (db_step_stmt(stmt) == DB_ROW) {
cJSON* pubkey_obj = cJSON_CreateObject();
const char* pubkey = (const char*)db_column_text_value(stmt, 0);
cJSON_AddStringToObject(pubkey_obj, "pubkey", pubkey ? pubkey : "");
@@ -3739,9 +3819,9 @@ int handle_stats_query_unified(cJSON* event, char* error_message, size_t error_s
cJSON_AddItemToObject(response, "top_pubkeys", top_pubkeys);
// Get database creation timestamp (oldest event)
if (db_prepare("SELECT MIN(created_at) FROM events", &stmt) == SQLITE_OK) {
if (db_step_stmt(stmt) == SQLITE_ROW) {
sqlite3_int64 oldest_timestamp = db_column_int64_value(stmt, 0);
if (db_prepare("SELECT MIN(created_at) FROM events", &stmt) == DB_OK) {
if (db_step_stmt(stmt) == DB_ROW) {
long long oldest_timestamp = db_column_int64_value(stmt, 0);
if (oldest_timestamp > 0) {
cJSON_AddNumberToObject(response, "database_created_at", (double)oldest_timestamp);
}
@@ -3750,9 +3830,9 @@ int handle_stats_query_unified(cJSON* event, char* error_message, size_t error_s
}
// Get latest event timestamp
if (db_prepare("SELECT MAX(created_at) FROM events", &stmt) == SQLITE_OK) {
if (db_step_stmt(stmt) == SQLITE_ROW) {
sqlite3_int64 latest_timestamp = db_column_int64_value(stmt, 0);
if (db_prepare("SELECT MAX(created_at) FROM events", &stmt) == DB_OK) {
if (db_step_stmt(stmt) == DB_ROW) {
long long latest_timestamp = db_column_int64_value(stmt, 0);
if (latest_timestamp > 0) {
cJSON_AddNumberToObject(response, "latest_event_at", (double)latest_timestamp);
}
@@ -3767,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;
}
@@ -3774,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;
}
@@ -3884,7 +3973,7 @@ int handle_config_update_unified(cJSON* event, char* error_message, size_t error
// Begin transaction for atomic config updates
int rc = db_exec_sql("BEGIN IMMEDIATE TRANSACTION");
if (rc != SQLITE_OK) {
if (rc != DB_OK) {
cJSON_Delete(config_objects_array);
snprintf(error_message, error_size, "failed to begin config update transaction");
return -1;
@@ -3960,10 +4049,10 @@ int handle_config_update_unified(cJSON* event, char* error_message, size_t error
// Check if the config key exists in the table
const char* check_sql = "SELECT COUNT(*) FROM config WHERE key = ?";
sqlite3_stmt* check_stmt;
db_stmt_t* check_stmt;
int check_rc = db_prepare(check_sql, &check_stmt);
if (check_rc != SQLITE_OK) {
if (check_rc != DB_OK) {
DEBUG_ERROR("Failed to prepare config existence check");
validation_errors++;
continue;
@@ -3972,7 +4061,7 @@ int handle_config_update_unified(cJSON* event, char* error_message, size_t error
db_bind_text_param(check_stmt, 1, key);
int config_exists = 0;
if (db_step_stmt(check_stmt) == SQLITE_ROW) {
if (db_step_stmt(check_stmt) == DB_ROW) {
config_exists = db_column_int_value(check_stmt, 0) > 0;
}
db_finalize_stmt(check_stmt);
@@ -3996,12 +4085,12 @@ int handle_config_update_unified(cJSON* event, char* error_message, size_t error
// Check if this config requires restart
const char* requires_restart_sql = "SELECT requires_restart FROM config WHERE key = ?";
sqlite3_stmt* restart_stmt;
db_stmt_t* restart_stmt;
int requires_restart = 0;
if (db_prepare(requires_restart_sql, &restart_stmt) == SQLITE_OK) {
if (db_prepare(requires_restart_sql, &restart_stmt) == DB_OK) {
db_bind_text_param(restart_stmt, 1, key);
if (db_step_stmt(restart_stmt) == SQLITE_ROW) {
if (db_step_stmt(restart_stmt) == DB_ROW) {
requires_restart = db_column_int_value(restart_stmt, 0);
}
db_finalize_stmt(restart_stmt);
@@ -4295,10 +4384,10 @@ int populate_all_config_values_atomic(const char* admin_pubkey, const char* rela
}
// Prepare INSERT OR REPLACE statement with all required fields
sqlite3_stmt* stmt = NULL;
db_stmt_t* stmt = NULL;
const char* sql = "INSERT OR REPLACE INTO config (key, value, data_type, description, category, requires_restart) VALUES (?, ?, ?, ?, ?, ?)";
rc = db_prepare(sql, &stmt);
if (rc != SQLITE_OK) {
if (rc != DB_OK) {
DEBUG_ERROR("Failed to prepare statement: %s", db_last_error());
db_exec_sql("ROLLBACK;");
return -1;
@@ -4378,7 +4467,7 @@ int populate_all_config_values_atomic(const char* admin_pubkey, const char* rela
db_bind_int_param(stmt, 6, requires_restart);
rc = db_step_stmt(stmt);
if (rc != SQLITE_DONE) {
if (rc != DB_DONE) {
DEBUG_ERROR("Failed to insert config key '%s': %s",
key, db_last_error());
db_finalize_stmt(stmt);
@@ -4396,7 +4485,7 @@ int populate_all_config_values_atomic(const char* admin_pubkey, const char* rela
db_bind_text_param(stmt, 5, "authentication");
db_bind_int_param(stmt, 6, 0); // does not require restart
rc = db_step_stmt(stmt);
if (rc != SQLITE_DONE) {
if (rc != DB_DONE) {
DEBUG_ERROR("Failed to insert admin_pubkey: %s", db_last_error());
db_finalize_stmt(stmt);
db_exec_sql("ROLLBACK;");
@@ -4412,7 +4501,7 @@ int populate_all_config_values_atomic(const char* admin_pubkey, const char* rela
db_bind_text_param(stmt, 5, "relay");
db_bind_int_param(stmt, 6, 0); // does not require restart
rc = db_step_stmt(stmt);
if (rc != SQLITE_DONE) {
if (rc != DB_DONE) {
DEBUG_ERROR("Failed to insert relay_pubkey: %s", db_last_error());
db_finalize_stmt(stmt);
db_exec_sql("ROLLBACK;");
@@ -4429,7 +4518,7 @@ int populate_all_config_values_atomic(const char* admin_pubkey, const char* rela
db_bind_text_param(stmt, 5, "monitoring");
db_bind_int_param(stmt, 6, 0); // does not require restart
rc = db_step_stmt(stmt);
if (rc != SQLITE_DONE) {
if (rc != DB_DONE) {
DEBUG_ERROR("Failed to insert kind_24567_reporting_throttle_sec: %s", db_last_error());
db_finalize_stmt(stmt);
db_exec_sql("ROLLBACK;");
@@ -4454,6 +4543,159 @@ int populate_all_config_values_atomic(const char* admin_pubkey, const char* rela
// CONFIGURATION CACHE MANAGEMENT
// ================================
#define CONFIG_CACHE_MAX_ITEMS 256
typedef struct {
char key[128];
char value[CONFIG_VALUE_MAX_LENGTH + 1];
int in_use;
} config_cache_entry_t;
static config_cache_entry_t g_config_cache[CONFIG_CACHE_MAX_ITEMS];
static pthread_mutex_t g_config_cache_mutex = PTHREAD_MUTEX_INITIALIZER;
static int find_cache_index_by_key(const char* key) {
if (!key) {
return -1;
}
for (int i = 0; i < CONFIG_CACHE_MAX_ITEMS; i++) {
if (g_config_cache[i].in_use && strcmp(g_config_cache[i].key, key) == 0) {
return i;
}
}
return -1;
}
static int find_empty_cache_index(void) {
for (int i = 0; i < CONFIG_CACHE_MAX_ITEMS; i++) {
if (!g_config_cache[i].in_use) {
return i;
}
}
return -1;
}
static void clear_config_cache_locked(void) {
memset(g_config_cache, 0, sizeof(g_config_cache));
}
void invalidate_config_cache(void) {
pthread_mutex_lock(&g_config_cache_mutex);
clear_config_cache_locked();
pthread_mutex_unlock(&g_config_cache_mutex);
}
void force_config_cache_refresh(void) {
invalidate_config_cache();
}
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 (!key) {
return NULL;
}
pthread_mutex_lock(&g_config_cache_mutex);
int idx = find_cache_index_by_key(key);
if (idx >= 0) {
const char* result = strdup(g_config_cache[idx].value);
pthread_mutex_unlock(&g_config_cache_mutex);
return result;
}
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;
}
pthread_mutex_lock(&g_config_cache_mutex);
idx = find_cache_index_by_key(key);
if (idx < 0) {
idx = find_empty_cache_index();
}
if (idx >= 0) {
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, db_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);
return db_value;
}
const char* get_admin_pubkey_cached(void) {
return get_config_value_from_table("admin_pubkey");
}
const char* get_relay_pubkey_cached(void) {
return get_config_value_from_table("relay_pubkey");
}
// ================================
// HYBRID CONFIG ACCESS FUNCTIONS
@@ -4483,15 +4725,15 @@ int is_config_table_ready(void) {
if (!db_is_available()) return 0;
const char* sql = "SELECT COUNT(*) FROM sqlite_master WHERE type='table' AND name='config'";
sqlite3_stmt* stmt;
db_stmt_t* stmt;
int rc = db_prepare(sql, &stmt);
if (rc != SQLITE_OK) {
if (rc != DB_OK) {
return 0;
}
int table_exists = 0;
if (db_step_stmt(stmt) == SQLITE_ROW) {
if (db_step_stmt(stmt) == DB_ROW) {
table_exists = db_column_int_value(stmt, 0) > 0;
}
db_finalize_stmt(stmt);
@@ -4503,12 +4745,12 @@ int is_config_table_ready(void) {
// Check if table has configuration data
const char* count_sql = "SELECT COUNT(*) FROM config";
rc = db_prepare(count_sql, &stmt);
if (rc != SQLITE_OK) {
if (rc != DB_OK) {
return 0;
}
int config_count = 0;
if (db_step_stmt(stmt) == SQLITE_ROW) {
if (db_step_stmt(stmt) == DB_ROW) {
config_count = db_column_int_value(stmt, 0);
}
db_finalize_stmt(stmt);
@@ -4731,7 +4973,7 @@ int process_startup_config_event(const cJSON* event) {
// Begin transaction for atomic config updates
int rc = db_exec_sql("BEGIN IMMEDIATE TRANSACTION");
if (rc != SQLITE_OK) {
if (rc != DB_OK) {
DEBUG_ERROR("Failed to begin startup config transaction");
return -1;
}
@@ -4867,10 +5109,10 @@ cJSON* generate_config_event_from_table(void) {
// Query all configuration values from the config table
const char* sql = "SELECT key, value FROM config ORDER BY key";
sqlite3_stmt* stmt;
db_stmt_t* stmt;
int rc = db_prepare(sql, &stmt);
if (rc != SQLITE_OK) {
if (rc != DB_OK) {
DEBUG_ERROR("Failed to prepare config query for event generation");
cJSON_Delete(tags);
cJSON_Delete(event);
@@ -4880,7 +5122,7 @@ cJSON* generate_config_event_from_table(void) {
int config_items_added = 0;
// Add each config item as a tag
while (db_step_stmt(stmt) == SQLITE_ROW) {
while (db_step_stmt(stmt) == DB_ROW) {
const char* key = (const char*)db_column_text_value(stmt, 0);
const char* value = (const char*)db_column_text_value(stmt, 1);
+1 -1
View File
@@ -1,7 +1,6 @@
#ifndef CONFIG_H
#define CONFIG_H
#include <sqlite3.h>
#include <cjson/cJSON.h>
#include <time.h>
#include <pthread.h>
@@ -78,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);
+316 -128
View File
@@ -4,6 +4,7 @@
#include "debug.h"
#include "config.h"
#include <sqlite3.h>
#include <stdio.h>
#include <stdlib.h>
#include <string.h>
@@ -12,87 +13,173 @@
extern sqlite3* g_db;
extern char g_database_path[512];
int db_is_available(void) {
return g_db != NULL;
// Optional per-thread connection override (used by thread pool workers)
static __thread sqlite3* g_thread_db = NULL;
static sqlite3* db_active_connection(void) {
return g_thread_db ? g_thread_db : g_db;
}
sqlite3* db_get_handle(void) {
return g_db;
typedef struct db_stmt {
sqlite3_stmt* stmt;
} db_stmt_t;
int db_init(const char* connection_string) {
if (!connection_string || connection_string[0] == '\0') return DB_MISUSE;
if (g_db) return DB_OK;
int rc = sqlite3_open(connection_string, &g_db);
if (rc != SQLITE_OK) {
if (g_db) {
sqlite3_close(g_db);
g_db = NULL;
}
return DB_ERROR;
}
strncpy(g_database_path, connection_string, sizeof(g_database_path) - 1);
g_database_path[sizeof(g_database_path) - 1] = '\0';
return DB_OK;
}
void db_close(void) {
if (!g_db) return;
sqlite3_close(g_db);
g_db = NULL;
}
int db_set_thread_connection(void* connection) {
g_thread_db = (sqlite3*)connection;
return DB_OK;
}
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;
}
const char* db_last_error(void) {
if (!g_db) return "database not available";
return sqlite3_errmsg(g_db);
sqlite3* db = db_active_connection();
if (!db) return "database not available";
return sqlite3_errmsg(db);
}
const char* db_get_database_path(void) {
return g_database_path;
}
int db_prepare(const char* sql, sqlite3_stmt** out_stmt) {
if (!g_db || !sql || !out_stmt) return SQLITE_MISUSE;
return sqlite3_prepare_v2(g_db, sql, -1, out_stmt, NULL);
int db_prepare(const char* sql, db_stmt_t** out_stmt) {
sqlite3* db = db_active_connection();
if (!db || !sql || !out_stmt) return DB_MISUSE;
sqlite3_stmt* raw_stmt = NULL;
int rc = sqlite3_prepare_v2(db, sql, -1, &raw_stmt, NULL);
if (rc != SQLITE_OK) return rc;
db_stmt_t* wrapper = (db_stmt_t*)malloc(sizeof(db_stmt_t));
if (!wrapper) {
sqlite3_finalize(raw_stmt);
return DB_ERROR;
}
wrapper->stmt = raw_stmt;
*out_stmt = wrapper;
return DB_OK;
}
int db_bind_text_param(sqlite3_stmt* stmt, int index, const char* value) {
if (!stmt) return SQLITE_MISUSE;
return sqlite3_bind_text(stmt, index, value ? value : "", -1, SQLITE_TRANSIENT);
int db_bind_text_param(db_stmt_t* stmt, int index, const char* value) {
if (!stmt || !stmt->stmt) return DB_MISUSE;
return sqlite3_bind_text(stmt->stmt, index, value ? value : "", -1, SQLITE_TRANSIENT);
}
int db_bind_int_param(sqlite3_stmt* stmt, int index, int value) {
if (!stmt) return SQLITE_MISUSE;
return sqlite3_bind_int(stmt, index, value);
int db_bind_int_param(db_stmt_t* stmt, int index, int value) {
if (!stmt || !stmt->stmt) return DB_MISUSE;
return sqlite3_bind_int(stmt->stmt, index, value);
}
int db_bind_int64_param(sqlite3_stmt* stmt, int index, long long value) {
if (!stmt) return SQLITE_MISUSE;
return sqlite3_bind_int64(stmt, index, (sqlite3_int64)value);
int db_bind_int64_param(db_stmt_t* stmt, int index, long long value) {
if (!stmt || !stmt->stmt) return DB_MISUSE;
return sqlite3_bind_int64(stmt->stmt, index, (sqlite3_int64)value);
}
int db_step_stmt(sqlite3_stmt* stmt) {
if (!stmt) return SQLITE_MISUSE;
return sqlite3_step(stmt);
int db_step_stmt(db_stmt_t* stmt) {
if (!stmt || !stmt->stmt) return DB_MISUSE;
return sqlite3_step(stmt->stmt);
}
int db_reset_stmt(sqlite3_stmt* stmt) {
if (!stmt) return SQLITE_MISUSE;
return sqlite3_reset(stmt);
int db_reset_stmt(db_stmt_t* stmt) {
if (!stmt || !stmt->stmt) return DB_MISUSE;
return sqlite3_reset(stmt->stmt);
}
const char* db_column_text_value(sqlite3_stmt* stmt, int col) {
if (!stmt) return NULL;
return (const char*)sqlite3_column_text(stmt, col);
const char* db_column_text_value(db_stmt_t* stmt, int col) {
if (!stmt || !stmt->stmt) return NULL;
return (const char*)sqlite3_column_text(stmt->stmt, col);
}
int db_column_int_value(sqlite3_stmt* stmt, int col) {
if (!stmt) return 0;
return sqlite3_column_int(stmt, col);
int db_column_int_value(db_stmt_t* stmt, int col) {
if (!stmt || !stmt->stmt) return 0;
return sqlite3_column_int(stmt->stmt, col);
}
long long db_column_int64_value(sqlite3_stmt* stmt, int col) {
if (!stmt) return 0;
return (long long)sqlite3_column_int64(stmt, col);
long long db_column_int64_value(db_stmt_t* stmt, int col) {
if (!stmt || !stmt->stmt) return 0;
return (long long)sqlite3_column_int64(stmt->stmt, col);
}
double db_column_double_value(sqlite3_stmt* stmt, int col) {
if (!stmt) return 0.0;
return sqlite3_column_double(stmt, col);
double db_column_double_value(db_stmt_t* stmt, int col) {
if (!stmt || !stmt->stmt) return 0.0;
return sqlite3_column_double(stmt->stmt, col);
}
void db_finalize_stmt(sqlite3_stmt* stmt) {
if (stmt) sqlite3_finalize(stmt);
void db_finalize_stmt(db_stmt_t* stmt) {
if (!stmt) return;
if (stmt->stmt) sqlite3_finalize(stmt->stmt);
free(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);
@@ -105,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);
@@ -124,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);
@@ -133,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 "
@@ -141,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;
@@ -154,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);
@@ -165,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 "
@@ -173,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);
@@ -184,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 "
@@ -192,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);
@@ -225,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;
@@ -245,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 <= ? "
@@ -256,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;
}
@@ -268,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;
@@ -356,10 +450,11 @@ int db_count_active_whitelist_rules(void) {
}
int db_count_with_sql(const char* sql, const char** bind_params, int bind_param_count, int* out_count) {
if (!g_db || !sql || !out_count) return -1;
sqlite3* db = db_active_connection();
if (!db || !sql || !out_count) return -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;
}
@@ -381,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;
}
@@ -501,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);
@@ -528,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) {
@@ -545,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);
@@ -564,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) {
@@ -602,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);
@@ -636,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 =
@@ -644,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) {
@@ -684,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) {
@@ -716,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);
@@ -736,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;
}
@@ -760,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;
}
@@ -777,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;
}
@@ -796,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);
@@ -810,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) {
@@ -827,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");
@@ -845,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;
}
@@ -866,11 +975,57 @@ int db_store_config_event(const cJSON* event) {
return (rc == SQLITE_DONE) ? 0 : -1;
}
static int db_insert_event_tags_json_with_db(sqlite3* db, const char* event_id, const char* tags_json) {
if (!db || !event_id || !tags_json) {
return -1;
}
cJSON* tags = cJSON_Parse(tags_json);
if (!tags || !cJSON_IsArray(tags)) {
if (tags) cJSON_Delete(tags);
return 0;
}
const char* sql = "INSERT INTO event_tags (event_id, tag_name, tag_value, tag_index) VALUES (?, ?, ?, ?)";
sqlite3_stmt* stmt = NULL;
if (sqlite3_prepare_v2(db, sql, -1, &stmt, NULL) != SQLITE_OK) {
cJSON_Delete(tags);
return -1;
}
int tag_index = 0;
cJSON* tag = NULL;
cJSON_ArrayForEach(tag, tags) {
if (cJSON_IsArray(tag) && cJSON_GetArraySize(tag) >= 2) {
cJSON* name = cJSON_GetArrayItem(tag, 0);
cJSON* value = cJSON_GetArrayItem(tag, 1);
if (cJSON_IsString(name) && cJSON_IsString(value)) {
sqlite3_reset(stmt);
sqlite3_clear_bindings(stmt);
sqlite3_bind_text(stmt, 1, event_id, -1, SQLITE_STATIC);
sqlite3_bind_text(stmt, 2, cJSON_GetStringValue(name), -1, SQLITE_STATIC);
sqlite3_bind_text(stmt, 3, cJSON_GetStringValue(value), -1, SQLITE_STATIC);
sqlite3_bind_int(stmt, 4, tag_index);
if (sqlite3_step(stmt) != SQLITE_DONE) {
DEBUG_WARN("Failed to insert event tag for %s: %s", event_id, sqlite3_errmsg(db));
}
}
}
tag_index++;
}
sqlite3_finalize(stmt);
cJSON_Delete(tags);
return 0;
}
int db_insert_event_with_json(const char* id, const char* pubkey, long long created_at,
int kind, const char* event_type, const char* content,
const char* sig, const char* tags_json, const char* event_json,
int* out_step_rc, int* out_extended_errcode) {
if (!g_db || !id || !pubkey || !event_type || !content || !sig || !tags_json || !event_json) {
sqlite3* db = db_active_connection();
if (!db || !id || !pubkey || !event_type || !content || !sig || !tags_json || !event_json) {
return -1;
}
@@ -879,7 +1034,7 @@ int db_insert_event_with_json(const char* id, const char* pubkey, long long crea
"VALUES (?, ?, ?, ?, ?, ?, ?, ?, ?)";
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;
}
@@ -894,9 +1049,13 @@ int db_insert_event_with_json(const char* id, const char* pubkey, long long crea
sqlite3_bind_text(stmt, 9, event_json, -1, SQLITE_TRANSIENT);
int step_rc = sqlite3_step(stmt);
int extended_errcode = sqlite3_extended_errcode(g_db);
int extended_errcode = sqlite3_extended_errcode(db);
sqlite3_finalize(stmt);
if (step_rc == SQLITE_DONE) {
(void)db_insert_event_tags_json_with_db(db, id, tags_json);
}
if (out_step_rc) {
*out_step_rc = step_rc;
}
@@ -908,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;
}
@@ -930,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;
@@ -945,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;
}
@@ -987,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;
}
@@ -1010,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;
}
@@ -1029,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;
}
@@ -1057,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));
}
}
}
@@ -1069,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;
}
@@ -1087,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) {
@@ -1108,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);
@@ -1131,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);
@@ -1147,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;
@@ -1166,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;
}
@@ -1181,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;
}
@@ -1202,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;
}
+33 -13
View File
@@ -3,27 +3,45 @@
#include <stddef.h>
#include <time.h>
#include <sqlite3.h>
#include <cjson/cJSON.h>
// Generic helpers
int db_init(const char* connection_string);
void db_close(void);
int db_is_available(void);
sqlite3* db_get_handle(void);
const char* db_last_error(void);
const char* db_get_database_path(void);
// Per-thread connection override (used by thread pool workers)
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
#define DB_CONSTRAINT 19
#define DB_MISUSE 21
#define DB_ROW 100
#define DB_DONE 101
typedef struct db_stmt db_stmt_t;
// Generic statement helpers (Phase 1 migration)
int db_prepare(const char* sql, sqlite3_stmt** out_stmt);
int db_bind_text_param(sqlite3_stmt* stmt, int index, const char* value);
int db_bind_int_param(sqlite3_stmt* stmt, int index, int value);
int db_bind_int64_param(sqlite3_stmt* stmt, int index, long long value);
int db_step_stmt(sqlite3_stmt* stmt);
int db_reset_stmt(sqlite3_stmt* stmt);
const char* db_column_text_value(sqlite3_stmt* stmt, int col);
int db_column_int_value(sqlite3_stmt* stmt, int col);
long long db_column_int64_value(sqlite3_stmt* stmt, int col);
double db_column_double_value(sqlite3_stmt* stmt, int col);
void db_finalize_stmt(sqlite3_stmt* stmt);
int db_prepare(const char* sql, db_stmt_t** out_stmt);
int db_bind_text_param(db_stmt_t* stmt, int index, const char* value);
int db_bind_int_param(db_stmt_t* stmt, int index, int value);
int db_bind_int64_param(db_stmt_t* stmt, int index, long long value);
int db_step_stmt(db_stmt_t* stmt);
int db_reset_stmt(db_stmt_t* stmt);
const char* db_column_text_value(db_stmt_t* stmt, int col);
int db_column_int_value(db_stmt_t* stmt, int col);
long long db_column_int64_value(db_stmt_t* stmt, int col);
double db_column_double_value(db_stmt_t* stmt, int col);
void db_finalize_stmt(db_stmt_t* stmt);
// Subscription logging
int db_log_subscription_created(const char* sub_id, const char* wsi_ptr,
@@ -96,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
+72 -54
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>
@@ -102,8 +103,7 @@ void ip_ban_init(void) {
}
void ip_ban_load_from_db(void) {
sqlite3* db = db_get_handle();
if (!db || !g_initialized) return;
if (!db_is_available() || !g_initialized) return;
// Create table if it doesn't exist (handles existing databases)
const char* create_sql =
@@ -121,18 +121,16 @@ void ip_ban_load_from_db(void) {
" first_seen INTEGER NOT NULL DEFAULT 0,"
" updated_at INTEGER NOT NULL DEFAULT (strftime('%s', 'now'))"
");";
char* err = NULL;
if (sqlite3_exec(db, create_sql, NULL, NULL, &err) != SQLITE_OK) {
DEBUG_ERROR("Failed to create ip_bans table: %s", err ? err : "unknown");
if (err) sqlite3_free(err);
if (db_exec_sql(create_sql) != 0) {
DEBUG_ERROR("Failed to create ip_bans table: %s", db_last_error());
return;
}
// Migration: Add idle_* columns if they don't exist (ignore errors if already exists)
sqlite3_exec(db, "ALTER TABLE ip_bans ADD COLUMN idle_failure_count INTEGER NOT NULL DEFAULT 0", NULL, NULL, NULL);
sqlite3_exec(db, "ALTER TABLE ip_bans ADD COLUMN idle_ban_count INTEGER NOT NULL DEFAULT 0", NULL, NULL, NULL);
sqlite3_exec(db, "ALTER TABLE ip_bans ADD COLUMN idle_banned_until INTEGER NOT NULL DEFAULT 0", NULL, NULL, NULL);
sqlite3_exec(db, "ALTER TABLE ip_bans ADD COLUMN idle_first_failure INTEGER NOT NULL DEFAULT 0", NULL, NULL, NULL);
(void)db_exec_sql("ALTER TABLE ip_bans ADD COLUMN idle_failure_count INTEGER NOT NULL DEFAULT 0");
(void)db_exec_sql("ALTER TABLE ip_bans ADD COLUMN idle_ban_count INTEGER NOT NULL DEFAULT 0");
(void)db_exec_sql("ALTER TABLE ip_bans ADD COLUMN idle_banned_until INTEGER NOT NULL DEFAULT 0");
(void)db_exec_sql("ALTER TABLE ip_bans ADD COLUMN idle_first_failure INTEGER NOT NULL DEFAULT 0");
const char* sql =
"SELECT ip, failure_count, ban_count, banned_until, first_failure,"
@@ -141,16 +139,16 @@ void ip_ban_load_from_db(void) {
" idle_failure_count, idle_ban_count, idle_banned_until, idle_first_failure"
" FROM ip_bans";
sqlite3_stmt* stmt;
if (sqlite3_prepare_v2(db, sql, -1, &stmt, NULL) != SQLITE_OK) {
db_stmt_t* stmt;
if (db_prepare(sql, &stmt) != DB_OK) {
DEBUG_ERROR("Failed to prepare ip_bans load query");
return;
}
int loaded = 0;
pthread_mutex_lock(&g_ban_mutex);
while (sqlite3_step(stmt) == SQLITE_ROW) {
const char* ip = (const char*)sqlite3_column_text(stmt, 0);
while (db_step_stmt(stmt) == DB_ROW) {
const char* ip = db_column_text_value(stmt, 0);
if (!ip) continue;
int idx = find_slot(ip);
@@ -160,25 +158,25 @@ void ip_ban_load_from_db(void) {
entry->state = IP_BAN_ACTIVE;
strncpy(entry->ip, ip, sizeof(entry->ip) - 1);
entry->ip[sizeof(entry->ip) - 1] = '\0';
entry->failure_count = sqlite3_column_int(stmt, 1);
entry->ban_count = sqlite3_column_int(stmt, 2);
entry->banned_until = (time_t)sqlite3_column_int64(stmt, 3);
entry->first_failure = (time_t)sqlite3_column_int64(stmt, 4);
entry->has_authed_successfully = sqlite3_column_int(stmt, 5);
entry->last_success_at = (time_t)sqlite3_column_int64(stmt, 6);
entry->total_connections = sqlite3_column_int(stmt, 7);
entry->total_failures = sqlite3_column_int(stmt, 8);
entry->total_successes = sqlite3_column_int(stmt, 9);
entry->first_seen = (time_t)sqlite3_column_int64(stmt, 10);
entry->failure_count = db_column_int_value(stmt, 1);
entry->ban_count = db_column_int_value(stmt, 2);
entry->banned_until = (time_t)db_column_int64_value(stmt, 3);
entry->first_failure = (time_t)db_column_int64_value(stmt, 4);
entry->has_authed_successfully = db_column_int_value(stmt, 5);
entry->last_success_at = (time_t)db_column_int64_value(stmt, 6);
entry->total_connections = db_column_int_value(stmt, 7);
entry->total_failures = db_column_int_value(stmt, 8);
entry->total_successes = db_column_int_value(stmt, 9);
entry->first_seen = (time_t)db_column_int64_value(stmt, 10);
// Load idle tracking fields (default to 0 if columns don't exist yet)
entry->idle_failure_count = sqlite3_column_int(stmt, 11);
entry->idle_ban_count = sqlite3_column_int(stmt, 12);
entry->idle_banned_until = (time_t)sqlite3_column_int64(stmt, 13);
entry->idle_first_failure = (time_t)sqlite3_column_int64(stmt, 14);
entry->idle_failure_count = db_column_int_value(stmt, 11);
entry->idle_ban_count = db_column_int_value(stmt, 12);
entry->idle_banned_until = (time_t)db_column_int64_value(stmt, 13);
entry->idle_first_failure = (time_t)db_column_int64_value(stmt, 14);
loaded++;
}
pthread_mutex_unlock(&g_ban_mutex);
sqlite3_finalize(stmt);
db_finalize_stmt(stmt);
// Count how many are still actively banned
time_t now = time(NULL);
@@ -196,8 +194,7 @@ void ip_ban_load_from_db(void) {
}
void ip_ban_save_to_db(void) {
sqlite3* db = db_get_handle();
if (!db || !g_initialized) return;
if (!db_is_available() || !g_initialized) return;
const char* upsert_sql =
"INSERT OR REPLACE INTO ip_bans"
@@ -207,13 +204,13 @@ void ip_ban_save_to_db(void) {
" idle_failure_count, idle_ban_count, idle_banned_until, idle_first_failure)"
" VALUES (?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, strftime('%s', 'now'), ?, ?, ?, ?)";
sqlite3_stmt* stmt;
if (sqlite3_prepare_v2(db, upsert_sql, -1, &stmt, NULL) != SQLITE_OK) {
db_stmt_t* stmt;
if (db_prepare(upsert_sql, &stmt) != DB_OK) {
DEBUG_ERROR("Failed to prepare ip_bans save query");
return;
}
sqlite3_exec(db, "BEGIN TRANSACTION", NULL, NULL, NULL);
(void)db_exec_sql("BEGIN TRANSACTION");
int saved = 0;
pthread_mutex_lock(&g_ban_mutex);
@@ -221,25 +218,25 @@ void ip_ban_save_to_db(void) {
ip_ban_entry_t* entry = &g_ban_table[i];
if (entry->state != IP_BAN_ACTIVE) continue;
sqlite3_reset(stmt);
sqlite3_bind_text(stmt, 1, entry->ip, -1, SQLITE_STATIC);
sqlite3_bind_int(stmt, 2, entry->failure_count);
sqlite3_bind_int(stmt, 3, entry->ban_count);
sqlite3_bind_int64(stmt, 4, (sqlite3_int64)entry->banned_until);
sqlite3_bind_int64(stmt, 5, (sqlite3_int64)entry->first_failure);
sqlite3_bind_int(stmt, 6, entry->has_authed_successfully);
sqlite3_bind_int64(stmt, 7, (sqlite3_int64)entry->last_success_at);
sqlite3_bind_int(stmt, 8, entry->total_connections);
sqlite3_bind_int(stmt, 9, entry->total_failures);
sqlite3_bind_int(stmt, 10, entry->total_successes);
sqlite3_bind_int64(stmt, 11, (sqlite3_int64)entry->first_seen);
(void)db_reset_stmt(stmt);
db_bind_text_param(stmt, 1, entry->ip);
db_bind_int_param(stmt, 2, entry->failure_count);
db_bind_int_param(stmt, 3, entry->ban_count);
db_bind_int64_param(stmt, 4, (long long)entry->banned_until);
db_bind_int64_param(stmt, 5, (long long)entry->first_failure);
db_bind_int_param(stmt, 6, entry->has_authed_successfully);
db_bind_int64_param(stmt, 7, (long long)entry->last_success_at);
db_bind_int_param(stmt, 8, entry->total_connections);
db_bind_int_param(stmt, 9, entry->total_failures);
db_bind_int_param(stmt, 10, entry->total_successes);
db_bind_int64_param(stmt, 11, (long long)entry->first_seen);
// Idle tracking fields
sqlite3_bind_int(stmt, 12, entry->idle_failure_count);
sqlite3_bind_int(stmt, 13, entry->idle_ban_count);
sqlite3_bind_int64(stmt, 14, (sqlite3_int64)entry->idle_banned_until);
sqlite3_bind_int64(stmt, 15, (sqlite3_int64)entry->idle_first_failure);
db_bind_int_param(stmt, 12, entry->idle_failure_count);
db_bind_int_param(stmt, 13, entry->idle_ban_count);
db_bind_int64_param(stmt, 14, (long long)entry->idle_banned_until);
db_bind_int64_param(stmt, 15, (long long)entry->idle_first_failure);
if (sqlite3_step(stmt) != SQLITE_DONE) {
if (db_step_stmt(stmt) != DB_DONE) {
DEBUG_WARN("Failed to save ip_ban entry for %s", entry->ip);
} else {
saved++;
@@ -247,14 +244,23 @@ void ip_ban_save_to_db(void) {
}
pthread_mutex_unlock(&g_ban_mutex);
sqlite3_finalize(stmt);
sqlite3_exec(db, "COMMIT", NULL, NULL, NULL);
db_finalize_stmt(stmt);
(void)db_exec_sql("COMMIT");
DEBUG_TRACE("IP ban table saved: %d entries written to DB", saved);
}
// 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);
@@ -560,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;
+470 -184
View File
@@ -115,6 +115,12 @@ int process_admin_event_in_config(cJSON* event, char* error_message, size_t erro
// Forward declaration for NIP-45 COUNT message handling
int handle_count_message(const char* sub_id, cJSON* filters, struct lws *wsi, struct per_session_data *pss);
// Forward declaration for NOTICE message support
void send_notice_message(struct lws* wsi, struct per_session_data* pss, const char* message);
// Thread pool wake callback (called by worker threads)
static void wake_event_loop_from_thread_pool(void* ctx);
// Parameter binding helpers for SQL queries
static void add_bind_param(char*** params, int* count, int* capacity, const char* value) {
if (*count >= *capacity) {
@@ -131,11 +137,308 @@ static void free_bind_params(char** params, int count) {
free(params);
}
typedef struct req_async_state {
char sub_id[SUBSCRIPTION_ID_MAX_LENGTH];
struct lws* wsi_token;
pthread_mutex_t mutex;
int pending_jobs;
} req_async_state_t;
typedef struct req_async_submit_ctx {
req_async_state_t* state;
} req_async_submit_ctx_t;
typedef struct req_async_completion {
req_async_state_t* state;
thread_pool_status_t status;
thread_pool_req_result_t* req_result;
struct req_async_completion* next;
} req_async_completion_t;
static pthread_mutex_t g_req_async_completion_mutex = PTHREAD_MUTEX_INITIALIZER;
static req_async_completion_t* g_req_async_completion_head = NULL;
static req_async_completion_t* g_req_async_completion_tail = NULL;
static void free_req_payload_main(void* p) {
thread_pool_req_payload_t* payload = (thread_pool_req_payload_t*)p;
if (!payload) return;
free(payload->sql);
if (payload->bind_params) {
for (int i = 0; i < payload->bind_param_count; i++) {
free(payload->bind_params[i]);
}
free(payload->bind_params);
}
free(payload);
}
static req_async_state_t* req_async_state_create(const char* sub_id, struct lws* wsi) {
req_async_state_t* state = calloc(1, sizeof(*state));
if (!state) {
return NULL;
}
if (sub_id) {
strncpy(state->sub_id, sub_id, sizeof(state->sub_id) - 1);
state->sub_id[sizeof(state->sub_id) - 1] = '\0';
}
state->wsi_token = wsi;
pthread_mutex_init(&state->mutex, NULL);
return state;
}
static void req_async_state_free(req_async_state_t* state) {
if (!state) return;
pthread_mutex_destroy(&state->mutex);
free(state);
}
static void req_async_state_increment_pending(req_async_state_t* state) {
if (!state) return;
pthread_mutex_lock(&state->mutex);
state->pending_jobs++;
pthread_mutex_unlock(&state->mutex);
}
static int req_async_state_decrement_pending(req_async_state_t* state) {
if (!state) return 0;
pthread_mutex_lock(&state->mutex);
if (state->pending_jobs > 0) {
state->pending_jobs--;
}
int remaining = state->pending_jobs;
pthread_mutex_unlock(&state->mutex);
return remaining;
}
static void req_async_completion_push(req_async_completion_t* completion) {
if (!completion) return;
pthread_mutex_lock(&g_req_async_completion_mutex);
completion->next = NULL;
if (!g_req_async_completion_tail) {
g_req_async_completion_head = completion;
g_req_async_completion_tail = completion;
} else {
g_req_async_completion_tail->next = completion;
g_req_async_completion_tail = completion;
}
pthread_mutex_unlock(&g_req_async_completion_mutex);
}
static req_async_completion_t* req_async_completion_pop(void) {
pthread_mutex_lock(&g_req_async_completion_mutex);
req_async_completion_t* completion = g_req_async_completion_head;
if (completion) {
g_req_async_completion_head = completion->next;
if (!g_req_async_completion_head) {
g_req_async_completion_tail = NULL;
}
}
pthread_mutex_unlock(&g_req_async_completion_mutex);
return completion;
}
static int resolve_req_async_target(req_async_state_t* state, struct lws** out_wsi, struct per_session_data** out_pss) {
if (!state || !out_wsi || !out_pss) {
return 0;
}
*out_wsi = NULL;
*out_pss = NULL;
pthread_mutex_lock(&g_subscription_manager.subscriptions_lock);
subscription_t* sub = g_subscription_manager.active_subscriptions;
while (sub) {
if (sub->active && sub->wsi == state->wsi_token && strcmp(sub->id, state->sub_id) == 0) {
*out_wsi = sub->wsi;
break;
}
sub = sub->next;
}
pthread_mutex_unlock(&g_subscription_manager.subscriptions_lock);
if (!*out_wsi) {
return 0;
}
*out_pss = (struct per_session_data*)lws_wsi_user(*out_wsi);
return (*out_pss != NULL);
}
static void send_eose_message(struct lws* wsi, struct per_session_data* pss, const char* sub_id) {
if (!wsi || !pss || !sub_id) {
return;
}
cJSON* eose_response = cJSON_CreateArray();
if (!eose_response) {
return;
}
cJSON_AddItemToArray(eose_response, cJSON_CreateString("EOSE"));
cJSON_AddItemToArray(eose_response, cJSON_CreateString(sub_id));
char *eose_str = cJSON_Print(eose_response);
if (eose_str) {
size_t eose_len = strlen(eose_str);
DEBUG_TRACE("WS_FRAME_SEND: type=EOSE len=%zu data=%.100s%s",
eose_len,
eose_str,
eose_len > 100 ? "..." : "");
if (queue_message(wsi, pss, eose_str, eose_len, LWS_WRITE_TEXT) != 0) {
DEBUG_ERROR("Failed to queue EOSE message");
}
free(eose_str);
}
cJSON_Delete(eose_response);
}
static void req_async_result_cb(const thread_pool_result_t* result, void* user_ctx) {
req_async_submit_ctx_t* ctx = (req_async_submit_ctx_t*)user_ctx;
if (!ctx || !ctx->state || !result) {
if (result && result->result_data) {
thread_pool_free_req_result((thread_pool_req_result_t*)result->result_data);
}
if (ctx) free(ctx);
return;
}
req_async_completion_t* completion = calloc(1, sizeof(*completion));
if (!completion) {
if (result->result_data) {
thread_pool_free_req_result((thread_pool_req_result_t*)result->result_data);
}
free(ctx);
return;
}
completion->state = ctx->state;
completion->status = result->status;
completion->req_result = (thread_pool_req_result_t*)result->result_data;
req_async_completion_push(completion);
wake_event_loop_from_thread_pool(NULL);
free(ctx);
}
static int submit_req_query_async(req_async_state_t* state, const char* sql, const char** bind_params, int bind_param_count) {
if (!state || !sql) {
return -1;
}
thread_pool_req_payload_t* payload = calloc(1, sizeof(*payload));
if (!payload) {
return -1;
}
payload->sql = strdup(sql);
payload->bind_param_count = bind_param_count;
if (!payload->sql) {
free_req_payload_main(payload);
return -1;
}
if (bind_param_count > 0) {
payload->bind_params = calloc((size_t)bind_param_count, sizeof(char*));
if (!payload->bind_params) {
free_req_payload_main(payload);
return -1;
}
for (int i = 0; i < bind_param_count; i++) {
const char* v = (bind_params && bind_params[i]) ? bind_params[i] : "";
payload->bind_params[i] = strdup(v);
if (!payload->bind_params[i]) {
free_req_payload_main(payload);
return -1;
}
}
}
req_async_submit_ctx_t* ctx = calloc(1, sizeof(*ctx));
if (!ctx) {
free_req_payload_main(payload);
return -1;
}
ctx->state = state;
thread_pool_job_t job;
memset(&job, 0, sizeof(job));
job.type = THREAD_POOL_JOB_REQ_QUERY;
job.payload = payload;
job.payload_size = sizeof(*payload);
job.payload_free = free_req_payload_main;
job.result_cb = req_async_result_cb;
job.result_cb_ctx = ctx;
req_async_state_increment_pending(state);
thread_pool_status_t submit_rc = thread_pool_submit_read(&job, NULL);
if (submit_rc != THREAD_POOL_STATUS_OK) {
req_async_state_decrement_pending(state);
free(ctx);
free_req_payload_main(payload);
return -1;
}
return 0;
}
void process_req_async_completions(void) {
req_async_completion_t* completion = NULL;
while ((completion = req_async_completion_pop()) != NULL) {
struct lws* target_wsi = NULL;
struct per_session_data* target_pss = NULL;
int has_target = resolve_req_async_target(completion->state, &target_wsi, &target_pss);
if (has_target && completion->status == THREAD_POOL_STATUS_OK && completion->req_result) {
for (int r = 0; r < completion->req_result->row_count; r++) {
const char* event_json_str = completion->req_result->event_json_rows[r];
if (!event_json_str) {
continue;
}
size_t sub_id_len = strlen(completion->state->sub_id);
size_t event_json_len = strlen(event_json_str);
size_t msg_len = 10 + sub_id_len + 3 + event_json_len + 1;
unsigned char* buf = malloc(LWS_PRE + msg_len + 1);
if (!buf) {
continue;
}
char* msg_ptr = (char*)(buf + LWS_PRE);
snprintf(msg_ptr, msg_len + 1, "[\"EVENT\",\"%s\",%s]", completion->state->sub_id, event_json_str);
size_t actual_len = strlen(msg_ptr);
if (queue_message_take_ownership(target_wsi, target_pss, buf, actual_len, LWS_WRITE_TEXT) != 0) {
DEBUG_ERROR("Failed to queue async EVENT message for sub=%s", completion->state->sub_id);
}
}
} else if (has_target && completion->status != THREAD_POOL_STATUS_OK) {
send_notice_message(target_wsi, target_pss, "error: failed to execute subscription query");
}
int remaining = req_async_state_decrement_pending(completion->state);
if (remaining == 0) {
if (has_target) {
send_eose_message(target_wsi, target_pss, completion->state->sub_id);
}
req_async_state_free(completion->state);
}
if (completion->req_result) {
thread_pool_free_req_result(completion->req_result);
}
free(completion);
}
}
// Forward declaration for enhanced admin event authorization
int is_authorized_admin_event(cJSON* event, char* error_message, size_t error_size);
// Forward declaration for NOTICE message support
void send_notice_message(struct lws* wsi, struct per_session_data* pss, const char* message);
// Forward declarations for NIP-42 authentication functions
void send_nip42_auth_challenge(struct lws* wsi, struct per_session_data* pss);
@@ -164,8 +467,6 @@ int handle_nip11_http_request(struct lws* wsi, const char* accept_header);
// Forward declaration for WebSocket relay server
int start_websocket_relay(int port_override, int strict_port);
// Thread pool wake callback (called by worker threads)
static void wake_event_loop_from_thread_pool(void* ctx);
// Forward declarations for IP ban system
void ip_ban_init(void);
@@ -425,8 +726,8 @@ int init_database(const char* database_path_override) {
// Clean up stale WAL files before opening database
cleanup_stale_wal_files(db_path);
int rc = sqlite3_open(db_path, &g_db);
if (rc != SQLITE_OK) {
int rc = db_init(db_path);
if (rc != DB_OK) {
DEBUG_ERROR("Cannot open database");
DEBUG_TRACE("Exiting init_database() - failed to open database");
return -1;
@@ -437,7 +738,7 @@ int init_database(const char* database_path_override) {
// Check config table row count immediately after database open
int row_count = 0;
if (db_get_config_row_count(&row_count) == 0) {
DEBUG_LOG("Config table row count immediately after sqlite3_open(): %d", row_count);
DEBUG_LOG("Config table row count immediately after db_init(): %d", row_count);
} else {
DEBUG_LOG("Config table count unavailable immediately after sqlite3_open() (table may not exist yet)");
}
@@ -600,8 +901,7 @@ void close_database() {
DEBUG_WARN("WAL checkpoint warning");
}
sqlite3_close(g_db);
g_db = NULL;
db_close();
DEBUG_LOG("Database connection closed");
}
@@ -675,16 +975,16 @@ int store_event_tags(const char* event_id, cJSON* tags) {
return db_store_event_tags_cjson(event_id, tags);
}
// Store event in database
int store_event(cJSON* event) {
if (thread_pool_is_running()) {
DEBUG_TRACE("Thread pool scaffold active: store_event() still using synchronous DB path");
}
if (!g_db || !event) {
// Core event storage path.
// Returns:
// 0 = inserted into DB
// 1 = handled without insert (duplicate or ephemeral)
// -1 = failure
int store_event_core(cJSON* event) {
if (!db_is_available() || !event) {
return -1;
}
// Extract event fields
cJSON* id = cJSON_GetObjectItemCaseSensitive(event, "id");
cJSON* pubkey = cJSON_GetObjectItemCaseSensitive(event, "pubkey");
@@ -693,12 +993,12 @@ int store_event(cJSON* event) {
cJSON* content = cJSON_GetObjectItemCaseSensitive(event, "content");
cJSON* sig = cJSON_GetObjectItemCaseSensitive(event, "sig");
cJSON* tags = cJSON_GetObjectItemCaseSensitive(event, "tags");
if (!id || !pubkey || !created_at || !kind || !content || !sig) {
DEBUG_ERROR("Invalid event - missing required fields");
return -1;
}
// Classify event type
event_type_t type = classify_event_kind((int)cJSON_GetNumberValue(kind));
@@ -706,7 +1006,7 @@ int store_event(cJSON* event) {
if (type == EVENT_TYPE_EPHEMERAL) {
DEBUG_LOG("Ephemeral event (kind %d) - broadcasting only, not storing",
(int)cJSON_GetNumberValue(kind));
return 0; // Success - event was handled but not stored
return 1;
}
// Serialize tags to JSON (use empty array if no tags)
@@ -716,12 +1016,12 @@ int store_event(cJSON* event) {
} else {
tags_json = strdup("[]");
}
if (!tags_json) {
DEBUG_ERROR("Failed to serialize tags to JSON");
return -1;
}
// Serialize full event JSON for fast retrieval (use PrintUnformatted for compact storage)
char* event_json = cJSON_PrintUnformatted(event);
if (!event_json) {
@@ -729,35 +1029,40 @@ int store_event(cJSON* event) {
free(tags_json);
return -1;
}
int rc = SQLITE_ERROR;
int extended_errcode = 0;
if (db_insert_event_with_json(cJSON_GetStringValue(id),
cJSON_GetStringValue(pubkey),
(long long)cJSON_GetNumberValue(created_at),
(int)cJSON_GetNumberValue(kind),
event_type_to_string(type),
cJSON_GetStringValue(content),
cJSON_GetStringValue(sig),
tags_json,
event_json,
&rc,
&extended_errcode) != 0) {
thread_pool_store_event_payload_t payload;
memset(&payload, 0, sizeof(payload));
payload.id = (char*)cJSON_GetStringValue(id);
payload.pubkey = (char*)cJSON_GetStringValue(pubkey);
payload.created_at = (long long)cJSON_GetNumberValue(created_at);
payload.kind = (int)cJSON_GetNumberValue(kind);
payload.event_type = (char*)event_type_to_string(type);
payload.content = (char*)cJSON_GetStringValue(content);
payload.sig = (char*)cJSON_GetStringValue(sig);
payload.tags_json = tags_json;
payload.event_json = event_json;
thread_pool_store_event_result_t tp_result;
memset(&tp_result, 0, sizeof(tp_result));
if (thread_pool_execute_store_event_sync(&payload, &tp_result) != 0) {
DEBUG_ERROR("Failed to execute event insert operation");
free(tags_json);
free(event_json);
return -1;
}
if (rc != SQLITE_DONE) {
int rc = tp_result.step_rc;
int extended_errcode = tp_result.extended_errcode;
if (rc != DB_DONE) {
const char* err_msg = db_last_error();
if (rc != SQLITE_CONSTRAINT) {
if (rc != DB_CONSTRAINT) {
DEBUG_ERROR("INSERT failed: rc=%d, extended_errcode=%d, msg=%s", rc, extended_errcode, err_msg);
}
}
if (rc != SQLITE_DONE) {
if (rc == SQLITE_CONSTRAINT) {
if (rc != DB_DONE) {
if (rc == DB_CONSTRAINT) {
DEBUG_WARN("Event already exists in database");
// Add TRACE level debug to show both events
@@ -781,7 +1086,7 @@ int store_event(cJSON* event) {
free(tags_json);
free(event_json);
return 0; // Not an error, just duplicate
return 1;
}
char error_msg[256];
snprintf(error_msg, sizeof(error_msg), "Failed to insert event: %s", db_last_error());
@@ -793,13 +1098,18 @@ int store_event(cJSON* event) {
free(tags_json);
free(event_json);
return 0;
}
// Main-thread-only post-store follow-up actions.
void store_event_post_actions(cJSON* event) {
if (!event) {
return;
}
// Call monitoring hook after successful event storage
monitoring_on_event_stored();
// After successful event storage, insert denormalized tags
store_event_tags(cJSON_GetStringValue(id), tags);
// Check if this is a kind 3 event from the admin — trigger WoT sync
cJSON* kind_obj = cJSON_GetObjectItemCaseSensitive(event, "kind");
cJSON* pubkey_obj = cJSON_GetObjectItemCaseSensitive(event, "pubkey");
@@ -815,7 +1125,17 @@ int store_event(cJSON* event) {
if (admin_pubkey) free((char*)admin_pubkey);
}
}
}
// Backward-compatible wrapper for synchronous call sites.
int store_event(cJSON* event) {
int core_rc = store_event_core(event);
if (core_rc < 0) {
return -1;
}
if (core_rc == 0) {
store_event_post_actions(event);
}
return 0;
}
@@ -823,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) {
@@ -901,10 +1221,6 @@ static int is_only_kind_99999_request(cJSON* filters) {
}
int handle_req_message(const char* sub_id, cJSON* filters, struct lws *wsi, struct per_session_data *pss) {
if (thread_pool_is_running()) {
DEBUG_TRACE("Thread pool scaffold active: handle_req_message() still using synchronous DB path");
}
if (!cJSON_IsArray(filters)) {
DEBUG_ERROR("REQ filters is not an array");
return 0;
@@ -1104,7 +1420,18 @@ int handle_req_message(const char* sub_id, cJSON* filters, struct lws *wsi, stru
}
int events_sent = config_events_sent; // Start with synthetic config events
int submitted_jobs = 0;
int expiration_enabled = get_config_bool("expiration_enabled", 1);
int filter_responses = get_config_bool("expiration_filter", 1);
time_t query_now = time(NULL);
req_async_state_t* async_state = req_async_state_create(sub_id, wsi);
if (!async_state) {
DEBUG_ERROR("Failed to allocate async REQ state");
free_bind_params(bind_params, bind_param_count);
return events_sent;
}
// Process each filter in the array
for (int i = 0; i < cJSON_GetArraySize(filters); i++) {
cJSON* filter = cJSON_GetArrayItem(filters, i);
@@ -1128,13 +1455,26 @@ int handle_req_message(const char* sub_id, cJSON* filters, struct lws *wsi, stru
// Build SQL query based on filter - exclude ephemeral events (kinds 20000-29999) from historical queries
// Select event_json for fast retrieval (no JSON reconstruction needed)
char sql[1024] = "SELECT event_json FROM events WHERE 1=1 AND (kind < 20000 OR kind >= 30000)";
char sql[1408] = "SELECT event_json FROM events WHERE 1=1 AND (kind < 20000 OR kind >= 30000)";
char* sql_ptr = sql + strlen(sql);
int remaining = sizeof(sql) - strlen(sql);
// Note: Expiration filtering will be done at application level
// after retrieving events to ensure compatibility with all SQLite versions
// Phase 3: push expiration filtering into SQL using indexed event_tags table.
if (expiration_enabled && filter_responses) {
snprintf(sql_ptr, remaining,
" AND NOT EXISTS (SELECT 1 FROM event_tags et_exp "
"WHERE et_exp.event_id = events.id "
"AND et_exp.tag_name = ? "
"AND CAST(et_exp.tag_value AS INTEGER) <= ?)");
sql_ptr += strlen(sql_ptr);
remaining = sizeof(sql) - strlen(sql);
add_bind_param(&bind_params, &bind_param_count, &bind_param_capacity, "expiration");
char now_buf[32];
snprintf(now_buf, sizeof(now_buf), "%lld", (long long)query_now);
add_bind_param(&bind_params, &bind_param_count, &bind_param_capacity, now_buf);
}
// Handle kinds filter
cJSON* kinds = cJSON_GetObjectItemCaseSensitive(filter, "kinds");
if (kinds && cJSON_IsArray(kinds)) {
@@ -1143,7 +1483,7 @@ int handle_req_message(const char* sub_id, cJSON* filters, struct lws *wsi, stru
snprintf(sql_ptr, remaining, " AND kind IN (");
sql_ptr += strlen(sql_ptr);
remaining = sizeof(sql) - strlen(sql);
for (int k = 0; k < kind_count; k++) {
cJSON* kind = cJSON_GetArrayItem(kinds, k);
if (cJSON_IsNumber(kind)) {
@@ -1162,7 +1502,7 @@ int handle_req_message(const char* sub_id, cJSON* filters, struct lws *wsi, stru
remaining = sizeof(sql) - strlen(sql);
}
}
// Handle authors filter
cJSON* authors = cJSON_GetObjectItemCaseSensitive(filter, "authors");
if (authors && cJSON_IsArray(authors)) {
@@ -1208,8 +1548,8 @@ int handle_req_message(const char* sub_id, cJSON* filters, struct lws *wsi, stru
if (ids && cJSON_IsArray(ids)) {
int id_count = 0;
// Count valid ids
for (int i = 0; i < cJSON_GetArraySize(ids); i++) {
cJSON* id = cJSON_GetArrayItem(ids, i);
for (int j = 0; j < cJSON_GetArraySize(ids); j++) {
cJSON* id = cJSON_GetArrayItem(ids, j);
if (cJSON_IsString(id)) {
id_count++;
}
@@ -1219,8 +1559,8 @@ int handle_req_message(const char* sub_id, cJSON* filters, struct lws *wsi, stru
sql_ptr += strlen(sql_ptr);
remaining = sizeof(sql) - strlen(sql);
for (int i = 0; i < id_count; i++) {
if (i > 0) {
for (int j = 0; j < id_count; j++) {
if (j > 0) {
snprintf(sql_ptr, remaining, ",");
sql_ptr++;
remaining--;
@@ -1234,8 +1574,8 @@ int handle_req_message(const char* sub_id, cJSON* filters, struct lws *wsi, stru
remaining = sizeof(sql) - strlen(sql);
// Add id values to bind params
for (int i = 0; i < cJSON_GetArraySize(ids); i++) {
cJSON* id = cJSON_GetArrayItem(ids, i);
for (int j = 0; j < cJSON_GetArraySize(ids); j++) {
cJSON* id = cJSON_GetArrayItem(ids, j);
if (cJSON_IsString(id)) {
add_bind_param(&bind_params, &bind_param_count, &bind_param_capacity, cJSON_GetStringValue(id));
}
@@ -1254,8 +1594,8 @@ int handle_req_message(const char* sub_id, cJSON* filters, struct lws *wsi, stru
if (cJSON_IsArray(filter_item)) {
int tag_value_count = 0;
// Count valid tag values
for (int i = 0; i < cJSON_GetArraySize(filter_item); i++) {
cJSON* tag_value = cJSON_GetArrayItem(filter_item, i);
for (int j = 0; j < cJSON_GetArraySize(filter_item); j++) {
cJSON* tag_value = cJSON_GetArrayItem(filter_item, j);
if (cJSON_IsString(tag_value)) {
tag_value_count++;
}
@@ -1266,8 +1606,8 @@ int handle_req_message(const char* sub_id, cJSON* filters, struct lws *wsi, stru
sql_ptr += strlen(sql_ptr);
remaining = sizeof(sql) - strlen(sql);
for (int i = 0; i < tag_value_count; i++) {
if (i > 0) {
for (int j = 0; j < tag_value_count; j++) {
if (j > 0) {
snprintf(sql_ptr, remaining, ",");
sql_ptr++;
remaining--;
@@ -1282,8 +1622,8 @@ int handle_req_message(const char* sub_id, cJSON* filters, struct lws *wsi, stru
// Add tag name and values to bind params
add_bind_param(&bind_params, &bind_param_count, &bind_param_capacity, tag_name);
for (int i = 0; i < cJSON_GetArraySize(filter_item); i++) {
cJSON* tag_value = cJSON_GetArrayItem(filter_item, i);
for (int j = 0; j < cJSON_GetArraySize(filter_item); j++) {
cJSON* tag_value = cJSON_GetArrayItem(filter_item, j);
if (cJSON_IsString(tag_value)) {
add_bind_param(&bind_params, &bind_param_count, &bind_param_capacity, cJSON_GetStringValue(tag_value));
}
@@ -1302,12 +1642,12 @@ int handle_req_message(const char* sub_id, cJSON* filters, struct lws *wsi, stru
// Escape single quotes in search term for SQL safety
char escaped_search[256];
size_t escaped_len = 0;
for (size_t i = 0; search_term[i] && escaped_len < sizeof(escaped_search) - 1; i++) {
if (search_term[i] == '\'') {
for (size_t j = 0; search_term[j] && escaped_len < sizeof(escaped_search) - 1; j++) {
if (search_term[j] == '\'') {
escaped_search[escaped_len++] = '\'';
escaped_search[escaped_len++] = '\'';
} else {
escaped_search[escaped_len++] = search_term[i];
escaped_search[escaped_len++] = search_term[j];
}
}
escaped_search[escaped_len] = '\0';
@@ -1328,7 +1668,7 @@ int handle_req_message(const char* sub_id, cJSON* filters, struct lws *wsi, stru
sql_ptr += strlen(sql_ptr);
remaining = sizeof(sql) - strlen(sql);
}
// Handle until filter
cJSON* until = cJSON_GetObjectItemCaseSensitive(filter, "until");
if (until && cJSON_IsNumber(until)) {
@@ -1336,12 +1676,12 @@ int handle_req_message(const char* sub_id, cJSON* filters, struct lws *wsi, stru
sql_ptr += strlen(sql_ptr);
remaining = sizeof(sql) - strlen(sql);
}
// Add ordering and limit
snprintf(sql_ptr, remaining, " ORDER BY created_at DESC");
sql_ptr += strlen(sql_ptr);
remaining = sizeof(sql) - strlen(sql);
// Handle limit filter
cJSON* limit = cJSON_GetObjectItemCaseSensitive(filter, "limit");
if (limit && cJSON_IsNumber(limit)) {
@@ -1354,123 +1694,27 @@ int handle_req_message(const char* sub_id, cJSON* filters, struct lws *wsi, stru
snprintf(sql_ptr, remaining, " LIMIT 500");
}
// Start query timing
struct timespec query_start, query_end;
clock_gettime(CLOCK_MONOTONIC, &query_start);
// Execute query and send events
sqlite3_stmt* stmt;
int rc = db_prepare(sql, &stmt);
if (rc != SQLITE_OK) {
char error_msg[256];
snprintf(error_msg, sizeof(error_msg), "Failed to prepare subscription query: %s", db_last_error());
DEBUG_ERROR(error_msg);
// Log the failed query so we can see what SQL was generated
if (g_debug_level >= DEBUG_LEVEL_DEBUG) {
time_t now = time(NULL);
struct tm* tm_info = localtime(&now);
char timestamp[32];
strftime(timestamp, sizeof(timestamp), "%Y-%m-%d %H:%M:%S", tm_info);
fprintf(stderr, "[%s] [QUERY_FAILED] type=REQ sub=%s ip=%s error=%s sql=%s\n",
timestamp,
sub_id,
pss ? pss->client_ip : "N/A",
db_last_error(),
sql);
fflush(stderr);
}
// Submit async REQ query (results processed on lws thread via completion queue)
if (submit_req_query_async(async_state, sql, (const char**)bind_params, bind_param_count) != 0) {
DEBUG_ERROR("Failed to submit async REQ query for subscription %s", sub_id);
continue;
}
// Track query execution for abuse detection
submitted_jobs++;
if (pss) {
pss->db_queries_executed++;
}
// Bind parameters
for (int i = 0; i < bind_param_count; i++) {
db_bind_text_param(stmt, i + 1, bind_params[i]);
}
// Cache config values outside the row loop (performance fix)
int expiration_enabled = get_config_bool("expiration_enabled", 1);
int filter_responses = get_config_bool("expiration_filter", 1);
int row_count = 0;
while (db_step_stmt(stmt) == SQLITE_ROW) {
row_count++;
// Track rows returned for abuse detection
if (pss) {
pss->db_rows_returned++;
}
// Get pre-serialized event JSON (no reconstruction needed!)
const char* event_json_str = db_column_text_value(stmt, 0);
if (!event_json_str) {
DEBUG_ERROR("Event has NULL event_json field");
continue;
}
// Parse event JSON only for expiration check
cJSON* event = cJSON_Parse(event_json_str);
if (!event) {
DEBUG_ERROR("Failed to parse event_json from database");
continue;
}
// Check expiration filtering (NIP-40) at application level
// (expiration_enabled and filter_responses are cached outside the loop)
if (expiration_enabled && filter_responses) {
time_t current_time = time(NULL);
if (is_event_expired(event, current_time)) {
// Skip this expired event
cJSON_Delete(event);
continue;
}
}
// Build EVENT message using zero-copy path: allocate with LWS_PRE prefix,
// write directly, transfer ownership to queue — no memcpy.
// Format: ["EVENT","<sub_id>",<event_json>]
size_t sub_id_len = strlen(sub_id);
size_t event_json_len = strlen(event_json_str);
size_t msg_len = 10 + sub_id_len + 3 + event_json_len + 1;
unsigned char* buf = malloc(LWS_PRE + msg_len + 1);
if (buf) {
char* msg_ptr = (char*)(buf + LWS_PRE);
snprintf(msg_ptr, msg_len + 1, "[\"EVENT\",\"%s\",%s]", sub_id, event_json_str);
size_t actual_len = strlen(msg_ptr);
// queue_message_take_ownership takes buf ownership — no memcpy, no free needed here
if (queue_message_take_ownership(wsi, pss, buf, actual_len, LWS_WRITE_TEXT) != 0) {
DEBUG_ERROR("Failed to queue EVENT message for sub=%s", sub_id);
// buf already freed by queue_message_take_ownership on failure
}
}
cJSON_Delete(event);
events_sent++;
}
db_finalize_stmt(stmt);
// Stop query timing and log
clock_gettime(CLOCK_MONOTONIC, &query_end);
long elapsed_us = (query_end.tv_sec - query_start.tv_sec) * 1000000L +
(query_end.tv_nsec - query_start.tv_nsec) / 1000L;
log_query_execution("REQ", sub_id, pss ? pss->client_ip : NULL,
sql, elapsed_us, row_count);
}
// Cleanup bind params
free_bind_params(bind_params, bind_param_count);
return events_sent;
if (submitted_jobs == 0) {
req_async_state_free(async_state);
return events_sent;
}
return HANDLE_REQ_ASYNC_PENDING;
}
/////////////////////////////////////////////////////////////////////////////////////////
/////////////////////////////////////////////////////////////////////////////////////////
@@ -1987,6 +2231,44 @@ 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
@@ -2052,12 +2334,12 @@ int main(int argc, char* argv[]) {
ip_ban_load_from_db();
// Optional thread pool scaffold initialization (execution wiring is future work)
int thread_pool_enabled = get_config_bool("thread_pool_enabled", 0);
int thread_pool_enabled = get_config_bool("thread_pool_enabled", 1);
int thread_pool_initialized = 0;
if (thread_pool_enabled) {
thread_pool_config_t tp_cfg;
memset(&tp_cfg, 0, sizeof(tp_cfg));
tp_cfg.reader_threads = get_config_int("thread_pool_readers", 4);
tp_cfg.reader_threads = get_config_int("thread_pool_readers", 1);
tp_cfg.max_queue_depth = get_config_int("thread_pool_max_queue_depth", 4096);
tp_cfg.db_path = g_database_path;
tp_cfg.wake_loop_cb = wake_event_loop_from_thread_pool;
@@ -2071,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
+21 -3
View File
@@ -12,9 +12,9 @@
// Version information (auto-updated by build system)
// Using CRELAY_ prefix to avoid conflicts with nostr_core_lib VERSION macros
#define CRELAY_VERSION_MAJOR 2
#define CRELAY_VERSION_MINOR 0
#define CRELAY_VERSION_PATCH 3
#define CRELAY_VERSION "v2.0.3"
#define CRELAY_VERSION_MINOR 1
#define CRELAY_VERSION_PATCH 9
#define CRELAY_VERSION "v2.1.9"
// Relay metadata (authoritative source for NIP-11 information)
#define RELAY_NAME "C-Relay"
@@ -28,4 +28,22 @@
#define POSTING_POLICY ""
#define PAYMENTS_URL ""
// Forward declaration to avoid pulling cJSON headers into all includers.
typedef struct cJSON cJSON;
// Async REQ handler status used by websocket callback to defer EOSE until worker completion
#define HANDLE_REQ_ASYNC_PENDING (-2)
// Async EVENT storage split:
// - store_event_core(): worker-safe core DB write path
// returns 0 when inserted, 1 when handled without insert (duplicate/ephemeral), -1 on error
// - store_event_post_actions(): main-thread-only follow-up actions (monitoring/tags/WoT sync)
// - store_event(): legacy wrapper preserving original behavior for synchronous call sites
int store_event_core(cJSON* event);
void store_event_post_actions(cJSON* event);
int store_event(cJSON* event);
// Drains completed async REQ jobs and queues EVENT/EOSE on the lws service thread.
void process_req_async_completions(void);
#endif /* MAIN_H */
+71 -36
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);
@@ -23,6 +23,67 @@ int get_config_bool(const char* key, int default_value);
#define HTTP_STATUS_INTERNAL_SERVER_ERROR 500
#define NIP11_CACHE_TTL_SEC 5
static pthread_mutex_t g_nip11_cache_mutex = PTHREAD_MUTEX_INITIALIZER;
static char* g_nip11_cached_json = NULL;
static size_t g_nip11_cached_json_len = 0;
static time_t g_nip11_cache_expires_at = 0;
cJSON* generate_relay_info_json();
static void invalidate_nip11_cache_locked(void) {
if (g_nip11_cached_json) {
free(g_nip11_cached_json);
g_nip11_cached_json = NULL;
}
g_nip11_cached_json_len = 0;
g_nip11_cache_expires_at = 0;
}
static char* get_nip11_json_cached_dup(size_t* out_len) {
time_t now = time(NULL);
pthread_mutex_lock(&g_nip11_cache_mutex);
if (g_nip11_cached_json && now < g_nip11_cache_expires_at) {
char* copy = strdup(g_nip11_cached_json);
if (copy && out_len) {
*out_len = g_nip11_cached_json_len;
}
pthread_mutex_unlock(&g_nip11_cache_mutex);
return copy;
}
pthread_mutex_unlock(&g_nip11_cache_mutex);
cJSON* info_json = generate_relay_info_json();
if (!info_json) {
return NULL;
}
char* fresh_json = cJSON_Print(info_json);
cJSON_Delete(info_json);
if (!fresh_json) {
return NULL;
}
size_t fresh_len = strlen(fresh_json);
pthread_mutex_lock(&g_nip11_cache_mutex);
invalidate_nip11_cache_locked();
g_nip11_cached_json = strdup(fresh_json);
if (g_nip11_cached_json) {
g_nip11_cached_json_len = fresh_len;
g_nip11_cache_expires_at = now + NIP11_CACHE_TTL_SEC;
}
pthread_mutex_unlock(&g_nip11_cache_mutex);
if (out_len) {
*out_len = fresh_len;
}
return fresh_json;
}
/////////////////////////////////////////////////////////////////////////////////////////
/////////////////////////////////////////////////////////////////////////////////////////
// NIP-11 RELAY INFORMATION DOCUMENT
@@ -80,8 +141,9 @@ void init_relay_info() {
// Clean up relay information JSON objects
void cleanup_relay_info() {
// NIP-11 relay information is now generated dynamically from config table
// No cleanup needed - data is fetched directly from database when requested
pthread_mutex_lock(&g_nip11_cache_mutex);
invalidate_nip11_cache_locked();
pthread_mutex_unlock(&g_nip11_cache_mutex);
}
// Generate NIP-11 compliant JSON document
@@ -184,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
@@ -348,9 +410,10 @@ int handle_nip11_http_request(struct lws* wsi, const char* accept_header) {
return -1; // Close connection
}
// Generate relay information JSON
cJSON* info_json = generate_relay_info_json();
if (!info_json) {
// Generate (or fetch cached) relay information JSON
size_t json_len = 0;
char* json_string = get_nip11_json_cached_dup(&json_len);
if (!json_string) {
DEBUG_ERROR("Failed to generate relay info JSON");
unsigned char buf[LWS_PRE + 256];
unsigned char *p = &buf[LWS_PRE];
@@ -373,34 +436,6 @@ int handle_nip11_http_request(struct lws* wsi, const char* accept_header) {
return -1;
}
char* json_string = cJSON_Print(info_json);
cJSON_Delete(info_json);
if (!json_string) {
DEBUG_ERROR("Failed to serialize relay info JSON");
unsigned char buf[LWS_PRE + 256];
unsigned char *p = &buf[LWS_PRE];
unsigned char *start = p;
unsigned char *end = &buf[sizeof(buf) - 1];
if (lws_add_http_header_status(wsi, HTTP_STATUS_INTERNAL_SERVER_ERROR, &p, end)) {
return -1;
}
if (lws_add_http_header_by_token(wsi, WSI_TOKEN_HTTP_CONTENT_TYPE, (unsigned char*)"text/plain", 10, &p, end)) {
return -1;
}
if (lws_add_http_header_content_length(wsi, 0, &p, end)) {
return -1;
}
if (lws_finalize_http_header(wsi, &p, end)) {
return -1;
}
lws_write(wsi, start, p - start, LWS_WRITE_HTTP_HEADERS);
return -1;
}
size_t json_len = strlen(json_string);
// Allocate session data to manage buffer lifetime across callbacks
struct nip11_session_data* session_data = malloc(sizeof(struct nip11_session_data));
if (!session_data) {
+22 -3
View File
@@ -129,6 +129,14 @@ typedef struct {
static nip42_challenge_manager_t g_challenge_manager = {0};
static int g_validator_initialized = 0;
typedef struct {
int has_active_whitelist_rules;
time_t last_refresh;
} auth_rules_fast_cache_t;
static auth_rules_fast_cache_t g_auth_rules_fast_cache = {0};
#define AUTH_RULES_CACHE_TTL_SEC 5
// Last rule violation details for status code mapping
struct {
char violation_type[100]; // "pubkey_blacklist", "hash_blacklist",
@@ -502,10 +510,10 @@ void nostr_request_result_free_file_data(nostr_request_result_t *result) {
/**
* Force cache refresh - cache no longer exists, function kept for compatibility
* Force cache refresh for auth rule fast cache.
*/
void nostr_request_validator_force_cache_refresh(void) {
// Cache no longer exists - direct database queries are used
g_auth_rules_fast_cache.last_refresh = 0;
}
/**
@@ -520,6 +528,17 @@ static int reload_auth_config(void) {
// Note: Blossom protocol validation removed - C-relay uses standard Nostr events only
static int has_active_whitelist_rules_cached(void) {
time_t now = time(NULL);
if (g_auth_rules_fast_cache.last_refresh == 0 ||
(now - g_auth_rules_fast_cache.last_refresh) >= AUTH_RULES_CACHE_TTL_SEC) {
g_auth_rules_fast_cache.has_active_whitelist_rules =
(db_count_active_whitelist_rules() > 0) ? 1 : 0;
g_auth_rules_fast_cache.last_refresh = now;
}
return g_auth_rules_fast_cache.has_active_whitelist_rules;
}
/**
* Check database authentication rules for the request
* Implements the 6-step rule evaluation engine from AUTH_API.md
@@ -553,7 +572,7 @@ int check_database_auth_rules(const char *pubkey, const char *operation __attrib
}
// Step 4: If any whitelist rules exist, deny by default
if (db_count_active_whitelist_rules() > 0) {
if (has_active_whitelist_rules_cached()) {
strcpy(g_last_rule_violation.violation_type, "whitelist_violation");
strcpy(g_last_rule_violation.reason,
"Public key not whitelisted for this operation");
+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
+686 -9
View File
@@ -2,8 +2,12 @@
#include "thread_pool.h"
#include "debug.h"
#include "db_ops.h"
#include "ip_ban.h"
#include <pthread.h>
#include <stdint.h>
#include <stdio.h>
#include <stdlib.h>
#include <string.h>
@@ -36,11 +40,93 @@ typedef struct {
thread_pool_wake_loop_cb wake_loop_cb;
void* wake_loop_ctx;
char db_path[512];
pthread_mutex_t state_mutex;
} thread_pool_state_t;
static thread_pool_state_t g_pool = {0};
typedef struct {
pthread_mutex_t mutex;
pthread_cond_t cond;
int done;
thread_pool_status_t status;
void* result_data;
size_t result_size;
} thread_pool_wait_ctx_t;
static void wait_ctx_init(thread_pool_wait_ctx_t* ctx) {
memset(ctx, 0, sizeof(*ctx));
pthread_mutex_init(&ctx->mutex, NULL);
pthread_cond_init(&ctx->cond, NULL);
}
static void wait_ctx_destroy(thread_pool_wait_ctx_t* ctx) {
pthread_mutex_destroy(&ctx->mutex);
pthread_cond_destroy(&ctx->cond);
}
static void wait_ctx_result_cb(const thread_pool_result_t* result, void* user_ctx) {
thread_pool_wait_ctx_t* ctx = (thread_pool_wait_ctx_t*)user_ctx;
if (!ctx || !result) return;
pthread_mutex_lock(&ctx->mutex);
ctx->status = result->status;
ctx->result_data = result->result_data;
ctx->result_size = result->result_size;
ctx->done = 1;
pthread_cond_signal(&ctx->cond);
pthread_mutex_unlock(&ctx->mutex);
}
static void wait_ctx_wait(thread_pool_wait_ctx_t* ctx) {
pthread_mutex_lock(&ctx->mutex);
while (!ctx->done) {
pthread_cond_wait(&ctx->cond, &ctx->mutex);
}
pthread_mutex_unlock(&ctx->mutex);
}
static void free_count_payload(void* p) {
thread_pool_count_payload_t* payload = (thread_pool_count_payload_t*)p;
if (!payload) return;
free(payload->sql);
if (payload->bind_params) {
for (int i = 0; i < payload->bind_param_count; i++) {
free(payload->bind_params[i]);
}
free(payload->bind_params);
}
free(payload);
}
static void free_req_payload(void* p) {
thread_pool_req_payload_t* payload = (thread_pool_req_payload_t*)p;
if (!payload) return;
free(payload->sql);
if (payload->bind_params) {
for (int i = 0; i < payload->bind_param_count; i++) {
free(payload->bind_params[i]);
}
free(payload->bind_params);
}
free(payload);
}
static void free_store_event_payload(void* p) {
thread_pool_store_event_payload_t* payload = (thread_pool_store_event_payload_t*)p;
if (!payload) return;
free(payload->id);
free(payload->pubkey);
free(payload->event_type);
free(payload->content);
free(payload->sig);
free(payload->tags_json);
free(payload->event_json);
free(payload);
}
static void queue_init(thread_pool_queue_t* q, int max_size) {
memset(q, 0, sizeof(*q));
q->max_size = (max_size > 0) ? max_size : 4096;
@@ -112,13 +198,27 @@ static void wake_event_loop(void) {
}
}
static void complete_job_with_status(thread_pool_job_node_t* node, thread_pool_status_t status, const char* message) {
static void* open_worker_connection(void) {
void* db_conn = NULL;
if (db_open_worker_connection(g_pool.db_path, &db_conn) != 0) {
return NULL;
}
return db_conn;
}
static void complete_job_with_result(thread_pool_job_node_t* node,
thread_pool_status_t status,
const char* message,
void* result_data,
size_t result_size) {
thread_pool_result_t result;
memset(&result, 0, sizeof(result));
result.job_id = node->job_id;
result.type = node->job.type;
result.status = status;
result.session = node->job.session;
result.result_data = result_data;
result.result_size = result_size;
result.message = message;
if (node->job.result_cb) {
@@ -132,29 +232,317 @@ static void complete_job_with_status(thread_pool_job_node_t* node, thread_pool_s
free(node);
}
static void execute_count_job(thread_pool_job_node_t* node) {
thread_pool_count_payload_t* payload = (thread_pool_count_payload_t*)node->job.payload;
if (!payload || !payload->sql) {
complete_job_with_result(node, THREAD_POOL_STATUS_INTERNAL_ERROR,
"COUNT payload missing", NULL, 0);
return;
}
int count = 0;
if (db_count_with_sql(payload->sql, (const char**)payload->bind_params,
payload->bind_param_count, &count) != 0) {
complete_job_with_result(node, THREAD_POOL_STATUS_INTERNAL_ERROR,
"COUNT query failed", NULL, 0);
return;
}
thread_pool_count_result_t* out = calloc(1, sizeof(*out));
if (!out) {
complete_job_with_result(node, THREAD_POOL_STATUS_INTERNAL_ERROR,
"COUNT result allocation failed", NULL, 0);
return;
}
out->count = count;
complete_job_with_result(node, THREAD_POOL_STATUS_OK,
"COUNT query completed", out, sizeof(*out));
}
static void execute_req_job(thread_pool_job_node_t* node) {
thread_pool_req_payload_t* payload = (thread_pool_req_payload_t*)node->job.payload;
if (!payload || !payload->sql) {
complete_job_with_result(node, THREAD_POOL_STATUS_INTERNAL_ERROR,
"REQ payload missing", NULL, 0);
return;
}
db_stmt_t* stmt = NULL;
if (db_prepare(payload->sql, &stmt) != DB_OK) {
complete_job_with_result(node, THREAD_POOL_STATUS_INTERNAL_ERROR,
"REQ prepare failed", NULL, 0);
return;
}
for (int i = 0; i < payload->bind_param_count; i++) {
const char* v = (payload->bind_params && payload->bind_params[i]) ? payload->bind_params[i] : "";
(void)db_bind_text_param(stmt, i + 1, v);
}
int cap = (payload->row_limit > 0) ? payload->row_limit : 500;
if (cap < 1) cap = 1;
thread_pool_req_result_t* out = calloc(1, sizeof(*out));
if (!out) {
db_finalize_stmt(stmt);
complete_job_with_result(node, THREAD_POOL_STATUS_INTERNAL_ERROR,
"REQ result allocation failed", NULL, 0);
return;
}
out->event_json_rows = calloc((size_t)cap, sizeof(char*));
if (!out->event_json_rows) {
db_finalize_stmt(stmt);
free(out);
complete_job_with_result(node, THREAD_POOL_STATUS_INTERNAL_ERROR,
"REQ row array allocation failed", NULL, 0);
return;
}
int rc = DB_OK;
while ((rc = db_step_stmt(stmt)) == DB_ROW) {
if (out->row_count >= cap) break;
const char* row = db_column_text_value(stmt, 0);
if (!row) continue;
out->event_json_rows[out->row_count] = strdup(row);
if (!out->event_json_rows[out->row_count]) {
break;
}
out->row_count++;
}
db_finalize_stmt(stmt);
complete_job_with_result(node, THREAD_POOL_STATUS_OK,
"REQ query completed", out, sizeof(*out));
}
static void execute_store_event_job(thread_pool_job_node_t* node) {
thread_pool_store_event_payload_t* payload = (thread_pool_store_event_payload_t*)node->job.payload;
if (!payload || !payload->id || !payload->pubkey || !payload->content || !payload->sig) {
complete_job_with_result(node, THREAD_POOL_STATUS_INTERNAL_ERROR,
"STORE_EVENT payload missing required fields", NULL, 0);
return;
}
thread_pool_store_event_result_t* out = calloc(1, sizeof(*out));
if (!out) {
complete_job_with_result(node, THREAD_POOL_STATUS_INTERNAL_ERROR,
"STORE_EVENT result allocation failed", NULL, 0);
return;
}
int step_rc = DB_ERROR;
int extended_errcode = 0;
if (db_insert_event_with_json(payload->id,
payload->pubkey,
payload->created_at,
payload->kind,
payload->event_type ? payload->event_type : "regular",
payload->content,
payload->sig,
payload->tags_json ? payload->tags_json : "[]",
payload->event_json ? payload->event_json : "{}",
&step_rc,
&extended_errcode) != 0) {
free(out);
complete_job_with_result(node, THREAD_POOL_STATUS_INTERNAL_ERROR,
"STORE_EVENT execution failed", NULL, 0);
return;
}
out->step_rc = step_rc;
out->extended_errcode = extended_errcode;
complete_job_with_result(node, THREAD_POOL_STATUS_OK,
"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:
execute_req_job(node);
break;
case THREAD_POOL_JOB_COUNT_QUERY:
execute_count_job(node);
break;
default:
complete_job_with_result(node, THREAD_POOL_STATUS_NOT_IMPLEMENTED,
"Read job type not implemented", NULL, 0);
break;
}
}
static void execute_write_job(thread_pool_job_node_t* node) {
switch (node->job.type) {
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);
break;
}
}
static void* reader_worker_main(void* arg) {
(void)arg;
int reader_index = (int)(intptr_t)arg;
char thread_name[16];
snprintf(thread_name, sizeof(thread_name), "db-read-%d", reader_index);
pthread_setname_np(pthread_self(), thread_name);
void* worker_db = open_worker_connection();
if (!worker_db) {
DEBUG_ERROR("Reader worker failed to open SQLite connection");
return NULL;
}
while (g_pool.running) {
thread_pool_job_node_t* node = queue_pop(&g_pool.read_q);
if (!node) break;
// Scaffold only: execution wiring is intentionally deferred.
complete_job_with_status(node, THREAD_POOL_STATUS_NOT_IMPLEMENTED,
"Read worker scaffold active; execution not wired yet");
execute_read_job(node);
}
db_close_worker_connection(worker_db);
return NULL;
}
static void* writer_worker_main(void* arg) {
(void)arg;
pthread_setname_np(pthread_self(), "db-write");
void* worker_db = open_worker_connection();
if (!worker_db) {
DEBUG_ERROR("Writer worker failed to open SQLite connection");
return NULL;
}
while (g_pool.running) {
thread_pool_job_node_t* node = queue_pop(&g_pool.write_q);
if (!node) break;
// Scaffold only: execution wiring is intentionally deferred.
complete_job_with_status(node, THREAD_POOL_STATUS_NOT_IMPLEMENTED,
"Write worker scaffold active; execution not wired yet");
execute_write_job(node);
}
if (db_wal_checkpoint_truncate() != 0) {
DEBUG_WARN("Writer shutdown TRUNCATE checkpoint failed");
}
db_close_worker_connection(worker_db);
return NULL;
}
@@ -180,10 +568,21 @@ int thread_pool_init(const thread_pool_config_t* config) {
g_pool.next_job_id = 1;
g_pool.wake_loop_cb = config->wake_loop_cb;
g_pool.wake_loop_ctx = config->wake_loop_ctx;
const char* db_path = (config->db_path && config->db_path[0] != '\0') ? config->db_path : db_get_database_path();
if (!db_path || db_path[0] == '\0') {
free(g_pool.readers);
g_pool.readers = NULL;
pthread_mutex_unlock(&g_pool.state_mutex);
return -1;
}
strncpy(g_pool.db_path, db_path, sizeof(g_pool.db_path) - 1);
g_pool.db_path[sizeof(g_pool.db_path) - 1] = '\0';
g_pool.running = 1;
for (int i = 0; i < g_pool.reader_count; i++) {
if (pthread_create(&g_pool.readers[i], NULL, reader_worker_main, NULL) != 0) {
if (pthread_create(&g_pool.readers[i], NULL, reader_worker_main, (void*)(intptr_t)(i + 1)) != 0) {
g_pool.running = 0;
pthread_cond_broadcast(&g_pool.read_q.cond);
pthread_cond_broadcast(&g_pool.write_q.cond);
@@ -270,6 +669,284 @@ 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;
}
if (!thread_pool_is_running()) {
return db_count_with_sql(sql, bind_params, bind_param_count, out_count);
}
thread_pool_count_payload_t* payload = calloc(1, sizeof(*payload));
if (!payload) return -1;
payload->sql = strdup(sql);
payload->bind_param_count = bind_param_count;
if (!payload->sql) {
free_count_payload(payload);
return -1;
}
if (bind_param_count > 0) {
payload->bind_params = calloc((size_t)bind_param_count, sizeof(char*));
if (!payload->bind_params) {
free_count_payload(payload);
return -1;
}
for (int i = 0; i < bind_param_count; i++) {
const char* v = (bind_params && bind_params[i]) ? bind_params[i] : "";
payload->bind_params[i] = strdup(v);
if (!payload->bind_params[i]) {
free_count_payload(payload);
return -1;
}
}
}
thread_pool_wait_ctx_t wait_ctx;
wait_ctx_init(&wait_ctx);
thread_pool_job_t job;
memset(&job, 0, sizeof(job));
job.type = THREAD_POOL_JOB_COUNT_QUERY;
job.payload = payload;
job.payload_free = free_count_payload;
job.result_cb = wait_ctx_result_cb;
job.result_cb_ctx = &wait_ctx;
thread_pool_status_t submit_rc = thread_pool_submit_read(&job, NULL);
if (submit_rc != THREAD_POOL_STATUS_OK) {
free_count_payload(payload);
wait_ctx_destroy(&wait_ctx);
return -1;
}
wait_ctx_wait(&wait_ctx);
int rc = -1;
if (wait_ctx.status == THREAD_POOL_STATUS_OK && wait_ctx.result_data) {
thread_pool_count_result_t* result = (thread_pool_count_result_t*)wait_ctx.result_data;
*out_count = result->count;
free(result);
rc = 0;
}
wait_ctx_destroy(&wait_ctx);
return rc;
}
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) {
if (!sql || !out_result) {
return -1;
}
if (!thread_pool_is_running()) {
db_stmt_t* stmt = NULL;
if (db_prepare(sql, &stmt) != DB_OK) {
return -1;
}
for (int i = 0; i < bind_param_count; i++) {
const char* v = (bind_params && bind_params[i]) ? bind_params[i] : "";
(void)db_bind_text_param(stmt, i + 1, v);
}
int cap = (row_limit > 0) ? row_limit : 500;
if (cap < 1) cap = 1;
thread_pool_req_result_t* result = calloc(1, sizeof(*result));
if (!result) {
db_finalize_stmt(stmt);
return -1;
}
result->event_json_rows = calloc((size_t)cap, sizeof(char*));
if (!result->event_json_rows) {
free(result);
db_finalize_stmt(stmt);
return -1;
}
int step_rc = DB_OK;
while ((step_rc = db_step_stmt(stmt)) == DB_ROW) {
if (result->row_count >= cap) break;
const char* row = db_column_text_value(stmt, 0);
if (!row) continue;
result->event_json_rows[result->row_count] = strdup(row);
if (!result->event_json_rows[result->row_count]) {
break;
}
result->row_count++;
}
db_finalize_stmt(stmt);
*out_result = result;
return 0;
}
thread_pool_req_payload_t* payload = calloc(1, sizeof(*payload));
if (!payload) return -1;
payload->sql = strdup(sql);
payload->bind_param_count = bind_param_count;
payload->row_limit = row_limit;
if (!payload->sql) {
free_req_payload(payload);
return -1;
}
if (bind_param_count > 0) {
payload->bind_params = calloc((size_t)bind_param_count, sizeof(char*));
if (!payload->bind_params) {
free_req_payload(payload);
return -1;
}
for (int i = 0; i < bind_param_count; i++) {
const char* v = (bind_params && bind_params[i]) ? bind_params[i] : "";
payload->bind_params[i] = strdup(v);
if (!payload->bind_params[i]) {
free_req_payload(payload);
return -1;
}
}
}
thread_pool_wait_ctx_t wait_ctx;
wait_ctx_init(&wait_ctx);
thread_pool_job_t job;
memset(&job, 0, sizeof(job));
job.type = THREAD_POOL_JOB_REQ_QUERY;
job.payload = payload;
job.payload_free = free_req_payload;
job.result_cb = wait_ctx_result_cb;
job.result_cb_ctx = &wait_ctx;
thread_pool_status_t submit_rc = thread_pool_submit_read(&job, NULL);
if (submit_rc != THREAD_POOL_STATUS_OK) {
free_req_payload(payload);
wait_ctx_destroy(&wait_ctx);
return -1;
}
wait_ctx_wait(&wait_ctx);
int rc = -1;
if (wait_ctx.status == THREAD_POOL_STATUS_OK && wait_ctx.result_data) {
*out_result = (thread_pool_req_result_t*)wait_ctx.result_data;
rc = 0;
}
wait_ctx_destroy(&wait_ctx);
return rc;
}
void thread_pool_free_req_result(thread_pool_req_result_t* result) {
if (!result) return;
if (result->event_json_rows) {
for (int i = 0; i < result->row_count; i++) {
free(result->event_json_rows[i]);
}
free(result->event_json_rows);
}
free(result);
}
int thread_pool_execute_store_event_sync(const thread_pool_store_event_payload_t* payload,
thread_pool_store_event_result_t* out_result) {
if (!payload || !out_result || !payload->id || !payload->pubkey || !payload->content || !payload->sig) {
return -1;
}
if (!thread_pool_is_running()) {
int step_rc = DB_ERROR;
int extended_errcode = 0;
if (db_insert_event_with_json(payload->id,
payload->pubkey,
payload->created_at,
payload->kind,
payload->event_type ? payload->event_type : "regular",
payload->content,
payload->sig,
payload->tags_json ? payload->tags_json : "[]",
payload->event_json ? payload->event_json : "{}",
&step_rc,
&extended_errcode) != 0) {
return -1;
}
out_result->step_rc = step_rc;
out_result->extended_errcode = extended_errcode;
return 0;
}
thread_pool_store_event_payload_t* payload_copy = calloc(1, sizeof(*payload_copy));
if (!payload_copy) return -1;
payload_copy->id = payload->id ? strdup(payload->id) : NULL;
payload_copy->pubkey = payload->pubkey ? strdup(payload->pubkey) : NULL;
payload_copy->created_at = payload->created_at;
payload_copy->kind = payload->kind;
payload_copy->event_type = payload->event_type ? strdup(payload->event_type) : strdup("regular");
payload_copy->content = payload->content ? strdup(payload->content) : NULL;
payload_copy->sig = payload->sig ? strdup(payload->sig) : NULL;
payload_copy->tags_json = payload->tags_json ? strdup(payload->tags_json) : strdup("[]");
payload_copy->event_json = payload->event_json ? strdup(payload->event_json) : strdup("{}");
if (!payload_copy->id || !payload_copy->pubkey || !payload_copy->event_type ||
!payload_copy->content || !payload_copy->sig || !payload_copy->tags_json || !payload_copy->event_json) {
free_store_event_payload(payload_copy);
return -1;
}
thread_pool_wait_ctx_t wait_ctx;
wait_ctx_init(&wait_ctx);
thread_pool_job_t job;
memset(&job, 0, sizeof(job));
job.type = THREAD_POOL_JOB_STORE_EVENT;
job.payload = payload_copy;
job.payload_free = free_store_event_payload;
job.result_cb = wait_ctx_result_cb;
job.result_cb_ctx = &wait_ctx;
thread_pool_status_t submit_rc = thread_pool_submit_write(&job, NULL);
if (submit_rc != THREAD_POOL_STATUS_OK) {
free_store_event_payload(payload_copy);
wait_ctx_destroy(&wait_ctx);
return -1;
}
wait_ctx_wait(&wait_ctx);
int rc = -1;
if (wait_ctx.status == THREAD_POOL_STATUS_OK && wait_ctx.result_data) {
thread_pool_store_event_result_t* result = (thread_pool_store_event_result_t*)wait_ctx.result_data;
out_result->step_rc = result->step_rc;
out_result->extended_errcode = result->extended_errcode;
free(result);
rc = 0;
}
wait_ctx_destroy(&wait_ctx);
return rc;
}
__attribute__((constructor))
static void thread_pool_state_init_once(void) {
pthread_mutex_init(&g_pool.state_mutex, NULL);
+75
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;
@@ -56,6 +62,66 @@ typedef struct {
void* wake_loop_ctx;
} thread_pool_config_t;
typedef struct {
char* sql;
char** bind_params;
int bind_param_count;
} thread_pool_count_payload_t;
typedef struct {
char* sql;
char** bind_params;
int bind_param_count;
int row_limit;
} thread_pool_req_payload_t;
typedef struct {
char* id;
char* pubkey;
long long created_at;
int kind;
char* event_type;
char* content;
char* sig;
char* tags_json;
char* event_json;
} thread_pool_store_event_payload_t;
typedef struct {
int count;
} thread_pool_count_result_t;
typedef struct {
char** event_json_rows;
int row_count;
} thread_pool_req_result_t;
typedef struct {
int step_rc;
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);
@@ -63,6 +129,15 @@ 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);
void thread_pool_free_req_result(thread_pool_req_result_t* result);
int thread_pool_execute_store_event_sync(const thread_pool_store_event_payload_t* payload,
thread_pool_store_event_result_t* out_result);
#ifdef __cplusplus
}
#endif
+912 -154
View File
File diff suppressed because it is too large Load Diff
+4
View File
@@ -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;
+16 -12
View File
@@ -8,7 +8,7 @@ set -e
# Configuration
RELAY_HOST="127.0.0.1"
RELAY_PORT="8888"
TEST_TIMEOUT=5
TEST_TIMEOUT=10
# Colors for output
RED='\033[0;31m'
@@ -34,7 +34,7 @@ test_websocket_message() {
# Send message via websocat and capture response
local response
response=$(echo "$message" | timeout $TEST_TIMEOUT websocat -B 1048576 ws://$RELAY_HOST:$RELAY_PORT 2>/dev/null || echo 'CONNECTION_FAILED')
response=$(echo "$message" | timeout $TEST_TIMEOUT websocat -B 1048576 ws://$RELAY_HOST:$RELAY_PORT 2>/dev/null | head -1 || echo 'CONNECTION_FAILED')
if [[ "$response" == "CONNECTION_FAILED" ]]; then
echo -e "${RED}FAILED${NC} - Could not connect to relay"
@@ -42,8 +42,8 @@ test_websocket_message() {
return 1
fi
if [[ "$response" == "TIMEOUT" ]]; then
echo -e "${RED}FAILED${NC} - Connection timeout"
if [[ -z "$response" ]]; then
echo -e "${RED}FAILED${NC} - Empty response"
FAILED_TESTS=$((FAILED_TESTS + 1))
return 1
fi
@@ -119,8 +119,9 @@ test_websocket_message "Invalid author type" '["REQ","sub1",{"authors":[123]}]'
# Test 6: Invalid author hex
test_websocket_message "Invalid author hex" '["REQ","sub1",{"authors":["invalid-hex"]}]' "error: invalid author hex string"
# Test 7: Too many authors
test_websocket_message "Too many authors" '["REQ","sub1",{"authors":["a","a","a","a","a","a","a","a","a","a","a","a","a","a","a","a","a","a","a","a","a","a","a","a","a","a","a","a","a","a","a","a","a","a","a","a","a","a","a","a","a","a","a","a","a","a","a","a","a","a","a","a","a","a","a","a","a","a","a","a","a","a","a","a","a","a","a","a","a","a","a","a","a","a","a","a","a","a","a","a","a","a","a","a","a","a","a","a","a","a","a","a","a","a","a","a","a","a","a","a","a","a","a","a","a","a","a","a","a","a","a"]}]' "error: too many authors"
# Test 7: Too many authors (exceed MAX_AUTHORS_PER_FILTER with compact payload)
too_many_authors=$(yes '"a"' | head -n 1001 | paste -sd, -)
test_websocket_message "Too many authors" "[\"REQ\",\"sub1\",{\"authors\":[${too_many_authors}]}]" "error: too many authors"
echo
echo "=== Testing IDs Validation ==="
@@ -131,8 +132,9 @@ test_websocket_message "Invalid ID type" '["REQ","sub1",{"ids":[123]}]' "error:
# Test 9: Invalid ID hex
test_websocket_message "Invalid ID hex" '["REQ","sub1",{"ids":["invalid-hex"]}]' "error: invalid id hex string"
# Test 10: Too many IDs
test_websocket_message "Too many IDs" '["REQ","sub1",{"ids":["a","a","a","a","a","a","a","a","a","a","a","a","a","a","a","a","a","a","a","a","a","a","a","a","a","a","a","a","a","a","a","a","a","a","a","a","a","a","a","a","a","a","a","a","a","a","a","a","a","a","a","a","a","a","a","a","a","a","a","a","a","a","a","a","a","a","a","a","a","a","a","a","a","a","a","a","a","a","a","a","a","a","a","a","a","a","a","a","a","a","a","a","a","a","a","a","a","a","a","a","a","a","a","a","a","a","a","a","a","a","a"]}]' "error: too many ids"
# Test 10: Too many IDs (exceed MAX_IDS_PER_FILTER with compact payload)
too_many_ids=$(yes '"a"' | head -n 1001 | paste -sd, -)
test_websocket_message "Too many IDs" "[\"REQ\",\"sub1\",{\"ids\":[${too_many_ids}]}]" "error: too many ids"
echo
echo "=== Testing Kinds Validation ==="
@@ -146,8 +148,9 @@ test_websocket_message "Negative kind" '["REQ","sub1",{"kinds":[-1]}]' "error: i
# Test 13: Too large kind
test_websocket_message "Too large kind" '["REQ","sub1",{"kinds":[70000]}]' "error: invalid kind value"
# Test 14: Too many kinds
test_websocket_message "Too many kinds" '["REQ","sub1",{"kinds":[1,2,3,4,5,6,7,8,9,10,11,12,13,14,15,16,17,18,19,20,21,22,23,24,25,26,27,28,29,30,31,32,33,34,35,36,37,38,39,40,41,42,43,44,45,46,47,48,49,50,51,52]}]' "error: too many kinds"
# Test 14: Too many kinds (exceed MAX_KINDS_PER_FILTER=500)
too_many_kinds=$(seq -s, 1 501)
test_websocket_message "Too many kinds" "[\"REQ\",\"sub1\",{\"kinds\":[${too_many_kinds}]}]" "error: too many kinds"
echo
echo "=== Testing Timestamp Validation ==="
@@ -194,8 +197,9 @@ echo "=== Testing Tag Filter Validation ==="
# Test 25: Invalid tag filter type
test_websocket_message "Invalid tag filter type" '["REQ","sub1",{"#e":"not-an-array"}]' "error: #e must be an array"
# Test 26: Too many tag values
test_websocket_message "Too many tag values" '["REQ","sub1",{"#e":['$(printf '"a%.0s",' {1..101})'"a"]}]' "error: too many #e values"
# Test 26: Too many tag values (exceed MAX_TAG_VALUES_PER_FILTER=1000)
too_many_tag_values=$(yes '"a"' | head -n 1001 | paste -sd, -)
test_websocket_message "Too many tag values" "[\"REQ\",\"sub1\",{\"#e\":[${too_many_tag_values}]}]" "error: too many #e values"
# Test 27: Tag value too long
test_websocket_message "Tag value too long" '["REQ","sub1",{"#e":["'$(printf 'a%.0s' {1..1025})'"]}]' "error: #e value too long"
+26 -26
View File
@@ -1,5 +1,5 @@
=== NIP-42 Authentication Test Started ===
2026-04-01 05:46:11 - Starting NIP-42 authentication tests
2026-04-01 10:00:44 - Starting NIP-42 authentication tests
[INFO] === Starting NIP-42 Authentication Tests ===
[INFO] Checking dependencies...
[WARNING] wscat not found. Some manual WebSocket tests will be skipped
@@ -7,7 +7,7 @@
[SUCCESS] Dependencies check complete
[INFO] Test 1: Checking NIP-42 support in relay info
[SUCCESS] NIP-42 is advertised in supported NIPs
2026-04-01 05:46:11 - Supported NIPs: 1,2,4,9,11,12,13,15,16,20,22,33,40,42,50,70
2026-04-01 10:00:44 - Supported NIPs: 1,2,4,9,11,12,13,15,16,20,22,33,40,42,50,70
[INFO] Test 2: Testing AUTH challenge generation
[WARNING] Could not extract admin private key from relay.log - using manual test approach
[INFO] Manual test: Connect to relay and send an event without auth to trigger challenge
@@ -15,8 +15,8 @@
[INFO] Generated test keypair: test_pubkey
[INFO] Attempting to publish event without authentication...
[INFO] Publishing test event to relay...
2026-04-01 05:46:12 - Event publish result: connecting to localhost:8888... ok.
{"kind":1,"id":"334754e42c2bd54bdf733fb2c9a28c1e0f33d2275ca2c70f26062187793b37fe","pubkey":"40d9f28055baeba6a9bcac1942a17370add58a95a46985f66c5c14d485573983","created_at":1775036772,"tags":[],"content":"NIP-42 test event - should require auth","sig":"6e81c7335d34a770c72566250ac363aa5a126383a1efe2effe5b4b7e84bf600c4d9531b26e9be188fe884e9bd32aa384d49739815c7fba4594174277b90cbf7d"}
2026-04-01 10:00:45 - Event publish result: connecting to localhost:8888... ok.
{"kind":1,"id":"28174ed72f9ef5484a6596db6c6be181d2ed10e46d8c22a9f7a267becbc9a47a","pubkey":"b5f9adc3b362c0e81569a46648a3996708d6b47754d36c617683f18e1f1daf6b","created_at":1775052045,"tags":[],"content":"NIP-42 test event - should require auth","sig":"0041347b233a21d6542a263634f52f8ca7e3b5ecf07bc37c86f390f1a7ab0bc2051306b14aa70edff020bd7311b259d03c2c35a035b285f9e19051a924546666"}
publishing to ws://localhost:8888... success.
[SUCCESS] Relay requested authentication as expected
[INFO] Test 4: Testing WebSocket AUTH message handling
@@ -26,50 +26,50 @@ publishing to ws://localhost:8888... success.
[WARNING] Could not retrieve configuration events
[INFO] Test 6: Testing NIP-42 performance and stability
[INFO] Testing multiple authentication attempts...
2026-04-01 05:46:13 - Attempt 1: .187311847s - connecting to localhost:8888... ok.
{"kind":1,"id":"470a74d32e1b739e6bfd275c0d03ab1508fb5908c0fa01523e4a3bdce6a95db3","pubkey":"59d792f5cfd57b3a284ce769b5d4b1e8a6eb231c63239cdad7f07d791433e79a","created_at":1775036773,"tags":[],"content":"Performance test event 1","sig":"0c30a125cb67d336413f006a5dfa422d788e177254e496b966854bf45183238d23f6575dba37dec6fa46b6f6b906a25afafcbbcaa7d0c15add89dd7fd1a80c22"}
2026-04-01 10:00:46 - Attempt 1: .195528002s - connecting to localhost:8888... ok.
{"kind":1,"id":"72526c57e50d721bfcdcca23856f2c5d3021c3100f0121538d9d829042e9b8b5","pubkey":"29af457d7eea750c11c836dfbeed3076fa0bf1f1b67a404ee4c25733d9b54238","created_at":1775052046,"tags":[],"content":"Performance test event 1","sig":"1a10db86a39aaf47bda6d0cca5f888691bae32bfd85df8c178dddbf24861503a491bfe4da8d66dfe3e5e0afec7bf8644e7eb69c9f051c601673ae13284f9ab31"}
publishing to ws://localhost:8888... success.
2026-04-01 05:46:13 - Attempt 2: .185565503s - connecting to localhost:8888... ok.
{"kind":1,"id":"e4f9db77d30d8e71ef01b16dcb1179d03462aaf368aae30120898f3758a9d0b9","pubkey":"59d792f5cfd57b3a284ce769b5d4b1e8a6eb231c63239cdad7f07d791433e79a","created_at":1775036773,"tags":[],"content":"Performance test event 2","sig":"24a8f38c2cb230d95120e60a246a1a03ac17d4d195d25f2ae0b2f674c3383287eca53cd7fc61bc3cdc58433a3d2c2108558f4f1a98dcfc02a97b4ec8c537cdde"}
2026-04-01 10:00:46 - Attempt 2: .185198180s - connecting to localhost:8888... ok.
{"kind":1,"id":"84eeb907205806bb88fc353bb83f1bf0149f3c93c18695306d8480d36752878b","pubkey":"29af457d7eea750c11c836dfbeed3076fa0bf1f1b67a404ee4c25733d9b54238","created_at":1775052046,"tags":[],"content":"Performance test event 2","sig":"e568dbca3113966f61e69eff05576d296fac42b5ff2ab7341778acc02cf6e9440b59bf651a8a2995d89180a62ec20ffc1143e211cf2960c5abaa358a82992ac6"}
publishing to ws://localhost:8888... success.
2026-04-01 05:46:14 - Attempt 3: .185110183s - connecting to localhost:8888... ok.
{"kind":1,"id":"4a85e466756452e90358fe5d98fa41bda93b6bc77cb079bb1616ca8873f4fca1","pubkey":"59d792f5cfd57b3a284ce769b5d4b1e8a6eb231c63239cdad7f07d791433e79a","created_at":1775036774,"tags":[],"content":"Performance test event 3","sig":"72ab716ff3b4786309fd4cef27ae8cd9df1e3c1746b5e49ba45c070cafcfb72a5b7ba045e7d798f23b5d82989526b4d3c818509f0eec639c8ad265be85a4728e"}
2026-04-01 10:00:47 - Attempt 3: .197159540s - connecting to localhost:8888... ok.
{"kind":1,"id":"0874716ba0b2a845d379e703226513f90db6228ce958369f4dce5aff3df64c5a","pubkey":"29af457d7eea750c11c836dfbeed3076fa0bf1f1b67a404ee4c25733d9b54238","created_at":1775052046,"tags":[],"content":"Performance test event 3","sig":"e74e885eb464998bddf655c24c95a18d7299908c63fb82a47f8d14af65992f90e73794a9f289b550bc1dec1c0298112d2b25941be7e26e3f17dd66b4c5dc742e"}
publishing to ws://localhost:8888... success.
2026-04-01 05:46:14 - Attempt 4: .185725746s - connecting to localhost:8888... ok.
{"kind":1,"id":"4c05b6359da88f85c0aa4c94a296a465f71bddaa10d849ed0923f972ac0628a9","pubkey":"59d792f5cfd57b3a284ce769b5d4b1e8a6eb231c63239cdad7f07d791433e79a","created_at":1775036774,"tags":[],"content":"Performance test event 4","sig":"3b0c8bd1f3d965a259a0ab228ea4c47defef3ad563ebce860ec65ab0ee341c5305c0988ac88651664d3090ce2ff282a0fe4913231448158c423a74b6e3b2a7a9"}
2026-04-01 10:00:47 - Attempt 4: .202520319s - connecting to localhost:8888... ok.
{"kind":1,"id":"5614d1b795cd028e096720418bb4dbc64870d442c6295b9bb2931564a332099a","pubkey":"29af457d7eea750c11c836dfbeed3076fa0bf1f1b67a404ee4c25733d9b54238","created_at":1775052047,"tags":[],"content":"Performance test event 4","sig":"1c0645a791ab9df573b152b782884bcb5ec2a18b8f4e1c18d7dc2175977b7d1d572674169ea1d2b3e3fa8d228de90f6cc281d56a3ef471ba4f9510e920ed586e"}
publishing to ws://localhost:8888... success.
2026-04-01 05:46:15 - Attempt 5: .184467365s - connecting to localhost:8888... ok.
{"kind":1,"id":"0902e868c5e7909f0143fde7738d5e8f4c42ea0ced2c69a404cfeb6cd686ceaa","pubkey":"59d792f5cfd57b3a284ce769b5d4b1e8a6eb231c63239cdad7f07d791433e79a","created_at":1775036774,"tags":[],"content":"Performance test event 5","sig":"a0754e774e5bc3514dc4cf04ec51cab077413251cfce480e80badccc7660bfe8d1eac87d43c6e332d936cc171d24e8318e1ef5df7425a8218e01af661f291c47"}
2026-04-01 10:00:47 - Attempt 5: .222530325s - connecting to localhost:8888... ok.
{"kind":1,"id":"9139f1eb39c31c43cb9f02e60a0f75a0f99f6c7d9693a69a225c8b3e530f774a","pubkey":"29af457d7eea750c11c836dfbeed3076fa0bf1f1b67a404ee4c25733d9b54238","created_at":1775052047,"tags":[],"content":"Performance test event 5","sig":"bd245a7629c33619c0ed426f631e0c2413adb4488c281bea09886ceb6407b40e247d42f82e374e2f53ee5281f0d8361661a4c613c6414c59d09e5bb23eeeb02d"}
publishing to ws://localhost:8888... success.
[SUCCESS] Performance test completed: 5/5 successful responses
[INFO] Test 7: Testing kind-specific NIP-42 authentication requirements
[INFO] Generated test keypair for kind-specific tests: test_pubkey
[INFO] Testing kind 1 event (regular note) - should work without authentication...
2026-04-01 05:46:15 - Kind 1 event result: connecting to localhost:8888... ok.
{"kind":1,"id":"a1d9545c4ac16c2ab1fae4d5d77cabd368d765533b6f6d1f4dbb3c7859696a5e","pubkey":"06ff6b7ca3c72cc7cebb9fe70ea76b6f79b6226c4dc5adc4ebcc59220c23c9bf","created_at":1775036775,"tags":[],"content":"Regular note - should not require auth","sig":"520796b21690bf03b20da28f30ffc84c9c198d0a908c07540f232f789c7a0327861e1f580d499be15b244bdf66c5c4b1cdf621bfdc5f60b1313d7f40036ffab8"}
2026-04-01 10:00:48 - Kind 1 event result: connecting to localhost:8888... ok.
{"kind":1,"id":"def386c66576a91f144a7321e9520a3ed46f9fecc56d9e79d32a162f249cace9","pubkey":"7c9b00749c966e50d18d4b807b44d4612bf9a8f67925fac3f00d21918b6e7f9c","created_at":1775052048,"tags":[],"content":"Regular note - should not require auth","sig":"7ee8fbf45e96d040df1a688e9bf028faaa49b81b26eeae6452b3a32cd9fd941c27b41734f14268d3deef14dbb19e514d29789211fffb27bfa9b73d20cddcc83e"}
publishing to ws://localhost:8888... success.
[SUCCESS] Kind 1 event accepted without authentication (correct behavior)
[INFO] Testing kind 4 event (direct message) - should require authentication...
2026-04-01 05:46:25 - Kind 4 event result: connecting to localhost:8888... ok.
{"kind":4,"id":"c047f5a9fb6ff46828d0bb862e4bcc32120e50fc7804d651485e2cdbddaf993a","pubkey":"06ff6b7ca3c72cc7cebb9fe70ea76b6f79b6226c4dc5adc4ebcc59220c23c9bf","created_at":1775036775,"tags":[["p,test_pubkey"]],"content":"This is a direct message - should require auth","sig":"a7a027c302faf75e269d866a1b0137f2fbc02643d76435dc68707d2ba6560af95df709787f4a1a1bc7c1b9951615ad081a97cd7f694da94d5606d17100f0b0dc"}
2026-04-01 10:00:58 - Kind 4 event result: connecting to localhost:8888... ok.
{"kind":4,"id":"a2f2d176a835d17a0c9229c0ce3b1ba61a5eb81308e4a3b3446634f468721238","pubkey":"7c9b00749c966e50d18d4b807b44d4612bf9a8f67925fac3f00d21918b6e7f9c","created_at":1775052048,"tags":[["p,test_pubkey"]],"content":"This is a direct message - should require auth","sig":"441f9caba6ebcec6b6c55d13ffd9571f3fe441a54ddd0692f2868429a5087378d791c97289b8b990560dcf40f5c66e86ba6b41f1de78220a5f389c1f119bb887"}
publishing to ws://localhost:8888...
[SUCCESS] Kind 4 event requested authentication (correct behavior for DMs)
[INFO] Testing kind 14 event (chat message) - should require authentication...
2026-04-01 05:46:36 - Kind 14 event result: connecting to localhost:8888... ok.
{"kind":14,"id":"73e50afdaefdbb01ed021f8c888a87b00aa59e4d68c124101353fabcdfa59ae1","pubkey":"06ff6b7ca3c72cc7cebb9fe70ea76b6f79b6226c4dc5adc4ebcc59220c23c9bf","created_at":1775036786,"tags":[["p,test_pubkey"]],"content":"Chat message - should require auth","sig":"ec2c713e2add876856e6cb0d1d9790c43525c5139a0fe06fbee71898f1042173dc3af9187e6fbccfef7406c1af2f218631a1cdcb7ad73e34553cdf3aecc4f209"}
2026-04-01 10:01:09 - Kind 14 event result: connecting to localhost:8888... ok.
{"kind":14,"id":"e234a6faa63f95cd24f8596fbfac573730533bb0abcde47b4d341ff0acace657","pubkey":"7c9b00749c966e50d18d4b807b44d4612bf9a8f67925fac3f00d21918b6e7f9c","created_at":1775052059,"tags":[["p,test_pubkey"]],"content":"Chat message - should require auth","sig":"fd6ba0ddaa7e2379aa49c5cda6599d0a38264799b228a8e907b948bef645080603f7f910c587f6cfb1ef1bac946c47dc681fdab6abd248376cd552bcd535ef9e"}
publishing to ws://localhost:8888...
[SUCCESS] Kind 14 event requested authentication (correct behavior for DMs)
[INFO] Testing other event kinds - should work without authentication...
2026-04-01 05:46:36 - Kind 0 event result: connecting to localhost:8888... ok.
{"kind":0,"id":"51fe1630721cfa3472ba7c3af911a3164c788a48c6a678179c62be48ac3d1f1c","pubkey":"06ff6b7ca3c72cc7cebb9fe70ea76b6f79b6226c4dc5adc4ebcc59220c23c9bf","created_at":1775036796,"tags":[],"content":"Test event kind 0 - should not require auth","sig":"d72d827742fdc86daf1389c8dad40efc56c22b9744ad23547ea0b0a37598e5c18ccdac20b9ef2e6adf46d7683bff19f2cbc11fb00a252903efc1076c8f613b38"}
2026-04-01 10:01:09 - Kind 0 event result: connecting to localhost:8888... ok.
{"kind":0,"id":"d87b69e8a2c8f6d98229b55cde3f0fe1b70c6cafd080e4462209849338eafc76","pubkey":"7c9b00749c966e50d18d4b807b44d4612bf9a8f67925fac3f00d21918b6e7f9c","created_at":1775052069,"tags":[],"content":"Test event kind 0 - should not require auth","sig":"e3222bd4882c881d039e6790a6a8d9f8e20a71d0f9d5089e98604acaa4feff7c1b1b2d9b4171d640da3bba955408a9391a0fd52334cc4a4165fa932e5163f9dd"}
publishing to ws://localhost:8888... success.
[SUCCESS] Kind 0 event accepted without authentication (correct)
2026-04-01 05:46:36 - Kind 3 event result: connecting to localhost:8888... ok.
{"kind":3,"id":"bf4155eb8b54bdd73a0b64365666caf886433634693cc3c4ab1d586187fc6bae","pubkey":"06ff6b7ca3c72cc7cebb9fe70ea76b6f79b6226c4dc5adc4ebcc59220c23c9bf","created_at":1775036796,"tags":[],"content":"Test event kind 3 - should not require auth","sig":"d27f654ccc9f95e76aacafcce7d42528545fa90b77d422a17301de6931883d8c00a0a84af56ab5d37a0f425140dbfc227458557f9050597beec42cacd0a15864"}
2026-04-01 10:01:09 - Kind 3 event result: connecting to localhost:8888... ok.
{"kind":3,"id":"ac0d45d7db2818bb5e674d4760711349928cf3d5dc8f1fc3717d9e7274e26fb0","pubkey":"7c9b00749c966e50d18d4b807b44d4612bf9a8f67925fac3f00d21918b6e7f9c","created_at":1775052069,"tags":[],"content":"Test event kind 3 - should not require auth","sig":"b65b317c81bc733ee2ff5921430b34d722e8d177147c5d1cddf14036573223582a3b7232810fc13169eb7a61ab3b99cd37ce13991673fce142d1ea10c702305e"}
publishing to ws://localhost:8888... success.
[SUCCESS] Kind 3 event accepted without authentication (correct)
2026-04-01 05:46:37 - Kind 7 event result: connecting to localhost:8888... ok.
{"kind":7,"id":"658e0caf1c4b52062363849d55312070005124e8a10202df285078222e523132","pubkey":"06ff6b7ca3c72cc7cebb9fe70ea76b6f79b6226c4dc5adc4ebcc59220c23c9bf","created_at":1775036797,"tags":[],"content":"Test event kind 7 - should not require auth","sig":"50361b3aa6b85747befe20e50e317d8f905128930cec0263494434ec9029089d880ff23cdf071c5b63b8417eff216658f1229c0f0dc5174a9a16a6d5fe38fd4d"}
2026-04-01 10:01:10 - Kind 7 event result: connecting to localhost:8888... ok.
{"kind":7,"id":"ad7e77948a4742abfca2d63c5bb748f3db3940c85841f521b3b7d634bf09955a","pubkey":"7c9b00749c966e50d18d4b807b44d4612bf9a8f67925fac3f00d21918b6e7f9c","created_at":1775052070,"tags":[],"content":"Test event kind 7 - should not require auth","sig":"8a5e5a03956cec04d56934314a0876fccbd221546432aaccdbc249b5f9cedc221219abf6e2856b98675de205afaa0e91ea642933ec6724576a40db43d17aa758"}
publishing to ws://localhost:8888... success.
[SUCCESS] Kind 7 event accepted without authentication (correct)
[INFO] Kind-specific authentication test completed
+392
View File
@@ -0,0 +1,392 @@
2026-04-01 09:11:07 - ==========================================
2026-04-01 09:11:07 - C-Relay Comprehensive Test Suite Runner
2026-04-01 09:11:07 - ==========================================
2026-04-01 09:11:07 - Relay URL: ws://127.0.0.1:8888
2026-04-01 09:11:07 - Log file: test_results_20260401_091107.log
2026-04-01 09:11:07 - Report file: test_report_20260401_091107.html
2026-04-01 09:11:07 -
2026-04-01 09:11:07 - Checking relay status at ws://127.0.0.1:8888...
2026-04-01 09:11:07 - \033[0;32m✓ Relay HTTP endpoint is accessible\033[0m
2026-04-01 09:11:07 -
2026-04-01 09:11:07 - Starting comprehensive test execution...
2026-04-01 09:11:07 -
2026-04-01 09:11:07 - \033[0;34m=== SECURITY TEST SUITES ===\033[0m
2026-04-01 09:11:07 - ==========================================
2026-04-01 09:11:07 - Running Test Suite: SQL Injection Tests
2026-04-01 09:11:07 - Description: Comprehensive SQL injection vulnerability testing
2026-04-01 09:11:07 - ==========================================
==========================================
C-Relay SQL Injection Test Suite
==========================================
Testing against relay at ws://127.0.0.1:8888
=== Basic Connectivity Test ===
Testing Basic connectivity... PASSED - Valid query works
=== Authors Filter SQL Injection Tests ===
Testing Authors filter with payload: '; DROP TABLE events; --... PASSED - SQL injection blocked (rejected with error)
Testing Authors filter with payload: 1' OR '1'='1... PASSED - SQL injection blocked (rejected with error)
Testing Authors filter with payload: 1; SELECT * FROM sqlite_master; --... PASSED - SQL injection blocked (rejected with error)
Testing Authors filter with payload: ' OR 1=1 --... PASSED - SQL injection blocked (rejected with error)
Testing Authors filter with payload: admin'--... PASSED - SQL injection blocked (rejected with error)
Testing Authors filter with payload: 1' UNION SELECT password FROM users --... PASSED - SQL injection blocked (rejected with error)
Testing Authors filter with payload: 1' AND SLEEP(5) --... PASSED - SQL injection blocked (rejected with error)
Testing Authors filter with payload: 1' AND 1=1 WAITFOR DELAY '0:0:5' --... PASSED - SQL injection blocked (rejected with error)
Testing Authors filter with payload: 1' AND 1=CAST((SELECT version()) AS INT) --... PASSED - SQL injection blocked (rejected with error)
Testing Authors filter with payload: 1' AND 1=CONVERT(INT, (SELECT @@VERSION)) --... PASSED - SQL injection blocked (rejected with error)
Testing Authors filter with payload: ' UNION SELECT NULL,NULL,NULL --... PASSED - SQL injection blocked (rejected with error)
Testing Authors filter with payload: ' UNION SELECT 1,2,3 --... PASSED - SQL injection blocked (rejected with error)
Testing Authors filter with payload: ' UNION ALL SELECT NULL,NULL,NULL --... PASSED - SQL injection blocked (rejected with error)
Testing Authors filter with payload: '; SELECT * FROM events; --... PASSED - SQL injection blocked (rejected with error)
Testing Authors filter with payload: '; DELETE FROM events; --... PASSED - SQL injection blocked (rejected with error)
Testing Authors filter with payload: '; UPDATE events SET content='hacked' WHERE 1=1; --... PASSED - SQL injection blocked (rejected with error)
Testing Authors filter with payload: /*... PASSED - SQL injection blocked (rejected with error)
Testing Authors filter with payload: */... PASSED - SQL injection blocked (rejected with error)
Testing Authors filter with payload: /**/... PASSED - SQL injection blocked (rejected with error)
Testing Authors filter with payload: --... PASSED - SQL injection blocked (rejected with error)
Testing Authors filter with payload: #... PASSED - SQL injection blocked (rejected with error)
Testing Authors filter with payload: 0x53514C5F494E4A454354494F4E... PASSED - SQL injection blocked (rejected with error)
Testing Authors filter with payload: J1NSTCBJTkpFQ1RJT04gLS0=... PASSED - SQL injection blocked (rejected with error)
Testing Authors filter with payload: '))); DROP TABLE events; --... PASSED - SQL injection blocked (rejected with error)
Testing Authors filter with payload: ')) UNION SELECT NULL; --... PASSED - SQL injection blocked (rejected with error)
Testing Authors filter with payload: ' AND 1=1 --... PASSED - SQL injection blocked (rejected with error)
Testing Authors filter with payload: ' AND 1=2 --... PASSED - SQL injection blocked (rejected with error)
Testing Authors filter with payload: ' AND (SELECT COUNT(*) FROM events) > 0 --... PASSED - SQL injection blocked (rejected with error)
Testing Authors filter with payload: '; EXEC master..xp_cmdshell 'net user' --... PASSED - SQL injection blocked (rejected with error)
Testing Authors filter with payload: '; DECLARE @host varchar(1024); SELECT @host=(SELECT TOP 1 master..sys.fn_varbintohexstr(password_hash) FROM sys.sql_logins WHERE name='sa'); --... PASSED - SQL injection blocked (rejected with error)
=== IDs Filter SQL Injection Tests ===
Testing IDs filter with payload: '; DROP TABLE events; --... PASSED - SQL injection blocked (rejected with error)
Testing IDs filter with payload: 1' OR '1'='1... PASSED - SQL injection blocked (rejected with error)
Testing IDs filter with payload: 1; SELECT * FROM sqlite_master; --... PASSED - SQL injection blocked (rejected with error)
Testing IDs filter with payload: ' OR 1=1 --... PASSED - SQL injection blocked (rejected with error)
Testing IDs filter with payload: admin'--... PASSED - SQL injection blocked (rejected with error)
Testing IDs filter with payload: 1' UNION SELECT password FROM users --... PASSED - SQL injection blocked (rejected with error)
Testing IDs filter with payload: 1' AND SLEEP(5) --... PASSED - SQL injection blocked (rejected with error)
Testing IDs filter with payload: 1' AND 1=1 WAITFOR DELAY '0:0:5' --... PASSED - SQL injection blocked (rejected with error)
Testing IDs filter with payload: 1' AND 1=CAST((SELECT version()) AS INT) --... PASSED - SQL injection blocked (rejected with error)
Testing IDs filter with payload: 1' AND 1=CONVERT(INT, (SELECT @@VERSION)) --... PASSED - SQL injection blocked (rejected with error)
Testing IDs filter with payload: ' UNION SELECT NULL,NULL,NULL --... PASSED - SQL injection blocked (rejected with error)
Testing IDs filter with payload: ' UNION SELECT 1,2,3 --... PASSED - SQL injection blocked (rejected with error)
Testing IDs filter with payload: ' UNION ALL SELECT NULL,NULL,NULL --... PASSED - SQL injection blocked (rejected with error)
Testing IDs filter with payload: '; SELECT * FROM events; --... PASSED - SQL injection blocked (rejected with error)
Testing IDs filter with payload: '; DELETE FROM events; --... PASSED - SQL injection blocked (rejected with error)
Testing IDs filter with payload: '; UPDATE events SET content='hacked' WHERE 1=1; --... PASSED - SQL injection blocked (rejected with error)
Testing IDs filter with payload: /*... PASSED - SQL injection blocked (rejected with error)
Testing IDs filter with payload: */... PASSED - SQL injection blocked (rejected with error)
Testing IDs filter with payload: /**/... PASSED - SQL injection blocked (rejected with error)
Testing IDs filter with payload: --... PASSED - SQL injection blocked (rejected with error)
Testing IDs filter with payload: #... PASSED - SQL injection blocked (rejected with error)
Testing IDs filter with payload: 0x53514C5F494E4A454354494F4E... PASSED - SQL injection blocked (rejected with error)
Testing IDs filter with payload: J1NSTCBJTkpFQ1RJT04gLS0=... PASSED - SQL injection blocked (rejected with error)
Testing IDs filter with payload: '))); DROP TABLE events; --... PASSED - SQL injection blocked (rejected with error)
Testing IDs filter with payload: ')) UNION SELECT NULL; --... PASSED - SQL injection blocked (rejected with error)
Testing IDs filter with payload: ' AND 1=1 --... PASSED - SQL injection blocked (rejected with error)
Testing IDs filter with payload: ' AND 1=2 --... PASSED - SQL injection blocked (rejected with error)
Testing IDs filter with payload: ' AND (SELECT COUNT(*) FROM events) > 0 --... PASSED - SQL injection blocked (rejected with error)
Testing IDs filter with payload: '; EXEC master..xp_cmdshell 'net user' --... PASSED - SQL injection blocked (rejected with error)
Testing IDs filter with payload: '; DECLARE @host varchar(1024); SELECT @host=(SELECT TOP 1 master..sys.fn_varbintohexstr(password_hash) FROM sys.sql_logins WHERE name='sa'); --... PASSED - SQL injection blocked (rejected with error)
=== Kinds Filter SQL Injection Tests ===
Testing Kinds filter with string injection... PASSED - SQL injection blocked (rejected with error)
Testing Kinds filter with negative value... PASSED - SQL injection blocked (rejected with error)
Testing Kinds filter with very large value... PASSED - SQL injection blocked (rejected with error)
=== Search Filter SQL Injection Tests ===
Testing Search filter with payload: '; DROP TABLE events; --... PASSED - SQL injection blocked (rejected with error)
Testing Search filter with payload: 1' OR '1'='1... PASSED - SQL injection blocked (query sanitized)
Testing Search filter with payload: 1; SELECT * FROM sqlite_master; --... PASSED - SQL injection blocked (rejected with error)
Testing Search filter with payload: ' OR 1=1 --... PASSED - SQL injection blocked (rejected with error)
Testing Search filter with payload: admin'--... PASSED - SQL injection blocked (rejected with error)
Testing Search filter with payload: 1' UNION SELECT password FROM users --... PASSED - SQL injection blocked (rejected with error)
Testing Search filter with payload: 1' AND SLEEP(5) --... PASSED - SQL injection blocked (rejected with error)
Testing Search filter with payload: 1' AND 1=1 WAITFOR DELAY '0:0:5' --... PASSED - SQL injection blocked (rejected with error)
Testing Search filter with payload: 1' AND 1=CAST((SELECT version()) AS INT) --... PASSED - SQL injection blocked (rejected with error)
Testing Search filter with payload: 1' AND 1=CONVERT(INT, (SELECT @@VERSION)) --... PASSED - SQL injection blocked (rejected with error)
Testing Search filter with payload: ' UNION SELECT NULL,NULL,NULL --... PASSED - SQL injection blocked (rejected with error)
Testing Search filter with payload: ' UNION SELECT 1,2,3 --... PASSED - SQL injection blocked (rejected with error)
Testing Search filter with payload: ' UNION ALL SELECT NULL,NULL,NULL --... PASSED - SQL injection blocked (rejected with error)
Testing Search filter with payload: '; SELECT * FROM events; --... PASSED - SQL injection blocked (rejected with error)
Testing Search filter with payload: '; DELETE FROM events; --... PASSED - SQL injection blocked (rejected with error)
Testing Search filter with payload: '; UPDATE events SET content='hacked' WHERE 1=1; --... PASSED - SQL injection blocked (rejected with error)
Testing Search filter with payload: /*... PASSED - SQL injection blocked (rejected with error)
Testing Search filter with payload: */... PASSED - SQL injection blocked (rejected with error)
Testing Search filter with payload: /**/... PASSED - SQL injection blocked (rejected with error)
Testing Search filter with payload: --... PASSED - SQL injection blocked (rejected with error)
Testing Search filter with payload: #... PASSED - SQL injection blocked (query sanitized)
Testing Search filter with payload: 0x53514C5F494E4A454354494F4E... PASSED - SQL injection blocked (query sanitized)
Testing Search filter with payload: J1NSTCBJTkpFQ1RJT04gLS0=... PASSED - SQL injection blocked (query sanitized)
Testing Search filter with payload: '))); DROP TABLE events; --... PASSED - SQL injection blocked (rejected with error)
Testing Search filter with payload: ')) UNION SELECT NULL; --... PASSED - SQL injection blocked (rejected with error)
Testing Search filter with payload: ' AND 1=1 --... PASSED - SQL injection blocked (rejected with error)
Testing Search filter with payload: ' AND 1=2 --... PASSED - SQL injection blocked (rejected with error)
Testing Search filter with payload: ' AND (SELECT COUNT(*) FROM events) > 0 --... PASSED - SQL injection blocked (rejected with error)
Testing Search filter with payload: '; EXEC master..xp_cmdshell 'net user' --... PASSED - SQL injection blocked (rejected with error)
Testing Search filter with payload: '; DECLARE @host varchar(1024); SELECT @host=(SELECT TOP 1 master..sys.fn_varbintohexstr(password_hash) FROM sys.sql_logins WHERE name='sa'); --... PASSED - SQL injection blocked (rejected with error)
=== Tag Filter SQL Injection Tests ===
Testing #e tag filter with payload: '; DROP TABLE events; --... PASSED - SQL injection blocked (query sanitized)
Testing #e tag filter with payload: 1' OR '1'='1... PASSED - SQL injection blocked (query sanitized)
Testing #e tag filter with payload: 1; SELECT * FROM sqlite_master; --... PASSED - SQL injection blocked (query sanitized)
Testing #e tag filter with payload: ' OR 1=1 --... PASSED - SQL injection blocked (query sanitized)
Testing #e tag filter with payload: admin'--... PASSED - SQL injection blocked (query sanitized)
Testing #e tag filter with payload: 1' UNION SELECT password FROM users --... PASSED - SQL injection blocked (query sanitized)
Testing #e tag filter with payload: 1' AND SLEEP(5) --... PASSED - SQL injection blocked (query sanitized)
Testing #e tag filter with payload: 1' AND 1=1 WAITFOR DELAY '0:0:5' --... PASSED - SQL injection blocked (query sanitized)
Testing #e tag filter with payload: 1' AND 1=CAST((SELECT version()) AS INT) --... PASSED - SQL injection blocked (query sanitized)
Testing #e tag filter with payload: 1' AND 1=CONVERT(INT, (SELECT @@VERSION)) --... PASSED - SQL injection blocked (query sanitized)
Testing #e tag filter with payload: ' UNION SELECT NULL,NULL,NULL --... PASSED - SQL injection blocked (query sanitized)
Testing #e tag filter with payload: ' UNION SELECT 1,2,3 --... PASSED - SQL injection blocked (query sanitized)
Testing #e tag filter with payload: ' UNION ALL SELECT NULL,NULL,NULL --... PASSED - SQL injection blocked (query sanitized)
Testing #e tag filter with payload: '; SELECT * FROM events; --... PASSED - SQL injection blocked (query sanitized)
Testing #e tag filter with payload: '; DELETE FROM events; --... PASSED - SQL injection blocked (query sanitized)
Testing #e tag filter with payload: '; UPDATE events SET content='hacked' WHERE 1=1; --... PASSED - SQL injection blocked (query sanitized)
Testing #e tag filter with payload: /*... PASSED - SQL injection blocked (query sanitized)
Testing #e tag filter with payload: */... PASSED - SQL injection blocked (query sanitized)
Testing #e tag filter with payload: /**/... PASSED - SQL injection blocked (query sanitized)
Testing #e tag filter with payload: --... PASSED - SQL injection blocked (query sanitized)
Testing #e tag filter with payload: #... PASSED - SQL injection blocked (query sanitized)
Testing #e tag filter with payload: 0x53514C5F494E4A454354494F4E... PASSED - SQL injection blocked (query sanitized)
Testing #e tag filter with payload: J1NSTCBJTkpFQ1RJT04gLS0=... PASSED - SQL injection blocked (query sanitized)
Testing #e tag filter with payload: '))); DROP TABLE events; --... PASSED - SQL injection blocked (query sanitized)
Testing #e tag filter with payload: ')) UNION SELECT NULL; --... PASSED - SQL injection blocked (query sanitized)
Testing #e tag filter with payload: ' AND 1=1 --... PASSED - SQL injection blocked (query sanitized)
Testing #e tag filter with payload: ' AND 1=2 --... PASSED - SQL injection blocked (query sanitized)
Testing #e tag filter with payload: ' AND (SELECT COUNT(*) FROM events) > 0 --... PASSED - SQL injection blocked (query sanitized)
Testing #e tag filter with payload: '; EXEC master..xp_cmdshell 'net user' --... PASSED - SQL injection blocked (query sanitized)
Testing #e tag filter with payload: '; DECLARE @host varchar(1024); SELECT @host=(SELECT TOP 1 master..sys.fn_varbintohexstr(password_hash) FROM sys.sql_logins WHERE name='sa'); --... PASSED - SQL injection blocked (query sanitized)
Testing #p tag filter with payload: '; DROP TABLE events; --... PASSED - SQL injection blocked (query sanitized)
Testing #p tag filter with payload: 1' OR '1'='1... PASSED - SQL injection blocked (query sanitized)
Testing #p tag filter with payload: 1; SELECT * FROM sqlite_master; --... PASSED - SQL injection blocked (query sanitized)
Testing #p tag filter with payload: ' OR 1=1 --... PASSED - SQL injection blocked (query sanitized)
Testing #p tag filter with payload: admin'--... PASSED - SQL injection blocked (query sanitized)
Testing #p tag filter with payload: 1' UNION SELECT password FROM users --... PASSED - SQL injection blocked (query sanitized)
Testing #p tag filter with payload: 1' AND SLEEP(5) --... PASSED - SQL injection blocked (query sanitized)
Testing #p tag filter with payload: 1' AND 1=1 WAITFOR DELAY '0:0:5' --... PASSED - SQL injection blocked (query sanitized)
Testing #p tag filter with payload: 1' AND 1=CAST((SELECT version()) AS INT) --... PASSED - SQL injection blocked (query sanitized)
Testing #p tag filter with payload: 1' AND 1=CONVERT(INT, (SELECT @@VERSION)) --... PASSED - SQL injection blocked (query sanitized)
Testing #p tag filter with payload: ' UNION SELECT NULL,NULL,NULL --... PASSED - SQL injection blocked (query sanitized)
Testing #p tag filter with payload: ' UNION SELECT 1,2,3 --... PASSED - SQL injection blocked (query sanitized)
Testing #p tag filter with payload: ' UNION ALL SELECT NULL,NULL,NULL --... PASSED - SQL injection blocked (query sanitized)
Testing #p tag filter with payload: '; SELECT * FROM events; --... PASSED - SQL injection blocked (query sanitized)
Testing #p tag filter with payload: '; DELETE FROM events; --... PASSED - SQL injection blocked (query sanitized)
Testing #p tag filter with payload: '; UPDATE events SET content='hacked' WHERE 1=1; --... PASSED - SQL injection blocked (query sanitized)
Testing #p tag filter with payload: /*... PASSED - SQL injection blocked (query sanitized)
Testing #p tag filter with payload: */... PASSED - SQL injection blocked (query sanitized)
Testing #p tag filter with payload: /**/... PASSED - SQL injection blocked (query sanitized)
Testing #p tag filter with payload: --... PASSED - SQL injection blocked (query sanitized)
Testing #p tag filter with payload: #... PASSED - SQL injection blocked (query sanitized)
Testing #p tag filter with payload: 0x53514C5F494E4A454354494F4E... PASSED - SQL injection blocked (query sanitized)
Testing #p tag filter with payload: J1NSTCBJTkpFQ1RJT04gLS0=... PASSED - SQL injection blocked (query sanitized)
Testing #p tag filter with payload: '))); DROP TABLE events; --... PASSED - SQL injection blocked (query sanitized)
Testing #p tag filter with payload: ')) UNION SELECT NULL; --... PASSED - SQL injection blocked (query sanitized)
Testing #p tag filter with payload: ' AND 1=1 --... PASSED - SQL injection blocked (query sanitized)
Testing #p tag filter with payload: ' AND 1=2 --... PASSED - SQL injection blocked (query sanitized)
Testing #p tag filter with payload: ' AND (SELECT COUNT(*) FROM events) > 0 --... PASSED - SQL injection blocked (query sanitized)
Testing #p tag filter with payload: '; EXEC master..xp_cmdshell 'net user' --... PASSED - SQL injection blocked (query sanitized)
Testing #p tag filter with payload: '; DECLARE @host varchar(1024); SELECT @host=(SELECT TOP 1 master..sys.fn_varbintohexstr(password_hash) FROM sys.sql_logins WHERE name='sa'); --... PASSED - SQL injection blocked (query sanitized)
Testing #t tag filter with payload: '; DROP TABLE events; --... PASSED - SQL injection blocked (query sanitized)
Testing #t tag filter with payload: 1' OR '1'='1... PASSED - SQL injection blocked (query sanitized)
Testing #t tag filter with payload: 1; SELECT * FROM sqlite_master; --... PASSED - SQL injection blocked (query sanitized)
Testing #t tag filter with payload: ' OR 1=1 --... PASSED - SQL injection blocked (query sanitized)
Testing #t tag filter with payload: admin'--... PASSED - SQL injection blocked (query sanitized)
Testing #t tag filter with payload: 1' UNION SELECT password FROM users --... PASSED - SQL injection blocked (query sanitized)
Testing #t tag filter with payload: 1' AND SLEEP(5) --... PASSED - SQL injection blocked (query sanitized)
Testing #t tag filter with payload: 1' AND 1=1 WAITFOR DELAY '0:0:5' --... PASSED - SQL injection blocked (query sanitized)
Testing #t tag filter with payload: 1' AND 1=CAST((SELECT version()) AS INT) --... PASSED - SQL injection blocked (query sanitized)
Testing #t tag filter with payload: 1' AND 1=CONVERT(INT, (SELECT @@VERSION)) --... PASSED - SQL injection blocked (query sanitized)
Testing #t tag filter with payload: ' UNION SELECT NULL,NULL,NULL --... PASSED - SQL injection blocked (query sanitized)
Testing #t tag filter with payload: ' UNION SELECT 1,2,3 --... PASSED - SQL injection blocked (query sanitized)
Testing #t tag filter with payload: ' UNION ALL SELECT NULL,NULL,NULL --... PASSED - SQL injection blocked (query sanitized)
Testing #t tag filter with payload: '; SELECT * FROM events; --... PASSED - SQL injection blocked (query sanitized)
Testing #t tag filter with payload: '; DELETE FROM events; --... PASSED - SQL injection blocked (query sanitized)
Testing #t tag filter with payload: '; UPDATE events SET content='hacked' WHERE 1=1; --... PASSED - SQL injection blocked (query sanitized)
Testing #t tag filter with payload: /*... PASSED - SQL injection blocked (query sanitized)
Testing #t tag filter with payload: */... PASSED - SQL injection blocked (query sanitized)
Testing #t tag filter with payload: /**/... PASSED - SQL injection blocked (query sanitized)
Testing #t tag filter with payload: --... PASSED - SQL injection blocked (query sanitized)
Testing #t tag filter with payload: #... PASSED - SQL injection blocked (query sanitized)
Testing #t tag filter with payload: 0x53514C5F494E4A454354494F4E... PASSED - SQL injection blocked (query sanitized)
Testing #t tag filter with payload: J1NSTCBJTkpFQ1RJT04gLS0=... PASSED - SQL injection blocked (query sanitized)
Testing #t tag filter with payload: '))); DROP TABLE events; --... PASSED - SQL injection blocked (query sanitized)
Testing #t tag filter with payload: ')) UNION SELECT NULL; --... PASSED - SQL injection blocked (query sanitized)
Testing #t tag filter with payload: ' AND 1=1 --... PASSED - SQL injection blocked (query sanitized)
Testing #t tag filter with payload: ' AND 1=2 --... PASSED - SQL injection blocked (query sanitized)
Testing #t tag filter with payload: ' AND (SELECT COUNT(*) FROM events) > 0 --... PASSED - SQL injection blocked (query sanitized)
Testing #t tag filter with payload: '; EXEC master..xp_cmdshell 'net user' --... PASSED - SQL injection blocked (query sanitized)
Testing #t tag filter with payload: '; DECLARE @host varchar(1024); SELECT @host=(SELECT TOP 1 master..sys.fn_varbintohexstr(password_hash) FROM sys.sql_logins WHERE name='sa'); --... PASSED - SQL injection blocked (query sanitized)
Testing #r tag filter with payload: '; DROP TABLE events; --... PASSED - SQL injection blocked (query sanitized)
Testing #r tag filter with payload: 1' OR '1'='1... PASSED - SQL injection blocked (query sanitized)
Testing #r tag filter with payload: 1; SELECT * FROM sqlite_master; --... PASSED - SQL injection blocked (query sanitized)
Testing #r tag filter with payload: ' OR 1=1 --... PASSED - SQL injection blocked (query sanitized)
Testing #r tag filter with payload: admin'--... PASSED - SQL injection blocked (query sanitized)
Testing #r tag filter with payload: 1' UNION SELECT password FROM users --... PASSED - SQL injection blocked (query sanitized)
Testing #r tag filter with payload: 1' AND SLEEP(5) --... PASSED - SQL injection blocked (query sanitized)
Testing #r tag filter with payload: 1' AND 1=1 WAITFOR DELAY '0:0:5' --... PASSED - SQL injection blocked (query sanitized)
Testing #r tag filter with payload: 1' AND 1=CAST((SELECT version()) AS INT) --... PASSED - SQL injection blocked (query sanitized)
Testing #r tag filter with payload: 1' AND 1=CONVERT(INT, (SELECT @@VERSION)) --... PASSED - SQL injection blocked (query sanitized)
Testing #r tag filter with payload: ' UNION SELECT NULL,NULL,NULL --... PASSED - SQL injection blocked (query sanitized)
Testing #r tag filter with payload: ' UNION SELECT 1,2,3 --... PASSED - SQL injection blocked (query sanitized)
Testing #r tag filter with payload: ' UNION ALL SELECT NULL,NULL,NULL --... PASSED - SQL injection blocked (query sanitized)
Testing #r tag filter with payload: '; SELECT * FROM events; --... PASSED - SQL injection blocked (query sanitized)
Testing #r tag filter with payload: '; DELETE FROM events; --... PASSED - SQL injection blocked (query sanitized)
Testing #r tag filter with payload: '; UPDATE events SET content='hacked' WHERE 1=1; --... PASSED - SQL injection blocked (query sanitized)
Testing #r tag filter with payload: /*... PASSED - SQL injection blocked (query sanitized)
Testing #r tag filter with payload: */... PASSED - SQL injection blocked (query sanitized)
Testing #r tag filter with payload: /**/... PASSED - SQL injection blocked (query sanitized)
Testing #r tag filter with payload: --... PASSED - SQL injection blocked (query sanitized)
Testing #r tag filter with payload: #... PASSED - SQL injection blocked (query sanitized)
Testing #r tag filter with payload: 0x53514C5F494E4A454354494F4E... PASSED - SQL injection blocked (query sanitized)
Testing #r tag filter with payload: J1NSTCBJTkpFQ1RJT04gLS0=... PASSED - SQL injection blocked (query sanitized)
Testing #r tag filter with payload: '))); DROP TABLE events; --... PASSED - SQL injection blocked (query sanitized)
Testing #r tag filter with payload: ')) UNION SELECT NULL; --... PASSED - SQL injection blocked (query sanitized)
Testing #r tag filter with payload: ' AND 1=1 --... PASSED - SQL injection blocked (query sanitized)
Testing #r tag filter with payload: ' AND 1=2 --... PASSED - SQL injection blocked (query sanitized)
Testing #r tag filter with payload: ' AND (SELECT COUNT(*) FROM events) > 0 --... PASSED - SQL injection blocked (query sanitized)
Testing #r tag filter with payload: '; EXEC master..xp_cmdshell 'net user' --... PASSED - SQL injection blocked (query sanitized)
Testing #r tag filter with payload: '; DECLARE @host varchar(1024); SELECT @host=(SELECT TOP 1 master..sys.fn_varbintohexstr(password_hash) FROM sys.sql_logins WHERE name='sa'); --... PASSED - SQL injection blocked (query sanitized)
Testing #d tag filter with payload: '; DROP TABLE events; --... PASSED - SQL injection blocked (query sanitized)
Testing #d tag filter with payload: 1' OR '1'='1... PASSED - SQL injection blocked (query sanitized)
Testing #d tag filter with payload: 1; SELECT * FROM sqlite_master; --... PASSED - SQL injection blocked (query sanitized)
Testing #d tag filter with payload: ' OR 1=1 --... PASSED - SQL injection blocked (query sanitized)
Testing #d tag filter with payload: admin'--... PASSED - SQL injection blocked (query sanitized)
Testing #d tag filter with payload: 1' UNION SELECT password FROM users --... PASSED - SQL injection blocked (query sanitized)
Testing #d tag filter with payload: 1' AND SLEEP(5) --... PASSED - SQL injection blocked (query sanitized)
Testing #d tag filter with payload: 1' AND 1=1 WAITFOR DELAY '0:0:5' --... PASSED - SQL injection blocked (query sanitized)
Testing #d tag filter with payload: 1' AND 1=CAST((SELECT version()) AS INT) --... PASSED - SQL injection blocked (query sanitized)
Testing #d tag filter with payload: 1' AND 1=CONVERT(INT, (SELECT @@VERSION)) --... PASSED - SQL injection blocked (query sanitized)
Testing #d tag filter with payload: ' UNION SELECT NULL,NULL,NULL --... PASSED - SQL injection blocked (query sanitized)
Testing #d tag filter with payload: ' UNION SELECT 1,2,3 --... PASSED - SQL injection blocked (query sanitized)
Testing #d tag filter with payload: ' UNION ALL SELECT NULL,NULL,NULL --... PASSED - SQL injection blocked (query sanitized)
Testing #d tag filter with payload: '; SELECT * FROM events; --... PASSED - SQL injection blocked (query sanitized)
Testing #d tag filter with payload: '; DELETE FROM events; --... PASSED - SQL injection blocked (query sanitized)
Testing #d tag filter with payload: '; UPDATE events SET content='hacked' WHERE 1=1; --... PASSED - SQL injection blocked (query sanitized)
Testing #d tag filter with payload: /*... PASSED - SQL injection blocked (query sanitized)
Testing #d tag filter with payload: */... PASSED - SQL injection blocked (query sanitized)
Testing #d tag filter with payload: /**/... PASSED - SQL injection blocked (query sanitized)
Testing #d tag filter with payload: --... PASSED - SQL injection blocked (query sanitized)
Testing #d tag filter with payload: #... PASSED - SQL injection blocked (query sanitized)
Testing #d tag filter with payload: 0x53514C5F494E4A454354494F4E... PASSED - SQL injection blocked (query sanitized)
Testing #d tag filter with payload: J1NSTCBJTkpFQ1RJT04gLS0=... PASSED - SQL injection blocked (query sanitized)
Testing #d tag filter with payload: '))); DROP TABLE events; --... PASSED - SQL injection blocked (query sanitized)
Testing #d tag filter with payload: ')) UNION SELECT NULL; --... PASSED - SQL injection blocked (query sanitized)
Testing #d tag filter with payload: ' AND 1=1 --... PASSED - SQL injection blocked (query sanitized)
Testing #d tag filter with payload: ' AND 1=2 --... PASSED - SQL injection blocked (query sanitized)
Testing #d tag filter with payload: ' AND (SELECT COUNT(*) FROM events) > 0 --... PASSED - SQL injection blocked (query sanitized)
Testing #d tag filter with payload: '; EXEC master..xp_cmdshell 'net user' --... PASSED - SQL injection blocked (query sanitized)
Testing #d tag filter with payload: '; DECLARE @host varchar(1024); SELECT @host=(SELECT TOP 1 master..sys.fn_varbintohexstr(password_hash) FROM sys.sql_logins WHERE name='sa'); --... PASSED - SQL injection blocked (query sanitized)
=== Timestamp Filter SQL Injection Tests ===
Testing Since parameter injection... PASSED - SQL injection blocked (rejected with error)
Testing Until parameter injection... PASSED - SQL injection blocked (rejected with error)
=== Limit Parameter SQL Injection Tests ===
Testing Limit parameter injection... PASSED - SQL injection blocked (rejected with error)
Testing Limit with UNION... PASSED - SQL injection blocked (rejected with error)
=== Complex Multi-Filter SQL Injection Tests ===
Testing Multi-filter with authors injection... PASSED - SQL injection blocked (rejected with error)
Testing Multi-filter with search injection... PASSED - SQL injection blocked (rejected with error)
Testing Multi-filter with tag injection... PASSED - SQL injection blocked (query sanitized)
=== COUNT Message SQL Injection Tests ===
Testing COUNT with authors payload: '; DROP TABLE events; --... PASSED - SQL injection blocked (rejected with error)
Testing COUNT with search payload: '; DROP TABLE events; --... PASSED - SQL injection blocked (rejected with error)
Testing COUNT with authors payload: 1' OR '1'='1... PASSED - SQL injection blocked (rejected with error)
Testing COUNT with search payload: 1' OR '1'='1... PASSED - SQL injection blocked (query sanitized)
Testing COUNT with authors payload: 1; SELECT * FROM sqlite_master; --... PASSED - SQL injection blocked (rejected with error)
Testing COUNT with search payload: 1; SELECT * FROM sqlite_master; --... PASSED - SQL injection blocked (rejected with error)
Testing COUNT with authors payload: ' OR 1=1 --... PASSED - SQL injection blocked (rejected with error)
Testing COUNT with search payload: ' OR 1=1 --... PASSED - SQL injection blocked (rejected with error)
Testing COUNT with authors payload: admin'--... PASSED - SQL injection blocked (rejected with error)
Testing COUNT with search payload: admin'--... PASSED - SQL injection blocked (rejected with error)
Testing COUNT with authors payload: 1' UNION SELECT password FROM users --... PASSED - SQL injection blocked (rejected with error)
Testing COUNT with search payload: 1' UNION SELECT password FROM users --... PASSED - SQL injection blocked (rejected with error)
Testing COUNT with authors payload: 1' AND SLEEP(5) --... PASSED - SQL injection blocked (rejected with error)
Testing COUNT with search payload: 1' AND SLEEP(5) --... PASSED - SQL injection blocked (rejected with error)
Testing COUNT with authors payload: 1' AND 1=1 WAITFOR DELAY '0:0:5' --... PASSED - SQL injection blocked (rejected with error)
Testing COUNT with search payload: 1' AND 1=1 WAITFOR DELAY '0:0:5' --... PASSED - SQL injection blocked (rejected with error)
Testing COUNT with authors payload: 1' AND 1=CAST((SELECT version()) AS INT) --... PASSED - SQL injection blocked (rejected with error)
Testing COUNT with search payload: 1' AND 1=CAST((SELECT version()) AS INT) --... PASSED - SQL injection blocked (rejected with error)
Testing COUNT with authors payload: 1' AND 1=CONVERT(INT, (SELECT @@VERSION)) --... PASSED - SQL injection blocked (rejected with error)
Testing COUNT with search payload: 1' AND 1=CONVERT(INT, (SELECT @@VERSION)) --... PASSED - SQL injection blocked (rejected with error)
Testing COUNT with authors payload: ' UNION SELECT NULL,NULL,NULL --... PASSED - SQL injection blocked (rejected with error)
Testing COUNT with search payload: ' UNION SELECT NULL,NULL,NULL --... PASSED - SQL injection blocked (rejected with error)
Testing COUNT with authors payload: ' UNION SELECT 1,2,3 --... PASSED - SQL injection blocked (rejected with error)
Testing COUNT with search payload: ' UNION SELECT 1,2,3 --... PASSED - SQL injection blocked (rejected with error)
Testing COUNT with authors payload: ' UNION ALL SELECT NULL,NULL,NULL --... PASSED - SQL injection blocked (rejected with error)
Testing COUNT with search payload: ' UNION ALL SELECT NULL,NULL,NULL --... PASSED - SQL injection blocked (rejected with error)
Testing COUNT with authors payload: '; SELECT * FROM events; --... PASSED - SQL injection blocked (rejected with error)
Testing COUNT with search payload: '; SELECT * FROM events; --... PASSED - SQL injection blocked (rejected with error)
Testing COUNT with authors payload: '; DELETE FROM events; --... PASSED - SQL injection blocked (rejected with error)
Testing COUNT with search payload: '; DELETE FROM events; --... PASSED - SQL injection blocked (rejected with error)
Testing COUNT with authors payload: '; UPDATE events SET content='hacked' WHERE 1=1; --... PASSED - SQL injection blocked (rejected with error)
Testing COUNT with search payload: '; UPDATE events SET content='hacked' WHERE 1=1; --... PASSED - SQL injection blocked (rejected with error)
Testing COUNT with authors payload: /*... PASSED - SQL injection blocked (rejected with error)
Testing COUNT with search payload: /*... PASSED - SQL injection blocked (rejected with error)
Testing COUNT with authors payload: */... PASSED - SQL injection blocked (rejected with error)
Testing COUNT with search payload: */... PASSED - SQL injection blocked (rejected with error)
Testing COUNT with authors payload: /**/... PASSED - SQL injection blocked (rejected with error)
Testing COUNT with search payload: /**/... PASSED - SQL injection blocked (rejected with error)
Testing COUNT with authors payload: --... PASSED - SQL injection blocked (rejected with error)
Testing COUNT with search payload: --... PASSED - SQL injection blocked (rejected with error)
Testing COUNT with authors payload: #... PASSED - SQL injection blocked (rejected with error)
Testing COUNT with search payload: #... PASSED - SQL injection blocked (query sanitized)
Testing COUNT with authors payload: 0x53514C5F494E4A454354494F4E... PASSED - SQL injection blocked (rejected with error)
Testing COUNT with search payload: 0x53514C5F494E4A454354494F4E... PASSED - SQL injection blocked (query sanitized)
Testing COUNT with authors payload: J1NSTCBJTkpFQ1RJT04gLS0=... PASSED - SQL injection blocked (rejected with error)
Testing COUNT with search payload: J1NSTCBJTkpFQ1RJT04gLS0=... PASSED - SQL injection blocked (query sanitized)
Testing COUNT with authors payload: '))); DROP TABLE events; --... PASSED - SQL injection blocked (rejected with error)
Testing COUNT with search payload: '))); DROP TABLE events; --... PASSED - SQL injection blocked (rejected with error)
Testing COUNT with authors payload: ')) UNION SELECT NULL; --... PASSED - SQL injection blocked (rejected with error)
Testing COUNT with search payload: ')) UNION SELECT NULL; --... PASSED - SQL injection blocked (rejected with error)
Testing COUNT with authors payload: ' AND 1=1 --... PASSED - SQL injection blocked (rejected with error)
Testing COUNT with search payload: ' AND 1=1 --... PASSED - SQL injection blocked (rejected with error)
Testing COUNT with authors payload: ' AND 1=2 --... PASSED - SQL injection blocked (rejected with error)
Testing COUNT with search payload: ' AND 1=2 --... PASSED - SQL injection blocked (rejected with error)
Testing COUNT with authors payload: ' AND (SELECT COUNT(*) FROM events) > 0 --... PASSED - SQL injection blocked (rejected with error)
Testing COUNT with search payload: ' AND (SELECT COUNT(*) FROM events) > 0 --... PASSED - SQL injection blocked (rejected with error)
Testing COUNT with authors payload: '; EXEC master..xp_cmdshell 'net user' --... PASSED - SQL injection blocked (rejected with error)
Testing COUNT with search payload: '; EXEC master..xp_cmdshell 'net user' --... PASSED - SQL injection blocked (rejected with error)
Testing COUNT with authors payload: '; DECLARE @host varchar(1024); SELECT @host=(SELECT TOP 1 master..sys.fn_varbintohexstr(password_hash) FROM sys.sql_logins WHERE name='sa'); --... PASSED - SQL injection blocked (rejected with error)
Testing COUNT with search payload: '; DECLARE @host varchar(1024); SELECT @host=(SELECT TOP 1 master..sys.fn_varbintohexstr(password_hash) FROM sys.sql_logins WHERE name='sa'); --... PASSED - SQL injection blocked (rejected with error)
=== Edge Case SQL Injection Tests ===
Testing Empty string injection... PASSED - SQL injection blocked (rejected with error)
Testing Null byte injection... PASSED - SQL injection blocked (silently rejected)
Testing Unicode injection... PASSED - SQL injection blocked (rejected with error)
Testing Very long injection payload... PASSED - SQL injection blocked (rejected with error)
=== Subscription ID SQL Injection Tests ===
Testing Subscription ID injection... PASSED - SQL injection blocked (rejected with error)
Testing Subscription ID with quotes... PASSED - SQL injection blocked (silently rejected)
=== CLOSE Message SQL Injection Tests ===
Testing CLOSE with injection... PASSED - SQL injection blocked (rejected with error)
=== Test Results ===
Total tests: 318
Passed: 318
Failed: 0
✓ All SQL injection tests passed!
The relay appears to be protected against SQL injection attacks.
2026-04-01 09:11:14 - \033[0;32m✓ SQL Injection Tests PASSED\033[0m (Duration: 7s)
2026-04-01 09:11:14 - ==========================================
2026-04-01 09:11:14 - Running Test Suite: Filter Validation Tests
2026-04-01 09:11:14 - Description: Input validation for REQ and COUNT messages
2026-04-01 09:11:14 - ==========================================
=== C-Relay Filter Validation Tests ===
Testing against relay at ws://127.0.0.1:8888
Testing Valid REQ message... PASSED
Testing Valid COUNT message... PASSED
=== Testing Filter Array Validation ===
Testing Non-object filter... PASSED
Testing Too many filters... PASSED
=== Testing Authors Validation ===
Testing Invalid author type... PASSED
Testing Invalid author hex... PASSED
Testing Too many authors... FAILED - Expected error 'error: too many authors', got: ["NOTICE", "error: invalid author hex string"]
2026-04-01 09:11:14 - \033[0;31m✗ Filter Validation Tests FAILED\033[0m (Duration: 0s)
+733
View File
@@ -0,0 +1,733 @@
2026-04-01 09:57:29 - ==========================================
2026-04-01 09:57:30 - C-Relay Comprehensive Test Suite Runner
2026-04-01 09:57:30 - ==========================================
2026-04-01 09:57:30 - Relay URL: ws://127.0.0.1:8888
2026-04-01 09:57:30 - Log file: test_results_20260401_095729.log
2026-04-01 09:57:30 - Report file: test_report_20260401_095729.html
2026-04-01 09:57:30 -
2026-04-01 09:57:30 - Checking relay status at ws://127.0.0.1:8888...
2026-04-01 09:57:30 - \033[0;32m✓ Relay HTTP endpoint is accessible\033[0m
2026-04-01 09:57:30 -
2026-04-01 09:57:30 - Starting comprehensive test execution...
2026-04-01 09:57:30 -
2026-04-01 09:57:30 - \033[0;34m=== SECURITY TEST SUITES ===\033[0m
2026-04-01 09:57:30 - ==========================================
2026-04-01 09:57:30 - Running Test Suite: SQL Injection Tests
2026-04-01 09:57:30 - Description: Comprehensive SQL injection vulnerability testing
2026-04-01 09:57:30 - ==========================================
==========================================
C-Relay SQL Injection Test Suite
==========================================
Testing against relay at ws://127.0.0.1:8888
=== Basic Connectivity Test ===
Testing Basic connectivity... PASSED - Valid query works
=== Authors Filter SQL Injection Tests ===
Testing Authors filter with payload: '; DROP TABLE events; --... PASSED - SQL injection blocked (rejected with error)
Testing Authors filter with payload: 1' OR '1'='1... PASSED - SQL injection blocked (rejected with error)
Testing Authors filter with payload: 1; SELECT * FROM sqlite_master; --... PASSED - SQL injection blocked (rejected with error)
Testing Authors filter with payload: ' OR 1=1 --... PASSED - SQL injection blocked (rejected with error)
Testing Authors filter with payload: admin'--... PASSED - SQL injection blocked (rejected with error)
Testing Authors filter with payload: 1' UNION SELECT password FROM users --... PASSED - SQL injection blocked (rejected with error)
Testing Authors filter with payload: 1' AND SLEEP(5) --... PASSED - SQL injection blocked (rejected with error)
Testing Authors filter with payload: 1' AND 1=1 WAITFOR DELAY '0:0:5' --... PASSED - SQL injection blocked (rejected with error)
Testing Authors filter with payload: 1' AND 1=CAST((SELECT version()) AS INT) --... PASSED - SQL injection blocked (rejected with error)
Testing Authors filter with payload: 1' AND 1=CONVERT(INT, (SELECT @@VERSION)) --... PASSED - SQL injection blocked (rejected with error)
Testing Authors filter with payload: ' UNION SELECT NULL,NULL,NULL --... PASSED - SQL injection blocked (rejected with error)
Testing Authors filter with payload: ' UNION SELECT 1,2,3 --... PASSED - SQL injection blocked (rejected with error)
Testing Authors filter with payload: ' UNION ALL SELECT NULL,NULL,NULL --... PASSED - SQL injection blocked (rejected with error)
Testing Authors filter with payload: '; SELECT * FROM events; --... PASSED - SQL injection blocked (rejected with error)
Testing Authors filter with payload: '; DELETE FROM events; --... PASSED - SQL injection blocked (rejected with error)
Testing Authors filter with payload: '; UPDATE events SET content='hacked' WHERE 1=1; --... PASSED - SQL injection blocked (rejected with error)
Testing Authors filter with payload: /*... PASSED - SQL injection blocked (rejected with error)
Testing Authors filter with payload: */... PASSED - SQL injection blocked (rejected with error)
Testing Authors filter with payload: /**/... PASSED - SQL injection blocked (rejected with error)
Testing Authors filter with payload: --... PASSED - SQL injection blocked (rejected with error)
Testing Authors filter with payload: #... PASSED - SQL injection blocked (rejected with error)
Testing Authors filter with payload: 0x53514C5F494E4A454354494F4E... PASSED - SQL injection blocked (rejected with error)
Testing Authors filter with payload: J1NSTCBJTkpFQ1RJT04gLS0=... PASSED - SQL injection blocked (rejected with error)
Testing Authors filter with payload: '))); DROP TABLE events; --... PASSED - SQL injection blocked (rejected with error)
Testing Authors filter with payload: ')) UNION SELECT NULL; --... PASSED - SQL injection blocked (rejected with error)
Testing Authors filter with payload: ' AND 1=1 --... PASSED - SQL injection blocked (rejected with error)
Testing Authors filter with payload: ' AND 1=2 --... PASSED - SQL injection blocked (rejected with error)
Testing Authors filter with payload: ' AND (SELECT COUNT(*) FROM events) > 0 --... PASSED - SQL injection blocked (rejected with error)
Testing Authors filter with payload: '; EXEC master..xp_cmdshell 'net user' --... PASSED - SQL injection blocked (rejected with error)
Testing Authors filter with payload: '; DECLARE @host varchar(1024); SELECT @host=(SELECT TOP 1 master..sys.fn_varbintohexstr(password_hash) FROM sys.sql_logins WHERE name='sa'); --... PASSED - SQL injection blocked (rejected with error)
=== IDs Filter SQL Injection Tests ===
Testing IDs filter with payload: '; DROP TABLE events; --... PASSED - SQL injection blocked (rejected with error)
Testing IDs filter with payload: 1' OR '1'='1... PASSED - SQL injection blocked (rejected with error)
Testing IDs filter with payload: 1; SELECT * FROM sqlite_master; --... PASSED - SQL injection blocked (rejected with error)
Testing IDs filter with payload: ' OR 1=1 --... PASSED - SQL injection blocked (rejected with error)
Testing IDs filter with payload: admin'--... PASSED - SQL injection blocked (rejected with error)
Testing IDs filter with payload: 1' UNION SELECT password FROM users --... PASSED - SQL injection blocked (rejected with error)
Testing IDs filter with payload: 1' AND SLEEP(5) --... PASSED - SQL injection blocked (rejected with error)
Testing IDs filter with payload: 1' AND 1=1 WAITFOR DELAY '0:0:5' --... PASSED - SQL injection blocked (rejected with error)
Testing IDs filter with payload: 1' AND 1=CAST((SELECT version()) AS INT) --... PASSED - SQL injection blocked (rejected with error)
Testing IDs filter with payload: 1' AND 1=CONVERT(INT, (SELECT @@VERSION)) --... PASSED - SQL injection blocked (rejected with error)
Testing IDs filter with payload: ' UNION SELECT NULL,NULL,NULL --... PASSED - SQL injection blocked (rejected with error)
Testing IDs filter with payload: ' UNION SELECT 1,2,3 --... PASSED - SQL injection blocked (rejected with error)
Testing IDs filter with payload: ' UNION ALL SELECT NULL,NULL,NULL --... PASSED - SQL injection blocked (rejected with error)
Testing IDs filter with payload: '; SELECT * FROM events; --... PASSED - SQL injection blocked (rejected with error)
Testing IDs filter with payload: '; DELETE FROM events; --... PASSED - SQL injection blocked (rejected with error)
Testing IDs filter with payload: '; UPDATE events SET content='hacked' WHERE 1=1; --... PASSED - SQL injection blocked (rejected with error)
Testing IDs filter with payload: /*... PASSED - SQL injection blocked (rejected with error)
Testing IDs filter with payload: */... PASSED - SQL injection blocked (rejected with error)
Testing IDs filter with payload: /**/... PASSED - SQL injection blocked (rejected with error)
Testing IDs filter with payload: --... PASSED - SQL injection blocked (rejected with error)
Testing IDs filter with payload: #... PASSED - SQL injection blocked (rejected with error)
Testing IDs filter with payload: 0x53514C5F494E4A454354494F4E... PASSED - SQL injection blocked (rejected with error)
Testing IDs filter with payload: J1NSTCBJTkpFQ1RJT04gLS0=... PASSED - SQL injection blocked (rejected with error)
Testing IDs filter with payload: '))); DROP TABLE events; --... PASSED - SQL injection blocked (rejected with error)
Testing IDs filter with payload: ')) UNION SELECT NULL; --... PASSED - SQL injection blocked (rejected with error)
Testing IDs filter with payload: ' AND 1=1 --... PASSED - SQL injection blocked (rejected with error)
Testing IDs filter with payload: ' AND 1=2 --... PASSED - SQL injection blocked (rejected with error)
Testing IDs filter with payload: ' AND (SELECT COUNT(*) FROM events) > 0 --... PASSED - SQL injection blocked (rejected with error)
Testing IDs filter with payload: '; EXEC master..xp_cmdshell 'net user' --... PASSED - SQL injection blocked (rejected with error)
Testing IDs filter with payload: '; DECLARE @host varchar(1024); SELECT @host=(SELECT TOP 1 master..sys.fn_varbintohexstr(password_hash) FROM sys.sql_logins WHERE name='sa'); --... PASSED - SQL injection blocked (rejected with error)
=== Kinds Filter SQL Injection Tests ===
Testing Kinds filter with string injection... PASSED - SQL injection blocked (rejected with error)
Testing Kinds filter with negative value... PASSED - SQL injection blocked (rejected with error)
Testing Kinds filter with very large value... PASSED - SQL injection blocked (rejected with error)
=== Search Filter SQL Injection Tests ===
Testing Search filter with payload: '; DROP TABLE events; --... PASSED - SQL injection blocked (rejected with error)
Testing Search filter with payload: 1' OR '1'='1... PASSED - SQL injection blocked (query sanitized)
Testing Search filter with payload: 1; SELECT * FROM sqlite_master; --... PASSED - SQL injection blocked (rejected with error)
Testing Search filter with payload: ' OR 1=1 --... PASSED - SQL injection blocked (rejected with error)
Testing Search filter with payload: admin'--... PASSED - SQL injection blocked (rejected with error)
Testing Search filter with payload: 1' UNION SELECT password FROM users --... PASSED - SQL injection blocked (rejected with error)
Testing Search filter with payload: 1' AND SLEEP(5) --... PASSED - SQL injection blocked (rejected with error)
Testing Search filter with payload: 1' AND 1=1 WAITFOR DELAY '0:0:5' --... PASSED - SQL injection blocked (rejected with error)
Testing Search filter with payload: 1' AND 1=CAST((SELECT version()) AS INT) --... PASSED - SQL injection blocked (rejected with error)
Testing Search filter with payload: 1' AND 1=CONVERT(INT, (SELECT @@VERSION)) --... PASSED - SQL injection blocked (rejected with error)
Testing Search filter with payload: ' UNION SELECT NULL,NULL,NULL --... PASSED - SQL injection blocked (rejected with error)
Testing Search filter with payload: ' UNION SELECT 1,2,3 --... PASSED - SQL injection blocked (rejected with error)
Testing Search filter with payload: ' UNION ALL SELECT NULL,NULL,NULL --... PASSED - SQL injection blocked (rejected with error)
Testing Search filter with payload: '; SELECT * FROM events; --... PASSED - SQL injection blocked (rejected with error)
Testing Search filter with payload: '; DELETE FROM events; --... PASSED - SQL injection blocked (rejected with error)
Testing Search filter with payload: '; UPDATE events SET content='hacked' WHERE 1=1; --... PASSED - SQL injection blocked (rejected with error)
Testing Search filter with payload: /*... PASSED - SQL injection blocked (rejected with error)
Testing Search filter with payload: */... PASSED - SQL injection blocked (rejected with error)
Testing Search filter with payload: /**/... PASSED - SQL injection blocked (rejected with error)
Testing Search filter with payload: --... PASSED - SQL injection blocked (rejected with error)
Testing Search filter with payload: #... PASSED - SQL injection blocked (query sanitized)
Testing Search filter with payload: 0x53514C5F494E4A454354494F4E... PASSED - SQL injection blocked (query sanitized)
Testing Search filter with payload: J1NSTCBJTkpFQ1RJT04gLS0=... PASSED - SQL injection blocked (query sanitized)
Testing Search filter with payload: '))); DROP TABLE events; --... PASSED - SQL injection blocked (rejected with error)
Testing Search filter with payload: ')) UNION SELECT NULL; --... PASSED - SQL injection blocked (rejected with error)
Testing Search filter with payload: ' AND 1=1 --... PASSED - SQL injection blocked (rejected with error)
Testing Search filter with payload: ' AND 1=2 --... PASSED - SQL injection blocked (rejected with error)
Testing Search filter with payload: ' AND (SELECT COUNT(*) FROM events) > 0 --... PASSED - SQL injection blocked (rejected with error)
Testing Search filter with payload: '; EXEC master..xp_cmdshell 'net user' --... PASSED - SQL injection blocked (rejected with error)
Testing Search filter with payload: '; DECLARE @host varchar(1024); SELECT @host=(SELECT TOP 1 master..sys.fn_varbintohexstr(password_hash) FROM sys.sql_logins WHERE name='sa'); --... PASSED - SQL injection blocked (rejected with error)
=== Tag Filter SQL Injection Tests ===
Testing #e tag filter with payload: '; DROP TABLE events; --... PASSED - SQL injection blocked (query sanitized)
Testing #e tag filter with payload: 1' OR '1'='1... PASSED - SQL injection blocked (query sanitized)
Testing #e tag filter with payload: 1; SELECT * FROM sqlite_master; --... PASSED - SQL injection blocked (query sanitized)
Testing #e tag filter with payload: ' OR 1=1 --... PASSED - SQL injection blocked (query sanitized)
Testing #e tag filter with payload: admin'--... PASSED - SQL injection blocked (query sanitized)
Testing #e tag filter with payload: 1' UNION SELECT password FROM users --... PASSED - SQL injection blocked (query sanitized)
Testing #e tag filter with payload: 1' AND SLEEP(5) --... PASSED - SQL injection blocked (query sanitized)
Testing #e tag filter with payload: 1' AND 1=1 WAITFOR DELAY '0:0:5' --... PASSED - SQL injection blocked (query sanitized)
Testing #e tag filter with payload: 1' AND 1=CAST((SELECT version()) AS INT) --... PASSED - SQL injection blocked (query sanitized)
Testing #e tag filter with payload: 1' AND 1=CONVERT(INT, (SELECT @@VERSION)) --... PASSED - SQL injection blocked (query sanitized)
Testing #e tag filter with payload: ' UNION SELECT NULL,NULL,NULL --... PASSED - SQL injection blocked (query sanitized)
Testing #e tag filter with payload: ' UNION SELECT 1,2,3 --... PASSED - SQL injection blocked (query sanitized)
Testing #e tag filter with payload: ' UNION ALL SELECT NULL,NULL,NULL --... PASSED - SQL injection blocked (query sanitized)
Testing #e tag filter with payload: '; SELECT * FROM events; --... PASSED - SQL injection blocked (query sanitized)
Testing #e tag filter with payload: '; DELETE FROM events; --... PASSED - SQL injection blocked (query sanitized)
Testing #e tag filter with payload: '; UPDATE events SET content='hacked' WHERE 1=1; --... PASSED - SQL injection blocked (query sanitized)
Testing #e tag filter with payload: /*... PASSED - SQL injection blocked (query sanitized)
Testing #e tag filter with payload: */... PASSED - SQL injection blocked (query sanitized)
Testing #e tag filter with payload: /**/... PASSED - SQL injection blocked (query sanitized)
Testing #e tag filter with payload: --... PASSED - SQL injection blocked (query sanitized)
Testing #e tag filter with payload: #... PASSED - SQL injection blocked (query sanitized)
Testing #e tag filter with payload: 0x53514C5F494E4A454354494F4E... PASSED - SQL injection blocked (query sanitized)
Testing #e tag filter with payload: J1NSTCBJTkpFQ1RJT04gLS0=... PASSED - SQL injection blocked (query sanitized)
Testing #e tag filter with payload: '))); DROP TABLE events; --... PASSED - SQL injection blocked (query sanitized)
Testing #e tag filter with payload: ')) UNION SELECT NULL; --... PASSED - SQL injection blocked (query sanitized)
Testing #e tag filter with payload: ' AND 1=1 --... PASSED - SQL injection blocked (query sanitized)
Testing #e tag filter with payload: ' AND 1=2 --... PASSED - SQL injection blocked (query sanitized)
Testing #e tag filter with payload: ' AND (SELECT COUNT(*) FROM events) > 0 --... PASSED - SQL injection blocked (query sanitized)
Testing #e tag filter with payload: '; EXEC master..xp_cmdshell 'net user' --... PASSED - SQL injection blocked (query sanitized)
Testing #e tag filter with payload: '; DECLARE @host varchar(1024); SELECT @host=(SELECT TOP 1 master..sys.fn_varbintohexstr(password_hash) FROM sys.sql_logins WHERE name='sa'); --... PASSED - SQL injection blocked (query sanitized)
Testing #p tag filter with payload: '; DROP TABLE events; --... PASSED - SQL injection blocked (query sanitized)
Testing #p tag filter with payload: 1' OR '1'='1... PASSED - SQL injection blocked (query sanitized)
Testing #p tag filter with payload: 1; SELECT * FROM sqlite_master; --... PASSED - SQL injection blocked (query sanitized)
Testing #p tag filter with payload: ' OR 1=1 --... PASSED - SQL injection blocked (query sanitized)
Testing #p tag filter with payload: admin'--... PASSED - SQL injection blocked (query sanitized)
Testing #p tag filter with payload: 1' UNION SELECT password FROM users --... PASSED - SQL injection blocked (query sanitized)
Testing #p tag filter with payload: 1' AND SLEEP(5) --... PASSED - SQL injection blocked (query sanitized)
Testing #p tag filter with payload: 1' AND 1=1 WAITFOR DELAY '0:0:5' --... PASSED - SQL injection blocked (query sanitized)
Testing #p tag filter with payload: 1' AND 1=CAST((SELECT version()) AS INT) --... PASSED - SQL injection blocked (query sanitized)
Testing #p tag filter with payload: 1' AND 1=CONVERT(INT, (SELECT @@VERSION)) --... PASSED - SQL injection blocked (query sanitized)
Testing #p tag filter with payload: ' UNION SELECT NULL,NULL,NULL --... PASSED - SQL injection blocked (query sanitized)
Testing #p tag filter with payload: ' UNION SELECT 1,2,3 --... PASSED - SQL injection blocked (query sanitized)
Testing #p tag filter with payload: ' UNION ALL SELECT NULL,NULL,NULL --... PASSED - SQL injection blocked (query sanitized)
Testing #p tag filter with payload: '; SELECT * FROM events; --... PASSED - SQL injection blocked (query sanitized)
Testing #p tag filter with payload: '; DELETE FROM events; --... PASSED - SQL injection blocked (query sanitized)
Testing #p tag filter with payload: '; UPDATE events SET content='hacked' WHERE 1=1; --... PASSED - SQL injection blocked (query sanitized)
Testing #p tag filter with payload: /*... PASSED - SQL injection blocked (query sanitized)
Testing #p tag filter with payload: */... PASSED - SQL injection blocked (query sanitized)
Testing #p tag filter with payload: /**/... PASSED - SQL injection blocked (query sanitized)
Testing #p tag filter with payload: --... PASSED - SQL injection blocked (query sanitized)
Testing #p tag filter with payload: #... PASSED - SQL injection blocked (query sanitized)
Testing #p tag filter with payload: 0x53514C5F494E4A454354494F4E... PASSED - SQL injection blocked (query sanitized)
Testing #p tag filter with payload: J1NSTCBJTkpFQ1RJT04gLS0=... PASSED - SQL injection blocked (query sanitized)
Testing #p tag filter with payload: '))); DROP TABLE events; --... PASSED - SQL injection blocked (query sanitized)
Testing #p tag filter with payload: ')) UNION SELECT NULL; --... PASSED - SQL injection blocked (query sanitized)
Testing #p tag filter with payload: ' AND 1=1 --... PASSED - SQL injection blocked (query sanitized)
Testing #p tag filter with payload: ' AND 1=2 --... PASSED - SQL injection blocked (query sanitized)
Testing #p tag filter with payload: ' AND (SELECT COUNT(*) FROM events) > 0 --... PASSED - SQL injection blocked (query sanitized)
Testing #p tag filter with payload: '; EXEC master..xp_cmdshell 'net user' --... PASSED - SQL injection blocked (query sanitized)
Testing #p tag filter with payload: '; DECLARE @host varchar(1024); SELECT @host=(SELECT TOP 1 master..sys.fn_varbintohexstr(password_hash) FROM sys.sql_logins WHERE name='sa'); --... PASSED - SQL injection blocked (query sanitized)
Testing #t tag filter with payload: '; DROP TABLE events; --... PASSED - SQL injection blocked (query sanitized)
Testing #t tag filter with payload: 1' OR '1'='1... PASSED - SQL injection blocked (query sanitized)
Testing #t tag filter with payload: 1; SELECT * FROM sqlite_master; --... PASSED - SQL injection blocked (query sanitized)
Testing #t tag filter with payload: ' OR 1=1 --... PASSED - SQL injection blocked (query sanitized)
Testing #t tag filter with payload: admin'--... PASSED - SQL injection blocked (query sanitized)
Testing #t tag filter with payload: 1' UNION SELECT password FROM users --... PASSED - SQL injection blocked (query sanitized)
Testing #t tag filter with payload: 1' AND SLEEP(5) --... PASSED - SQL injection blocked (query sanitized)
Testing #t tag filter with payload: 1' AND 1=1 WAITFOR DELAY '0:0:5' --... PASSED - SQL injection blocked (query sanitized)
Testing #t tag filter with payload: 1' AND 1=CAST((SELECT version()) AS INT) --... PASSED - SQL injection blocked (query sanitized)
Testing #t tag filter with payload: 1' AND 1=CONVERT(INT, (SELECT @@VERSION)) --... PASSED - SQL injection blocked (query sanitized)
Testing #t tag filter with payload: ' UNION SELECT NULL,NULL,NULL --... PASSED - SQL injection blocked (query sanitized)
Testing #t tag filter with payload: ' UNION SELECT 1,2,3 --... PASSED - SQL injection blocked (query sanitized)
Testing #t tag filter with payload: ' UNION ALL SELECT NULL,NULL,NULL --... PASSED - SQL injection blocked (query sanitized)
Testing #t tag filter with payload: '; SELECT * FROM events; --... PASSED - SQL injection blocked (query sanitized)
Testing #t tag filter with payload: '; DELETE FROM events; --... PASSED - SQL injection blocked (query sanitized)
Testing #t tag filter with payload: '; UPDATE events SET content='hacked' WHERE 1=1; --... PASSED - SQL injection blocked (query sanitized)
Testing #t tag filter with payload: /*... PASSED - SQL injection blocked (query sanitized)
Testing #t tag filter with payload: */... PASSED - SQL injection blocked (query sanitized)
Testing #t tag filter with payload: /**/... PASSED - SQL injection blocked (query sanitized)
Testing #t tag filter with payload: --... PASSED - SQL injection blocked (query sanitized)
Testing #t tag filter with payload: #... PASSED - SQL injection blocked (query sanitized)
Testing #t tag filter with payload: 0x53514C5F494E4A454354494F4E... PASSED - SQL injection blocked (query sanitized)
Testing #t tag filter with payload: J1NSTCBJTkpFQ1RJT04gLS0=... PASSED - SQL injection blocked (query sanitized)
Testing #t tag filter with payload: '))); DROP TABLE events; --... PASSED - SQL injection blocked (query sanitized)
Testing #t tag filter with payload: ')) UNION SELECT NULL; --... PASSED - SQL injection blocked (query sanitized)
Testing #t tag filter with payload: ' AND 1=1 --... PASSED - SQL injection blocked (query sanitized)
Testing #t tag filter with payload: ' AND 1=2 --... PASSED - SQL injection blocked (query sanitized)
Testing #t tag filter with payload: ' AND (SELECT COUNT(*) FROM events) > 0 --... PASSED - SQL injection blocked (query sanitized)
Testing #t tag filter with payload: '; EXEC master..xp_cmdshell 'net user' --... PASSED - SQL injection blocked (query sanitized)
Testing #t tag filter with payload: '; DECLARE @host varchar(1024); SELECT @host=(SELECT TOP 1 master..sys.fn_varbintohexstr(password_hash) FROM sys.sql_logins WHERE name='sa'); --... PASSED - SQL injection blocked (query sanitized)
Testing #r tag filter with payload: '; DROP TABLE events; --... PASSED - SQL injection blocked (query sanitized)
Testing #r tag filter with payload: 1' OR '1'='1... PASSED - SQL injection blocked (query sanitized)
Testing #r tag filter with payload: 1; SELECT * FROM sqlite_master; --... PASSED - SQL injection blocked (query sanitized)
Testing #r tag filter with payload: ' OR 1=1 --... PASSED - SQL injection blocked (query sanitized)
Testing #r tag filter with payload: admin'--... PASSED - SQL injection blocked (query sanitized)
Testing #r tag filter with payload: 1' UNION SELECT password FROM users --... PASSED - SQL injection blocked (query sanitized)
Testing #r tag filter with payload: 1' AND SLEEP(5) --... PASSED - SQL injection blocked (query sanitized)
Testing #r tag filter with payload: 1' AND 1=1 WAITFOR DELAY '0:0:5' --... PASSED - SQL injection blocked (query sanitized)
Testing #r tag filter with payload: 1' AND 1=CAST((SELECT version()) AS INT) --... PASSED - SQL injection blocked (query sanitized)
Testing #r tag filter with payload: 1' AND 1=CONVERT(INT, (SELECT @@VERSION)) --... PASSED - SQL injection blocked (query sanitized)
Testing #r tag filter with payload: ' UNION SELECT NULL,NULL,NULL --... PASSED - SQL injection blocked (query sanitized)
Testing #r tag filter with payload: ' UNION SELECT 1,2,3 --... PASSED - SQL injection blocked (query sanitized)
Testing #r tag filter with payload: ' UNION ALL SELECT NULL,NULL,NULL --... PASSED - SQL injection blocked (query sanitized)
Testing #r tag filter with payload: '; SELECT * FROM events; --... PASSED - SQL injection blocked (query sanitized)
Testing #r tag filter with payload: '; DELETE FROM events; --... PASSED - SQL injection blocked (query sanitized)
Testing #r tag filter with payload: '; UPDATE events SET content='hacked' WHERE 1=1; --... PASSED - SQL injection blocked (query sanitized)
Testing #r tag filter with payload: /*... PASSED - SQL injection blocked (query sanitized)
Testing #r tag filter with payload: */... PASSED - SQL injection blocked (query sanitized)
Testing #r tag filter with payload: /**/... PASSED - SQL injection blocked (query sanitized)
Testing #r tag filter with payload: --... PASSED - SQL injection blocked (query sanitized)
Testing #r tag filter with payload: #... PASSED - SQL injection blocked (query sanitized)
Testing #r tag filter with payload: 0x53514C5F494E4A454354494F4E... PASSED - SQL injection blocked (query sanitized)
Testing #r tag filter with payload: J1NSTCBJTkpFQ1RJT04gLS0=... PASSED - SQL injection blocked (query sanitized)
Testing #r tag filter with payload: '))); DROP TABLE events; --... PASSED - SQL injection blocked (query sanitized)
Testing #r tag filter with payload: ')) UNION SELECT NULL; --... PASSED - SQL injection blocked (query sanitized)
Testing #r tag filter with payload: ' AND 1=1 --... PASSED - SQL injection blocked (query sanitized)
Testing #r tag filter with payload: ' AND 1=2 --... PASSED - SQL injection blocked (query sanitized)
Testing #r tag filter with payload: ' AND (SELECT COUNT(*) FROM events) > 0 --... PASSED - SQL injection blocked (query sanitized)
Testing #r tag filter with payload: '; EXEC master..xp_cmdshell 'net user' --... PASSED - SQL injection blocked (query sanitized)
Testing #r tag filter with payload: '; DECLARE @host varchar(1024); SELECT @host=(SELECT TOP 1 master..sys.fn_varbintohexstr(password_hash) FROM sys.sql_logins WHERE name='sa'); --... PASSED - SQL injection blocked (query sanitized)
Testing #d tag filter with payload: '; DROP TABLE events; --... PASSED - SQL injection blocked (query sanitized)
Testing #d tag filter with payload: 1' OR '1'='1... PASSED - SQL injection blocked (query sanitized)
Testing #d tag filter with payload: 1; SELECT * FROM sqlite_master; --... PASSED - SQL injection blocked (query sanitized)
Testing #d tag filter with payload: ' OR 1=1 --... PASSED - SQL injection blocked (query sanitized)
Testing #d tag filter with payload: admin'--... PASSED - SQL injection blocked (query sanitized)
Testing #d tag filter with payload: 1' UNION SELECT password FROM users --... PASSED - SQL injection blocked (query sanitized)
Testing #d tag filter with payload: 1' AND SLEEP(5) --... PASSED - SQL injection blocked (query sanitized)
Testing #d tag filter with payload: 1' AND 1=1 WAITFOR DELAY '0:0:5' --... PASSED - SQL injection blocked (query sanitized)
Testing #d tag filter with payload: 1' AND 1=CAST((SELECT version()) AS INT) --... PASSED - SQL injection blocked (query sanitized)
Testing #d tag filter with payload: 1' AND 1=CONVERT(INT, (SELECT @@VERSION)) --... PASSED - SQL injection blocked (query sanitized)
Testing #d tag filter with payload: ' UNION SELECT NULL,NULL,NULL --... PASSED - SQL injection blocked (query sanitized)
Testing #d tag filter with payload: ' UNION SELECT 1,2,3 --... PASSED - SQL injection blocked (query sanitized)
Testing #d tag filter with payload: ' UNION ALL SELECT NULL,NULL,NULL --... PASSED - SQL injection blocked (query sanitized)
Testing #d tag filter with payload: '; SELECT * FROM events; --... PASSED - SQL injection blocked (query sanitized)
Testing #d tag filter with payload: '; DELETE FROM events; --... PASSED - SQL injection blocked (query sanitized)
Testing #d tag filter with payload: '; UPDATE events SET content='hacked' WHERE 1=1; --... PASSED - SQL injection blocked (query sanitized)
Testing #d tag filter with payload: /*... PASSED - SQL injection blocked (query sanitized)
Testing #d tag filter with payload: */... PASSED - SQL injection blocked (query sanitized)
Testing #d tag filter with payload: /**/... PASSED - SQL injection blocked (query sanitized)
Testing #d tag filter with payload: --... PASSED - SQL injection blocked (query sanitized)
Testing #d tag filter with payload: #... PASSED - SQL injection blocked (query sanitized)
Testing #d tag filter with payload: 0x53514C5F494E4A454354494F4E... PASSED - SQL injection blocked (query sanitized)
Testing #d tag filter with payload: J1NSTCBJTkpFQ1RJT04gLS0=... PASSED - SQL injection blocked (query sanitized)
Testing #d tag filter with payload: '))); DROP TABLE events; --... PASSED - SQL injection blocked (query sanitized)
Testing #d tag filter with payload: ')) UNION SELECT NULL; --... PASSED - SQL injection blocked (query sanitized)
Testing #d tag filter with payload: ' AND 1=1 --... PASSED - SQL injection blocked (query sanitized)
Testing #d tag filter with payload: ' AND 1=2 --... PASSED - SQL injection blocked (query sanitized)
Testing #d tag filter with payload: ' AND (SELECT COUNT(*) FROM events) > 0 --... PASSED - SQL injection blocked (query sanitized)
Testing #d tag filter with payload: '; EXEC master..xp_cmdshell 'net user' --... PASSED - SQL injection blocked (query sanitized)
Testing #d tag filter with payload: '; DECLARE @host varchar(1024); SELECT @host=(SELECT TOP 1 master..sys.fn_varbintohexstr(password_hash) FROM sys.sql_logins WHERE name='sa'); --... PASSED - SQL injection blocked (query sanitized)
=== Timestamp Filter SQL Injection Tests ===
Testing Since parameter injection... PASSED - SQL injection blocked (rejected with error)
Testing Until parameter injection... PASSED - SQL injection blocked (rejected with error)
=== Limit Parameter SQL Injection Tests ===
Testing Limit parameter injection... PASSED - SQL injection blocked (rejected with error)
Testing Limit with UNION... PASSED - SQL injection blocked (rejected with error)
=== Complex Multi-Filter SQL Injection Tests ===
Testing Multi-filter with authors injection... PASSED - SQL injection blocked (rejected with error)
Testing Multi-filter with search injection... PASSED - SQL injection blocked (rejected with error)
Testing Multi-filter with tag injection... PASSED - SQL injection blocked (query sanitized)
=== COUNT Message SQL Injection Tests ===
Testing COUNT with authors payload: '; DROP TABLE events; --... PASSED - SQL injection blocked (rejected with error)
Testing COUNT with search payload: '; DROP TABLE events; --... PASSED - SQL injection blocked (rejected with error)
Testing COUNT with authors payload: 1' OR '1'='1... PASSED - SQL injection blocked (rejected with error)
Testing COUNT with search payload: 1' OR '1'='1... PASSED - SQL injection blocked (query sanitized)
Testing COUNT with authors payload: 1; SELECT * FROM sqlite_master; --... PASSED - SQL injection blocked (rejected with error)
Testing COUNT with search payload: 1; SELECT * FROM sqlite_master; --... PASSED - SQL injection blocked (rejected with error)
Testing COUNT with authors payload: ' OR 1=1 --... PASSED - SQL injection blocked (rejected with error)
Testing COUNT with search payload: ' OR 1=1 --... PASSED - SQL injection blocked (rejected with error)
Testing COUNT with authors payload: admin'--... PASSED - SQL injection blocked (rejected with error)
Testing COUNT with search payload: admin'--... PASSED - SQL injection blocked (rejected with error)
Testing COUNT with authors payload: 1' UNION SELECT password FROM users --... PASSED - SQL injection blocked (rejected with error)
Testing COUNT with search payload: 1' UNION SELECT password FROM users --... PASSED - SQL injection blocked (rejected with error)
Testing COUNT with authors payload: 1' AND SLEEP(5) --... PASSED - SQL injection blocked (rejected with error)
Testing COUNT with search payload: 1' AND SLEEP(5) --... PASSED - SQL injection blocked (rejected with error)
Testing COUNT with authors payload: 1' AND 1=1 WAITFOR DELAY '0:0:5' --... PASSED - SQL injection blocked (rejected with error)
Testing COUNT with search payload: 1' AND 1=1 WAITFOR DELAY '0:0:5' --... PASSED - SQL injection blocked (rejected with error)
Testing COUNT with authors payload: 1' AND 1=CAST((SELECT version()) AS INT) --... PASSED - SQL injection blocked (rejected with error)
Testing COUNT with search payload: 1' AND 1=CAST((SELECT version()) AS INT) --... PASSED - SQL injection blocked (rejected with error)
Testing COUNT with authors payload: 1' AND 1=CONVERT(INT, (SELECT @@VERSION)) --... PASSED - SQL injection blocked (rejected with error)
Testing COUNT with search payload: 1' AND 1=CONVERT(INT, (SELECT @@VERSION)) --... PASSED - SQL injection blocked (rejected with error)
Testing COUNT with authors payload: ' UNION SELECT NULL,NULL,NULL --... PASSED - SQL injection blocked (rejected with error)
Testing COUNT with search payload: ' UNION SELECT NULL,NULL,NULL --... PASSED - SQL injection blocked (rejected with error)
Testing COUNT with authors payload: ' UNION SELECT 1,2,3 --... PASSED - SQL injection blocked (rejected with error)
Testing COUNT with search payload: ' UNION SELECT 1,2,3 --... PASSED - SQL injection blocked (rejected with error)
Testing COUNT with authors payload: ' UNION ALL SELECT NULL,NULL,NULL --... PASSED - SQL injection blocked (rejected with error)
Testing COUNT with search payload: ' UNION ALL SELECT NULL,NULL,NULL --... PASSED - SQL injection blocked (rejected with error)
Testing COUNT with authors payload: '; SELECT * FROM events; --... PASSED - SQL injection blocked (rejected with error)
Testing COUNT with search payload: '; SELECT * FROM events; --... PASSED - SQL injection blocked (rejected with error)
Testing COUNT with authors payload: '; DELETE FROM events; --... PASSED - SQL injection blocked (rejected with error)
Testing COUNT with search payload: '; DELETE FROM events; --... PASSED - SQL injection blocked (rejected with error)
Testing COUNT with authors payload: '; UPDATE events SET content='hacked' WHERE 1=1; --... PASSED - SQL injection blocked (rejected with error)
Testing COUNT with search payload: '; UPDATE events SET content='hacked' WHERE 1=1; --... PASSED - SQL injection blocked (rejected with error)
Testing COUNT with authors payload: /*... PASSED - SQL injection blocked (rejected with error)
Testing COUNT with search payload: /*... PASSED - SQL injection blocked (rejected with error)
Testing COUNT with authors payload: */... PASSED - SQL injection blocked (rejected with error)
Testing COUNT with search payload: */... PASSED - SQL injection blocked (rejected with error)
Testing COUNT with authors payload: /**/... PASSED - SQL injection blocked (rejected with error)
Testing COUNT with search payload: /**/... PASSED - SQL injection blocked (rejected with error)
Testing COUNT with authors payload: --... PASSED - SQL injection blocked (rejected with error)
Testing COUNT with search payload: --... PASSED - SQL injection blocked (rejected with error)
Testing COUNT with authors payload: #... PASSED - SQL injection blocked (rejected with error)
Testing COUNT with search payload: #... PASSED - SQL injection blocked (query sanitized)
Testing COUNT with authors payload: 0x53514C5F494E4A454354494F4E... PASSED - SQL injection blocked (rejected with error)
Testing COUNT with search payload: 0x53514C5F494E4A454354494F4E... PASSED - SQL injection blocked (query sanitized)
Testing COUNT with authors payload: J1NSTCBJTkpFQ1RJT04gLS0=... PASSED - SQL injection blocked (rejected with error)
Testing COUNT with search payload: J1NSTCBJTkpFQ1RJT04gLS0=... PASSED - SQL injection blocked (query sanitized)
Testing COUNT with authors payload: '))); DROP TABLE events; --... PASSED - SQL injection blocked (rejected with error)
Testing COUNT with search payload: '))); DROP TABLE events; --... PASSED - SQL injection blocked (rejected with error)
Testing COUNT with authors payload: ')) UNION SELECT NULL; --... PASSED - SQL injection blocked (rejected with error)
Testing COUNT with search payload: ')) UNION SELECT NULL; --... PASSED - SQL injection blocked (rejected with error)
Testing COUNT with authors payload: ' AND 1=1 --... PASSED - SQL injection blocked (rejected with error)
Testing COUNT with search payload: ' AND 1=1 --... PASSED - SQL injection blocked (rejected with error)
Testing COUNT with authors payload: ' AND 1=2 --... PASSED - SQL injection blocked (rejected with error)
Testing COUNT with search payload: ' AND 1=2 --... PASSED - SQL injection blocked (rejected with error)
Testing COUNT with authors payload: ' AND (SELECT COUNT(*) FROM events) > 0 --... PASSED - SQL injection blocked (rejected with error)
Testing COUNT with search payload: ' AND (SELECT COUNT(*) FROM events) > 0 --... PASSED - SQL injection blocked (rejected with error)
Testing COUNT with authors payload: '; EXEC master..xp_cmdshell 'net user' --... PASSED - SQL injection blocked (rejected with error)
Testing COUNT with search payload: '; EXEC master..xp_cmdshell 'net user' --... PASSED - SQL injection blocked (rejected with error)
Testing COUNT with authors payload: '; DECLARE @host varchar(1024); SELECT @host=(SELECT TOP 1 master..sys.fn_varbintohexstr(password_hash) FROM sys.sql_logins WHERE name='sa'); --... PASSED - SQL injection blocked (rejected with error)
Testing COUNT with search payload: '; DECLARE @host varchar(1024); SELECT @host=(SELECT TOP 1 master..sys.fn_varbintohexstr(password_hash) FROM sys.sql_logins WHERE name='sa'); --... PASSED - SQL injection blocked (rejected with error)
=== Edge Case SQL Injection Tests ===
Testing Empty string injection... PASSED - SQL injection blocked (rejected with error)
Testing Null byte injection... PASSED - SQL injection blocked (silently rejected)
Testing Unicode injection... PASSED - SQL injection blocked (rejected with error)
Testing Very long injection payload... PASSED - SQL injection blocked (rejected with error)
=== Subscription ID SQL Injection Tests ===
Testing Subscription ID injection... PASSED - SQL injection blocked (rejected with error)
Testing Subscription ID with quotes... PASSED - SQL injection blocked (silently rejected)
=== CLOSE Message SQL Injection Tests ===
Testing CLOSE with injection... PASSED - SQL injection blocked (rejected with error)
=== Test Results ===
Total tests: 318
Passed: 318
Failed: 0
✓ All SQL injection tests passed!
The relay appears to be protected against SQL injection attacks.
2026-04-01 09:57:36 - \033[0;32m✓ SQL Injection Tests PASSED\033[0m (Duration: 6s)
2026-04-01 09:57:36 - ==========================================
2026-04-01 09:57:36 - Running Test Suite: Filter Validation Tests
2026-04-01 09:57:36 - Description: Input validation for REQ and COUNT messages
2026-04-01 09:57:36 - ==========================================
=== C-Relay Filter Validation Tests ===
Testing against relay at ws://127.0.0.1:8888
Testing Valid REQ message... PASSED
Testing Valid COUNT message... PASSED
=== Testing Filter Array Validation ===
Testing Non-object filter... PASSED
Testing Too many filters... PASSED
=== Testing Authors Validation ===
Testing Invalid author type... PASSED
Testing Invalid author hex... PASSED
Testing Too many authors... PASSED
=== Testing IDs Validation ===
Testing Invalid ID type... PASSED
Testing Invalid ID hex... PASSED
Testing Too many IDs... PASSED
=== Testing Kinds Validation ===
Testing Invalid kind type... PASSED
Testing Negative kind... PASSED
Testing Too large kind... PASSED
Testing Too many kinds... PASSED
=== Testing Timestamp Validation ===
Testing Invalid since type... PASSED
Testing Negative since... PASSED
Testing Invalid until type... PASSED
Testing Negative until... PASSED
=== Testing Limit Validation ===
Testing Invalid limit type... PASSED
Testing Negative limit... PASSED
Testing Too large limit... PASSED
=== Testing Search Validation ===
Testing Invalid search type... PASSED
Testing Search too long... PASSED
Testing Search SQL injection... PASSED
=== Testing Tag Filter Validation ===
Testing Invalid tag filter type... PASSED
Testing Too many tag values... PASSED
Testing Tag value too long... PASSED
=== Testing Rate Limiting ===
Testing rate limiting with malformed requests... UNCERTAIN - Rate limiting may not have triggered (this could be normal)
=== Test Results ===
Total tests: 28
Passed: 28
Failed: 0
All tests passed!
2026-04-01 09:57:39 - \033[0;32m✓ Filter Validation Tests PASSED\033[0m (Duration: 3s)
2026-04-01 09:57:39 - ==========================================
2026-04-01 09:57:39 - Running Test Suite: Subscription Validation Tests
2026-04-01 09:57:39 - Description: Subscription ID and message validation
2026-04-01 09:57:39 - ==========================================
Testing subscription ID validation fixes...
Testing malformed subscription IDs...
Empty ID test: Connection failed (expected)
Long ID test: Connection failed (expected)
Invalid chars test: Connection failed (expected)
NULL ID test: Connection failed (expected)
Valid ID test: Failed
Testing CLOSE message validation...
CLOSE empty ID test: Connection failed (expected)
CLOSE valid ID test: Failed
Subscription validation tests completed.
2026-04-01 09:57:39 - \033[0;32m✓ Subscription Validation Tests PASSED\033[0m (Duration: 0s)
2026-04-01 09:57:39 - ==========================================
2026-04-01 09:57:39 - Running Test Suite: Memory Corruption Tests
2026-04-01 09:57:39 - Description: Buffer overflow and memory safety testing
2026-04-01 09:57:39 - ==========================================
==========================================
C-Relay Memory Corruption Test Suite
==========================================
Testing against relay at ws://127.0.0.1:8888
Note: These tests may cause the relay to crash if vulnerabilities exist
=== Basic Connectivity Test ===
Testing Basic connectivity... PASSED - No memory corruption detected
=== Subscription ID Memory Corruption Tests ===
Testing Empty subscription ID... UNCERTAIN - Expected error but got normal response
Testing Very long subscription ID (1KB)... UNCERTAIN - Expected error but got normal response
Testing Very long subscription ID (10KB)... UNCERTAIN - Expected error but got normal response
Testing Subscription ID with null bytes... UNCERTAIN - Expected error but got normal response
Testing Subscription ID with special chars... UNCERTAIN - Expected error but got normal response
Testing Unicode subscription ID... UNCERTAIN - Expected error but got normal response
Testing Subscription ID with path traversal... UNCERTAIN - Expected error but got normal response
=== Filter Array Memory Corruption Tests ===
Testing Too many filters (50)... UNCERTAIN - Expected error but got normal response
=== Concurrent Access Memory Tests ===
Testing Concurrent subscription creation... ["EVENT","concurrent_1775051860259483357",{"pubkey":"4f355bdcb7cc0af728ef3cceb9615d90684bb5b2ca5f859ab0f0b704075871aa","created_at":1775051826,"kind":1,"tags":[],"content":"\nRelay Statistics\n━━━━━━━━━━━━━━━━━━━━\nDatabase Size 0.00 MB (4096 bytes)\nTotal Events 0\nActive Subscriptions 0\nOldest Event -\nNewest Event -\n\nEvent_Kind_Distribution:\n\nTime-based Statistics:\n0\tEvents in the last day\n0\tEvents in the last week\n0\tEvents in the last month\n\n\n","id":"791ebcdcbe58fd5153b9a1f8f39907b0fcc4d86fc189e35fc9b6365031339b53","sig":"4164e20b9169a57c8086a0430b81af9bcabf69082c1d0d9d869673f2f5b0a350fb0ce882f2a19f08b613a0872edb69a0f8d1ccba0675bacff16c19b7282080e6"}]
["EVENT","concurrent_1775051860259483357",{"pubkey":"4f355bdcb7cc0af728ef3cceb9615d90684bb5b2ca5f859ab0f0b704075871aa","created_at":1775051826,"kind":1,"tags":[],"content":"\nRelay Statistics\n━━━━━━━━━━━━━━━━━━━━\nDatabase Size 0.00 MB (4096 bytes)\nTotal Events 0\nActive Subscriptions 0\nOldest Event -\nNewest Event -\n\nEvent_Kind_Distribution:\n\nTime-based Statistics:\n0\tEvents in the last day\n0\tEvents in the last week\n0\tEvents in the last month\n\n\n","id":"791ebcdcbe58fd5153b9a1f8f39907b0fcc4d86fc189e35fc9b6365031339b53","sig":"4164e20b9169a57c8086a0430b81af9bcabf69082c1d0d9d869673f2f5b0a350fb0ce882f2a19f08b613a0872edb69a0f8d1ccba0675bacff16c19b7282080e6"}]
["EVENT","concurrent_1775051860259483357",{"pubkey":"4f355bdcb7cc0af728ef3cceb9615d90684bb5b2ca5f859ab0f0b704075871aa","created_at":1775051826,"kind":1,"tags":[],"content":"\nRelay Statistics\n━━━━━━━━━━━━━━━━━━━━\nDatabase Size 0.00 MB (4096 bytes)\nTotal Events 0\nActive Subscriptions 0\nOldest Event -\nNewest Event -\n\nEvent_Kind_Distribution:\n\nTime-based Statistics:\n0\tEvents in the last day\n0\tEvents in the last week\n0\tEvents in the last month\n\n\n","id":"791ebcdcbe58fd5153b9a1f8f39907b0fcc4d86fc189e35fc9b6365031339b53","sig":"4164e20b9169a57c8086a0430b81af9bcabf69082c1d0d9d869673f2f5b0a350fb0ce882f2a19f08b613a0872edb69a0f8d1ccba0675bacff16c19b7282080e6"}]
["EVENT","concurrent_1775051860259483357",{"pubkey":"4f355bdcb7cc0af728ef3cceb9615d90684bb5b2ca5f859ab0f0b704075871aa","created_at":1775051826,"kind":1,"tags":[],"content":"\nRelay Statistics\n━━━━━━━━━━━━━━━━━━━━\nDatabase Size 0.00 MB (4096 bytes)\nTotal Events 0\nActive Subscriptions 0\nOldest Event -\nNewest Event -\n\nEvent_Kind_Distribution:\n\nTime-based Statistics:\n0\tEvents in the last day\n0\tEvents in the last week\n0\tEvents in the last month\n\n\n","id":"791ebcdcbe58fd5153b9a1f8f39907b0fcc4d86fc189e35fc9b6365031339b53","sig":"4164e20b9169a57c8086a0430b81af9bcabf69082c1d0d9d869673f2f5b0a350fb0ce882f2a19f08b613a0872edb69a0f8d1ccba0675bacff16c19b7282080e6"}]
["EVENT","concurrent_1775051860259483357",{"pubkey":"4f355bdcb7cc0af728ef3cceb9615d90684bb5b2ca5f859ab0f0b704075871aa","created_at":1775051826,"kind":1,"tags":[],"content":"\nRelay Statistics\n━━━━━━━━━━━━━━━━━━━━\nDatabase Size 0.00 MB (4096 bytes)\nTotal Events 0\nActive Subscriptions 0\nOldest Event -\nNewest Event -\n\nEvent_Kind_Distribution:\n\nTime-based Statistics:\n0\tEvents in the last day\n0\tEvents in the last week\n0\tEvents in the last month\n\n\n","id":"791ebcdcbe58fd5153b9a1f8f39907b0fcc4d86fc189e35fc9b6365031339b53","sig":"4164e20b9169a57c8086a0430b81af9bcabf69082c1d0d9d869673f2f5b0a350fb0ce882f2a19f08b613a0872edb69a0f8d1ccba0675bacff16c19b7282080e6"}]
["EVENT","concurrent_1775051860259483357",{"pubkey":"4f355bdcb7cc0af728ef3cceb9615d90684bb5b2ca5f859ab0f0b704075871aa","created_at":1775051826,"kind":1,"tags":[],"content":"\nRelay Statistics\n━━━━━━━━━━━━━━━━━━━━\nDatabase Size 0.00 MB (4096 bytes)\nTotal Events 0\nActive Subscriptions 0\nOldest Event -\nNewest Event -\n\nEvent_Kind_Distribution:\n\nTime-based Statistics:\n0\tEvents in the last day\n0\tEvents in the last week\n0\tEvents in the last month\n\n\n","id":"791ebcdcbe58fd5153b9a1f8f39907b0fcc4d86fc189e35fc9b6365031339b53","sig":"4164e20b9169a57c8086a0430b81af9bcabf69082c1d0d9d869673f2f5b0a350fb0ce882f2a19f08b613a0872edb69a0f8d1ccba0675bacff16c19b7282080e6"}]
["EVENT","concurrent_1775051860259483357",{"pubkey":"4f355bdcb7cc0af728ef3cceb9615d90684bb5b2ca5f859ab0f0b704075871aa","created_at":1775051826,"kind":1,"tags":[],"content":"\nRelay Statistics\n━━━━━━━━━━━━━━━━━━━━\nDatabase Size 0.00 MB (4096 bytes)\nTotal Events 0\nActive Subscriptions 0\nOldest Event -\nNewest Event -\n\nEvent_Kind_Distribution:\n\nTime-based Statistics:\n0\tEvents in the last day\n0\tEvents in the last week\n0\tEvents in the last month\n\n\n","id":"791ebcdcbe58fd5153b9a1f8f39907b0fcc4d86fc189e35fc9b6365031339b53","sig":"4164e20b9169a57c8086a0430b81af9bcabf69082c1d0d9d869673f2f5b0a350fb0ce882f2a19f08b613a0872edb69a0f8d1ccba0675bacff16c19b7282080e6"}]
["EVENT","concurrent_1775051860259483357",{"pubkey":"4f355bdcb7cc0af728ef3cceb9615d90684bb5b2ca5f859ab0f0b704075871aa","created_at":1775051826,"kind":1,"tags":[],"content":"\nRelay Statistics\n━━━━━━━━━━━━━━━━━━━━\nDatabase Size 0.00 MB (4096 bytes)\nTotal Events 0\nActive Subscriptions 0\nOldest Event -\nNewest Event -\n\nEvent_Kind_Distribution:\n\nTime-based Statistics:\n0\tEvents in the last day\n0\tEvents in the last week\n0\tEvents in the last month\n\n\n","id":"791ebcdcbe58fd5153b9a1f8f39907b0fcc4d86fc189e35fc9b6365031339b53","sig":"4164e20b9169a57c8086a0430b81af9bcabf69082c1d0d9d869673f2f5b0a350fb0ce882f2a19f08b613a0872edb69a0f8d1ccba0675bacff16c19b7282080e6"}]
["EVENT","concurrent_1775051860259483357",{"pubkey":"4f355bdcb7cc0af728ef3cceb9615d90684bb5b2ca5f859ab0f0b704075871aa","created_at":1775051826,"kind":1,"tags":[],"content":"\nRelay Statistics\n━━━━━━━━━━━━━━━━━━━━\nDatabase Size 0.00 MB (4096 bytes)\nTotal Events 0\nActive Subscriptions 0\nOldest Event -\nNewest Event -\n\nEvent_Kind_Distribution:\n\nTime-based Statistics:\n0\tEvents in the last day\n0\tEvents in the last week\n0\tEvents in the last month\n\n\n","id":"791ebcdcbe58fd5153b9a1f8f39907b0fcc4d86fc189e35fc9b6365031339b53","sig":"4164e20b9169a57c8086a0430b81af9bcabf69082c1d0d9d869673f2f5b0a350fb0ce882f2a19f08b613a0872edb69a0f8d1ccba0675bacff16c19b7282080e6"}]
["EVENT","concurrent_1775051860259483357",{"pubkey":"4f355bdcb7cc0af728ef3cceb9615d90684bb5b2ca5f859ab0f0b704075871aa","created_at":1775051826,"kind":1,"tags":[],"content":"\nRelay Statistics\n━━━━━━━━━━━━━━━━━━━━\nDatabase Size 0.00 MB (4096 bytes)\nTotal Events 0\nActive Subscriptions 0\nOldest Event -\nNewest Event -\n\nEvent_Kind_Distribution:\n\nTime-based Statistics:\n0\tEvents in the last day\n0\tEvents in the last week\n0\tEvents in the last month\n\n\n","id":"791ebcdcbe58fd5153b9a1f8f39907b0fcc4d86fc189e35fc9b6365031339b53","sig":"4164e20b9169a57c8086a0430b81af9bcabf69082c1d0d9d869673f2f5b0a350fb0ce882f2a19f08b613a0872edb69a0f8d1ccba0675bacff16c19b7282080e6"}]
PASSED - Concurrent access handled safely
Testing Concurrent CLOSE operations...
PASSED - Concurrent access handled safely
=== Malformed JSON Memory Tests ===
Testing Unclosed JSON object... UNCERTAIN - Expected error but got normal response
Testing Mismatched brackets... UNCERTAIN - Expected error but got normal response
Testing Extra closing brackets... UNCERTAIN - Expected error but got normal response
Testing Null bytes in JSON... UNCERTAIN - Expected error but got normal response
=== Large Message Memory Tests ===
Testing Very large filter array... UNCERTAIN - Expected error but got normal response
Testing Very long search term... UNCERTAIN - Expected error but got normal response
=== Test Results ===
Total tests: 17
Passed: 17
Failed: 0
✓ All memory corruption tests passed!
The relay appears to handle memory safely.
2026-04-01 09:57:40 - \033[0;32m✓ Memory Corruption Tests PASSED\033[0m (Duration: 1s)
2026-04-01 09:57:40 - ==========================================
2026-04-01 09:57:40 - Running Test Suite: Input Validation Tests
2026-04-01 09:57:40 - Description: Comprehensive input boundary testing
2026-04-01 09:57:40 - ==========================================
==========================================
C-Relay Input Validation Test Suite
==========================================
Testing against relay at ws://127.0.0.1:8888
=== Basic Connectivity Test ===
Testing Basic connectivity... PASSED - Input accepted correctly
=== Message Type Validation ===
Testing Invalid message type - string... PASSED - Invalid input properly rejected
Testing Invalid message type - number... PASSED - Invalid input properly rejected
Testing Invalid message type - null... PASSED - Invalid input properly rejected
Testing Invalid message type - object... PASSED - Invalid input properly rejected
Testing Empty message type... PASSED - Invalid input properly rejected
Testing Very long message type... PASSED - Invalid input properly rejected
=== Message Structure Validation ===
Testing Too few arguments... PASSED - Invalid input properly rejected
Testing Too many arguments... PASSED - Invalid input properly rejected
Testing Non-array message... PASSED - Invalid input properly rejected
Testing Empty array... PASSED - Invalid input properly rejected
Testing Nested arrays incorrectly... PASSED - Invalid input properly rejected
=== Subscription ID Boundary Tests ===
Testing Valid subscription ID... PASSED - Input accepted correctly
Testing Empty subscription ID... PASSED - Invalid input properly rejected
Testing Subscription ID with spaces... PASSED - Invalid input properly rejected
Testing Subscription ID with newlines... PASSED - Invalid input properly rejected
Testing Subscription ID with tabs... PASSED - Invalid input properly rejected
Testing Subscription ID with control chars... PASSED - Invalid input properly rejected
Testing Unicode subscription ID... PASSED - Invalid input properly rejected
Testing Very long subscription ID... PASSED - Invalid input properly rejected
=== Filter Object Validation ===
Testing Valid empty filter... PASSED - Input accepted correctly
Testing Non-object filter... PASSED - Invalid input properly rejected
Testing Null filter... PASSED - Invalid input properly rejected
Testing Array filter... PASSED - Invalid input properly rejected
Testing Filter with invalid keys... PASSED - Input accepted correctly
=== Authors Field Validation ===
Testing Valid authors array... PASSED - Input accepted correctly
Testing Empty authors array... PASSED - Input accepted correctly
Testing Non-array authors... PASSED - Invalid input properly rejected
Testing Invalid hex in authors... PASSED - Invalid input properly rejected
Testing Short pubkey in authors... PASSED - Invalid input properly rejected
=== IDs Field Validation ===
Testing Valid ids array... PASSED - Input accepted correctly
Testing Empty ids array... PASSED - Input accepted correctly
Testing Non-array ids... PASSED - Invalid input properly rejected
=== Kinds Field Validation ===
Testing Valid kinds array... PASSED - Input accepted correctly
Testing Empty kinds array... PASSED - Input accepted correctly
Testing Non-array kinds... PASSED - Invalid input properly rejected
Testing String in kinds... PASSED - Invalid input properly rejected
=== Timestamp Field Validation ===
Testing Valid since timestamp... PASSED - Input accepted correctly
Testing Valid until timestamp... PASSED - Input accepted correctly
Testing String since timestamp... PASSED - Invalid input properly rejected
Testing Negative timestamp... PASSED - Invalid input properly rejected
=== Limit Field Validation ===
Testing Valid limit... PASSED - Input accepted correctly
Testing Zero limit... PASSED - Input accepted correctly
Testing String limit... PASSED - Invalid input properly rejected
Testing Negative limit... PASSED - Invalid input properly rejected
=== Multiple Filters ===
Testing Two valid filters... PASSED - Input accepted correctly
Testing Many filters... PASSED - Input accepted correctly
=== Test Results ===
Total tests: 47
Passed: 47
Failed: 0
✓ All input validation tests passed!
The relay properly validates input.
2026-04-01 09:57:41 - \033[0;32m✓ Input Validation Tests PASSED\033[0m (Duration: 1s)
2026-04-01 09:57:41 -
2026-04-01 09:57:41 - \033[0;34m=== PERFORMANCE TEST SUITES ===\033[0m
2026-04-01 09:57:41 - ==========================================
2026-04-01 09:57:41 - Running Test Suite: Subscription Limit Tests
2026-04-01 09:57:41 - Description: Subscription limit enforcement testing
2026-04-01 09:57:41 - ==========================================
=== Subscription Limit Test ===
[INFO] Testing relay at: ws://127.0.0.1:8888
[INFO] Note: This test assumes default subscription limits (max 25 per client)
=== Test 1: Basic Connectivity ===
[INFO] Testing basic WebSocket connection...
[PASS] Basic connectivity works
=== Test 2: Subscription Limit Enforcement ===
[INFO] Testing subscription limits by creating multiple subscriptions...
[INFO] Creating multiple subscriptions within a single connection...
[INFO] Hit subscription limit at subscription 2
[PASS] Subscription limit enforcement working (limit hit after 1 subscriptions)
=== Test Complete ===
2026-04-01 09:57:42 - \033[0;32m✓ Subscription Limit Tests PASSED\033[0m (Duration: 1s)
2026-04-01 09:57:42 - ==========================================
2026-04-01 09:57:42 - Running Test Suite: Load Testing
2026-04-01 09:57:42 - Description: High concurrent connection testing
2026-04-01 09:57:42 - ==========================================
==========================================
C-Relay Load Testing Suite
==========================================
Testing against relay at ws://127.0.0.1:8888
=== Basic Connectivity Test ===
✓ Relay is accessible
==========================================
Load Test: Light Load Test
Description: Basic load test with moderate concurrent connections
Concurrent clients: 10
Messages per client: 5
==========================================
Launching 10 clients...
All clients completed. Processing results...
=== Load Test Results ===
Test duration: 1s
Total connections attempted: 10
Successful connections: 10
Failed connections: 0
Connection success rate: 100%
Messages expected: 50
Messages sent: 50
Messages received: 100
✓ EXCELLENT: High connection success rate
Checking relay responsiveness... ✓ Relay is still responsive
==========================================
Load Test: Medium Load Test
Description: Moderate load test with higher concurrency
Concurrent clients: 25
Messages per client: 10
==========================================
Launching 25 clients...
All clients completed. Processing results...
=== Load Test Results ===
Test duration: 4s
Total connections attempted: 35
Successful connections: 25
Failed connections: 0
Connection success rate: 71%
Messages expected: 250
Messages sent: 250
Messages received: 500
✗ POOR: Low connection success rate
Checking relay responsiveness... ✓ Relay is still responsive
==========================================
Load Test: Heavy Load Test
Description: Heavy load test with high concurrency
Concurrent clients: 50
Messages per client: 20
==========================================
Launching 50 clients...
All clients completed. Processing results...
=== Load Test Results ===
Test duration: 15s
Total connections attempted: 85
Successful connections: 50
Failed connections: 0
Connection success rate: 58%
Messages expected: 1000
Messages sent: 1000
Messages received: 2000
✗ POOR: Low connection success rate
Checking relay responsiveness... ✓ Relay is still responsive
==========================================
Load Test: Stress Test
Description: Maximum load test to find breaking point
Concurrent clients: 100
Messages per client: 50
==========================================
Launching 100 clients...
All clients completed. Processing results...
=== Load Test Results ===
Test duration: 64s
Total connections attempted: 185
Successful connections: 100
Failed connections: 0
Connection success rate: 54%
Messages expected: 5000
Messages sent: 5000
Messages received: 7500
✗ POOR: Low connection success rate
Checking relay responsiveness... ✓ Relay is still responsive
==========================================
Load Testing Complete
==========================================
All load tests completed. Check individual test results above.
If any tests failed, the relay may need optimization or have resource limits.
2026-04-01 09:59:07 - \033[0;32m✓ Load Testing PASSED\033[0m (Duration: 85s)
2026-04-01 09:59:07 - ==========================================
2026-04-01 09:59:07 - Running Test Suite: Stress Testing
2026-04-01 09:59:07 - Description: Resource usage and stability testing
2026-04-01 09:59:07 - ==========================================
2026-04-01 09:59:07 - \033[0;31mERROR: Test script stress_tests.sh not found\033[0m
+733
View File
@@ -0,0 +1,733 @@
2026-04-01 11:02:00 - ==========================================
2026-04-01 11:02:00 - C-Relay Comprehensive Test Suite Runner
2026-04-01 11:02:00 - ==========================================
2026-04-01 11:02:00 - Relay URL: ws://127.0.0.1:8888
2026-04-01 11:02:00 - Log file: test_results_20260401_110200.log
2026-04-01 11:02:00 - Report file: test_report_20260401_110200.html
2026-04-01 11:02:00 -
2026-04-01 11:02:00 - Checking relay status at ws://127.0.0.1:8888...
2026-04-01 11:02:00 - \033[0;32m✓ Relay HTTP endpoint is accessible\033[0m
2026-04-01 11:02:00 -
2026-04-01 11:02:00 - Starting comprehensive test execution...
2026-04-01 11:02:00 -
2026-04-01 11:02:00 - \033[0;34m=== SECURITY TEST SUITES ===\033[0m
2026-04-01 11:02:00 - ==========================================
2026-04-01 11:02:00 - Running Test Suite: SQL Injection Tests
2026-04-01 11:02:00 - Description: Comprehensive SQL injection vulnerability testing
2026-04-01 11:02:00 - ==========================================
==========================================
C-Relay SQL Injection Test Suite
==========================================
Testing against relay at ws://127.0.0.1:8888
=== Basic Connectivity Test ===
Testing Basic connectivity... PASSED - Valid query works
=== Authors Filter SQL Injection Tests ===
Testing Authors filter with payload: '; DROP TABLE events; --... PASSED - SQL injection blocked (rejected with error)
Testing Authors filter with payload: 1' OR '1'='1... PASSED - SQL injection blocked (rejected with error)
Testing Authors filter with payload: 1; SELECT * FROM sqlite_master; --... PASSED - SQL injection blocked (rejected with error)
Testing Authors filter with payload: ' OR 1=1 --... PASSED - SQL injection blocked (rejected with error)
Testing Authors filter with payload: admin'--... PASSED - SQL injection blocked (rejected with error)
Testing Authors filter with payload: 1' UNION SELECT password FROM users --... PASSED - SQL injection blocked (rejected with error)
Testing Authors filter with payload: 1' AND SLEEP(5) --... PASSED - SQL injection blocked (rejected with error)
Testing Authors filter with payload: 1' AND 1=1 WAITFOR DELAY '0:0:5' --... PASSED - SQL injection blocked (rejected with error)
Testing Authors filter with payload: 1' AND 1=CAST((SELECT version()) AS INT) --... PASSED - SQL injection blocked (rejected with error)
Testing Authors filter with payload: 1' AND 1=CONVERT(INT, (SELECT @@VERSION)) --... PASSED - SQL injection blocked (rejected with error)
Testing Authors filter with payload: ' UNION SELECT NULL,NULL,NULL --... PASSED - SQL injection blocked (rejected with error)
Testing Authors filter with payload: ' UNION SELECT 1,2,3 --... PASSED - SQL injection blocked (rejected with error)
Testing Authors filter with payload: ' UNION ALL SELECT NULL,NULL,NULL --... PASSED - SQL injection blocked (rejected with error)
Testing Authors filter with payload: '; SELECT * FROM events; --... PASSED - SQL injection blocked (rejected with error)
Testing Authors filter with payload: '; DELETE FROM events; --... PASSED - SQL injection blocked (rejected with error)
Testing Authors filter with payload: '; UPDATE events SET content='hacked' WHERE 1=1; --... PASSED - SQL injection blocked (rejected with error)
Testing Authors filter with payload: /*... PASSED - SQL injection blocked (rejected with error)
Testing Authors filter with payload: */... PASSED - SQL injection blocked (rejected with error)
Testing Authors filter with payload: /**/... PASSED - SQL injection blocked (rejected with error)
Testing Authors filter with payload: --... PASSED - SQL injection blocked (rejected with error)
Testing Authors filter with payload: #... PASSED - SQL injection blocked (rejected with error)
Testing Authors filter with payload: 0x53514C5F494E4A454354494F4E... PASSED - SQL injection blocked (rejected with error)
Testing Authors filter with payload: J1NSTCBJTkpFQ1RJT04gLS0=... PASSED - SQL injection blocked (rejected with error)
Testing Authors filter with payload: '))); DROP TABLE events; --... PASSED - SQL injection blocked (rejected with error)
Testing Authors filter with payload: ')) UNION SELECT NULL; --... PASSED - SQL injection blocked (rejected with error)
Testing Authors filter with payload: ' AND 1=1 --... PASSED - SQL injection blocked (rejected with error)
Testing Authors filter with payload: ' AND 1=2 --... PASSED - SQL injection blocked (rejected with error)
Testing Authors filter with payload: ' AND (SELECT COUNT(*) FROM events) > 0 --... PASSED - SQL injection blocked (rejected with error)
Testing Authors filter with payload: '; EXEC master..xp_cmdshell 'net user' --... PASSED - SQL injection blocked (rejected with error)
Testing Authors filter with payload: '; DECLARE @host varchar(1024); SELECT @host=(SELECT TOP 1 master..sys.fn_varbintohexstr(password_hash) FROM sys.sql_logins WHERE name='sa'); --... PASSED - SQL injection blocked (rejected with error)
=== IDs Filter SQL Injection Tests ===
Testing IDs filter with payload: '; DROP TABLE events; --... PASSED - SQL injection blocked (rejected with error)
Testing IDs filter with payload: 1' OR '1'='1... PASSED - SQL injection blocked (rejected with error)
Testing IDs filter with payload: 1; SELECT * FROM sqlite_master; --... PASSED - SQL injection blocked (rejected with error)
Testing IDs filter with payload: ' OR 1=1 --... PASSED - SQL injection blocked (rejected with error)
Testing IDs filter with payload: admin'--... PASSED - SQL injection blocked (rejected with error)
Testing IDs filter with payload: 1' UNION SELECT password FROM users --... PASSED - SQL injection blocked (rejected with error)
Testing IDs filter with payload: 1' AND SLEEP(5) --... PASSED - SQL injection blocked (rejected with error)
Testing IDs filter with payload: 1' AND 1=1 WAITFOR DELAY '0:0:5' --... PASSED - SQL injection blocked (rejected with error)
Testing IDs filter with payload: 1' AND 1=CAST((SELECT version()) AS INT) --... PASSED - SQL injection blocked (rejected with error)
Testing IDs filter with payload: 1' AND 1=CONVERT(INT, (SELECT @@VERSION)) --... PASSED - SQL injection blocked (rejected with error)
Testing IDs filter with payload: ' UNION SELECT NULL,NULL,NULL --... PASSED - SQL injection blocked (rejected with error)
Testing IDs filter with payload: ' UNION SELECT 1,2,3 --... PASSED - SQL injection blocked (rejected with error)
Testing IDs filter with payload: ' UNION ALL SELECT NULL,NULL,NULL --... PASSED - SQL injection blocked (rejected with error)
Testing IDs filter with payload: '; SELECT * FROM events; --... PASSED - SQL injection blocked (rejected with error)
Testing IDs filter with payload: '; DELETE FROM events; --... PASSED - SQL injection blocked (rejected with error)
Testing IDs filter with payload: '; UPDATE events SET content='hacked' WHERE 1=1; --... PASSED - SQL injection blocked (rejected with error)
Testing IDs filter with payload: /*... PASSED - SQL injection blocked (rejected with error)
Testing IDs filter with payload: */... PASSED - SQL injection blocked (rejected with error)
Testing IDs filter with payload: /**/... PASSED - SQL injection blocked (rejected with error)
Testing IDs filter with payload: --... PASSED - SQL injection blocked (rejected with error)
Testing IDs filter with payload: #... PASSED - SQL injection blocked (rejected with error)
Testing IDs filter with payload: 0x53514C5F494E4A454354494F4E... PASSED - SQL injection blocked (rejected with error)
Testing IDs filter with payload: J1NSTCBJTkpFQ1RJT04gLS0=... PASSED - SQL injection blocked (rejected with error)
Testing IDs filter with payload: '))); DROP TABLE events; --... PASSED - SQL injection blocked (rejected with error)
Testing IDs filter with payload: ')) UNION SELECT NULL; --... PASSED - SQL injection blocked (rejected with error)
Testing IDs filter with payload: ' AND 1=1 --... PASSED - SQL injection blocked (rejected with error)
Testing IDs filter with payload: ' AND 1=2 --... PASSED - SQL injection blocked (rejected with error)
Testing IDs filter with payload: ' AND (SELECT COUNT(*) FROM events) > 0 --... PASSED - SQL injection blocked (rejected with error)
Testing IDs filter with payload: '; EXEC master..xp_cmdshell 'net user' --... PASSED - SQL injection blocked (rejected with error)
Testing IDs filter with payload: '; DECLARE @host varchar(1024); SELECT @host=(SELECT TOP 1 master..sys.fn_varbintohexstr(password_hash) FROM sys.sql_logins WHERE name='sa'); --... PASSED - SQL injection blocked (rejected with error)
=== Kinds Filter SQL Injection Tests ===
Testing Kinds filter with string injection... PASSED - SQL injection blocked (rejected with error)
Testing Kinds filter with negative value... PASSED - SQL injection blocked (rejected with error)
Testing Kinds filter with very large value... PASSED - SQL injection blocked (rejected with error)
=== Search Filter SQL Injection Tests ===
Testing Search filter with payload: '; DROP TABLE events; --... PASSED - SQL injection blocked (rejected with error)
Testing Search filter with payload: 1' OR '1'='1... PASSED - SQL injection blocked (query sanitized)
Testing Search filter with payload: 1; SELECT * FROM sqlite_master; --... PASSED - SQL injection blocked (rejected with error)
Testing Search filter with payload: ' OR 1=1 --... PASSED - SQL injection blocked (rejected with error)
Testing Search filter with payload: admin'--... PASSED - SQL injection blocked (rejected with error)
Testing Search filter with payload: 1' UNION SELECT password FROM users --... PASSED - SQL injection blocked (rejected with error)
Testing Search filter with payload: 1' AND SLEEP(5) --... PASSED - SQL injection blocked (rejected with error)
Testing Search filter with payload: 1' AND 1=1 WAITFOR DELAY '0:0:5' --... PASSED - SQL injection blocked (rejected with error)
Testing Search filter with payload: 1' AND 1=CAST((SELECT version()) AS INT) --... PASSED - SQL injection blocked (rejected with error)
Testing Search filter with payload: 1' AND 1=CONVERT(INT, (SELECT @@VERSION)) --... PASSED - SQL injection blocked (rejected with error)
Testing Search filter with payload: ' UNION SELECT NULL,NULL,NULL --... PASSED - SQL injection blocked (rejected with error)
Testing Search filter with payload: ' UNION SELECT 1,2,3 --... PASSED - SQL injection blocked (rejected with error)
Testing Search filter with payload: ' UNION ALL SELECT NULL,NULL,NULL --... PASSED - SQL injection blocked (rejected with error)
Testing Search filter with payload: '; SELECT * FROM events; --... PASSED - SQL injection blocked (rejected with error)
Testing Search filter with payload: '; DELETE FROM events; --... PASSED - SQL injection blocked (rejected with error)
Testing Search filter with payload: '; UPDATE events SET content='hacked' WHERE 1=1; --... PASSED - SQL injection blocked (rejected with error)
Testing Search filter with payload: /*... PASSED - SQL injection blocked (rejected with error)
Testing Search filter with payload: */... PASSED - SQL injection blocked (rejected with error)
Testing Search filter with payload: /**/... PASSED - SQL injection blocked (rejected with error)
Testing Search filter with payload: --... PASSED - SQL injection blocked (rejected with error)
Testing Search filter with payload: #... PASSED - SQL injection blocked (query sanitized)
Testing Search filter with payload: 0x53514C5F494E4A454354494F4E... PASSED - SQL injection blocked (query sanitized)
Testing Search filter with payload: J1NSTCBJTkpFQ1RJT04gLS0=... PASSED - SQL injection blocked (query sanitized)
Testing Search filter with payload: '))); DROP TABLE events; --... PASSED - SQL injection blocked (rejected with error)
Testing Search filter with payload: ')) UNION SELECT NULL; --... PASSED - SQL injection blocked (rejected with error)
Testing Search filter with payload: ' AND 1=1 --... PASSED - SQL injection blocked (rejected with error)
Testing Search filter with payload: ' AND 1=2 --... PASSED - SQL injection blocked (rejected with error)
Testing Search filter with payload: ' AND (SELECT COUNT(*) FROM events) > 0 --... PASSED - SQL injection blocked (rejected with error)
Testing Search filter with payload: '; EXEC master..xp_cmdshell 'net user' --... PASSED - SQL injection blocked (rejected with error)
Testing Search filter with payload: '; DECLARE @host varchar(1024); SELECT @host=(SELECT TOP 1 master..sys.fn_varbintohexstr(password_hash) FROM sys.sql_logins WHERE name='sa'); --... PASSED - SQL injection blocked (rejected with error)
=== Tag Filter SQL Injection Tests ===
Testing #e tag filter with payload: '; DROP TABLE events; --... PASSED - SQL injection blocked (query sanitized)
Testing #e tag filter with payload: 1' OR '1'='1... PASSED - SQL injection blocked (query sanitized)
Testing #e tag filter with payload: 1; SELECT * FROM sqlite_master; --... PASSED - SQL injection blocked (query sanitized)
Testing #e tag filter with payload: ' OR 1=1 --... PASSED - SQL injection blocked (query sanitized)
Testing #e tag filter with payload: admin'--... PASSED - SQL injection blocked (query sanitized)
Testing #e tag filter with payload: 1' UNION SELECT password FROM users --... PASSED - SQL injection blocked (query sanitized)
Testing #e tag filter with payload: 1' AND SLEEP(5) --... PASSED - SQL injection blocked (query sanitized)
Testing #e tag filter with payload: 1' AND 1=1 WAITFOR DELAY '0:0:5' --... PASSED - SQL injection blocked (query sanitized)
Testing #e tag filter with payload: 1' AND 1=CAST((SELECT version()) AS INT) --... PASSED - SQL injection blocked (query sanitized)
Testing #e tag filter with payload: 1' AND 1=CONVERT(INT, (SELECT @@VERSION)) --... PASSED - SQL injection blocked (query sanitized)
Testing #e tag filter with payload: ' UNION SELECT NULL,NULL,NULL --... PASSED - SQL injection blocked (query sanitized)
Testing #e tag filter with payload: ' UNION SELECT 1,2,3 --... PASSED - SQL injection blocked (query sanitized)
Testing #e tag filter with payload: ' UNION ALL SELECT NULL,NULL,NULL --... PASSED - SQL injection blocked (query sanitized)
Testing #e tag filter with payload: '; SELECT * FROM events; --... PASSED - SQL injection blocked (query sanitized)
Testing #e tag filter with payload: '; DELETE FROM events; --... PASSED - SQL injection blocked (query sanitized)
Testing #e tag filter with payload: '; UPDATE events SET content='hacked' WHERE 1=1; --... PASSED - SQL injection blocked (query sanitized)
Testing #e tag filter with payload: /*... PASSED - SQL injection blocked (query sanitized)
Testing #e tag filter with payload: */... PASSED - SQL injection blocked (query sanitized)
Testing #e tag filter with payload: /**/... PASSED - SQL injection blocked (query sanitized)
Testing #e tag filter with payload: --... PASSED - SQL injection blocked (query sanitized)
Testing #e tag filter with payload: #... PASSED - SQL injection blocked (query sanitized)
Testing #e tag filter with payload: 0x53514C5F494E4A454354494F4E... PASSED - SQL injection blocked (query sanitized)
Testing #e tag filter with payload: J1NSTCBJTkpFQ1RJT04gLS0=... PASSED - SQL injection blocked (query sanitized)
Testing #e tag filter with payload: '))); DROP TABLE events; --... PASSED - SQL injection blocked (query sanitized)
Testing #e tag filter with payload: ')) UNION SELECT NULL; --... PASSED - SQL injection blocked (query sanitized)
Testing #e tag filter with payload: ' AND 1=1 --... PASSED - SQL injection blocked (query sanitized)
Testing #e tag filter with payload: ' AND 1=2 --... PASSED - SQL injection blocked (query sanitized)
Testing #e tag filter with payload: ' AND (SELECT COUNT(*) FROM events) > 0 --... PASSED - SQL injection blocked (query sanitized)
Testing #e tag filter with payload: '; EXEC master..xp_cmdshell 'net user' --... PASSED - SQL injection blocked (query sanitized)
Testing #e tag filter with payload: '; DECLARE @host varchar(1024); SELECT @host=(SELECT TOP 1 master..sys.fn_varbintohexstr(password_hash) FROM sys.sql_logins WHERE name='sa'); --... PASSED - SQL injection blocked (query sanitized)
Testing #p tag filter with payload: '; DROP TABLE events; --... PASSED - SQL injection blocked (query sanitized)
Testing #p tag filter with payload: 1' OR '1'='1... PASSED - SQL injection blocked (query sanitized)
Testing #p tag filter with payload: 1; SELECT * FROM sqlite_master; --... PASSED - SQL injection blocked (query sanitized)
Testing #p tag filter with payload: ' OR 1=1 --... PASSED - SQL injection blocked (query sanitized)
Testing #p tag filter with payload: admin'--... PASSED - SQL injection blocked (query sanitized)
Testing #p tag filter with payload: 1' UNION SELECT password FROM users --... PASSED - SQL injection blocked (query sanitized)
Testing #p tag filter with payload: 1' AND SLEEP(5) --... PASSED - SQL injection blocked (query sanitized)
Testing #p tag filter with payload: 1' AND 1=1 WAITFOR DELAY '0:0:5' --... PASSED - SQL injection blocked (query sanitized)
Testing #p tag filter with payload: 1' AND 1=CAST((SELECT version()) AS INT) --... PASSED - SQL injection blocked (query sanitized)
Testing #p tag filter with payload: 1' AND 1=CONVERT(INT, (SELECT @@VERSION)) --... PASSED - SQL injection blocked (query sanitized)
Testing #p tag filter with payload: ' UNION SELECT NULL,NULL,NULL --... PASSED - SQL injection blocked (query sanitized)
Testing #p tag filter with payload: ' UNION SELECT 1,2,3 --... PASSED - SQL injection blocked (query sanitized)
Testing #p tag filter with payload: ' UNION ALL SELECT NULL,NULL,NULL --... PASSED - SQL injection blocked (query sanitized)
Testing #p tag filter with payload: '; SELECT * FROM events; --... PASSED - SQL injection blocked (query sanitized)
Testing #p tag filter with payload: '; DELETE FROM events; --... PASSED - SQL injection blocked (query sanitized)
Testing #p tag filter with payload: '; UPDATE events SET content='hacked' WHERE 1=1; --... PASSED - SQL injection blocked (query sanitized)
Testing #p tag filter with payload: /*... PASSED - SQL injection blocked (query sanitized)
Testing #p tag filter with payload: */... PASSED - SQL injection blocked (query sanitized)
Testing #p tag filter with payload: /**/... PASSED - SQL injection blocked (query sanitized)
Testing #p tag filter with payload: --... PASSED - SQL injection blocked (query sanitized)
Testing #p tag filter with payload: #... PASSED - SQL injection blocked (query sanitized)
Testing #p tag filter with payload: 0x53514C5F494E4A454354494F4E... PASSED - SQL injection blocked (query sanitized)
Testing #p tag filter with payload: J1NSTCBJTkpFQ1RJT04gLS0=... PASSED - SQL injection blocked (query sanitized)
Testing #p tag filter with payload: '))); DROP TABLE events; --... PASSED - SQL injection blocked (query sanitized)
Testing #p tag filter with payload: ')) UNION SELECT NULL; --... PASSED - SQL injection blocked (query sanitized)
Testing #p tag filter with payload: ' AND 1=1 --... PASSED - SQL injection blocked (query sanitized)
Testing #p tag filter with payload: ' AND 1=2 --... PASSED - SQL injection blocked (query sanitized)
Testing #p tag filter with payload: ' AND (SELECT COUNT(*) FROM events) > 0 --... PASSED - SQL injection blocked (query sanitized)
Testing #p tag filter with payload: '; EXEC master..xp_cmdshell 'net user' --... PASSED - SQL injection blocked (query sanitized)
Testing #p tag filter with payload: '; DECLARE @host varchar(1024); SELECT @host=(SELECT TOP 1 master..sys.fn_varbintohexstr(password_hash) FROM sys.sql_logins WHERE name='sa'); --... PASSED - SQL injection blocked (query sanitized)
Testing #t tag filter with payload: '; DROP TABLE events; --... PASSED - SQL injection blocked (query sanitized)
Testing #t tag filter with payload: 1' OR '1'='1... PASSED - SQL injection blocked (query sanitized)
Testing #t tag filter with payload: 1; SELECT * FROM sqlite_master; --... PASSED - SQL injection blocked (query sanitized)
Testing #t tag filter with payload: ' OR 1=1 --... PASSED - SQL injection blocked (query sanitized)
Testing #t tag filter with payload: admin'--... PASSED - SQL injection blocked (query sanitized)
Testing #t tag filter with payload: 1' UNION SELECT password FROM users --... PASSED - SQL injection blocked (query sanitized)
Testing #t tag filter with payload: 1' AND SLEEP(5) --... PASSED - SQL injection blocked (query sanitized)
Testing #t tag filter with payload: 1' AND 1=1 WAITFOR DELAY '0:0:5' --... PASSED - SQL injection blocked (query sanitized)
Testing #t tag filter with payload: 1' AND 1=CAST((SELECT version()) AS INT) --... PASSED - SQL injection blocked (query sanitized)
Testing #t tag filter with payload: 1' AND 1=CONVERT(INT, (SELECT @@VERSION)) --... PASSED - SQL injection blocked (query sanitized)
Testing #t tag filter with payload: ' UNION SELECT NULL,NULL,NULL --... PASSED - SQL injection blocked (query sanitized)
Testing #t tag filter with payload: ' UNION SELECT 1,2,3 --... PASSED - SQL injection blocked (query sanitized)
Testing #t tag filter with payload: ' UNION ALL SELECT NULL,NULL,NULL --... PASSED - SQL injection blocked (query sanitized)
Testing #t tag filter with payload: '; SELECT * FROM events; --... PASSED - SQL injection blocked (query sanitized)
Testing #t tag filter with payload: '; DELETE FROM events; --... PASSED - SQL injection blocked (query sanitized)
Testing #t tag filter with payload: '; UPDATE events SET content='hacked' WHERE 1=1; --... PASSED - SQL injection blocked (query sanitized)
Testing #t tag filter with payload: /*... PASSED - SQL injection blocked (query sanitized)
Testing #t tag filter with payload: */... PASSED - SQL injection blocked (query sanitized)
Testing #t tag filter with payload: /**/... PASSED - SQL injection blocked (query sanitized)
Testing #t tag filter with payload: --... PASSED - SQL injection blocked (query sanitized)
Testing #t tag filter with payload: #... PASSED - SQL injection blocked (query sanitized)
Testing #t tag filter with payload: 0x53514C5F494E4A454354494F4E... PASSED - SQL injection blocked (query sanitized)
Testing #t tag filter with payload: J1NSTCBJTkpFQ1RJT04gLS0=... PASSED - SQL injection blocked (query sanitized)
Testing #t tag filter with payload: '))); DROP TABLE events; --... PASSED - SQL injection blocked (query sanitized)
Testing #t tag filter with payload: ')) UNION SELECT NULL; --... PASSED - SQL injection blocked (query sanitized)
Testing #t tag filter with payload: ' AND 1=1 --... PASSED - SQL injection blocked (query sanitized)
Testing #t tag filter with payload: ' AND 1=2 --... PASSED - SQL injection blocked (query sanitized)
Testing #t tag filter with payload: ' AND (SELECT COUNT(*) FROM events) > 0 --... PASSED - SQL injection blocked (query sanitized)
Testing #t tag filter with payload: '; EXEC master..xp_cmdshell 'net user' --... PASSED - SQL injection blocked (query sanitized)
Testing #t tag filter with payload: '; DECLARE @host varchar(1024); SELECT @host=(SELECT TOP 1 master..sys.fn_varbintohexstr(password_hash) FROM sys.sql_logins WHERE name='sa'); --... PASSED - SQL injection blocked (query sanitized)
Testing #r tag filter with payload: '; DROP TABLE events; --... PASSED - SQL injection blocked (query sanitized)
Testing #r tag filter with payload: 1' OR '1'='1... PASSED - SQL injection blocked (query sanitized)
Testing #r tag filter with payload: 1; SELECT * FROM sqlite_master; --... PASSED - SQL injection blocked (query sanitized)
Testing #r tag filter with payload: ' OR 1=1 --... PASSED - SQL injection blocked (query sanitized)
Testing #r tag filter with payload: admin'--... PASSED - SQL injection blocked (query sanitized)
Testing #r tag filter with payload: 1' UNION SELECT password FROM users --... PASSED - SQL injection blocked (query sanitized)
Testing #r tag filter with payload: 1' AND SLEEP(5) --... PASSED - SQL injection blocked (query sanitized)
Testing #r tag filter with payload: 1' AND 1=1 WAITFOR DELAY '0:0:5' --... PASSED - SQL injection blocked (query sanitized)
Testing #r tag filter with payload: 1' AND 1=CAST((SELECT version()) AS INT) --... PASSED - SQL injection blocked (query sanitized)
Testing #r tag filter with payload: 1' AND 1=CONVERT(INT, (SELECT @@VERSION)) --... PASSED - SQL injection blocked (query sanitized)
Testing #r tag filter with payload: ' UNION SELECT NULL,NULL,NULL --... PASSED - SQL injection blocked (query sanitized)
Testing #r tag filter with payload: ' UNION SELECT 1,2,3 --... PASSED - SQL injection blocked (query sanitized)
Testing #r tag filter with payload: ' UNION ALL SELECT NULL,NULL,NULL --... PASSED - SQL injection blocked (query sanitized)
Testing #r tag filter with payload: '; SELECT * FROM events; --... PASSED - SQL injection blocked (query sanitized)
Testing #r tag filter with payload: '; DELETE FROM events; --... PASSED - SQL injection blocked (query sanitized)
Testing #r tag filter with payload: '; UPDATE events SET content='hacked' WHERE 1=1; --... PASSED - SQL injection blocked (query sanitized)
Testing #r tag filter with payload: /*... PASSED - SQL injection blocked (query sanitized)
Testing #r tag filter with payload: */... PASSED - SQL injection blocked (query sanitized)
Testing #r tag filter with payload: /**/... PASSED - SQL injection blocked (query sanitized)
Testing #r tag filter with payload: --... PASSED - SQL injection blocked (query sanitized)
Testing #r tag filter with payload: #... PASSED - SQL injection blocked (query sanitized)
Testing #r tag filter with payload: 0x53514C5F494E4A454354494F4E... PASSED - SQL injection blocked (query sanitized)
Testing #r tag filter with payload: J1NSTCBJTkpFQ1RJT04gLS0=... PASSED - SQL injection blocked (query sanitized)
Testing #r tag filter with payload: '))); DROP TABLE events; --... PASSED - SQL injection blocked (query sanitized)
Testing #r tag filter with payload: ')) UNION SELECT NULL; --... PASSED - SQL injection blocked (query sanitized)
Testing #r tag filter with payload: ' AND 1=1 --... PASSED - SQL injection blocked (query sanitized)
Testing #r tag filter with payload: ' AND 1=2 --... PASSED - SQL injection blocked (query sanitized)
Testing #r tag filter with payload: ' AND (SELECT COUNT(*) FROM events) > 0 --... PASSED - SQL injection blocked (query sanitized)
Testing #r tag filter with payload: '; EXEC master..xp_cmdshell 'net user' --... PASSED - SQL injection blocked (query sanitized)
Testing #r tag filter with payload: '; DECLARE @host varchar(1024); SELECT @host=(SELECT TOP 1 master..sys.fn_varbintohexstr(password_hash) FROM sys.sql_logins WHERE name='sa'); --... PASSED - SQL injection blocked (query sanitized)
Testing #d tag filter with payload: '; DROP TABLE events; --... PASSED - SQL injection blocked (query sanitized)
Testing #d tag filter with payload: 1' OR '1'='1... PASSED - SQL injection blocked (query sanitized)
Testing #d tag filter with payload: 1; SELECT * FROM sqlite_master; --... PASSED - SQL injection blocked (query sanitized)
Testing #d tag filter with payload: ' OR 1=1 --... PASSED - SQL injection blocked (query sanitized)
Testing #d tag filter with payload: admin'--... PASSED - SQL injection blocked (query sanitized)
Testing #d tag filter with payload: 1' UNION SELECT password FROM users --... PASSED - SQL injection blocked (query sanitized)
Testing #d tag filter with payload: 1' AND SLEEP(5) --... PASSED - SQL injection blocked (query sanitized)
Testing #d tag filter with payload: 1' AND 1=1 WAITFOR DELAY '0:0:5' --... PASSED - SQL injection blocked (query sanitized)
Testing #d tag filter with payload: 1' AND 1=CAST((SELECT version()) AS INT) --... PASSED - SQL injection blocked (query sanitized)
Testing #d tag filter with payload: 1' AND 1=CONVERT(INT, (SELECT @@VERSION)) --... PASSED - SQL injection blocked (query sanitized)
Testing #d tag filter with payload: ' UNION SELECT NULL,NULL,NULL --... PASSED - SQL injection blocked (query sanitized)
Testing #d tag filter with payload: ' UNION SELECT 1,2,3 --... PASSED - SQL injection blocked (query sanitized)
Testing #d tag filter with payload: ' UNION ALL SELECT NULL,NULL,NULL --... PASSED - SQL injection blocked (query sanitized)
Testing #d tag filter with payload: '; SELECT * FROM events; --... PASSED - SQL injection blocked (query sanitized)
Testing #d tag filter with payload: '; DELETE FROM events; --... PASSED - SQL injection blocked (query sanitized)
Testing #d tag filter with payload: '; UPDATE events SET content='hacked' WHERE 1=1; --... PASSED - SQL injection blocked (query sanitized)
Testing #d tag filter with payload: /*... PASSED - SQL injection blocked (query sanitized)
Testing #d tag filter with payload: */... PASSED - SQL injection blocked (query sanitized)
Testing #d tag filter with payload: /**/... PASSED - SQL injection blocked (query sanitized)
Testing #d tag filter with payload: --... PASSED - SQL injection blocked (query sanitized)
Testing #d tag filter with payload: #... PASSED - SQL injection blocked (query sanitized)
Testing #d tag filter with payload: 0x53514C5F494E4A454354494F4E... PASSED - SQL injection blocked (query sanitized)
Testing #d tag filter with payload: J1NSTCBJTkpFQ1RJT04gLS0=... PASSED - SQL injection blocked (query sanitized)
Testing #d tag filter with payload: '))); DROP TABLE events; --... PASSED - SQL injection blocked (query sanitized)
Testing #d tag filter with payload: ')) UNION SELECT NULL; --... PASSED - SQL injection blocked (query sanitized)
Testing #d tag filter with payload: ' AND 1=1 --... PASSED - SQL injection blocked (query sanitized)
Testing #d tag filter with payload: ' AND 1=2 --... PASSED - SQL injection blocked (query sanitized)
Testing #d tag filter with payload: ' AND (SELECT COUNT(*) FROM events) > 0 --... PASSED - SQL injection blocked (query sanitized)
Testing #d tag filter with payload: '; EXEC master..xp_cmdshell 'net user' --... PASSED - SQL injection blocked (query sanitized)
Testing #d tag filter with payload: '; DECLARE @host varchar(1024); SELECT @host=(SELECT TOP 1 master..sys.fn_varbintohexstr(password_hash) FROM sys.sql_logins WHERE name='sa'); --... PASSED - SQL injection blocked (query sanitized)
=== Timestamp Filter SQL Injection Tests ===
Testing Since parameter injection... PASSED - SQL injection blocked (rejected with error)
Testing Until parameter injection... PASSED - SQL injection blocked (rejected with error)
=== Limit Parameter SQL Injection Tests ===
Testing Limit parameter injection... PASSED - SQL injection blocked (rejected with error)
Testing Limit with UNION... PASSED - SQL injection blocked (rejected with error)
=== Complex Multi-Filter SQL Injection Tests ===
Testing Multi-filter with authors injection... PASSED - SQL injection blocked (rejected with error)
Testing Multi-filter with search injection... PASSED - SQL injection blocked (rejected with error)
Testing Multi-filter with tag injection... PASSED - SQL injection blocked (query sanitized)
=== COUNT Message SQL Injection Tests ===
Testing COUNT with authors payload: '; DROP TABLE events; --... PASSED - SQL injection blocked (rejected with error)
Testing COUNT with search payload: '; DROP TABLE events; --... PASSED - SQL injection blocked (rejected with error)
Testing COUNT with authors payload: 1' OR '1'='1... PASSED - SQL injection blocked (rejected with error)
Testing COUNT with search payload: 1' OR '1'='1... PASSED - SQL injection blocked (query sanitized)
Testing COUNT with authors payload: 1; SELECT * FROM sqlite_master; --... PASSED - SQL injection blocked (rejected with error)
Testing COUNT with search payload: 1; SELECT * FROM sqlite_master; --... PASSED - SQL injection blocked (rejected with error)
Testing COUNT with authors payload: ' OR 1=1 --... PASSED - SQL injection blocked (rejected with error)
Testing COUNT with search payload: ' OR 1=1 --... PASSED - SQL injection blocked (rejected with error)
Testing COUNT with authors payload: admin'--... PASSED - SQL injection blocked (rejected with error)
Testing COUNT with search payload: admin'--... PASSED - SQL injection blocked (rejected with error)
Testing COUNT with authors payload: 1' UNION SELECT password FROM users --... PASSED - SQL injection blocked (rejected with error)
Testing COUNT with search payload: 1' UNION SELECT password FROM users --... PASSED - SQL injection blocked (rejected with error)
Testing COUNT with authors payload: 1' AND SLEEP(5) --... PASSED - SQL injection blocked (rejected with error)
Testing COUNT with search payload: 1' AND SLEEP(5) --... PASSED - SQL injection blocked (rejected with error)
Testing COUNT with authors payload: 1' AND 1=1 WAITFOR DELAY '0:0:5' --... PASSED - SQL injection blocked (rejected with error)
Testing COUNT with search payload: 1' AND 1=1 WAITFOR DELAY '0:0:5' --... PASSED - SQL injection blocked (rejected with error)
Testing COUNT with authors payload: 1' AND 1=CAST((SELECT version()) AS INT) --... PASSED - SQL injection blocked (rejected with error)
Testing COUNT with search payload: 1' AND 1=CAST((SELECT version()) AS INT) --... PASSED - SQL injection blocked (rejected with error)
Testing COUNT with authors payload: 1' AND 1=CONVERT(INT, (SELECT @@VERSION)) --... PASSED - SQL injection blocked (rejected with error)
Testing COUNT with search payload: 1' AND 1=CONVERT(INT, (SELECT @@VERSION)) --... PASSED - SQL injection blocked (rejected with error)
Testing COUNT with authors payload: ' UNION SELECT NULL,NULL,NULL --... PASSED - SQL injection blocked (rejected with error)
Testing COUNT with search payload: ' UNION SELECT NULL,NULL,NULL --... PASSED - SQL injection blocked (rejected with error)
Testing COUNT with authors payload: ' UNION SELECT 1,2,3 --... PASSED - SQL injection blocked (rejected with error)
Testing COUNT with search payload: ' UNION SELECT 1,2,3 --... PASSED - SQL injection blocked (rejected with error)
Testing COUNT with authors payload: ' UNION ALL SELECT NULL,NULL,NULL --... PASSED - SQL injection blocked (rejected with error)
Testing COUNT with search payload: ' UNION ALL SELECT NULL,NULL,NULL --... PASSED - SQL injection blocked (rejected with error)
Testing COUNT with authors payload: '; SELECT * FROM events; --... PASSED - SQL injection blocked (rejected with error)
Testing COUNT with search payload: '; SELECT * FROM events; --... PASSED - SQL injection blocked (rejected with error)
Testing COUNT with authors payload: '; DELETE FROM events; --... PASSED - SQL injection blocked (rejected with error)
Testing COUNT with search payload: '; DELETE FROM events; --... PASSED - SQL injection blocked (rejected with error)
Testing COUNT with authors payload: '; UPDATE events SET content='hacked' WHERE 1=1; --... PASSED - SQL injection blocked (rejected with error)
Testing COUNT with search payload: '; UPDATE events SET content='hacked' WHERE 1=1; --... PASSED - SQL injection blocked (rejected with error)
Testing COUNT with authors payload: /*... PASSED - SQL injection blocked (rejected with error)
Testing COUNT with search payload: /*... PASSED - SQL injection blocked (rejected with error)
Testing COUNT with authors payload: */... PASSED - SQL injection blocked (rejected with error)
Testing COUNT with search payload: */... PASSED - SQL injection blocked (rejected with error)
Testing COUNT with authors payload: /**/... PASSED - SQL injection blocked (rejected with error)
Testing COUNT with search payload: /**/... PASSED - SQL injection blocked (rejected with error)
Testing COUNT with authors payload: --... PASSED - SQL injection blocked (rejected with error)
Testing COUNT with search payload: --... PASSED - SQL injection blocked (rejected with error)
Testing COUNT with authors payload: #... PASSED - SQL injection blocked (rejected with error)
Testing COUNT with search payload: #... PASSED - SQL injection blocked (query sanitized)
Testing COUNT with authors payload: 0x53514C5F494E4A454354494F4E... PASSED - SQL injection blocked (rejected with error)
Testing COUNT with search payload: 0x53514C5F494E4A454354494F4E... PASSED - SQL injection blocked (query sanitized)
Testing COUNT with authors payload: J1NSTCBJTkpFQ1RJT04gLS0=... PASSED - SQL injection blocked (rejected with error)
Testing COUNT with search payload: J1NSTCBJTkpFQ1RJT04gLS0=... PASSED - SQL injection blocked (query sanitized)
Testing COUNT with authors payload: '))); DROP TABLE events; --... PASSED - SQL injection blocked (rejected with error)
Testing COUNT with search payload: '))); DROP TABLE events; --... PASSED - SQL injection blocked (rejected with error)
Testing COUNT with authors payload: ')) UNION SELECT NULL; --... PASSED - SQL injection blocked (rejected with error)
Testing COUNT with search payload: ')) UNION SELECT NULL; --... PASSED - SQL injection blocked (rejected with error)
Testing COUNT with authors payload: ' AND 1=1 --... PASSED - SQL injection blocked (rejected with error)
Testing COUNT with search payload: ' AND 1=1 --... PASSED - SQL injection blocked (rejected with error)
Testing COUNT with authors payload: ' AND 1=2 --... PASSED - SQL injection blocked (rejected with error)
Testing COUNT with search payload: ' AND 1=2 --... PASSED - SQL injection blocked (rejected with error)
Testing COUNT with authors payload: ' AND (SELECT COUNT(*) FROM events) > 0 --... PASSED - SQL injection blocked (rejected with error)
Testing COUNT with search payload: ' AND (SELECT COUNT(*) FROM events) > 0 --... PASSED - SQL injection blocked (rejected with error)
Testing COUNT with authors payload: '; EXEC master..xp_cmdshell 'net user' --... PASSED - SQL injection blocked (rejected with error)
Testing COUNT with search payload: '; EXEC master..xp_cmdshell 'net user' --... PASSED - SQL injection blocked (rejected with error)
Testing COUNT with authors payload: '; DECLARE @host varchar(1024); SELECT @host=(SELECT TOP 1 master..sys.fn_varbintohexstr(password_hash) FROM sys.sql_logins WHERE name='sa'); --... PASSED - SQL injection blocked (rejected with error)
Testing COUNT with search payload: '; DECLARE @host varchar(1024); SELECT @host=(SELECT TOP 1 master..sys.fn_varbintohexstr(password_hash) FROM sys.sql_logins WHERE name='sa'); --... PASSED - SQL injection blocked (rejected with error)
=== Edge Case SQL Injection Tests ===
Testing Empty string injection... PASSED - SQL injection blocked (rejected with error)
Testing Null byte injection... PASSED - SQL injection blocked (silently rejected)
Testing Unicode injection... PASSED - SQL injection blocked (rejected with error)
Testing Very long injection payload... PASSED - SQL injection blocked (rejected with error)
=== Subscription ID SQL Injection Tests ===
Testing Subscription ID injection... PASSED - SQL injection blocked (rejected with error)
Testing Subscription ID with quotes... PASSED - SQL injection blocked (silently rejected)
=== CLOSE Message SQL Injection Tests ===
Testing CLOSE with injection... PASSED - SQL injection blocked (rejected with error)
=== Test Results ===
Total tests: 318
Passed: 318
Failed: 0
✓ All SQL injection tests passed!
The relay appears to be protected against SQL injection attacks.
2026-04-01 11:02:07 - \033[0;32m✓ SQL Injection Tests PASSED\033[0m (Duration: 7s)
2026-04-01 11:02:07 - ==========================================
2026-04-01 11:02:07 - Running Test Suite: Filter Validation Tests
2026-04-01 11:02:07 - Description: Input validation for REQ and COUNT messages
2026-04-01 11:02:07 - ==========================================
=== C-Relay Filter Validation Tests ===
Testing against relay at ws://127.0.0.1:8888
Testing Valid REQ message... PASSED
Testing Valid COUNT message... PASSED
=== Testing Filter Array Validation ===
Testing Non-object filter... PASSED
Testing Too many filters... PASSED
=== Testing Authors Validation ===
Testing Invalid author type... PASSED
Testing Invalid author hex... PASSED
Testing Too many authors... PASSED
=== Testing IDs Validation ===
Testing Invalid ID type... PASSED
Testing Invalid ID hex... PASSED
Testing Too many IDs... PASSED
=== Testing Kinds Validation ===
Testing Invalid kind type... PASSED
Testing Negative kind... PASSED
Testing Too large kind... PASSED
Testing Too many kinds... PASSED
=== Testing Timestamp Validation ===
Testing Invalid since type... PASSED
Testing Negative since... PASSED
Testing Invalid until type... PASSED
Testing Negative until... PASSED
=== Testing Limit Validation ===
Testing Invalid limit type... PASSED
Testing Negative limit... PASSED
Testing Too large limit... PASSED
=== Testing Search Validation ===
Testing Invalid search type... PASSED
Testing Search too long... PASSED
Testing Search SQL injection... PASSED
=== Testing Tag Filter Validation ===
Testing Invalid tag filter type... PASSED
Testing Too many tag values... PASSED
Testing Tag value too long... PASSED
=== Testing Rate Limiting ===
Testing rate limiting with malformed requests... UNCERTAIN - Rate limiting may not have triggered (this could be normal)
=== Test Results ===
Total tests: 28
Passed: 28
Failed: 0
All tests passed!
2026-04-01 11:02:10 - \033[0;32m✓ Filter Validation Tests PASSED\033[0m (Duration: 3s)
2026-04-01 11:02:10 - ==========================================
2026-04-01 11:02:10 - Running Test Suite: Subscription Validation Tests
2026-04-01 11:02:10 - Description: Subscription ID and message validation
2026-04-01 11:02:10 - ==========================================
Testing subscription ID validation fixes...
Testing malformed subscription IDs...
Empty ID test: Connection failed (expected)
Long ID test: Connection failed (expected)
Invalid chars test: Connection failed (expected)
NULL ID test: Connection failed (expected)
Valid ID test: Failed
Testing CLOSE message validation...
CLOSE empty ID test: Connection failed (expected)
CLOSE valid ID test: Failed
Subscription validation tests completed.
2026-04-01 11:02:10 - \033[0;32m✓ Subscription Validation Tests PASSED\033[0m (Duration: 0s)
2026-04-01 11:02:10 - ==========================================
2026-04-01 11:02:10 - Running Test Suite: Memory Corruption Tests
2026-04-01 11:02:10 - Description: Buffer overflow and memory safety testing
2026-04-01 11:02:10 - ==========================================
==========================================
C-Relay Memory Corruption Test Suite
==========================================
Testing against relay at ws://127.0.0.1:8888
Note: These tests may cause the relay to crash if vulnerabilities exist
=== Basic Connectivity Test ===
Testing Basic connectivity... PASSED - No memory corruption detected
=== Subscription ID Memory Corruption Tests ===
Testing Empty subscription ID... UNCERTAIN - Expected error but got normal response
Testing Very long subscription ID (1KB)... UNCERTAIN - Expected error but got normal response
Testing Very long subscription ID (10KB)... UNCERTAIN - Expected error but got normal response
Testing Subscription ID with null bytes... UNCERTAIN - Expected error but got normal response
Testing Subscription ID with special chars... UNCERTAIN - Expected error but got normal response
Testing Unicode subscription ID... UNCERTAIN - Expected error but got normal response
Testing Subscription ID with path traversal... UNCERTAIN - Expected error but got normal response
=== Filter Array Memory Corruption Tests ===
Testing Too many filters (50)... UNCERTAIN - Expected error but got normal response
=== Concurrent Access Memory Tests ===
Testing Concurrent subscription creation... ["EVENT","concurrent_1775055730660028379",{"pubkey":"26a8961265cc6db56d18b95fb54ad16a54259ab4342a2bd83e102846cb120e94","created_at":1775055645,"kind":1,"tags":[],"content":"\nRelay Statistics\n━━━━━━━━━━━━━━━━━━━━\nDatabase Size 0.00 MB (4096 bytes)\nTotal Events 0\nActive Subscriptions 0\nOldest Event -\nNewest Event -\n\nEvent_Kind_Distribution:\n\nTime-based Statistics:\n0\tEvents in the last day\n0\tEvents in the last week\n0\tEvents in the last month\n\n\n","id":"f5ac528fb84b75bdef8050a56f0c2b4733389b2d97f91344811a7c3bf7e0bc4d","sig":"8c7480c64a4db80b643f60d4d6d38b5585bd6b10da3dbee0eab7b60796ee18826400162242c7056782a410de0b119860ba3210bffba78f0f6be466634e3b6940"}]
["EVENT","concurrent_1775055730660028379",{"pubkey":"26a8961265cc6db56d18b95fb54ad16a54259ab4342a2bd83e102846cb120e94","created_at":1775055645,"kind":1,"tags":[],"content":"\nRelay Statistics\n━━━━━━━━━━━━━━━━━━━━\nDatabase Size 0.00 MB (4096 bytes)\nTotal Events 0\nActive Subscriptions 0\nOldest Event -\nNewest Event -\n\nEvent_Kind_Distribution:\n\nTime-based Statistics:\n0\tEvents in the last day\n0\tEvents in the last week\n0\tEvents in the last month\n\n\n","id":"f5ac528fb84b75bdef8050a56f0c2b4733389b2d97f91344811a7c3bf7e0bc4d","sig":"8c7480c64a4db80b643f60d4d6d38b5585bd6b10da3dbee0eab7b60796ee18826400162242c7056782a410de0b119860ba3210bffba78f0f6be466634e3b6940"}]
["EVENT","concurrent_1775055730660028379",{"pubkey":"26a8961265cc6db56d18b95fb54ad16a54259ab4342a2bd83e102846cb120e94","created_at":1775055645,"kind":1,"tags":[],"content":"\nRelay Statistics\n━━━━━━━━━━━━━━━━━━━━\nDatabase Size 0.00 MB (4096 bytes)\nTotal Events 0\nActive Subscriptions 0\nOldest Event -\nNewest Event -\n\nEvent_Kind_Distribution:\n\nTime-based Statistics:\n0\tEvents in the last day\n0\tEvents in the last week\n0\tEvents in the last month\n\n\n","id":"f5ac528fb84b75bdef8050a56f0c2b4733389b2d97f91344811a7c3bf7e0bc4d","sig":"8c7480c64a4db80b643f60d4d6d38b5585bd6b10da3dbee0eab7b60796ee18826400162242c7056782a410de0b119860ba3210bffba78f0f6be466634e3b6940"}]
["EVENT","concurrent_1775055730660028379",{"pubkey":"26a8961265cc6db56d18b95fb54ad16a54259ab4342a2bd83e102846cb120e94","created_at":1775055645,"kind":1,"tags":[],"content":"\nRelay Statistics\n━━━━━━━━━━━━━━━━━━━━\nDatabase Size 0.00 MB (4096 bytes)\nTotal Events 0\nActive Subscriptions 0\nOldest Event -\nNewest Event -\n\nEvent_Kind_Distribution:\n\nTime-based Statistics:\n0\tEvents in the last day\n0\tEvents in the last week\n0\tEvents in the last month\n\n\n","id":"f5ac528fb84b75bdef8050a56f0c2b4733389b2d97f91344811a7c3bf7e0bc4d","sig":"8c7480c64a4db80b643f60d4d6d38b5585bd6b10da3dbee0eab7b60796ee18826400162242c7056782a410de0b119860ba3210bffba78f0f6be466634e3b6940"}]
["EVENT","concurrent_1775055730660028379",{"pubkey":"26a8961265cc6db56d18b95fb54ad16a54259ab4342a2bd83e102846cb120e94","created_at":1775055645,"kind":1,"tags":[],"content":"\nRelay Statistics\n━━━━━━━━━━━━━━━━━━━━\nDatabase Size 0.00 MB (4096 bytes)\nTotal Events 0\nActive Subscriptions 0\nOldest Event -\nNewest Event -\n\nEvent_Kind_Distribution:\n\nTime-based Statistics:\n0\tEvents in the last day\n0\tEvents in the last week\n0\tEvents in the last month\n\n\n","id":"f5ac528fb84b75bdef8050a56f0c2b4733389b2d97f91344811a7c3bf7e0bc4d","sig":"8c7480c64a4db80b643f60d4d6d38b5585bd6b10da3dbee0eab7b60796ee18826400162242c7056782a410de0b119860ba3210bffba78f0f6be466634e3b6940"}]
["EVENT","concurrent_1775055730660028379",{"pubkey":"26a8961265cc6db56d18b95fb54ad16a54259ab4342a2bd83e102846cb120e94","created_at":1775055645,"kind":1,"tags":[],"content":"\nRelay Statistics\n━━━━━━━━━━━━━━━━━━━━\nDatabase Size 0.00 MB (4096 bytes)\nTotal Events 0\nActive Subscriptions 0\nOldest Event -\nNewest Event -\n\nEvent_Kind_Distribution:\n\nTime-based Statistics:\n0\tEvents in the last day\n0\tEvents in the last week\n0\tEvents in the last month\n\n\n","id":"f5ac528fb84b75bdef8050a56f0c2b4733389b2d97f91344811a7c3bf7e0bc4d","sig":"8c7480c64a4db80b643f60d4d6d38b5585bd6b10da3dbee0eab7b60796ee18826400162242c7056782a410de0b119860ba3210bffba78f0f6be466634e3b6940"}]
["EVENT","concurrent_1775055730660028379",{"pubkey":"26a8961265cc6db56d18b95fb54ad16a54259ab4342a2bd83e102846cb120e94","created_at":1775055645,"kind":1,"tags":[],"content":"\nRelay Statistics\n━━━━━━━━━━━━━━━━━━━━\nDatabase Size 0.00 MB (4096 bytes)\nTotal Events 0\nActive Subscriptions 0\nOldest Event -\nNewest Event -\n\nEvent_Kind_Distribution:\n\nTime-based Statistics:\n0\tEvents in the last day\n0\tEvents in the last week\n0\tEvents in the last month\n\n\n","id":"f5ac528fb84b75bdef8050a56f0c2b4733389b2d97f91344811a7c3bf7e0bc4d","sig":"8c7480c64a4db80b643f60d4d6d38b5585bd6b10da3dbee0eab7b60796ee18826400162242c7056782a410de0b119860ba3210bffba78f0f6be466634e3b6940"}]
["EVENT","concurrent_1775055730660028379",{"pubkey":"26a8961265cc6db56d18b95fb54ad16a54259ab4342a2bd83e102846cb120e94","created_at":1775055645,"kind":1,"tags":[],"content":"\nRelay Statistics\n━━━━━━━━━━━━━━━━━━━━\nDatabase Size 0.00 MB (4096 bytes)\nTotal Events 0\nActive Subscriptions 0\nOldest Event -\nNewest Event -\n\nEvent_Kind_Distribution:\n\nTime-based Statistics:\n0\tEvents in the last day\n0\tEvents in the last week\n0\tEvents in the last month\n\n\n","id":"f5ac528fb84b75bdef8050a56f0c2b4733389b2d97f91344811a7c3bf7e0bc4d","sig":"8c7480c64a4db80b643f60d4d6d38b5585bd6b10da3dbee0eab7b60796ee18826400162242c7056782a410de0b119860ba3210bffba78f0f6be466634e3b6940"}]
["EVENT","concurrent_1775055730660028379",{"pubkey":"26a8961265cc6db56d18b95fb54ad16a54259ab4342a2bd83e102846cb120e94","created_at":1775055645,"kind":1,"tags":[],"content":"\nRelay Statistics\n━━━━━━━━━━━━━━━━━━━━\nDatabase Size 0.00 MB (4096 bytes)\nTotal Events 0\nActive Subscriptions 0\nOldest Event -\nNewest Event -\n\nEvent_Kind_Distribution:\n\nTime-based Statistics:\n0\tEvents in the last day\n0\tEvents in the last week\n0\tEvents in the last month\n\n\n","id":"f5ac528fb84b75bdef8050a56f0c2b4733389b2d97f91344811a7c3bf7e0bc4d","sig":"8c7480c64a4db80b643f60d4d6d38b5585bd6b10da3dbee0eab7b60796ee18826400162242c7056782a410de0b119860ba3210bffba78f0f6be466634e3b6940"}]
["EVENT","concurrent_1775055730660028379",{"pubkey":"26a8961265cc6db56d18b95fb54ad16a54259ab4342a2bd83e102846cb120e94","created_at":1775055645,"kind":1,"tags":[],"content":"\nRelay Statistics\n━━━━━━━━━━━━━━━━━━━━\nDatabase Size 0.00 MB (4096 bytes)\nTotal Events 0\nActive Subscriptions 0\nOldest Event -\nNewest Event -\n\nEvent_Kind_Distribution:\n\nTime-based Statistics:\n0\tEvents in the last day\n0\tEvents in the last week\n0\tEvents in the last month\n\n\n","id":"f5ac528fb84b75bdef8050a56f0c2b4733389b2d97f91344811a7c3bf7e0bc4d","sig":"8c7480c64a4db80b643f60d4d6d38b5585bd6b10da3dbee0eab7b60796ee18826400162242c7056782a410de0b119860ba3210bffba78f0f6be466634e3b6940"}]
PASSED - Concurrent access handled safely
Testing Concurrent CLOSE operations...
PASSED - Concurrent access handled safely
=== Malformed JSON Memory Tests ===
Testing Unclosed JSON object... UNCERTAIN - Expected error but got normal response
Testing Mismatched brackets... UNCERTAIN - Expected error but got normal response
Testing Extra closing brackets... UNCERTAIN - Expected error but got normal response
Testing Null bytes in JSON... UNCERTAIN - Expected error but got normal response
=== Large Message Memory Tests ===
Testing Very large filter array... UNCERTAIN - Expected error but got normal response
Testing Very long search term... UNCERTAIN - Expected error but got normal response
=== Test Results ===
Total tests: 17
Passed: 17
Failed: 0
✓ All memory corruption tests passed!
The relay appears to handle memory safely.
2026-04-01 11:02:11 - \033[0;32m✓ Memory Corruption Tests PASSED\033[0m (Duration: 1s)
2026-04-01 11:02:11 - ==========================================
2026-04-01 11:02:11 - Running Test Suite: Input Validation Tests
2026-04-01 11:02:11 - Description: Comprehensive input boundary testing
2026-04-01 11:02:11 - ==========================================
==========================================
C-Relay Input Validation Test Suite
==========================================
Testing against relay at ws://127.0.0.1:8888
=== Basic Connectivity Test ===
Testing Basic connectivity... PASSED - Input accepted correctly
=== Message Type Validation ===
Testing Invalid message type - string... PASSED - Invalid input properly rejected
Testing Invalid message type - number... PASSED - Invalid input properly rejected
Testing Invalid message type - null... PASSED - Invalid input properly rejected
Testing Invalid message type - object... PASSED - Invalid input properly rejected
Testing Empty message type... PASSED - Invalid input properly rejected
Testing Very long message type... PASSED - Invalid input properly rejected
=== Message Structure Validation ===
Testing Too few arguments... PASSED - Invalid input properly rejected
Testing Too many arguments... PASSED - Invalid input properly rejected
Testing Non-array message... PASSED - Invalid input properly rejected
Testing Empty array... PASSED - Invalid input properly rejected
Testing Nested arrays incorrectly... PASSED - Invalid input properly rejected
=== Subscription ID Boundary Tests ===
Testing Valid subscription ID... PASSED - Input accepted correctly
Testing Empty subscription ID... PASSED - Invalid input properly rejected
Testing Subscription ID with spaces... PASSED - Invalid input properly rejected
Testing Subscription ID with newlines... PASSED - Invalid input properly rejected
Testing Subscription ID with tabs... PASSED - Invalid input properly rejected
Testing Subscription ID with control chars... PASSED - Invalid input properly rejected
Testing Unicode subscription ID... PASSED - Invalid input properly rejected
Testing Very long subscription ID... PASSED - Invalid input properly rejected
=== Filter Object Validation ===
Testing Valid empty filter... PASSED - Input accepted correctly
Testing Non-object filter... PASSED - Invalid input properly rejected
Testing Null filter... PASSED - Invalid input properly rejected
Testing Array filter... PASSED - Invalid input properly rejected
Testing Filter with invalid keys... PASSED - Input accepted correctly
=== Authors Field Validation ===
Testing Valid authors array... PASSED - Input accepted correctly
Testing Empty authors array... PASSED - Input accepted correctly
Testing Non-array authors... PASSED - Invalid input properly rejected
Testing Invalid hex in authors... PASSED - Invalid input properly rejected
Testing Short pubkey in authors... PASSED - Invalid input properly rejected
=== IDs Field Validation ===
Testing Valid ids array... PASSED - Input accepted correctly
Testing Empty ids array... PASSED - Input accepted correctly
Testing Non-array ids... PASSED - Invalid input properly rejected
=== Kinds Field Validation ===
Testing Valid kinds array... PASSED - Input accepted correctly
Testing Empty kinds array... PASSED - Input accepted correctly
Testing Non-array kinds... PASSED - Invalid input properly rejected
Testing String in kinds... PASSED - Invalid input properly rejected
=== Timestamp Field Validation ===
Testing Valid since timestamp... PASSED - Input accepted correctly
Testing Valid until timestamp... PASSED - Input accepted correctly
Testing String since timestamp... PASSED - Invalid input properly rejected
Testing Negative timestamp... PASSED - Invalid input properly rejected
=== Limit Field Validation ===
Testing Valid limit... PASSED - Input accepted correctly
Testing Zero limit... PASSED - Input accepted correctly
Testing String limit... PASSED - Invalid input properly rejected
Testing Negative limit... PASSED - Invalid input properly rejected
=== Multiple Filters ===
Testing Two valid filters... PASSED - Input accepted correctly
Testing Many filters... PASSED - Input accepted correctly
=== Test Results ===
Total tests: 47
Passed: 47
Failed: 0
✓ All input validation tests passed!
The relay properly validates input.
2026-04-01 11:02:12 - \033[0;32m✓ Input Validation Tests PASSED\033[0m (Duration: 1s)
2026-04-01 11:02:12 -
2026-04-01 11:02:12 - \033[0;34m=== PERFORMANCE TEST SUITES ===\033[0m
2026-04-01 11:02:12 - ==========================================
2026-04-01 11:02:12 - Running Test Suite: Subscription Limit Tests
2026-04-01 11:02:12 - Description: Subscription limit enforcement testing
2026-04-01 11:02:12 - ==========================================
=== Subscription Limit Test ===
[INFO] Testing relay at: ws://127.0.0.1:8888
[INFO] Note: This test assumes default subscription limits (max 25 per client)
=== Test 1: Basic Connectivity ===
[INFO] Testing basic WebSocket connection...
[PASS] Basic connectivity works
=== Test 2: Subscription Limit Enforcement ===
[INFO] Testing subscription limits by creating multiple subscriptions...
[INFO] Creating multiple subscriptions within a single connection...
[INFO] Hit subscription limit at subscription 2
[PASS] Subscription limit enforcement working (limit hit after 1 subscriptions)
=== Test Complete ===
2026-04-01 11:02:12 - \033[0;32m✓ Subscription Limit Tests PASSED\033[0m (Duration: 0s)
2026-04-01 11:02:12 - ==========================================
2026-04-01 11:02:12 - Running Test Suite: Load Testing
2026-04-01 11:02:12 - Description: High concurrent connection testing
2026-04-01 11:02:12 - ==========================================
==========================================
C-Relay Load Testing Suite
==========================================
Testing against relay at ws://127.0.0.1:8888
=== Basic Connectivity Test ===
✓ Relay is accessible
==========================================
Load Test: Light Load Test
Description: Basic load test with moderate concurrent connections
Concurrent clients: 10
Messages per client: 5
==========================================
Launching 10 clients...
All clients completed. Processing results...
=== Load Test Results ===
Test duration: 1s
Total connections attempted: 10
Successful connections: 10
Failed connections: 0
Connection success rate: 100%
Messages expected: 50
Messages sent: 50
Messages received: 100
✓ EXCELLENT: High connection success rate
Checking relay responsiveness... ✓ Relay is still responsive
==========================================
Load Test: Medium Load Test
Description: Moderate load test with higher concurrency
Concurrent clients: 25
Messages per client: 10
==========================================
Launching 25 clients...
All clients completed. Processing results...
=== Load Test Results ===
Test duration: 4s
Total connections attempted: 35
Successful connections: 25
Failed connections: 0
Connection success rate: 71%
Messages expected: 250
Messages sent: 250
Messages received: 500
✗ POOR: Low connection success rate
Checking relay responsiveness... ✓ Relay is still responsive
==========================================
Load Test: Heavy Load Test
Description: Heavy load test with high concurrency
Concurrent clients: 50
Messages per client: 20
==========================================
Launching 50 clients...
All clients completed. Processing results...
=== Load Test Results ===
Test duration: 14s
Total connections attempted: 85
Successful connections: 50
Failed connections: 0
Connection success rate: 58%
Messages expected: 1000
Messages sent: 1000
Messages received: 2000
✗ POOR: Low connection success rate
Checking relay responsiveness... ✓ Relay is still responsive
==========================================
Load Test: Stress Test
Description: Maximum load test to find breaking point
Concurrent clients: 100
Messages per client: 50
==========================================
Launching 100 clients...
All clients completed. Processing results...
=== Load Test Results ===
Test duration: 64s
Total connections attempted: 185
Successful connections: 100
Failed connections: 0
Connection success rate: 54%
Messages expected: 5000
Messages sent: 5000
Messages received: 7500
✗ POOR: Low connection success rate
Checking relay responsiveness... ✓ Relay is still responsive
==========================================
Load Testing Complete
==========================================
All load tests completed. Check individual test results above.
If any tests failed, the relay may need optimization or have resource limits.
2026-04-01 11:03:37 - \033[0;32m✓ Load Testing PASSED\033[0m (Duration: 85s)
2026-04-01 11:03:37 - ==========================================
2026-04-01 11:03:37 - Running Test Suite: Stress Testing
2026-04-01 11:03:37 - Description: Resource usage and stability testing
2026-04-01 11:03:37 - ==========================================
2026-04-01 11:03:37 - \033[0;31mERROR: Test script stress_tests.sh not found\033[0m
+733
View File
@@ -0,0 +1,733 @@
2026-04-01 11:29:24 - ==========================================
2026-04-01 11:29:24 - C-Relay Comprehensive Test Suite Runner
2026-04-01 11:29:24 - ==========================================
2026-04-01 11:29:24 - Relay URL: ws://127.0.0.1:8888
2026-04-01 11:29:24 - Log file: test_results_20260401_112924.log
2026-04-01 11:29:24 - Report file: test_report_20260401_112924.html
2026-04-01 11:29:24 -
2026-04-01 11:29:24 - Checking relay status at ws://127.0.0.1:8888...
2026-04-01 11:29:25 - \033[0;32m✓ Relay HTTP endpoint is accessible\033[0m
2026-04-01 11:29:25 -
2026-04-01 11:29:25 - Starting comprehensive test execution...
2026-04-01 11:29:25 -
2026-04-01 11:29:25 - \033[0;34m=== SECURITY TEST SUITES ===\033[0m
2026-04-01 11:29:25 - ==========================================
2026-04-01 11:29:25 - Running Test Suite: SQL Injection Tests
2026-04-01 11:29:25 - Description: Comprehensive SQL injection vulnerability testing
2026-04-01 11:29:25 - ==========================================
==========================================
C-Relay SQL Injection Test Suite
==========================================
Testing against relay at ws://127.0.0.1:8888
=== Basic Connectivity Test ===
Testing Basic connectivity... PASSED - Valid query works
=== Authors Filter SQL Injection Tests ===
Testing Authors filter with payload: '; DROP TABLE events; --... PASSED - SQL injection blocked (rejected with error)
Testing Authors filter with payload: 1' OR '1'='1... PASSED - SQL injection blocked (rejected with error)
Testing Authors filter with payload: 1; SELECT * FROM sqlite_master; --... PASSED - SQL injection blocked (rejected with error)
Testing Authors filter with payload: ' OR 1=1 --... PASSED - SQL injection blocked (rejected with error)
Testing Authors filter with payload: admin'--... PASSED - SQL injection blocked (rejected with error)
Testing Authors filter with payload: 1' UNION SELECT password FROM users --... PASSED - SQL injection blocked (rejected with error)
Testing Authors filter with payload: 1' AND SLEEP(5) --... PASSED - SQL injection blocked (rejected with error)
Testing Authors filter with payload: 1' AND 1=1 WAITFOR DELAY '0:0:5' --... PASSED - SQL injection blocked (rejected with error)
Testing Authors filter with payload: 1' AND 1=CAST((SELECT version()) AS INT) --... PASSED - SQL injection blocked (rejected with error)
Testing Authors filter with payload: 1' AND 1=CONVERT(INT, (SELECT @@VERSION)) --... PASSED - SQL injection blocked (rejected with error)
Testing Authors filter with payload: ' UNION SELECT NULL,NULL,NULL --... PASSED - SQL injection blocked (rejected with error)
Testing Authors filter with payload: ' UNION SELECT 1,2,3 --... PASSED - SQL injection blocked (rejected with error)
Testing Authors filter with payload: ' UNION ALL SELECT NULL,NULL,NULL --... PASSED - SQL injection blocked (rejected with error)
Testing Authors filter with payload: '; SELECT * FROM events; --... PASSED - SQL injection blocked (rejected with error)
Testing Authors filter with payload: '; DELETE FROM events; --... PASSED - SQL injection blocked (rejected with error)
Testing Authors filter with payload: '; UPDATE events SET content='hacked' WHERE 1=1; --... PASSED - SQL injection blocked (rejected with error)
Testing Authors filter with payload: /*... PASSED - SQL injection blocked (rejected with error)
Testing Authors filter with payload: */... PASSED - SQL injection blocked (rejected with error)
Testing Authors filter with payload: /**/... PASSED - SQL injection blocked (rejected with error)
Testing Authors filter with payload: --... PASSED - SQL injection blocked (rejected with error)
Testing Authors filter with payload: #... PASSED - SQL injection blocked (rejected with error)
Testing Authors filter with payload: 0x53514C5F494E4A454354494F4E... PASSED - SQL injection blocked (rejected with error)
Testing Authors filter with payload: J1NSTCBJTkpFQ1RJT04gLS0=... PASSED - SQL injection blocked (rejected with error)
Testing Authors filter with payload: '))); DROP TABLE events; --... PASSED - SQL injection blocked (rejected with error)
Testing Authors filter with payload: ')) UNION SELECT NULL; --... PASSED - SQL injection blocked (rejected with error)
Testing Authors filter with payload: ' AND 1=1 --... PASSED - SQL injection blocked (rejected with error)
Testing Authors filter with payload: ' AND 1=2 --... PASSED - SQL injection blocked (rejected with error)
Testing Authors filter with payload: ' AND (SELECT COUNT(*) FROM events) > 0 --... PASSED - SQL injection blocked (rejected with error)
Testing Authors filter with payload: '; EXEC master..xp_cmdshell 'net user' --... PASSED - SQL injection blocked (rejected with error)
Testing Authors filter with payload: '; DECLARE @host varchar(1024); SELECT @host=(SELECT TOP 1 master..sys.fn_varbintohexstr(password_hash) FROM sys.sql_logins WHERE name='sa'); --... PASSED - SQL injection blocked (rejected with error)
=== IDs Filter SQL Injection Tests ===
Testing IDs filter with payload: '; DROP TABLE events; --... PASSED - SQL injection blocked (rejected with error)
Testing IDs filter with payload: 1' OR '1'='1... PASSED - SQL injection blocked (rejected with error)
Testing IDs filter with payload: 1; SELECT * FROM sqlite_master; --... PASSED - SQL injection blocked (rejected with error)
Testing IDs filter with payload: ' OR 1=1 --... PASSED - SQL injection blocked (rejected with error)
Testing IDs filter with payload: admin'--... PASSED - SQL injection blocked (rejected with error)
Testing IDs filter with payload: 1' UNION SELECT password FROM users --... PASSED - SQL injection blocked (rejected with error)
Testing IDs filter with payload: 1' AND SLEEP(5) --... PASSED - SQL injection blocked (rejected with error)
Testing IDs filter with payload: 1' AND 1=1 WAITFOR DELAY '0:0:5' --... PASSED - SQL injection blocked (rejected with error)
Testing IDs filter with payload: 1' AND 1=CAST((SELECT version()) AS INT) --... PASSED - SQL injection blocked (rejected with error)
Testing IDs filter with payload: 1' AND 1=CONVERT(INT, (SELECT @@VERSION)) --... PASSED - SQL injection blocked (rejected with error)
Testing IDs filter with payload: ' UNION SELECT NULL,NULL,NULL --... PASSED - SQL injection blocked (rejected with error)
Testing IDs filter with payload: ' UNION SELECT 1,2,3 --... PASSED - SQL injection blocked (rejected with error)
Testing IDs filter with payload: ' UNION ALL SELECT NULL,NULL,NULL --... PASSED - SQL injection blocked (rejected with error)
Testing IDs filter with payload: '; SELECT * FROM events; --... PASSED - SQL injection blocked (rejected with error)
Testing IDs filter with payload: '; DELETE FROM events; --... PASSED - SQL injection blocked (rejected with error)
Testing IDs filter with payload: '; UPDATE events SET content='hacked' WHERE 1=1; --... PASSED - SQL injection blocked (rejected with error)
Testing IDs filter with payload: /*... PASSED - SQL injection blocked (rejected with error)
Testing IDs filter with payload: */... PASSED - SQL injection blocked (rejected with error)
Testing IDs filter with payload: /**/... PASSED - SQL injection blocked (rejected with error)
Testing IDs filter with payload: --... PASSED - SQL injection blocked (rejected with error)
Testing IDs filter with payload: #... PASSED - SQL injection blocked (rejected with error)
Testing IDs filter with payload: 0x53514C5F494E4A454354494F4E... PASSED - SQL injection blocked (rejected with error)
Testing IDs filter with payload: J1NSTCBJTkpFQ1RJT04gLS0=... PASSED - SQL injection blocked (rejected with error)
Testing IDs filter with payload: '))); DROP TABLE events; --... PASSED - SQL injection blocked (rejected with error)
Testing IDs filter with payload: ')) UNION SELECT NULL; --... PASSED - SQL injection blocked (rejected with error)
Testing IDs filter with payload: ' AND 1=1 --... PASSED - SQL injection blocked (rejected with error)
Testing IDs filter with payload: ' AND 1=2 --... PASSED - SQL injection blocked (rejected with error)
Testing IDs filter with payload: ' AND (SELECT COUNT(*) FROM events) > 0 --... PASSED - SQL injection blocked (rejected with error)
Testing IDs filter with payload: '; EXEC master..xp_cmdshell 'net user' --... PASSED - SQL injection blocked (rejected with error)
Testing IDs filter with payload: '; DECLARE @host varchar(1024); SELECT @host=(SELECT TOP 1 master..sys.fn_varbintohexstr(password_hash) FROM sys.sql_logins WHERE name='sa'); --... PASSED - SQL injection blocked (rejected with error)
=== Kinds Filter SQL Injection Tests ===
Testing Kinds filter with string injection... PASSED - SQL injection blocked (rejected with error)
Testing Kinds filter with negative value... PASSED - SQL injection blocked (rejected with error)
Testing Kinds filter with very large value... PASSED - SQL injection blocked (rejected with error)
=== Search Filter SQL Injection Tests ===
Testing Search filter with payload: '; DROP TABLE events; --... PASSED - SQL injection blocked (rejected with error)
Testing Search filter with payload: 1' OR '1'='1... PASSED - SQL injection blocked (query sanitized)
Testing Search filter with payload: 1; SELECT * FROM sqlite_master; --... PASSED - SQL injection blocked (rejected with error)
Testing Search filter with payload: ' OR 1=1 --... PASSED - SQL injection blocked (rejected with error)
Testing Search filter with payload: admin'--... PASSED - SQL injection blocked (rejected with error)
Testing Search filter with payload: 1' UNION SELECT password FROM users --... PASSED - SQL injection blocked (rejected with error)
Testing Search filter with payload: 1' AND SLEEP(5) --... PASSED - SQL injection blocked (rejected with error)
Testing Search filter with payload: 1' AND 1=1 WAITFOR DELAY '0:0:5' --... PASSED - SQL injection blocked (rejected with error)
Testing Search filter with payload: 1' AND 1=CAST((SELECT version()) AS INT) --... PASSED - SQL injection blocked (rejected with error)
Testing Search filter with payload: 1' AND 1=CONVERT(INT, (SELECT @@VERSION)) --... PASSED - SQL injection blocked (rejected with error)
Testing Search filter with payload: ' UNION SELECT NULL,NULL,NULL --... PASSED - SQL injection blocked (rejected with error)
Testing Search filter with payload: ' UNION SELECT 1,2,3 --... PASSED - SQL injection blocked (rejected with error)
Testing Search filter with payload: ' UNION ALL SELECT NULL,NULL,NULL --... PASSED - SQL injection blocked (rejected with error)
Testing Search filter with payload: '; SELECT * FROM events; --... PASSED - SQL injection blocked (rejected with error)
Testing Search filter with payload: '; DELETE FROM events; --... PASSED - SQL injection blocked (rejected with error)
Testing Search filter with payload: '; UPDATE events SET content='hacked' WHERE 1=1; --... PASSED - SQL injection blocked (rejected with error)
Testing Search filter with payload: /*... PASSED - SQL injection blocked (rejected with error)
Testing Search filter with payload: */... PASSED - SQL injection blocked (rejected with error)
Testing Search filter with payload: /**/... PASSED - SQL injection blocked (rejected with error)
Testing Search filter with payload: --... PASSED - SQL injection blocked (rejected with error)
Testing Search filter with payload: #... PASSED - SQL injection blocked (query sanitized)
Testing Search filter with payload: 0x53514C5F494E4A454354494F4E... PASSED - SQL injection blocked (query sanitized)
Testing Search filter with payload: J1NSTCBJTkpFQ1RJT04gLS0=... PASSED - SQL injection blocked (query sanitized)
Testing Search filter with payload: '))); DROP TABLE events; --... PASSED - SQL injection blocked (rejected with error)
Testing Search filter with payload: ')) UNION SELECT NULL; --... PASSED - SQL injection blocked (rejected with error)
Testing Search filter with payload: ' AND 1=1 --... PASSED - SQL injection blocked (rejected with error)
Testing Search filter with payload: ' AND 1=2 --... PASSED - SQL injection blocked (rejected with error)
Testing Search filter with payload: ' AND (SELECT COUNT(*) FROM events) > 0 --... PASSED - SQL injection blocked (rejected with error)
Testing Search filter with payload: '; EXEC master..xp_cmdshell 'net user' --... PASSED - SQL injection blocked (rejected with error)
Testing Search filter with payload: '; DECLARE @host varchar(1024); SELECT @host=(SELECT TOP 1 master..sys.fn_varbintohexstr(password_hash) FROM sys.sql_logins WHERE name='sa'); --... PASSED - SQL injection blocked (rejected with error)
=== Tag Filter SQL Injection Tests ===
Testing #e tag filter with payload: '; DROP TABLE events; --... PASSED - SQL injection blocked (query sanitized)
Testing #e tag filter with payload: 1' OR '1'='1... PASSED - SQL injection blocked (query sanitized)
Testing #e tag filter with payload: 1; SELECT * FROM sqlite_master; --... PASSED - SQL injection blocked (query sanitized)
Testing #e tag filter with payload: ' OR 1=1 --... PASSED - SQL injection blocked (query sanitized)
Testing #e tag filter with payload: admin'--... PASSED - SQL injection blocked (query sanitized)
Testing #e tag filter with payload: 1' UNION SELECT password FROM users --... PASSED - SQL injection blocked (query sanitized)
Testing #e tag filter with payload: 1' AND SLEEP(5) --... PASSED - SQL injection blocked (query sanitized)
Testing #e tag filter with payload: 1' AND 1=1 WAITFOR DELAY '0:0:5' --... PASSED - SQL injection blocked (query sanitized)
Testing #e tag filter with payload: 1' AND 1=CAST((SELECT version()) AS INT) --... PASSED - SQL injection blocked (query sanitized)
Testing #e tag filter with payload: 1' AND 1=CONVERT(INT, (SELECT @@VERSION)) --... PASSED - SQL injection blocked (query sanitized)
Testing #e tag filter with payload: ' UNION SELECT NULL,NULL,NULL --... PASSED - SQL injection blocked (query sanitized)
Testing #e tag filter with payload: ' UNION SELECT 1,2,3 --... PASSED - SQL injection blocked (query sanitized)
Testing #e tag filter with payload: ' UNION ALL SELECT NULL,NULL,NULL --... PASSED - SQL injection blocked (query sanitized)
Testing #e tag filter with payload: '; SELECT * FROM events; --... PASSED - SQL injection blocked (query sanitized)
Testing #e tag filter with payload: '; DELETE FROM events; --... PASSED - SQL injection blocked (query sanitized)
Testing #e tag filter with payload: '; UPDATE events SET content='hacked' WHERE 1=1; --... PASSED - SQL injection blocked (query sanitized)
Testing #e tag filter with payload: /*... PASSED - SQL injection blocked (query sanitized)
Testing #e tag filter with payload: */... PASSED - SQL injection blocked (query sanitized)
Testing #e tag filter with payload: /**/... PASSED - SQL injection blocked (query sanitized)
Testing #e tag filter with payload: --... PASSED - SQL injection blocked (query sanitized)
Testing #e tag filter with payload: #... PASSED - SQL injection blocked (query sanitized)
Testing #e tag filter with payload: 0x53514C5F494E4A454354494F4E... PASSED - SQL injection blocked (query sanitized)
Testing #e tag filter with payload: J1NSTCBJTkpFQ1RJT04gLS0=... PASSED - SQL injection blocked (query sanitized)
Testing #e tag filter with payload: '))); DROP TABLE events; --... PASSED - SQL injection blocked (query sanitized)
Testing #e tag filter with payload: ')) UNION SELECT NULL; --... PASSED - SQL injection blocked (query sanitized)
Testing #e tag filter with payload: ' AND 1=1 --... PASSED - SQL injection blocked (query sanitized)
Testing #e tag filter with payload: ' AND 1=2 --... PASSED - SQL injection blocked (query sanitized)
Testing #e tag filter with payload: ' AND (SELECT COUNT(*) FROM events) > 0 --... PASSED - SQL injection blocked (query sanitized)
Testing #e tag filter with payload: '; EXEC master..xp_cmdshell 'net user' --... PASSED - SQL injection blocked (query sanitized)
Testing #e tag filter with payload: '; DECLARE @host varchar(1024); SELECT @host=(SELECT TOP 1 master..sys.fn_varbintohexstr(password_hash) FROM sys.sql_logins WHERE name='sa'); --... PASSED - SQL injection blocked (query sanitized)
Testing #p tag filter with payload: '; DROP TABLE events; --... PASSED - SQL injection blocked (query sanitized)
Testing #p tag filter with payload: 1' OR '1'='1... PASSED - SQL injection blocked (query sanitized)
Testing #p tag filter with payload: 1; SELECT * FROM sqlite_master; --... PASSED - SQL injection blocked (query sanitized)
Testing #p tag filter with payload: ' OR 1=1 --... PASSED - SQL injection blocked (query sanitized)
Testing #p tag filter with payload: admin'--... PASSED - SQL injection blocked (query sanitized)
Testing #p tag filter with payload: 1' UNION SELECT password FROM users --... PASSED - SQL injection blocked (query sanitized)
Testing #p tag filter with payload: 1' AND SLEEP(5) --... PASSED - SQL injection blocked (query sanitized)
Testing #p tag filter with payload: 1' AND 1=1 WAITFOR DELAY '0:0:5' --... PASSED - SQL injection blocked (query sanitized)
Testing #p tag filter with payload: 1' AND 1=CAST((SELECT version()) AS INT) --... PASSED - SQL injection blocked (query sanitized)
Testing #p tag filter with payload: 1' AND 1=CONVERT(INT, (SELECT @@VERSION)) --... PASSED - SQL injection blocked (query sanitized)
Testing #p tag filter with payload: ' UNION SELECT NULL,NULL,NULL --... PASSED - SQL injection blocked (query sanitized)
Testing #p tag filter with payload: ' UNION SELECT 1,2,3 --... PASSED - SQL injection blocked (query sanitized)
Testing #p tag filter with payload: ' UNION ALL SELECT NULL,NULL,NULL --... PASSED - SQL injection blocked (query sanitized)
Testing #p tag filter with payload: '; SELECT * FROM events; --... PASSED - SQL injection blocked (query sanitized)
Testing #p tag filter with payload: '; DELETE FROM events; --... PASSED - SQL injection blocked (query sanitized)
Testing #p tag filter with payload: '; UPDATE events SET content='hacked' WHERE 1=1; --... PASSED - SQL injection blocked (query sanitized)
Testing #p tag filter with payload: /*... PASSED - SQL injection blocked (query sanitized)
Testing #p tag filter with payload: */... PASSED - SQL injection blocked (query sanitized)
Testing #p tag filter with payload: /**/... PASSED - SQL injection blocked (query sanitized)
Testing #p tag filter with payload: --... PASSED - SQL injection blocked (query sanitized)
Testing #p tag filter with payload: #... PASSED - SQL injection blocked (query sanitized)
Testing #p tag filter with payload: 0x53514C5F494E4A454354494F4E... PASSED - SQL injection blocked (query sanitized)
Testing #p tag filter with payload: J1NSTCBJTkpFQ1RJT04gLS0=... PASSED - SQL injection blocked (query sanitized)
Testing #p tag filter with payload: '))); DROP TABLE events; --... PASSED - SQL injection blocked (query sanitized)
Testing #p tag filter with payload: ')) UNION SELECT NULL; --... PASSED - SQL injection blocked (query sanitized)
Testing #p tag filter with payload: ' AND 1=1 --... PASSED - SQL injection blocked (query sanitized)
Testing #p tag filter with payload: ' AND 1=2 --... PASSED - SQL injection blocked (query sanitized)
Testing #p tag filter with payload: ' AND (SELECT COUNT(*) FROM events) > 0 --... PASSED - SQL injection blocked (query sanitized)
Testing #p tag filter with payload: '; EXEC master..xp_cmdshell 'net user' --... PASSED - SQL injection blocked (query sanitized)
Testing #p tag filter with payload: '; DECLARE @host varchar(1024); SELECT @host=(SELECT TOP 1 master..sys.fn_varbintohexstr(password_hash) FROM sys.sql_logins WHERE name='sa'); --... PASSED - SQL injection blocked (query sanitized)
Testing #t tag filter with payload: '; DROP TABLE events; --... PASSED - SQL injection blocked (query sanitized)
Testing #t tag filter with payload: 1' OR '1'='1... PASSED - SQL injection blocked (query sanitized)
Testing #t tag filter with payload: 1; SELECT * FROM sqlite_master; --... PASSED - SQL injection blocked (query sanitized)
Testing #t tag filter with payload: ' OR 1=1 --... PASSED - SQL injection blocked (query sanitized)
Testing #t tag filter with payload: admin'--... PASSED - SQL injection blocked (query sanitized)
Testing #t tag filter with payload: 1' UNION SELECT password FROM users --... PASSED - SQL injection blocked (query sanitized)
Testing #t tag filter with payload: 1' AND SLEEP(5) --... PASSED - SQL injection blocked (query sanitized)
Testing #t tag filter with payload: 1' AND 1=1 WAITFOR DELAY '0:0:5' --... PASSED - SQL injection blocked (query sanitized)
Testing #t tag filter with payload: 1' AND 1=CAST((SELECT version()) AS INT) --... PASSED - SQL injection blocked (query sanitized)
Testing #t tag filter with payload: 1' AND 1=CONVERT(INT, (SELECT @@VERSION)) --... PASSED - SQL injection blocked (query sanitized)
Testing #t tag filter with payload: ' UNION SELECT NULL,NULL,NULL --... PASSED - SQL injection blocked (query sanitized)
Testing #t tag filter with payload: ' UNION SELECT 1,2,3 --... PASSED - SQL injection blocked (query sanitized)
Testing #t tag filter with payload: ' UNION ALL SELECT NULL,NULL,NULL --... PASSED - SQL injection blocked (query sanitized)
Testing #t tag filter with payload: '; SELECT * FROM events; --... PASSED - SQL injection blocked (query sanitized)
Testing #t tag filter with payload: '; DELETE FROM events; --... PASSED - SQL injection blocked (query sanitized)
Testing #t tag filter with payload: '; UPDATE events SET content='hacked' WHERE 1=1; --... PASSED - SQL injection blocked (query sanitized)
Testing #t tag filter with payload: /*... PASSED - SQL injection blocked (query sanitized)
Testing #t tag filter with payload: */... PASSED - SQL injection blocked (query sanitized)
Testing #t tag filter with payload: /**/... PASSED - SQL injection blocked (query sanitized)
Testing #t tag filter with payload: --... PASSED - SQL injection blocked (query sanitized)
Testing #t tag filter with payload: #... PASSED - SQL injection blocked (query sanitized)
Testing #t tag filter with payload: 0x53514C5F494E4A454354494F4E... PASSED - SQL injection blocked (query sanitized)
Testing #t tag filter with payload: J1NSTCBJTkpFQ1RJT04gLS0=... PASSED - SQL injection blocked (query sanitized)
Testing #t tag filter with payload: '))); DROP TABLE events; --... PASSED - SQL injection blocked (query sanitized)
Testing #t tag filter with payload: ')) UNION SELECT NULL; --... PASSED - SQL injection blocked (query sanitized)
Testing #t tag filter with payload: ' AND 1=1 --... PASSED - SQL injection blocked (query sanitized)
Testing #t tag filter with payload: ' AND 1=2 --... PASSED - SQL injection blocked (query sanitized)
Testing #t tag filter with payload: ' AND (SELECT COUNT(*) FROM events) > 0 --... PASSED - SQL injection blocked (query sanitized)
Testing #t tag filter with payload: '; EXEC master..xp_cmdshell 'net user' --... PASSED - SQL injection blocked (query sanitized)
Testing #t tag filter with payload: '; DECLARE @host varchar(1024); SELECT @host=(SELECT TOP 1 master..sys.fn_varbintohexstr(password_hash) FROM sys.sql_logins WHERE name='sa'); --... PASSED - SQL injection blocked (query sanitized)
Testing #r tag filter with payload: '; DROP TABLE events; --... PASSED - SQL injection blocked (query sanitized)
Testing #r tag filter with payload: 1' OR '1'='1... PASSED - SQL injection blocked (query sanitized)
Testing #r tag filter with payload: 1; SELECT * FROM sqlite_master; --... PASSED - SQL injection blocked (query sanitized)
Testing #r tag filter with payload: ' OR 1=1 --... PASSED - SQL injection blocked (query sanitized)
Testing #r tag filter with payload: admin'--... PASSED - SQL injection blocked (query sanitized)
Testing #r tag filter with payload: 1' UNION SELECT password FROM users --... PASSED - SQL injection blocked (query sanitized)
Testing #r tag filter with payload: 1' AND SLEEP(5) --... PASSED - SQL injection blocked (query sanitized)
Testing #r tag filter with payload: 1' AND 1=1 WAITFOR DELAY '0:0:5' --... PASSED - SQL injection blocked (query sanitized)
Testing #r tag filter with payload: 1' AND 1=CAST((SELECT version()) AS INT) --... PASSED - SQL injection blocked (query sanitized)
Testing #r tag filter with payload: 1' AND 1=CONVERT(INT, (SELECT @@VERSION)) --... PASSED - SQL injection blocked (query sanitized)
Testing #r tag filter with payload: ' UNION SELECT NULL,NULL,NULL --... PASSED - SQL injection blocked (query sanitized)
Testing #r tag filter with payload: ' UNION SELECT 1,2,3 --... PASSED - SQL injection blocked (query sanitized)
Testing #r tag filter with payload: ' UNION ALL SELECT NULL,NULL,NULL --... PASSED - SQL injection blocked (query sanitized)
Testing #r tag filter with payload: '; SELECT * FROM events; --... PASSED - SQL injection blocked (query sanitized)
Testing #r tag filter with payload: '; DELETE FROM events; --... PASSED - SQL injection blocked (query sanitized)
Testing #r tag filter with payload: '; UPDATE events SET content='hacked' WHERE 1=1; --... PASSED - SQL injection blocked (query sanitized)
Testing #r tag filter with payload: /*... PASSED - SQL injection blocked (query sanitized)
Testing #r tag filter with payload: */... PASSED - SQL injection blocked (query sanitized)
Testing #r tag filter with payload: /**/... PASSED - SQL injection blocked (query sanitized)
Testing #r tag filter with payload: --... PASSED - SQL injection blocked (query sanitized)
Testing #r tag filter with payload: #... PASSED - SQL injection blocked (query sanitized)
Testing #r tag filter with payload: 0x53514C5F494E4A454354494F4E... PASSED - SQL injection blocked (query sanitized)
Testing #r tag filter with payload: J1NSTCBJTkpFQ1RJT04gLS0=... PASSED - SQL injection blocked (query sanitized)
Testing #r tag filter with payload: '))); DROP TABLE events; --... PASSED - SQL injection blocked (query sanitized)
Testing #r tag filter with payload: ')) UNION SELECT NULL; --... PASSED - SQL injection blocked (query sanitized)
Testing #r tag filter with payload: ' AND 1=1 --... PASSED - SQL injection blocked (query sanitized)
Testing #r tag filter with payload: ' AND 1=2 --... PASSED - SQL injection blocked (query sanitized)
Testing #r tag filter with payload: ' AND (SELECT COUNT(*) FROM events) > 0 --... PASSED - SQL injection blocked (query sanitized)
Testing #r tag filter with payload: '; EXEC master..xp_cmdshell 'net user' --... PASSED - SQL injection blocked (query sanitized)
Testing #r tag filter with payload: '; DECLARE @host varchar(1024); SELECT @host=(SELECT TOP 1 master..sys.fn_varbintohexstr(password_hash) FROM sys.sql_logins WHERE name='sa'); --... PASSED - SQL injection blocked (query sanitized)
Testing #d tag filter with payload: '; DROP TABLE events; --... PASSED - SQL injection blocked (query sanitized)
Testing #d tag filter with payload: 1' OR '1'='1... PASSED - SQL injection blocked (query sanitized)
Testing #d tag filter with payload: 1; SELECT * FROM sqlite_master; --... PASSED - SQL injection blocked (query sanitized)
Testing #d tag filter with payload: ' OR 1=1 --... PASSED - SQL injection blocked (query sanitized)
Testing #d tag filter with payload: admin'--... PASSED - SQL injection blocked (query sanitized)
Testing #d tag filter with payload: 1' UNION SELECT password FROM users --... PASSED - SQL injection blocked (query sanitized)
Testing #d tag filter with payload: 1' AND SLEEP(5) --... PASSED - SQL injection blocked (query sanitized)
Testing #d tag filter with payload: 1' AND 1=1 WAITFOR DELAY '0:0:5' --... PASSED - SQL injection blocked (query sanitized)
Testing #d tag filter with payload: 1' AND 1=CAST((SELECT version()) AS INT) --... PASSED - SQL injection blocked (query sanitized)
Testing #d tag filter with payload: 1' AND 1=CONVERT(INT, (SELECT @@VERSION)) --... PASSED - SQL injection blocked (query sanitized)
Testing #d tag filter with payload: ' UNION SELECT NULL,NULL,NULL --... PASSED - SQL injection blocked (query sanitized)
Testing #d tag filter with payload: ' UNION SELECT 1,2,3 --... PASSED - SQL injection blocked (query sanitized)
Testing #d tag filter with payload: ' UNION ALL SELECT NULL,NULL,NULL --... PASSED - SQL injection blocked (query sanitized)
Testing #d tag filter with payload: '; SELECT * FROM events; --... PASSED - SQL injection blocked (query sanitized)
Testing #d tag filter with payload: '; DELETE FROM events; --... PASSED - SQL injection blocked (query sanitized)
Testing #d tag filter with payload: '; UPDATE events SET content='hacked' WHERE 1=1; --... PASSED - SQL injection blocked (query sanitized)
Testing #d tag filter with payload: /*... PASSED - SQL injection blocked (query sanitized)
Testing #d tag filter with payload: */... PASSED - SQL injection blocked (query sanitized)
Testing #d tag filter with payload: /**/... PASSED - SQL injection blocked (query sanitized)
Testing #d tag filter with payload: --... PASSED - SQL injection blocked (query sanitized)
Testing #d tag filter with payload: #... PASSED - SQL injection blocked (query sanitized)
Testing #d tag filter with payload: 0x53514C5F494E4A454354494F4E... PASSED - SQL injection blocked (query sanitized)
Testing #d tag filter with payload: J1NSTCBJTkpFQ1RJT04gLS0=... PASSED - SQL injection blocked (query sanitized)
Testing #d tag filter with payload: '))); DROP TABLE events; --... PASSED - SQL injection blocked (query sanitized)
Testing #d tag filter with payload: ')) UNION SELECT NULL; --... PASSED - SQL injection blocked (query sanitized)
Testing #d tag filter with payload: ' AND 1=1 --... PASSED - SQL injection blocked (query sanitized)
Testing #d tag filter with payload: ' AND 1=2 --... PASSED - SQL injection blocked (query sanitized)
Testing #d tag filter with payload: ' AND (SELECT COUNT(*) FROM events) > 0 --... PASSED - SQL injection blocked (query sanitized)
Testing #d tag filter with payload: '; EXEC master..xp_cmdshell 'net user' --... PASSED - SQL injection blocked (query sanitized)
Testing #d tag filter with payload: '; DECLARE @host varchar(1024); SELECT @host=(SELECT TOP 1 master..sys.fn_varbintohexstr(password_hash) FROM sys.sql_logins WHERE name='sa'); --... PASSED - SQL injection blocked (query sanitized)
=== Timestamp Filter SQL Injection Tests ===
Testing Since parameter injection... PASSED - SQL injection blocked (rejected with error)
Testing Until parameter injection... PASSED - SQL injection blocked (rejected with error)
=== Limit Parameter SQL Injection Tests ===
Testing Limit parameter injection... PASSED - SQL injection blocked (rejected with error)
Testing Limit with UNION... PASSED - SQL injection blocked (rejected with error)
=== Complex Multi-Filter SQL Injection Tests ===
Testing Multi-filter with authors injection... PASSED - SQL injection blocked (rejected with error)
Testing Multi-filter with search injection... PASSED - SQL injection blocked (rejected with error)
Testing Multi-filter with tag injection... PASSED - SQL injection blocked (query sanitized)
=== COUNT Message SQL Injection Tests ===
Testing COUNT with authors payload: '; DROP TABLE events; --... PASSED - SQL injection blocked (rejected with error)
Testing COUNT with search payload: '; DROP TABLE events; --... PASSED - SQL injection blocked (rejected with error)
Testing COUNT with authors payload: 1' OR '1'='1... PASSED - SQL injection blocked (rejected with error)
Testing COUNT with search payload: 1' OR '1'='1... PASSED - SQL injection blocked (query sanitized)
Testing COUNT with authors payload: 1; SELECT * FROM sqlite_master; --... PASSED - SQL injection blocked (rejected with error)
Testing COUNT with search payload: 1; SELECT * FROM sqlite_master; --... PASSED - SQL injection blocked (rejected with error)
Testing COUNT with authors payload: ' OR 1=1 --... PASSED - SQL injection blocked (rejected with error)
Testing COUNT with search payload: ' OR 1=1 --... PASSED - SQL injection blocked (rejected with error)
Testing COUNT with authors payload: admin'--... PASSED - SQL injection blocked (rejected with error)
Testing COUNT with search payload: admin'--... PASSED - SQL injection blocked (rejected with error)
Testing COUNT with authors payload: 1' UNION SELECT password FROM users --... PASSED - SQL injection blocked (rejected with error)
Testing COUNT with search payload: 1' UNION SELECT password FROM users --... PASSED - SQL injection blocked (rejected with error)
Testing COUNT with authors payload: 1' AND SLEEP(5) --... PASSED - SQL injection blocked (rejected with error)
Testing COUNT with search payload: 1' AND SLEEP(5) --... PASSED - SQL injection blocked (rejected with error)
Testing COUNT with authors payload: 1' AND 1=1 WAITFOR DELAY '0:0:5' --... PASSED - SQL injection blocked (rejected with error)
Testing COUNT with search payload: 1' AND 1=1 WAITFOR DELAY '0:0:5' --... PASSED - SQL injection blocked (rejected with error)
Testing COUNT with authors payload: 1' AND 1=CAST((SELECT version()) AS INT) --... PASSED - SQL injection blocked (rejected with error)
Testing COUNT with search payload: 1' AND 1=CAST((SELECT version()) AS INT) --... PASSED - SQL injection blocked (rejected with error)
Testing COUNT with authors payload: 1' AND 1=CONVERT(INT, (SELECT @@VERSION)) --... PASSED - SQL injection blocked (rejected with error)
Testing COUNT with search payload: 1' AND 1=CONVERT(INT, (SELECT @@VERSION)) --... PASSED - SQL injection blocked (rejected with error)
Testing COUNT with authors payload: ' UNION SELECT NULL,NULL,NULL --... PASSED - SQL injection blocked (rejected with error)
Testing COUNT with search payload: ' UNION SELECT NULL,NULL,NULL --... PASSED - SQL injection blocked (rejected with error)
Testing COUNT with authors payload: ' UNION SELECT 1,2,3 --... PASSED - SQL injection blocked (rejected with error)
Testing COUNT with search payload: ' UNION SELECT 1,2,3 --... PASSED - SQL injection blocked (rejected with error)
Testing COUNT with authors payload: ' UNION ALL SELECT NULL,NULL,NULL --... PASSED - SQL injection blocked (rejected with error)
Testing COUNT with search payload: ' UNION ALL SELECT NULL,NULL,NULL --... PASSED - SQL injection blocked (rejected with error)
Testing COUNT with authors payload: '; SELECT * FROM events; --... PASSED - SQL injection blocked (rejected with error)
Testing COUNT with search payload: '; SELECT * FROM events; --... PASSED - SQL injection blocked (rejected with error)
Testing COUNT with authors payload: '; DELETE FROM events; --... PASSED - SQL injection blocked (rejected with error)
Testing COUNT with search payload: '; DELETE FROM events; --... PASSED - SQL injection blocked (rejected with error)
Testing COUNT with authors payload: '; UPDATE events SET content='hacked' WHERE 1=1; --... PASSED - SQL injection blocked (rejected with error)
Testing COUNT with search payload: '; UPDATE events SET content='hacked' WHERE 1=1; --... PASSED - SQL injection blocked (rejected with error)
Testing COUNT with authors payload: /*... PASSED - SQL injection blocked (rejected with error)
Testing COUNT with search payload: /*... PASSED - SQL injection blocked (rejected with error)
Testing COUNT with authors payload: */... PASSED - SQL injection blocked (rejected with error)
Testing COUNT with search payload: */... PASSED - SQL injection blocked (rejected with error)
Testing COUNT with authors payload: /**/... PASSED - SQL injection blocked (rejected with error)
Testing COUNT with search payload: /**/... PASSED - SQL injection blocked (rejected with error)
Testing COUNT with authors payload: --... PASSED - SQL injection blocked (rejected with error)
Testing COUNT with search payload: --... PASSED - SQL injection blocked (rejected with error)
Testing COUNT with authors payload: #... PASSED - SQL injection blocked (rejected with error)
Testing COUNT with search payload: #... PASSED - SQL injection blocked (query sanitized)
Testing COUNT with authors payload: 0x53514C5F494E4A454354494F4E... PASSED - SQL injection blocked (rejected with error)
Testing COUNT with search payload: 0x53514C5F494E4A454354494F4E... PASSED - SQL injection blocked (query sanitized)
Testing COUNT with authors payload: J1NSTCBJTkpFQ1RJT04gLS0=... PASSED - SQL injection blocked (rejected with error)
Testing COUNT with search payload: J1NSTCBJTkpFQ1RJT04gLS0=... PASSED - SQL injection blocked (query sanitized)
Testing COUNT with authors payload: '))); DROP TABLE events; --... PASSED - SQL injection blocked (rejected with error)
Testing COUNT with search payload: '))); DROP TABLE events; --... PASSED - SQL injection blocked (rejected with error)
Testing COUNT with authors payload: ')) UNION SELECT NULL; --... PASSED - SQL injection blocked (rejected with error)
Testing COUNT with search payload: ')) UNION SELECT NULL; --... PASSED - SQL injection blocked (rejected with error)
Testing COUNT with authors payload: ' AND 1=1 --... PASSED - SQL injection blocked (rejected with error)
Testing COUNT with search payload: ' AND 1=1 --... PASSED - SQL injection blocked (rejected with error)
Testing COUNT with authors payload: ' AND 1=2 --... PASSED - SQL injection blocked (rejected with error)
Testing COUNT with search payload: ' AND 1=2 --... PASSED - SQL injection blocked (rejected with error)
Testing COUNT with authors payload: ' AND (SELECT COUNT(*) FROM events) > 0 --... PASSED - SQL injection blocked (rejected with error)
Testing COUNT with search payload: ' AND (SELECT COUNT(*) FROM events) > 0 --... PASSED - SQL injection blocked (rejected with error)
Testing COUNT with authors payload: '; EXEC master..xp_cmdshell 'net user' --... PASSED - SQL injection blocked (rejected with error)
Testing COUNT with search payload: '; EXEC master..xp_cmdshell 'net user' --... PASSED - SQL injection blocked (rejected with error)
Testing COUNT with authors payload: '; DECLARE @host varchar(1024); SELECT @host=(SELECT TOP 1 master..sys.fn_varbintohexstr(password_hash) FROM sys.sql_logins WHERE name='sa'); --... PASSED - SQL injection blocked (rejected with error)
Testing COUNT with search payload: '; DECLARE @host varchar(1024); SELECT @host=(SELECT TOP 1 master..sys.fn_varbintohexstr(password_hash) FROM sys.sql_logins WHERE name='sa'); --... PASSED - SQL injection blocked (rejected with error)
=== Edge Case SQL Injection Tests ===
Testing Empty string injection... PASSED - SQL injection blocked (rejected with error)
Testing Null byte injection... PASSED - SQL injection blocked (silently rejected)
Testing Unicode injection... PASSED - SQL injection blocked (rejected with error)
Testing Very long injection payload... PASSED - SQL injection blocked (rejected with error)
=== Subscription ID SQL Injection Tests ===
Testing Subscription ID injection... PASSED - SQL injection blocked (rejected with error)
Testing Subscription ID with quotes... PASSED - SQL injection blocked (silently rejected)
=== CLOSE Message SQL Injection Tests ===
Testing CLOSE with injection... PASSED - SQL injection blocked (rejected with error)
=== Test Results ===
Total tests: 318
Passed: 318
Failed: 0
✓ All SQL injection tests passed!
The relay appears to be protected against SQL injection attacks.
2026-04-01 11:29:32 - \033[0;32m✓ SQL Injection Tests PASSED\033[0m (Duration: 7s)
2026-04-01 11:29:32 - ==========================================
2026-04-01 11:29:32 - Running Test Suite: Filter Validation Tests
2026-04-01 11:29:32 - Description: Input validation for REQ and COUNT messages
2026-04-01 11:29:32 - ==========================================
=== C-Relay Filter Validation Tests ===
Testing against relay at ws://127.0.0.1:8888
Testing Valid REQ message... PASSED
Testing Valid COUNT message... PASSED
=== Testing Filter Array Validation ===
Testing Non-object filter... PASSED
Testing Too many filters... PASSED
=== Testing Authors Validation ===
Testing Invalid author type... PASSED
Testing Invalid author hex... PASSED
Testing Too many authors... PASSED
=== Testing IDs Validation ===
Testing Invalid ID type... PASSED
Testing Invalid ID hex... PASSED
Testing Too many IDs... PASSED
=== Testing Kinds Validation ===
Testing Invalid kind type... PASSED
Testing Negative kind... PASSED
Testing Too large kind... PASSED
Testing Too many kinds... PASSED
=== Testing Timestamp Validation ===
Testing Invalid since type... PASSED
Testing Negative since... PASSED
Testing Invalid until type... PASSED
Testing Negative until... PASSED
=== Testing Limit Validation ===
Testing Invalid limit type... PASSED
Testing Negative limit... PASSED
Testing Too large limit... PASSED
=== Testing Search Validation ===
Testing Invalid search type... PASSED
Testing Search too long... PASSED
Testing Search SQL injection... PASSED
=== Testing Tag Filter Validation ===
Testing Invalid tag filter type... PASSED
Testing Too many tag values... PASSED
Testing Tag value too long... PASSED
=== Testing Rate Limiting ===
Testing rate limiting with malformed requests... UNCERTAIN - Rate limiting may not have triggered (this could be normal)
=== Test Results ===
Total tests: 28
Passed: 28
Failed: 0
All tests passed!
2026-04-01 11:29:35 - \033[0;32m✓ Filter Validation Tests PASSED\033[0m (Duration: 3s)
2026-04-01 11:29:35 - ==========================================
2026-04-01 11:29:35 - Running Test Suite: Subscription Validation Tests
2026-04-01 11:29:35 - Description: Subscription ID and message validation
2026-04-01 11:29:35 - ==========================================
Testing subscription ID validation fixes...
Testing malformed subscription IDs...
Empty ID test: Connection failed (expected)
Long ID test: Connection failed (expected)
Invalid chars test: Connection failed (expected)
NULL ID test: Connection failed (expected)
Valid ID test: Failed
Testing CLOSE message validation...
CLOSE empty ID test: Connection failed (expected)
CLOSE valid ID test: Failed
Subscription validation tests completed.
2026-04-01 11:29:35 - \033[0;32m✓ Subscription Validation Tests PASSED\033[0m (Duration: 0s)
2026-04-01 11:29:35 - ==========================================
2026-04-01 11:29:35 - Running Test Suite: Memory Corruption Tests
2026-04-01 11:29:35 - Description: Buffer overflow and memory safety testing
2026-04-01 11:29:35 - ==========================================
==========================================
C-Relay Memory Corruption Test Suite
==========================================
Testing against relay at ws://127.0.0.1:8888
Note: These tests may cause the relay to crash if vulnerabilities exist
=== Basic Connectivity Test ===
Testing Basic connectivity... PASSED - No memory corruption detected
=== Subscription ID Memory Corruption Tests ===
Testing Empty subscription ID... UNCERTAIN - Expected error but got normal response
Testing Very long subscription ID (1KB)... UNCERTAIN - Expected error but got normal response
Testing Very long subscription ID (10KB)... UNCERTAIN - Expected error but got normal response
Testing Subscription ID with null bytes... UNCERTAIN - Expected error but got normal response
Testing Subscription ID with special chars... UNCERTAIN - Expected error but got normal response
Testing Unicode subscription ID... UNCERTAIN - Expected error but got normal response
Testing Subscription ID with path traversal... UNCERTAIN - Expected error but got normal response
=== Filter Array Memory Corruption Tests ===
Testing Too many filters (50)... UNCERTAIN - Expected error but got normal response
=== Concurrent Access Memory Tests ===
Testing Concurrent subscription creation... ["EVENT","concurrent_1775057376377657551",{"pubkey":"7b013abdf0cbc8f994e66018539fbcb750f3be118a70d8bdcf3b3e86dd2ebb40","created_at":1775057288,"kind":1,"tags":[],"content":"\nRelay Statistics\n━━━━━━━━━━━━━━━━━━━━\nDatabase Size 0.00 MB (4096 bytes)\nTotal Events 0\nActive Subscriptions 0\nOldest Event -\nNewest Event -\n\nEvent_Kind_Distribution:\n\nTime-based Statistics:\n0\tEvents in the last day\n0\tEvents in the last week\n0\tEvents in the last month\n\n\n","id":"210f21884a3581064f4d181bc10e4088b5161216ddc7088f5c39e570240687b3","sig":"a04a697e6b3a53a1df4e3d7d06da2483f07d45c97a939076bd8b28216425a854af5f7e463542adbed9b71af68d36d7e0296be0048c3156a51ed0defa685a5171"}]
["EVENT","concurrent_1775057376377657551",{"pubkey":"7b013abdf0cbc8f994e66018539fbcb750f3be118a70d8bdcf3b3e86dd2ebb40","created_at":1775057288,"kind":1,"tags":[],"content":"\nRelay Statistics\n━━━━━━━━━━━━━━━━━━━━\nDatabase Size 0.00 MB (4096 bytes)\nTotal Events 0\nActive Subscriptions 0\nOldest Event -\nNewest Event -\n\nEvent_Kind_Distribution:\n\nTime-based Statistics:\n0\tEvents in the last day\n0\tEvents in the last week\n0\tEvents in the last month\n\n\n","id":"210f21884a3581064f4d181bc10e4088b5161216ddc7088f5c39e570240687b3","sig":"a04a697e6b3a53a1df4e3d7d06da2483f07d45c97a939076bd8b28216425a854af5f7e463542adbed9b71af68d36d7e0296be0048c3156a51ed0defa685a5171"}]
["EVENT","concurrent_1775057376377657551",{"pubkey":"7b013abdf0cbc8f994e66018539fbcb750f3be118a70d8bdcf3b3e86dd2ebb40","created_at":1775057288,"kind":1,"tags":[],"content":"\nRelay Statistics\n━━━━━━━━━━━━━━━━━━━━\nDatabase Size 0.00 MB (4096 bytes)\nTotal Events 0\nActive Subscriptions 0\nOldest Event -\nNewest Event -\n\nEvent_Kind_Distribution:\n\nTime-based Statistics:\n0\tEvents in the last day\n0\tEvents in the last week\n0\tEvents in the last month\n\n\n","id":"210f21884a3581064f4d181bc10e4088b5161216ddc7088f5c39e570240687b3","sig":"a04a697e6b3a53a1df4e3d7d06da2483f07d45c97a939076bd8b28216425a854af5f7e463542adbed9b71af68d36d7e0296be0048c3156a51ed0defa685a5171"}]
["EVENT","concurrent_1775057376377657551",{"pubkey":"7b013abdf0cbc8f994e66018539fbcb750f3be118a70d8bdcf3b3e86dd2ebb40","created_at":1775057288,"kind":1,"tags":[],"content":"\nRelay Statistics\n━━━━━━━━━━━━━━━━━━━━\nDatabase Size 0.00 MB (4096 bytes)\nTotal Events 0\nActive Subscriptions 0\nOldest Event -\nNewest Event -\n\nEvent_Kind_Distribution:\n\nTime-based Statistics:\n0\tEvents in the last day\n0\tEvents in the last week\n0\tEvents in the last month\n\n\n","id":"210f21884a3581064f4d181bc10e4088b5161216ddc7088f5c39e570240687b3","sig":"a04a697e6b3a53a1df4e3d7d06da2483f07d45c97a939076bd8b28216425a854af5f7e463542adbed9b71af68d36d7e0296be0048c3156a51ed0defa685a5171"}]
["EVENT","concurrent_1775057376377657551",{"pubkey":"7b013abdf0cbc8f994e66018539fbcb750f3be118a70d8bdcf3b3e86dd2ebb40","created_at":1775057288,"kind":1,"tags":[],"content":"\nRelay Statistics\n━━━━━━━━━━━━━━━━━━━━\nDatabase Size 0.00 MB (4096 bytes)\nTotal Events 0\nActive Subscriptions 0\nOldest Event -\nNewest Event -\n\nEvent_Kind_Distribution:\n\nTime-based Statistics:\n0\tEvents in the last day\n0\tEvents in the last week\n0\tEvents in the last month\n\n\n","id":"210f21884a3581064f4d181bc10e4088b5161216ddc7088f5c39e570240687b3","sig":"a04a697e6b3a53a1df4e3d7d06da2483f07d45c97a939076bd8b28216425a854af5f7e463542adbed9b71af68d36d7e0296be0048c3156a51ed0defa685a5171"}]
["EVENT","concurrent_1775057376377657551",{"pubkey":"7b013abdf0cbc8f994e66018539fbcb750f3be118a70d8bdcf3b3e86dd2ebb40","created_at":1775057288,"kind":1,"tags":[],"content":"\nRelay Statistics\n━━━━━━━━━━━━━━━━━━━━\nDatabase Size 0.00 MB (4096 bytes)\nTotal Events 0\nActive Subscriptions 0\nOldest Event -\nNewest Event -\n\nEvent_Kind_Distribution:\n\nTime-based Statistics:\n0\tEvents in the last day\n0\tEvents in the last week\n0\tEvents in the last month\n\n\n","id":"210f21884a3581064f4d181bc10e4088b5161216ddc7088f5c39e570240687b3","sig":"a04a697e6b3a53a1df4e3d7d06da2483f07d45c97a939076bd8b28216425a854af5f7e463542adbed9b71af68d36d7e0296be0048c3156a51ed0defa685a5171"}]
["EVENT","concurrent_1775057376377657551",{"pubkey":"7b013abdf0cbc8f994e66018539fbcb750f3be118a70d8bdcf3b3e86dd2ebb40","created_at":1775057288,"kind":1,"tags":[],"content":"\nRelay Statistics\n━━━━━━━━━━━━━━━━━━━━\nDatabase Size 0.00 MB (4096 bytes)\nTotal Events 0\nActive Subscriptions 0\nOldest Event -\nNewest Event -\n\nEvent_Kind_Distribution:\n\nTime-based Statistics:\n0\tEvents in the last day\n0\tEvents in the last week\n0\tEvents in the last month\n\n\n","id":"210f21884a3581064f4d181bc10e4088b5161216ddc7088f5c39e570240687b3","sig":"a04a697e6b3a53a1df4e3d7d06da2483f07d45c97a939076bd8b28216425a854af5f7e463542adbed9b71af68d36d7e0296be0048c3156a51ed0defa685a5171"}]
["EVENT","concurrent_1775057376377657551",{"pubkey":"7b013abdf0cbc8f994e66018539fbcb750f3be118a70d8bdcf3b3e86dd2ebb40","created_at":1775057288,"kind":1,"tags":[],"content":"\nRelay Statistics\n━━━━━━━━━━━━━━━━━━━━\nDatabase Size 0.00 MB (4096 bytes)\nTotal Events 0\nActive Subscriptions 0\nOldest Event -\nNewest Event -\n\nEvent_Kind_Distribution:\n\nTime-based Statistics:\n0\tEvents in the last day\n0\tEvents in the last week\n0\tEvents in the last month\n\n\n","id":"210f21884a3581064f4d181bc10e4088b5161216ddc7088f5c39e570240687b3","sig":"a04a697e6b3a53a1df4e3d7d06da2483f07d45c97a939076bd8b28216425a854af5f7e463542adbed9b71af68d36d7e0296be0048c3156a51ed0defa685a5171"}]
["EVENT","concurrent_1775057376377657551",{"pubkey":"7b013abdf0cbc8f994e66018539fbcb750f3be118a70d8bdcf3b3e86dd2ebb40","created_at":1775057288,"kind":1,"tags":[],"content":"\nRelay Statistics\n━━━━━━━━━━━━━━━━━━━━\nDatabase Size 0.00 MB (4096 bytes)\nTotal Events 0\nActive Subscriptions 0\nOldest Event -\nNewest Event -\n\nEvent_Kind_Distribution:\n\nTime-based Statistics:\n0\tEvents in the last day\n0\tEvents in the last week\n0\tEvents in the last month\n\n\n","id":"210f21884a3581064f4d181bc10e4088b5161216ddc7088f5c39e570240687b3","sig":"a04a697e6b3a53a1df4e3d7d06da2483f07d45c97a939076bd8b28216425a854af5f7e463542adbed9b71af68d36d7e0296be0048c3156a51ed0defa685a5171"}]
["EVENT","concurrent_1775057376377657551",{"pubkey":"7b013abdf0cbc8f994e66018539fbcb750f3be118a70d8bdcf3b3e86dd2ebb40","created_at":1775057288,"kind":1,"tags":[],"content":"\nRelay Statistics\n━━━━━━━━━━━━━━━━━━━━\nDatabase Size 0.00 MB (4096 bytes)\nTotal Events 0\nActive Subscriptions 0\nOldest Event -\nNewest Event -\n\nEvent_Kind_Distribution:\n\nTime-based Statistics:\n0\tEvents in the last day\n0\tEvents in the last week\n0\tEvents in the last month\n\n\n","id":"210f21884a3581064f4d181bc10e4088b5161216ddc7088f5c39e570240687b3","sig":"a04a697e6b3a53a1df4e3d7d06da2483f07d45c97a939076bd8b28216425a854af5f7e463542adbed9b71af68d36d7e0296be0048c3156a51ed0defa685a5171"}]
PASSED - Concurrent access handled safely
Testing Concurrent CLOSE operations...
PASSED - Concurrent access handled safely
=== Malformed JSON Memory Tests ===
Testing Unclosed JSON object... UNCERTAIN - Expected error but got normal response
Testing Mismatched brackets... UNCERTAIN - Expected error but got normal response
Testing Extra closing brackets... UNCERTAIN - Expected error but got normal response
Testing Null bytes in JSON... UNCERTAIN - Expected error but got normal response
=== Large Message Memory Tests ===
Testing Very large filter array... UNCERTAIN - Expected error but got normal response
Testing Very long search term... UNCERTAIN - Expected error but got normal response
=== Test Results ===
Total tests: 17
Passed: 17
Failed: 0
✓ All memory corruption tests passed!
The relay appears to handle memory safely.
2026-04-01 11:29:36 - \033[0;32m✓ Memory Corruption Tests PASSED\033[0m (Duration: 1s)
2026-04-01 11:29:36 - ==========================================
2026-04-01 11:29:36 - Running Test Suite: Input Validation Tests
2026-04-01 11:29:36 - Description: Comprehensive input boundary testing
2026-04-01 11:29:36 - ==========================================
==========================================
C-Relay Input Validation Test Suite
==========================================
Testing against relay at ws://127.0.0.1:8888
=== Basic Connectivity Test ===
Testing Basic connectivity... PASSED - Input accepted correctly
=== Message Type Validation ===
Testing Invalid message type - string... PASSED - Invalid input properly rejected
Testing Invalid message type - number... PASSED - Invalid input properly rejected
Testing Invalid message type - null... PASSED - Invalid input properly rejected
Testing Invalid message type - object... PASSED - Invalid input properly rejected
Testing Empty message type... PASSED - Invalid input properly rejected
Testing Very long message type... PASSED - Invalid input properly rejected
=== Message Structure Validation ===
Testing Too few arguments... PASSED - Invalid input properly rejected
Testing Too many arguments... PASSED - Invalid input properly rejected
Testing Non-array message... PASSED - Invalid input properly rejected
Testing Empty array... PASSED - Invalid input properly rejected
Testing Nested arrays incorrectly... PASSED - Invalid input properly rejected
=== Subscription ID Boundary Tests ===
Testing Valid subscription ID... PASSED - Input accepted correctly
Testing Empty subscription ID... PASSED - Invalid input properly rejected
Testing Subscription ID with spaces... PASSED - Invalid input properly rejected
Testing Subscription ID with newlines... PASSED - Invalid input properly rejected
Testing Subscription ID with tabs... PASSED - Invalid input properly rejected
Testing Subscription ID with control chars... PASSED - Invalid input properly rejected
Testing Unicode subscription ID... PASSED - Invalid input properly rejected
Testing Very long subscription ID... PASSED - Invalid input properly rejected
=== Filter Object Validation ===
Testing Valid empty filter... PASSED - Input accepted correctly
Testing Non-object filter... PASSED - Invalid input properly rejected
Testing Null filter... PASSED - Invalid input properly rejected
Testing Array filter... PASSED - Invalid input properly rejected
Testing Filter with invalid keys... PASSED - Input accepted correctly
=== Authors Field Validation ===
Testing Valid authors array... PASSED - Input accepted correctly
Testing Empty authors array... PASSED - Input accepted correctly
Testing Non-array authors... PASSED - Invalid input properly rejected
Testing Invalid hex in authors... PASSED - Invalid input properly rejected
Testing Short pubkey in authors... PASSED - Invalid input properly rejected
=== IDs Field Validation ===
Testing Valid ids array... PASSED - Input accepted correctly
Testing Empty ids array... PASSED - Input accepted correctly
Testing Non-array ids... PASSED - Invalid input properly rejected
=== Kinds Field Validation ===
Testing Valid kinds array... PASSED - Input accepted correctly
Testing Empty kinds array... PASSED - Input accepted correctly
Testing Non-array kinds... PASSED - Invalid input properly rejected
Testing String in kinds... PASSED - Invalid input properly rejected
=== Timestamp Field Validation ===
Testing Valid since timestamp... PASSED - Input accepted correctly
Testing Valid until timestamp... PASSED - Input accepted correctly
Testing String since timestamp... PASSED - Invalid input properly rejected
Testing Negative timestamp... PASSED - Invalid input properly rejected
=== Limit Field Validation ===
Testing Valid limit... PASSED - Input accepted correctly
Testing Zero limit... PASSED - Input accepted correctly
Testing String limit... PASSED - Invalid input properly rejected
Testing Negative limit... PASSED - Invalid input properly rejected
=== Multiple Filters ===
Testing Two valid filters... PASSED - Input accepted correctly
Testing Many filters... PASSED - Input accepted correctly
=== Test Results ===
Total tests: 47
Passed: 47
Failed: 0
✓ All input validation tests passed!
The relay properly validates input.
2026-04-01 11:29:38 - \033[0;32m✓ Input Validation Tests PASSED\033[0m (Duration: 2s)
2026-04-01 11:29:38 -
2026-04-01 11:29:38 - \033[0;34m=== PERFORMANCE TEST SUITES ===\033[0m
2026-04-01 11:29:38 - ==========================================
2026-04-01 11:29:38 - Running Test Suite: Subscription Limit Tests
2026-04-01 11:29:38 - Description: Subscription limit enforcement testing
2026-04-01 11:29:38 - ==========================================
=== Subscription Limit Test ===
[INFO] Testing relay at: ws://127.0.0.1:8888
[INFO] Note: This test assumes default subscription limits (max 25 per client)
=== Test 1: Basic Connectivity ===
[INFO] Testing basic WebSocket connection...
[PASS] Basic connectivity works
=== Test 2: Subscription Limit Enforcement ===
[INFO] Testing subscription limits by creating multiple subscriptions...
[INFO] Creating multiple subscriptions within a single connection...
[INFO] Hit subscription limit at subscription 2
[PASS] Subscription limit enforcement working (limit hit after 1 subscriptions)
=== Test Complete ===
2026-04-01 11:29:38 - \033[0;32m✓ Subscription Limit Tests PASSED\033[0m (Duration: 0s)
2026-04-01 11:29:38 - ==========================================
2026-04-01 11:29:38 - Running Test Suite: Load Testing
2026-04-01 11:29:38 - Description: High concurrent connection testing
2026-04-01 11:29:38 - ==========================================
==========================================
C-Relay Load Testing Suite
==========================================
Testing against relay at ws://127.0.0.1:8888
=== Basic Connectivity Test ===
✓ Relay is accessible
==========================================
Load Test: Light Load Test
Description: Basic load test with moderate concurrent connections
Concurrent clients: 10
Messages per client: 5
==========================================
Launching 10 clients...
All clients completed. Processing results...
=== Load Test Results ===
Test duration: 1s
Total connections attempted: 10
Successful connections: 10
Failed connections: 0
Connection success rate: 100%
Messages expected: 50
Messages sent: 50
Messages received: 100
✓ EXCELLENT: High connection success rate
Checking relay responsiveness... ✓ Relay is still responsive
==========================================
Load Test: Medium Load Test
Description: Moderate load test with higher concurrency
Concurrent clients: 25
Messages per client: 10
==========================================
Launching 25 clients...
All clients completed. Processing results...
=== Load Test Results ===
Test duration: 4s
Total connections attempted: 35
Successful connections: 25
Failed connections: 0
Connection success rate: 71%
Messages expected: 250
Messages sent: 250
Messages received: 500
✗ POOR: Low connection success rate
Checking relay responsiveness... ✓ Relay is still responsive
==========================================
Load Test: Heavy Load Test
Description: Heavy load test with high concurrency
Concurrent clients: 50
Messages per client: 20
==========================================
Launching 50 clients...
All clients completed. Processing results...
=== Load Test Results ===
Test duration: 14s
Total connections attempted: 85
Successful connections: 50
Failed connections: 0
Connection success rate: 58%
Messages expected: 1000
Messages sent: 1000
Messages received: 2000
✗ POOR: Low connection success rate
Checking relay responsiveness... ✓ Relay is still responsive
==========================================
Load Test: Stress Test
Description: Maximum load test to find breaking point
Concurrent clients: 100
Messages per client: 50
==========================================
Launching 100 clients...
All clients completed. Processing results...
=== Load Test Results ===
Test duration: 64s
Total connections attempted: 185
Successful connections: 100
Failed connections: 0
Connection success rate: 54%
Messages expected: 5000
Messages sent: 5000
Messages received: 7500
✗ POOR: Low connection success rate
Checking relay responsiveness... ✓ Relay is still responsive
==========================================
Load Testing Complete
==========================================
All load tests completed. Check individual test results above.
If any tests failed, the relay may need optimization or have resource limits.
2026-04-01 11:31:03 - \033[0;32m✓ Load Testing PASSED\033[0m (Duration: 85s)
2026-04-01 11:31:03 - ==========================================
2026-04-01 11:31:03 - Running Test Suite: Stress Testing
2026-04-01 11:31:03 - Description: Resource usage and stability testing
2026-04-01 11:31:03 - ==========================================
2026-04-01 11:31:03 - \033[0;31mERROR: Test script stress_tests.sh not found\033[0m
+183
View File
@@ -0,0 +1,183 @@
#!/bin/bash
# Thread-level CPU profiler for c-relay 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
set -euo pipefail
REMOTE_HOST="${REMOTE_HOST:-ubuntu@laantungir.com}"
DURATION="${DURATION:-600}" # seconds (default 10 minutes)
INTERVAL="${INTERVAL:-10}" # seconds between samples
OUTDIR_BASE="${OUTDIR_BASE:-thread_profile_runs}"
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}"
ENABLE_PERF="${ENABLE_PERF:-1}" # 1=run perf, 0=skip
mkdir -p "${RUN_DIR}"
echo "=========================================="
echo "C-Relay Thread CPU Profiler"
echo "=========================================="
echo "Remote host : ${REMOTE_HOST}"
echo "Duration : ${DURATION}s"
echo "Interval : ${INTERVAL}s"
echo "Run dir : ${RUN_DIR}"
echo "Perf record : ${ENABLE_PERF}"
echo ""
ssh "${REMOTE_HOST}" "bash -s" <<EOF
set -euo pipefail
REMOTE_DIR="${REMOTE_DIR}"
DURATION="${DURATION}"
INTERVAL="${INTERVAL}"
ENABLE_PERF="${ENABLE_PERF}"
mkdir -p "\${REMOTE_DIR}"
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)
if [ -z "\${PID}" ]; then
echo "ERROR: c_relay process not found (checked by cmdline pattern)" | tee -a "\${LOG_FILE}"
exit 1
fi
echo "Profiling PID: \${PID}" | tee -a "\${LOG_FILE}"
CLK_TCK=\$(getconf CLK_TCK)
echo "CLK_TCK=\${CLK_TCK}" | tee -a "\${LOG_FILE}"
echo "timestamp,tid,thread_name,cpu_pct,delta_ticks,total_ticks" > "\${CSV_FILE}"
# Start perf in background (if enabled and available)
PERF_PID=""
if [ "\${ENABLE_PERF}" = "1" ]; then
if command -v perf >/dev/null 2>&1; then
if sudo -n true >/dev/null 2>&1; then
echo "Starting perf record in background..." | tee -a "\${LOG_FILE}"
sudo perf record -g -p "\${PID}" -o "\${PERF_FILE}" -- sleep "\${DURATION}" >/dev/null 2>&1 &
PERF_PID=\$!
PERF_OWNER="\$(id -un)"
echo "perf_pid=\${PERF_PID}" | tee -a "\${LOG_FILE}"
else
echo "Skipping perf: sudo -n not available" | tee -a "\${LOG_FILE}"
fi
else
echo "Skipping perf: command not found" | tee -a "\${LOG_FILE}"
fi
fi
# Capture previous totals by tid
declare -A PREV_TOTAL
capture_totals() {
local pid="\$1"
for statf in /proc/\${pid}/task/*/stat; do
[ -f "\${statf}" ] || continue
local tid
tid=\${statf%/stat}
tid=\${tid##*/}
local total
total=\$(awk '{print \$14+\$15}' "\${statf}" 2>/dev/null || echo 0)
PREV_TOTAL[\${tid}]="\${total}"
done
}
capture_totals "\${PID}"
START_TS=\$(date +%s)
END_TS=\$((START_TS + DURATION))
while [ \$(date +%s) -lt \${END_TS} ]; do
NOW=\$(date +%s)
for statf in /proc/\${PID}/task/*/stat; do
[ -f "\${statf}" ] || continue
tid=\${statf%/stat}
tid=\${tid##*/}
total=\$(awk '{print \$14+\$15}' "\${statf}" 2>/dev/null || echo 0)
prev=\${PREV_TOTAL[\${tid}]:-\${total}}
delta=\$((total - prev))
if [ \${delta} -lt 0 ]; then
delta=0
fi
PREV_TOTAL[\${tid}]="\${total}"
name_file="/proc/\${PID}/task/\${tid}/comm"
if [ -f "\${name_file}" ]; then
tname=\$(tr -d '\n' < "\${name_file}")
else
tname="unknown"
fi
cpu_pct=\$(awk -v d="\${delta}" -v hz="\${CLK_TCK}" -v iv="\${INTERVAL}" 'BEGIN { printf "%.2f", (d / hz) * 100.0 / iv }')
echo "\${NOW},\${tid},\${tname},\${cpu_pct},\${delta},\${total}" >> "\${CSV_FILE}"
done
sleep "\${INTERVAL}"
done
if [ -n "\${PERF_PID}" ]; then
wait "\${PERF_PID}" || true
if [ -f "\${PERF_FILE}" ]; then
sudo perf report --stdio -i "\${PERF_FILE}" --sort=comm,symbol --no-children -n > "\${REMOTE_DIR}/perf_report.txt" 2>/dev/null || true
sudo chown "\${PERF_OWNER}:\${PERF_OWNER}" "\${PERF_FILE}" "\${REMOTE_DIR}/perf_report.txt" 2>/dev/null || true
fi
fi
echo "Done. Artifacts in \${REMOTE_DIR}" | tee -a "\${LOG_FILE}"
EOF
echo "Fetching artifacts..."
scp -q "${REMOTE_HOST}:${REMOTE_DIR}/thread_samples.csv" "${LOCAL_CSV}"
scp -q "${REMOTE_HOST}:${REMOTE_DIR}/run.log" "${RUN_DIR}/run.log" || true
scp -q "${REMOTE_HOST}:${REMOTE_DIR}/perf.data" "${RUN_DIR}/perf.data" || true
scp -q "${REMOTE_HOST}:${REMOTE_DIR}/perf_report.txt" "${RUN_DIR}/perf_report.txt" || true
echo "Generating summary..."
{
echo "=========================================="
echo "Thread CPU Summary (avg + max over run)"
echo "=========================================="
echo "Run timestamp (UTC): ${RUN_TS}"
echo "Duration: ${DURATION}s, Interval: ${INTERVAL}s"
echo ""
awk -F, '
NR==1 {next}
{
key=$2"|"$3;
cpu=$4+0;
sum[key]+=cpu;
cnt[key]++;
if (cpu > max[key]) max[key]=cpu;
}
END {
printf("%-10s %-18s %12s %12s\n", "TID", "THREAD", "AVG_CPU%", "MAX_CPU%");
for (k in sum) {
split(k, a, "|");
avg=sum[k]/cnt[k];
printf("%-10s %-18s %12.2f %12.2f\n", a[1], a[2], avg, max[k]);
}
}
' "${LOCAL_CSV}" | sort -k3,3nr
} | tee "${LOCAL_REPORT}"
echo ""
echo "Completed. Files:"
echo " ${LOCAL_CSV}"
echo " ${LOCAL_REPORT}"
if [ -f "${RUN_DIR}/perf_report.txt" ]; then
echo " ${RUN_DIR}/perf_report.txt"
fi
echo ""
echo "Top 20 summary:"
head -n 22 "${LOCAL_REPORT}"
Binary file not shown.
File diff suppressed because it is too large Load Diff
@@ -0,0 +1,5 @@
Profiling PID: 2224084
CLK_TCK=100
Starting perf record in background...
perf_pid=2224152
Done. Artifacts in /tmp/c_relay_thread_profile_20260402_002056
@@ -0,0 +1,241 @@
timestamp,tid,thread_name,cpu_pct,delta_ticks,total_ticks
1775089257,2224084,lws-main,0.00,0,17
1775089257,2224086,db-read-1,0.00,0,0
1775089257,2224087,db-write,0.00,0,0
1775089257,2224088,event-worker,0.00,0,0
1775089262,2224084,lws-main,0.00,0,17
1775089262,2224086,db-read-1,0.20,1,1
1775089262,2224087,db-write,10.40,52,52
1775089262,2224088,event-worker,0.00,0,0
1775089267,2224084,lws-main,0.20,1,18
1775089267,2224086,db-read-1,0.20,1,2
1775089267,2224087,db-write,2.60,13,65
1775089267,2224088,event-worker,0.00,0,0
1775089272,2224084,lws-main,0.00,0,18
1775089272,2224086,db-read-1,0.40,2,4
1775089272,2224087,db-write,0.20,1,66
1775089272,2224088,event-worker,0.00,0,0
1775089277,2224084,lws-main,0.00,0,18
1775089277,2224086,db-read-1,0.20,1,5
1775089277,2224087,db-write,96.00,480,546
1775089277,2224088,event-worker,0.00,0,0
1775089282,2224084,lws-main,0.60,3,21
1775089282,2224086,db-read-1,1.20,6,11
1775089282,2224087,db-write,100.20,501,1047
1775089282,2224088,event-worker,0.00,0,0
1775089287,2224084,lws-main,1.00,5,26
1775089287,2224086,db-read-1,1.00,5,16
1775089287,2224087,db-write,100.00,500,1547
1775089287,2224088,event-worker,0.00,0,0
1775089292,2224084,lws-main,1.40,7,33
1775089292,2224086,db-read-1,10.20,51,67
1775089292,2224087,db-write,100.40,502,2049
1775089292,2224088,event-worker,0.00,0,0
1775089297,2224084,lws-main,0.40,2,35
1775089297,2224086,db-read-1,1.20,6,73
1775089297,2224087,db-write,100.40,502,2551
1775089297,2224088,event-worker,0.00,0,0
1775089302,2224084,lws-main,0.00,0,35
1775089302,2224086,db-read-1,0.20,1,74
1775089302,2224087,db-write,100.20,501,3052
1775089302,2224088,event-worker,0.00,0,0
1775089307,2224084,lws-main,0.20,1,36
1775089307,2224086,db-read-1,0.00,0,74
1775089307,2224087,db-write,100.20,501,3553
1775089307,2224088,event-worker,0.00,0,0
1775089312,2224084,lws-main,0.20,1,37
1775089312,2224086,db-read-1,1.00,5,79
1775089312,2224087,db-write,100.40,502,4055
1775089312,2224088,event-worker,0.00,0,0
1775089317,2224084,lws-main,0.00,0,37
1775089317,2224086,db-read-1,0.40,2,81
1775089317,2224087,db-write,100.20,501,4556
1775089317,2224088,event-worker,0.00,0,0
1775089322,2224084,lws-main,0.40,2,39
1775089322,2224086,db-read-1,0.20,1,82
1775089322,2224087,db-write,99.80,499,5055
1775089322,2224088,event-worker,0.00,0,0
1775089327,2224084,lws-main,0.00,0,39
1775089327,2224086,db-read-1,0.00,0,82
1775089327,2224087,db-write,100.20,501,5556
1775089327,2224088,event-worker,0.00,0,0
1775089332,2224084,lws-main,0.40,2,41
1775089332,2224086,db-read-1,0.20,1,83
1775089332,2224087,db-write,100.00,500,6056
1775089332,2224088,event-worker,0.00,0,0
1775089337,2224084,lws-main,0.00,0,41
1775089337,2224086,db-read-1,0.20,1,84
1775089337,2224087,db-write,100.20,501,6557
1775089337,2224088,event-worker,0.00,0,0
1775089343,2224084,lws-main,0.40,2,43
1775089343,2224086,db-read-1,0.20,1,85
1775089343,2224087,db-write,100.20,501,7058
1775089343,2224088,event-worker,0.00,0,0
1775089348,2224084,lws-main,0.00,0,43
1775089348,2224086,db-read-1,0.20,1,86
1775089348,2224087,db-write,100.00,500,7558
1775089348,2224088,event-worker,0.00,0,0
1775089353,2224084,lws-main,0.40,2,45
1775089353,2224086,db-read-1,0.20,1,87
1775089353,2224087,db-write,100.20,501,8059
1775089353,2224088,event-worker,0.00,0,0
1775089358,2224084,lws-main,0.20,1,46
1775089358,2224086,db-read-1,0.20,1,88
1775089358,2224087,db-write,100.20,501,8560
1775089358,2224088,event-worker,0.00,0,0
1775089363,2224084,lws-main,0.80,4,50
1775089363,2224086,db-read-1,0.80,4,92
1775089363,2224087,db-write,100.20,501,9061
1775089363,2224088,event-worker,0.00,0,0
1775089368,2224084,lws-main,0.20,1,51
1775089368,2224086,db-read-1,0.60,3,95
1775089368,2224087,db-write,100.20,501,9562
1775089368,2224088,event-worker,0.00,0,0
1775089373,2224084,lws-main,0.40,2,53
1775089373,2224086,db-read-1,0.40,2,97
1775089373,2224087,db-write,100.00,500,10062
1775089373,2224088,event-worker,0.00,0,0
1775089378,2224084,lws-main,0.00,0,53
1775089378,2224086,db-read-1,0.20,1,98
1775089378,2224087,db-write,67.40,337,10399
1775089378,2224088,event-worker,0.00,0,0
1775089383,2224084,lws-main,0.20,1,54
1775089383,2224086,db-read-1,0.00,0,98
1775089383,2224087,db-write,99.80,499,10898
1775089383,2224088,event-worker,0.00,0,0
1775089388,2224084,lws-main,0.20,1,55
1775089388,2224086,db-read-1,0.20,1,99
1775089388,2224087,db-write,99.80,499,11397
1775089388,2224088,event-worker,0.00,0,0
1775089393,2224084,lws-main,0.20,1,56
1775089393,2224086,db-read-1,0.40,2,101
1775089393,2224087,db-write,100.20,501,11898
1775089393,2224088,event-worker,0.00,0,0
1775089398,2224084,lws-main,0.20,1,57
1775089398,2224086,db-read-1,0.00,0,101
1775089398,2224087,db-write,44.00,220,12118
1775089398,2224088,event-worker,0.00,0,0
1775089403,2224084,lws-main,0.20,1,58
1775089403,2224086,db-read-1,0.00,0,101
1775089403,2224087,db-write,31.00,155,12273
1775089403,2224088,event-worker,0.00,0,0
1775089408,2224084,lws-main,0.00,0,58
1775089408,2224086,db-read-1,0.00,0,101
1775089408,2224087,db-write,8.80,44,12317
1775089408,2224088,event-worker,0.00,0,0
1775089413,2224084,lws-main,0.20,1,59
1775089413,2224086,db-read-1,0.40,2,103
1775089413,2224087,db-write,80.40,402,12719
1775089413,2224088,event-worker,0.20,1,1
1775089418,2224084,lws-main,0.20,1,60
1775089418,2224086,db-read-1,0.00,0,103
1775089418,2224087,db-write,37.20,186,12905
1775089418,2224088,event-worker,0.00,0,1
1775089423,2224084,lws-main,0.20,1,61
1775089423,2224086,db-read-1,0.20,1,104
1775089423,2224087,db-write,43.00,215,13120
1775089423,2224088,event-worker,0.00,0,1
1775089428,2224084,lws-main,0.00,0,61
1775089428,2224086,db-read-1,0.00,0,104
1775089428,2224087,db-write,14.80,74,13194
1775089428,2224088,event-worker,0.00,0,1
1775089433,2224084,lws-main,0.20,1,62
1775089433,2224086,db-read-1,0.20,1,105
1775089433,2224087,db-write,39.40,197,13391
1775089433,2224088,event-worker,0.00,0,1
1775089438,2224084,lws-main,0.20,1,63
1775089438,2224086,db-read-1,0.80,4,109
1775089438,2224087,db-write,100.20,501,13892
1775089438,2224088,event-worker,0.00,0,1
1775089443,2224084,lws-main,0.20,1,64
1775089443,2224086,db-read-1,0.00,0,109
1775089443,2224087,db-write,100.40,502,14394
1775089443,2224088,event-worker,0.00,0,1
1775089448,2224084,lws-main,0.20,1,65
1775089448,2224086,db-read-1,0.00,0,109
1775089448,2224087,db-write,100.00,500,14894
1775089448,2224088,event-worker,0.00,0,1
1775089453,2224084,lws-main,0.00,0,65
1775089453,2224086,db-read-1,0.40,2,111
1775089453,2224087,db-write,100.20,501,15395
1775089453,2224088,event-worker,0.00,0,1
1775089458,2224084,lws-main,0.20,1,66
1775089458,2224086,db-read-1,0.60,3,114
1775089458,2224087,db-write,84.00,420,15815
1775089458,2224088,event-worker,0.00,0,1
1775089463,2224084,lws-main,0.00,0,66
1775089463,2224086,db-read-1,0.00,0,114
1775089463,2224087,db-write,32.00,160,15975
1775089463,2224088,event-worker,0.00,0,1
1775089468,2224084,lws-main,0.20,1,67
1775089468,2224086,db-read-1,0.20,1,115
1775089468,2224087,db-write,7.80,39,16014
1775089468,2224088,event-worker,0.00,0,1
1775089473,2224084,lws-main,0.20,1,68
1775089473,2224086,db-read-1,0.20,1,116
1775089473,2224087,db-write,42.60,213,16227
1775089473,2224088,event-worker,0.00,0,1
1775089478,2224084,lws-main,0.20,1,69
1775089478,2224086,db-read-1,0.20,1,117
1775089478,2224087,db-write,63.00,315,16542
1775089478,2224088,event-worker,0.00,0,1
1775089483,2224084,lws-main,0.00,0,69
1775089483,2224086,db-read-1,0.20,1,118
1775089483,2224087,db-write,50.80,254,16796
1775089483,2224088,event-worker,0.00,0,1
1775089488,2224084,lws-main,0.20,1,70
1775089488,2224086,db-read-1,0.20,1,119
1775089488,2224087,db-write,93.80,469,17265
1775089488,2224088,event-worker,0.00,0,1
1775089493,2224084,lws-main,0.00,0,70
1775089493,2224086,db-read-1,0.00,0,119
1775089493,2224087,db-write,100.40,502,17767
1775089493,2224088,event-worker,0.00,0,1
1775089498,2224084,lws-main,0.20,1,71
1775089498,2224086,db-read-1,0.20,1,120
1775089498,2224087,db-write,100.40,502,18269
1775089498,2224088,event-worker,0.00,0,1
1775089503,2224084,lws-main,0.20,1,72
1775089503,2224086,db-read-1,0.00,0,120
1775089503,2224087,db-write,100.20,501,18770
1775089503,2224088,event-worker,0.00,0,1
1775089508,2224084,lws-main,0.00,0,72
1775089508,2224086,db-read-1,0.60,3,123
1775089508,2224087,db-write,100.20,501,19271
1775089508,2224088,event-worker,0.00,0,1
1775089513,2224084,lws-main,0.20,1,73
1775089513,2224086,db-read-1,0.20,1,124
1775089513,2224087,db-write,100.20,501,19772
1775089513,2224088,event-worker,0.00,0,1
1775089518,2224084,lws-main,0.20,1,74
1775089518,2224086,db-read-1,0.40,2,126
1775089518,2224087,db-write,75.00,375,20147
1775089518,2224088,event-worker,0.00,0,1
1775089523,2224084,lws-main,0.00,0,74
1775089523,2224086,db-read-1,0.40,2,128
1775089523,2224087,db-write,43.40,217,20364
1775089523,2224088,event-worker,0.00,0,1
1775089528,2224084,lws-main,4.60,23,97
1775089528,2224086,db-read-1,4.00,20,148
1775089528,2224087,db-write,40.80,204,20568
1775089528,2224088,event-worker,0.20,1,2
1775089533,2224084,lws-main,3.40,17,114
1775089533,2224086,db-read-1,3.20,16,164
1775089533,2224087,db-write,98.80,494,21062
1775089533,2224088,event-worker,0.20,1,3
1775089538,2224084,lws-main,0.20,1,115
1775089538,2224086,db-read-1,0.40,2,166
1775089538,2224087,db-write,100.40,502,21564
1775089538,2224088,event-worker,0.00,0,3
1775089543,2224084,lws-main,0.20,1,116
1775089543,2224086,db-read-1,0.20,1,167
1775089543,2224087,db-write,100.20,501,22065
1775089543,2224088,event-worker,0.00,0,3
1775089548,2224084,lws-main,0.00,0,116
1775089548,2224086,db-read-1,0.20,1,168
1775089548,2224087,db-write,100.40,502,22567
1775089548,2224088,event-worker,0.00,0,3
1775089553,2224084,lws-main,0.20,1,117
1775089553,2224086,db-read-1,0.20,1,169
1775089553,2224087,db-write,100.40,502,23069
1775089553,2224088,event-worker,0.00,0,3
1 timestamp tid thread_name cpu_pct delta_ticks total_ticks
2 1775089257 2224084 lws-main 0.00 0 17
3 1775089257 2224086 db-read-1 0.00 0 0
4 1775089257 2224087 db-write 0.00 0 0
5 1775089257 2224088 event-worker 0.00 0 0
6 1775089262 2224084 lws-main 0.00 0 17
7 1775089262 2224086 db-read-1 0.20 1 1
8 1775089262 2224087 db-write 10.40 52 52
9 1775089262 2224088 event-worker 0.00 0 0
10 1775089267 2224084 lws-main 0.20 1 18
11 1775089267 2224086 db-read-1 0.20 1 2
12 1775089267 2224087 db-write 2.60 13 65
13 1775089267 2224088 event-worker 0.00 0 0
14 1775089272 2224084 lws-main 0.00 0 18
15 1775089272 2224086 db-read-1 0.40 2 4
16 1775089272 2224087 db-write 0.20 1 66
17 1775089272 2224088 event-worker 0.00 0 0
18 1775089277 2224084 lws-main 0.00 0 18
19 1775089277 2224086 db-read-1 0.20 1 5
20 1775089277 2224087 db-write 96.00 480 546
21 1775089277 2224088 event-worker 0.00 0 0
22 1775089282 2224084 lws-main 0.60 3 21
23 1775089282 2224086 db-read-1 1.20 6 11
24 1775089282 2224087 db-write 100.20 501 1047
25 1775089282 2224088 event-worker 0.00 0 0
26 1775089287 2224084 lws-main 1.00 5 26
27 1775089287 2224086 db-read-1 1.00 5 16
28 1775089287 2224087 db-write 100.00 500 1547
29 1775089287 2224088 event-worker 0.00 0 0
30 1775089292 2224084 lws-main 1.40 7 33
31 1775089292 2224086 db-read-1 10.20 51 67
32 1775089292 2224087 db-write 100.40 502 2049
33 1775089292 2224088 event-worker 0.00 0 0
34 1775089297 2224084 lws-main 0.40 2 35
35 1775089297 2224086 db-read-1 1.20 6 73
36 1775089297 2224087 db-write 100.40 502 2551
37 1775089297 2224088 event-worker 0.00 0 0
38 1775089302 2224084 lws-main 0.00 0 35
39 1775089302 2224086 db-read-1 0.20 1 74
40 1775089302 2224087 db-write 100.20 501 3052
41 1775089302 2224088 event-worker 0.00 0 0
42 1775089307 2224084 lws-main 0.20 1 36
43 1775089307 2224086 db-read-1 0.00 0 74
44 1775089307 2224087 db-write 100.20 501 3553
45 1775089307 2224088 event-worker 0.00 0 0
46 1775089312 2224084 lws-main 0.20 1 37
47 1775089312 2224086 db-read-1 1.00 5 79
48 1775089312 2224087 db-write 100.40 502 4055
49 1775089312 2224088 event-worker 0.00 0 0
50 1775089317 2224084 lws-main 0.00 0 37
51 1775089317 2224086 db-read-1 0.40 2 81
52 1775089317 2224087 db-write 100.20 501 4556
53 1775089317 2224088 event-worker 0.00 0 0
54 1775089322 2224084 lws-main 0.40 2 39
55 1775089322 2224086 db-read-1 0.20 1 82
56 1775089322 2224087 db-write 99.80 499 5055
57 1775089322 2224088 event-worker 0.00 0 0
58 1775089327 2224084 lws-main 0.00 0 39
59 1775089327 2224086 db-read-1 0.00 0 82
60 1775089327 2224087 db-write 100.20 501 5556
61 1775089327 2224088 event-worker 0.00 0 0
62 1775089332 2224084 lws-main 0.40 2 41
63 1775089332 2224086 db-read-1 0.20 1 83
64 1775089332 2224087 db-write 100.00 500 6056
65 1775089332 2224088 event-worker 0.00 0 0
66 1775089337 2224084 lws-main 0.00 0 41
67 1775089337 2224086 db-read-1 0.20 1 84
68 1775089337 2224087 db-write 100.20 501 6557
69 1775089337 2224088 event-worker 0.00 0 0
70 1775089343 2224084 lws-main 0.40 2 43
71 1775089343 2224086 db-read-1 0.20 1 85
72 1775089343 2224087 db-write 100.20 501 7058
73 1775089343 2224088 event-worker 0.00 0 0
74 1775089348 2224084 lws-main 0.00 0 43
75 1775089348 2224086 db-read-1 0.20 1 86
76 1775089348 2224087 db-write 100.00 500 7558
77 1775089348 2224088 event-worker 0.00 0 0
78 1775089353 2224084 lws-main 0.40 2 45
79 1775089353 2224086 db-read-1 0.20 1 87
80 1775089353 2224087 db-write 100.20 501 8059
81 1775089353 2224088 event-worker 0.00 0 0
82 1775089358 2224084 lws-main 0.20 1 46
83 1775089358 2224086 db-read-1 0.20 1 88
84 1775089358 2224087 db-write 100.20 501 8560
85 1775089358 2224088 event-worker 0.00 0 0
86 1775089363 2224084 lws-main 0.80 4 50
87 1775089363 2224086 db-read-1 0.80 4 92
88 1775089363 2224087 db-write 100.20 501 9061
89 1775089363 2224088 event-worker 0.00 0 0
90 1775089368 2224084 lws-main 0.20 1 51
91 1775089368 2224086 db-read-1 0.60 3 95
92 1775089368 2224087 db-write 100.20 501 9562
93 1775089368 2224088 event-worker 0.00 0 0
94 1775089373 2224084 lws-main 0.40 2 53
95 1775089373 2224086 db-read-1 0.40 2 97
96 1775089373 2224087 db-write 100.00 500 10062
97 1775089373 2224088 event-worker 0.00 0 0
98 1775089378 2224084 lws-main 0.00 0 53
99 1775089378 2224086 db-read-1 0.20 1 98
100 1775089378 2224087 db-write 67.40 337 10399
101 1775089378 2224088 event-worker 0.00 0 0
102 1775089383 2224084 lws-main 0.20 1 54
103 1775089383 2224086 db-read-1 0.00 0 98
104 1775089383 2224087 db-write 99.80 499 10898
105 1775089383 2224088 event-worker 0.00 0 0
106 1775089388 2224084 lws-main 0.20 1 55
107 1775089388 2224086 db-read-1 0.20 1 99
108 1775089388 2224087 db-write 99.80 499 11397
109 1775089388 2224088 event-worker 0.00 0 0
110 1775089393 2224084 lws-main 0.20 1 56
111 1775089393 2224086 db-read-1 0.40 2 101
112 1775089393 2224087 db-write 100.20 501 11898
113 1775089393 2224088 event-worker 0.00 0 0
114 1775089398 2224084 lws-main 0.20 1 57
115 1775089398 2224086 db-read-1 0.00 0 101
116 1775089398 2224087 db-write 44.00 220 12118
117 1775089398 2224088 event-worker 0.00 0 0
118 1775089403 2224084 lws-main 0.20 1 58
119 1775089403 2224086 db-read-1 0.00 0 101
120 1775089403 2224087 db-write 31.00 155 12273
121 1775089403 2224088 event-worker 0.00 0 0
122 1775089408 2224084 lws-main 0.00 0 58
123 1775089408 2224086 db-read-1 0.00 0 101
124 1775089408 2224087 db-write 8.80 44 12317
125 1775089408 2224088 event-worker 0.00 0 0
126 1775089413 2224084 lws-main 0.20 1 59
127 1775089413 2224086 db-read-1 0.40 2 103
128 1775089413 2224087 db-write 80.40 402 12719
129 1775089413 2224088 event-worker 0.20 1 1
130 1775089418 2224084 lws-main 0.20 1 60
131 1775089418 2224086 db-read-1 0.00 0 103
132 1775089418 2224087 db-write 37.20 186 12905
133 1775089418 2224088 event-worker 0.00 0 1
134 1775089423 2224084 lws-main 0.20 1 61
135 1775089423 2224086 db-read-1 0.20 1 104
136 1775089423 2224087 db-write 43.00 215 13120
137 1775089423 2224088 event-worker 0.00 0 1
138 1775089428 2224084 lws-main 0.00 0 61
139 1775089428 2224086 db-read-1 0.00 0 104
140 1775089428 2224087 db-write 14.80 74 13194
141 1775089428 2224088 event-worker 0.00 0 1
142 1775089433 2224084 lws-main 0.20 1 62
143 1775089433 2224086 db-read-1 0.20 1 105
144 1775089433 2224087 db-write 39.40 197 13391
145 1775089433 2224088 event-worker 0.00 0 1
146 1775089438 2224084 lws-main 0.20 1 63
147 1775089438 2224086 db-read-1 0.80 4 109
148 1775089438 2224087 db-write 100.20 501 13892
149 1775089438 2224088 event-worker 0.00 0 1
150 1775089443 2224084 lws-main 0.20 1 64
151 1775089443 2224086 db-read-1 0.00 0 109
152 1775089443 2224087 db-write 100.40 502 14394
153 1775089443 2224088 event-worker 0.00 0 1
154 1775089448 2224084 lws-main 0.20 1 65
155 1775089448 2224086 db-read-1 0.00 0 109
156 1775089448 2224087 db-write 100.00 500 14894
157 1775089448 2224088 event-worker 0.00 0 1
158 1775089453 2224084 lws-main 0.00 0 65
159 1775089453 2224086 db-read-1 0.40 2 111
160 1775089453 2224087 db-write 100.20 501 15395
161 1775089453 2224088 event-worker 0.00 0 1
162 1775089458 2224084 lws-main 0.20 1 66
163 1775089458 2224086 db-read-1 0.60 3 114
164 1775089458 2224087 db-write 84.00 420 15815
165 1775089458 2224088 event-worker 0.00 0 1
166 1775089463 2224084 lws-main 0.00 0 66
167 1775089463 2224086 db-read-1 0.00 0 114
168 1775089463 2224087 db-write 32.00 160 15975
169 1775089463 2224088 event-worker 0.00 0 1
170 1775089468 2224084 lws-main 0.20 1 67
171 1775089468 2224086 db-read-1 0.20 1 115
172 1775089468 2224087 db-write 7.80 39 16014
173 1775089468 2224088 event-worker 0.00 0 1
174 1775089473 2224084 lws-main 0.20 1 68
175 1775089473 2224086 db-read-1 0.20 1 116
176 1775089473 2224087 db-write 42.60 213 16227
177 1775089473 2224088 event-worker 0.00 0 1
178 1775089478 2224084 lws-main 0.20 1 69
179 1775089478 2224086 db-read-1 0.20 1 117
180 1775089478 2224087 db-write 63.00 315 16542
181 1775089478 2224088 event-worker 0.00 0 1
182 1775089483 2224084 lws-main 0.00 0 69
183 1775089483 2224086 db-read-1 0.20 1 118
184 1775089483 2224087 db-write 50.80 254 16796
185 1775089483 2224088 event-worker 0.00 0 1
186 1775089488 2224084 lws-main 0.20 1 70
187 1775089488 2224086 db-read-1 0.20 1 119
188 1775089488 2224087 db-write 93.80 469 17265
189 1775089488 2224088 event-worker 0.00 0 1
190 1775089493 2224084 lws-main 0.00 0 70
191 1775089493 2224086 db-read-1 0.00 0 119
192 1775089493 2224087 db-write 100.40 502 17767
193 1775089493 2224088 event-worker 0.00 0 1
194 1775089498 2224084 lws-main 0.20 1 71
195 1775089498 2224086 db-read-1 0.20 1 120
196 1775089498 2224087 db-write 100.40 502 18269
197 1775089498 2224088 event-worker 0.00 0 1
198 1775089503 2224084 lws-main 0.20 1 72
199 1775089503 2224086 db-read-1 0.00 0 120
200 1775089503 2224087 db-write 100.20 501 18770
201 1775089503 2224088 event-worker 0.00 0 1
202 1775089508 2224084 lws-main 0.00 0 72
203 1775089508 2224086 db-read-1 0.60 3 123
204 1775089508 2224087 db-write 100.20 501 19271
205 1775089508 2224088 event-worker 0.00 0 1
206 1775089513 2224084 lws-main 0.20 1 73
207 1775089513 2224086 db-read-1 0.20 1 124
208 1775089513 2224087 db-write 100.20 501 19772
209 1775089513 2224088 event-worker 0.00 0 1
210 1775089518 2224084 lws-main 0.20 1 74
211 1775089518 2224086 db-read-1 0.40 2 126
212 1775089518 2224087 db-write 75.00 375 20147
213 1775089518 2224088 event-worker 0.00 0 1
214 1775089523 2224084 lws-main 0.00 0 74
215 1775089523 2224086 db-read-1 0.40 2 128
216 1775089523 2224087 db-write 43.40 217 20364
217 1775089523 2224088 event-worker 0.00 0 1
218 1775089528 2224084 lws-main 4.60 23 97
219 1775089528 2224086 db-read-1 4.00 20 148
220 1775089528 2224087 db-write 40.80 204 20568
221 1775089528 2224088 event-worker 0.20 1 2
222 1775089533 2224084 lws-main 3.40 17 114
223 1775089533 2224086 db-read-1 3.20 16 164
224 1775089533 2224087 db-write 98.80 494 21062
225 1775089533 2224088 event-worker 0.20 1 3
226 1775089538 2224084 lws-main 0.20 1 115
227 1775089538 2224086 db-read-1 0.40 2 166
228 1775089538 2224087 db-write 100.40 502 21564
229 1775089538 2224088 event-worker 0.00 0 3
230 1775089543 2224084 lws-main 0.20 1 116
231 1775089543 2224086 db-read-1 0.20 1 167
232 1775089543 2224087 db-write 100.20 501 22065
233 1775089543 2224088 event-worker 0.00 0 3
234 1775089548 2224084 lws-main 0.00 0 116
235 1775089548 2224086 db-read-1 0.20 1 168
236 1775089548 2224087 db-write 100.40 502 22567
237 1775089548 2224088 event-worker 0.00 0 3
238 1775089553 2224084 lws-main 0.20 1 117
239 1775089553 2224086 db-read-1 0.20 1 169
240 1775089553 2224087 db-write 100.40 502 23069
241 1775089553 2224088 event-worker 0.00 0 3
@@ -0,0 +1,11 @@
==========================================
Thread CPU Summary (avg + max over run)
==========================================
Run timestamp (UTC): 20260402_002056
Duration: 300s, Interval: 5s
2224087 db-write 76.90 100.40
2224086 db-read-1 0.56 10.20
2224084 lws-main 0.33 4.60
2224088 event-worker 0.01 0.20
TID THREAD AVG_CPU% MAX_CPU%
Binary file not shown.
File diff suppressed because it is too large Load Diff
@@ -0,0 +1,5 @@
Profiling PID: 2242661
CLK_TCK=100
Starting perf record in background...
perf_pid=2242812
Done. Artifacts in /tmp/c_relay_thread_profile_20260402_011703
@@ -0,0 +1,145 @@
timestamp,tid,thread_name,cpu_pct,delta_ticks,total_ticks
1775092623,2242661,lws-main,0.00,0,34
1775092623,2242664,db-read-1,0.00,0,21
1775092623,2242665,db-write,0.40,2,1055
1775092623,2242666,event-worker,0.00,0,0
1775092628,2242661,lws-main,0.00,0,34
1775092628,2242664,db-read-1,0.00,0,21
1775092628,2242665,db-write,92.20,461,1516
1775092628,2242666,event-worker,0.00,0,0
1775092633,2242661,lws-main,0.00,0,34
1775092633,2242664,db-read-1,0.00,0,21
1775092633,2242665,db-write,0.00,0,1516
1775092633,2242666,event-worker,0.00,0,0
1775092638,2242661,lws-main,0.20,1,35
1775092638,2242664,db-read-1,0.00,0,21
1775092638,2242665,db-write,26.80,134,1650
1775092638,2242666,event-worker,0.00,0,0
1775092643,2242661,lws-main,0.00,0,35
1775092643,2242664,db-read-1,0.20,1,22
1775092643,2242665,db-write,13.60,68,1718
1775092643,2242666,event-worker,0.00,0,0
1775092648,2242661,lws-main,0.00,0,35
1775092648,2242664,db-read-1,0.00,0,22
1775092648,2242665,db-write,13.60,68,1786
1775092648,2242666,event-worker,0.00,0,0
1775092654,2242661,lws-main,0.20,1,36
1775092654,2242664,db-read-1,0.40,2,24
1775092654,2242665,db-write,0.00,0,1786
1775092654,2242666,event-worker,0.00,0,0
1775092659,2242661,lws-main,0.00,0,36
1775092659,2242664,db-read-1,0.00,0,24
1775092659,2242665,db-write,0.20,1,1787
1775092659,2242666,event-worker,0.00,0,0
1775092664,2242661,lws-main,1.80,9,45
1775092664,2242664,db-read-1,2.00,10,34
1775092664,2242665,db-write,27.60,138,1925
1775092664,2242666,event-worker,0.20,1,1
1775092669,2242661,lws-main,0.20,1,46
1775092669,2242664,db-read-1,0.20,1,35
1775092669,2242665,db-write,14.00,70,1995
1775092669,2242666,event-worker,0.20,1,2
1775092674,2242661,lws-main,0.40,2,48
1775092674,2242664,db-read-1,0.20,1,36
1775092674,2242665,db-write,34.80,174,2169
1775092674,2242666,event-worker,0.20,1,3
1775092679,2242661,lws-main,0.00,0,48
1775092679,2242664,db-read-1,0.40,2,38
1775092679,2242665,db-write,66.80,334,2503
1775092679,2242666,event-worker,0.00,0,3
1775092684,2242661,lws-main,0.00,0,48
1775092684,2242664,db-read-1,1.00,5,43
1775092684,2242665,db-write,21.60,108,2611
1775092684,2242666,event-worker,0.00,0,3
1775092689,2242661,lws-main,0.40,2,50
1775092689,2242664,db-read-1,0.20,1,44
1775092689,2242665,db-write,54.80,274,2885
1775092689,2242666,event-worker,0.00,0,3
1775092694,2242661,lws-main,0.20,1,51
1775092694,2242664,db-read-1,0.20,1,45
1775092694,2242665,db-write,67.20,336,3221
1775092694,2242666,event-worker,0.00,0,3
1775092699,2242661,lws-main,0.00,0,51
1775092699,2242664,db-read-1,0.60,3,48
1775092699,2242665,db-write,100.20,501,3722
1775092699,2242666,event-worker,0.00,0,3
1775092704,2242661,lws-main,0.40,2,53
1775092704,2242664,db-read-1,0.40,2,50
1775092704,2242665,db-write,93.00,465,4187
1775092704,2242666,event-worker,0.00,0,3
1775092709,2242661,lws-main,0.20,1,54
1775092709,2242664,db-read-1,0.40,2,52
1775092709,2242665,db-write,27.60,138,4325
1775092709,2242666,event-worker,0.00,0,3
1775092714,2242661,lws-main,0.20,1,55
1775092714,2242664,db-read-1,0.40,2,54
1775092714,2242665,db-write,90.20,451,4776
1775092714,2242666,event-worker,0.00,0,3
1775092719,2242661,lws-main,0.20,1,56
1775092719,2242664,db-read-1,0.20,1,55
1775092719,2242665,db-write,100.00,500,5276
1775092719,2242666,event-worker,0.00,0,3
1775092724,2242661,lws-main,0.00,0,56
1775092724,2242664,db-read-1,0.00,0,55
1775092724,2242665,db-write,100.20,501,5777
1775092724,2242666,event-worker,0.00,0,3
1775092729,2242661,lws-main,0.20,1,57
1775092729,2242664,db-read-1,0.20,1,56
1775092729,2242665,db-write,100.20,501,6278
1775092729,2242666,event-worker,0.00,0,3
1775092734,2242661,lws-main,0.20,1,58
1775092734,2242664,db-read-1,0.20,1,57
1775092734,2242665,db-write,100.40,502,6780
1775092734,2242666,event-worker,0.00,0,3
1775092739,2242661,lws-main,0.00,0,58
1775092739,2242664,db-read-1,0.00,0,57
1775092739,2242665,db-write,99.80,499,7279
1775092739,2242666,event-worker,0.00,0,3
1775092744,2242661,lws-main,0.20,1,59
1775092744,2242664,db-read-1,0.00,0,57
1775092744,2242665,db-write,100.60,503,7782
1775092744,2242666,event-worker,0.00,0,3
1775092749,2242661,lws-main,0.00,0,59
1775092749,2242664,db-read-1,0.00,0,57
1775092749,2242665,db-write,100.00,500,8282
1775092749,2242666,event-worker,0.00,0,3
1775092754,2242661,lws-main,0.20,1,60
1775092754,2242664,db-read-1,0.20,1,58
1775092754,2242665,db-write,30.40,152,8434
1775092754,2242666,event-worker,0.00,0,3
1775092759,2242661,lws-main,0.00,0,60
1775092759,2242664,db-read-1,0.00,0,58
1775092759,2242665,db-write,53.40,267,8701
1775092759,2242666,event-worker,0.00,0,3
1775092764,2242661,lws-main,0.40,2,62
1775092764,2242664,db-read-1,0.00,0,58
1775092764,2242665,db-write,50.00,250,8951
1775092764,2242666,event-worker,0.00,0,3
1775092769,2242661,lws-main,0.00,0,62
1775092769,2242664,db-read-1,0.20,1,59
1775092769,2242665,db-write,57.60,288,9239
1775092769,2242666,event-worker,0.00,0,3
1775092774,2242661,lws-main,0.20,1,63
1775092774,2242664,db-read-1,0.20,1,60
1775092774,2242665,db-write,54.40,272,9511
1775092774,2242666,event-worker,0.20,1,4
1775092779,2242661,lws-main,0.00,0,63
1775092779,2242664,db-read-1,0.20,1,61
1775092779,2242665,db-write,49.20,246,9757
1775092779,2242666,event-worker,0.00,0,4
1775092784,2242661,lws-main,0.20,1,64
1775092784,2242664,db-read-1,0.00,0,61
1775092784,2242665,db-write,31.00,155,9912
1775092784,2242666,event-worker,0.00,0,4
1775092789,2242661,lws-main,0.20,1,65
1775092789,2242664,db-read-1,0.00,0,61
1775092789,2242665,db-write,66.80,334,10246
1775092789,2242666,event-worker,0.00,0,4
1775092794,2242661,lws-main,0.00,0,65
1775092794,2242664,db-read-1,0.20,1,62
1775092794,2242665,db-write,40.20,201,10447
1775092794,2242666,event-worker,0.00,0,4
1775092799,2242661,lws-main,0.20,1,66
1775092799,2242664,db-read-1,0.20,1,63
1775092799,2242665,db-write,92.40,462,10909
1775092799,2242666,event-worker,0.00,0,4
1 timestamp tid thread_name cpu_pct delta_ticks total_ticks
2 1775092623 2242661 lws-main 0.00 0 34
3 1775092623 2242664 db-read-1 0.00 0 21
4 1775092623 2242665 db-write 0.40 2 1055
5 1775092623 2242666 event-worker 0.00 0 0
6 1775092628 2242661 lws-main 0.00 0 34
7 1775092628 2242664 db-read-1 0.00 0 21
8 1775092628 2242665 db-write 92.20 461 1516
9 1775092628 2242666 event-worker 0.00 0 0
10 1775092633 2242661 lws-main 0.00 0 34
11 1775092633 2242664 db-read-1 0.00 0 21
12 1775092633 2242665 db-write 0.00 0 1516
13 1775092633 2242666 event-worker 0.00 0 0
14 1775092638 2242661 lws-main 0.20 1 35
15 1775092638 2242664 db-read-1 0.00 0 21
16 1775092638 2242665 db-write 26.80 134 1650
17 1775092638 2242666 event-worker 0.00 0 0
18 1775092643 2242661 lws-main 0.00 0 35
19 1775092643 2242664 db-read-1 0.20 1 22
20 1775092643 2242665 db-write 13.60 68 1718
21 1775092643 2242666 event-worker 0.00 0 0
22 1775092648 2242661 lws-main 0.00 0 35
23 1775092648 2242664 db-read-1 0.00 0 22
24 1775092648 2242665 db-write 13.60 68 1786
25 1775092648 2242666 event-worker 0.00 0 0
26 1775092654 2242661 lws-main 0.20 1 36
27 1775092654 2242664 db-read-1 0.40 2 24
28 1775092654 2242665 db-write 0.00 0 1786
29 1775092654 2242666 event-worker 0.00 0 0
30 1775092659 2242661 lws-main 0.00 0 36
31 1775092659 2242664 db-read-1 0.00 0 24
32 1775092659 2242665 db-write 0.20 1 1787
33 1775092659 2242666 event-worker 0.00 0 0
34 1775092664 2242661 lws-main 1.80 9 45
35 1775092664 2242664 db-read-1 2.00 10 34
36 1775092664 2242665 db-write 27.60 138 1925
37 1775092664 2242666 event-worker 0.20 1 1
38 1775092669 2242661 lws-main 0.20 1 46
39 1775092669 2242664 db-read-1 0.20 1 35
40 1775092669 2242665 db-write 14.00 70 1995
41 1775092669 2242666 event-worker 0.20 1 2
42 1775092674 2242661 lws-main 0.40 2 48
43 1775092674 2242664 db-read-1 0.20 1 36
44 1775092674 2242665 db-write 34.80 174 2169
45 1775092674 2242666 event-worker 0.20 1 3
46 1775092679 2242661 lws-main 0.00 0 48
47 1775092679 2242664 db-read-1 0.40 2 38
48 1775092679 2242665 db-write 66.80 334 2503
49 1775092679 2242666 event-worker 0.00 0 3
50 1775092684 2242661 lws-main 0.00 0 48
51 1775092684 2242664 db-read-1 1.00 5 43
52 1775092684 2242665 db-write 21.60 108 2611
53 1775092684 2242666 event-worker 0.00 0 3
54 1775092689 2242661 lws-main 0.40 2 50
55 1775092689 2242664 db-read-1 0.20 1 44
56 1775092689 2242665 db-write 54.80 274 2885
57 1775092689 2242666 event-worker 0.00 0 3
58 1775092694 2242661 lws-main 0.20 1 51
59 1775092694 2242664 db-read-1 0.20 1 45
60 1775092694 2242665 db-write 67.20 336 3221
61 1775092694 2242666 event-worker 0.00 0 3
62 1775092699 2242661 lws-main 0.00 0 51
63 1775092699 2242664 db-read-1 0.60 3 48
64 1775092699 2242665 db-write 100.20 501 3722
65 1775092699 2242666 event-worker 0.00 0 3
66 1775092704 2242661 lws-main 0.40 2 53
67 1775092704 2242664 db-read-1 0.40 2 50
68 1775092704 2242665 db-write 93.00 465 4187
69 1775092704 2242666 event-worker 0.00 0 3
70 1775092709 2242661 lws-main 0.20 1 54
71 1775092709 2242664 db-read-1 0.40 2 52
72 1775092709 2242665 db-write 27.60 138 4325
73 1775092709 2242666 event-worker 0.00 0 3
74 1775092714 2242661 lws-main 0.20 1 55
75 1775092714 2242664 db-read-1 0.40 2 54
76 1775092714 2242665 db-write 90.20 451 4776
77 1775092714 2242666 event-worker 0.00 0 3
78 1775092719 2242661 lws-main 0.20 1 56
79 1775092719 2242664 db-read-1 0.20 1 55
80 1775092719 2242665 db-write 100.00 500 5276
81 1775092719 2242666 event-worker 0.00 0 3
82 1775092724 2242661 lws-main 0.00 0 56
83 1775092724 2242664 db-read-1 0.00 0 55
84 1775092724 2242665 db-write 100.20 501 5777
85 1775092724 2242666 event-worker 0.00 0 3
86 1775092729 2242661 lws-main 0.20 1 57
87 1775092729 2242664 db-read-1 0.20 1 56
88 1775092729 2242665 db-write 100.20 501 6278
89 1775092729 2242666 event-worker 0.00 0 3
90 1775092734 2242661 lws-main 0.20 1 58
91 1775092734 2242664 db-read-1 0.20 1 57
92 1775092734 2242665 db-write 100.40 502 6780
93 1775092734 2242666 event-worker 0.00 0 3
94 1775092739 2242661 lws-main 0.00 0 58
95 1775092739 2242664 db-read-1 0.00 0 57
96 1775092739 2242665 db-write 99.80 499 7279
97 1775092739 2242666 event-worker 0.00 0 3
98 1775092744 2242661 lws-main 0.20 1 59
99 1775092744 2242664 db-read-1 0.00 0 57
100 1775092744 2242665 db-write 100.60 503 7782
101 1775092744 2242666 event-worker 0.00 0 3
102 1775092749 2242661 lws-main 0.00 0 59
103 1775092749 2242664 db-read-1 0.00 0 57
104 1775092749 2242665 db-write 100.00 500 8282
105 1775092749 2242666 event-worker 0.00 0 3
106 1775092754 2242661 lws-main 0.20 1 60
107 1775092754 2242664 db-read-1 0.20 1 58
108 1775092754 2242665 db-write 30.40 152 8434
109 1775092754 2242666 event-worker 0.00 0 3
110 1775092759 2242661 lws-main 0.00 0 60
111 1775092759 2242664 db-read-1 0.00 0 58
112 1775092759 2242665 db-write 53.40 267 8701
113 1775092759 2242666 event-worker 0.00 0 3
114 1775092764 2242661 lws-main 0.40 2 62
115 1775092764 2242664 db-read-1 0.00 0 58
116 1775092764 2242665 db-write 50.00 250 8951
117 1775092764 2242666 event-worker 0.00 0 3
118 1775092769 2242661 lws-main 0.00 0 62
119 1775092769 2242664 db-read-1 0.20 1 59
120 1775092769 2242665 db-write 57.60 288 9239
121 1775092769 2242666 event-worker 0.00 0 3
122 1775092774 2242661 lws-main 0.20 1 63
123 1775092774 2242664 db-read-1 0.20 1 60
124 1775092774 2242665 db-write 54.40 272 9511
125 1775092774 2242666 event-worker 0.20 1 4
126 1775092779 2242661 lws-main 0.00 0 63
127 1775092779 2242664 db-read-1 0.20 1 61
128 1775092779 2242665 db-write 49.20 246 9757
129 1775092779 2242666 event-worker 0.00 0 4
130 1775092784 2242661 lws-main 0.20 1 64
131 1775092784 2242664 db-read-1 0.00 0 61
132 1775092784 2242665 db-write 31.00 155 9912
133 1775092784 2242666 event-worker 0.00 0 4
134 1775092789 2242661 lws-main 0.20 1 65
135 1775092789 2242664 db-read-1 0.00 0 61
136 1775092789 2242665 db-write 66.80 334 10246
137 1775092789 2242666 event-worker 0.00 0 4
138 1775092794 2242661 lws-main 0.00 0 65
139 1775092794 2242664 db-read-1 0.20 1 62
140 1775092794 2242665 db-write 40.20 201 10447
141 1775092794 2242666 event-worker 0.00 0 4
142 1775092799 2242661 lws-main 0.20 1 66
143 1775092799 2242664 db-read-1 0.20 1 63
144 1775092799 2242665 db-write 92.40 462 10909
145 1775092799 2242666 event-worker 0.00 0 4
@@ -0,0 +1,11 @@
==========================================
Thread CPU Summary (avg + max over run)
==========================================
Run timestamp (UTC): 20260402_011703
Duration: 180s, Interval: 5s
2242665 db-write 54.76 100.60
2242664 db-read-1 0.23 2.00
2242661 lws-main 0.18 1.80
2242666 event-worker 0.02 0.20
TID THREAD AVG_CPU% MAX_CPU%
Binary file not shown.
@@ -0,0 +1,622 @@
# To display the perf.data header info, please use --header/--header-only options.
#
#
# Total Lost Samples: 0
#
# Samples: 73K of event 'task-clock:ppp'
# Event count (approx.): 18314000000
#
# Overhead Samples Command Symbol IPC [IPC Coverage]
# ........ ............ ............ .............................................. ....................
#
13.73% 10055 db-write [.] sqlite3BtreeTableMoveto - -
|
---sqlite3BtreeTableMoveto
|
|--3.11%--0xfdd00007f040000
|
|--3.03%--0xfdd75007f000001
|
|--2.68%--0xfdd75007f040000
|
--2.25%--0xfdd00007f000001
10.11% 7408 db-write [.] sqlite3VdbeExec - -
|
---sqlite3VdbeExec
|
--4.88%--0
7.76% 5685 db-write [.] sqlite3GetVarint - -
|
---sqlite3GetVarint
|
|--2.86%--0xfdd00007f040000
|
|--2.61%--0xfdd75007f040000
|
|--0.76%--0xfdd75007f000001
|
--0.59%--0xfdd00007f000001
7.71% 5647 db-write [k] rep_movs_alternative - -
|
---rep_movs_alternative
copy_page_to_iter
filemap_read
generic_file_read_iter
ext4_file_read_iter
vfs_read
__x64_sys_pread64
x64_sys_call
do_syscall_64
entry_SYSCALL_64_after_hwframe
sccp
7.04% 5154 db-write [.] pcache1Fetch - -
|
---pcache1Fetch
5.84% 4275 db-write [.] memcpy - -
|
---memcpy
5.27% 3857 db-write [.] sccp - -
|
---sccp
4.31% 3154 db-write [k] do_syscall_64 - -
|
---do_syscall_64
entry_SYSCALL_64_after_hwframe
|
--4.30%--sccp
2.81% 2059 db-write [k] filemap_get_read_batch - -
|
---filemap_get_read_batch
|
--2.80%--filemap_get_pages
filemap_read
generic_file_read_iter
ext4_file_read_iter
vfs_read
__x64_sys_pread64
x64_sys_call
do_syscall_64
entry_SYSCALL_64_after_hwframe
sccp
2.45% 1798 db-write [.] getPageNormal - -
|
---getPageNormal
|
--0.82%--0x75d555b7ac18
0xa02000100010500
0x200
2.27% 1661 db-write [.] getCellInfo - -
|
---getCellInfo
|
|--1.16%--0x75d555b872d8
|
--0.85%--0
1.87% 1371 db-write [k] __fdget - -
|
---__fdget
|
--1.85%--__x64_sys_pread64
x64_sys_call
do_syscall_64
entry_SYSCALL_64_after_hwframe
sccp
1.83% 1340 db-write [.] pcache1Unpin - -
|
---pcache1Unpin
1.55% 1134 db-write [.] pread - -
|
---pread
1.53% 1123 db-write [.] getAndInitPage - -
|
---getAndInitPage
|
--0.91%--0x75d555b7ac18
0xa02000100010500
0x200
1.45% 1063 db-write [.] sqlite3BtreeNext.isra.0 - -
|
---sqlite3BtreeNext.isra.0
|
--1.29%--0
1.40% 1023 db-write [.] sqlite3VdbeIdxRowid - -
|
---sqlite3VdbeIdxRowid
1.34% 979 db-write [.] btreeParseCellPtr - -
|
---btreeParseCellPtr
|
--0.99%--0
1.19% 874 db-write [.] pcache1FetchStage2 - -
|
---pcache1FetchStage2
1.01% 742 db-write [.] sqlite3PcacheRelease - -
|
---sqlite3PcacheRelease
1.01% 741 db-write [.] sqlite3VdbeRecordCompareWithSkip - -
|
---sqlite3VdbeRecordCompareWithSkip
0.97% 709 db-write [.] memcmp - -
|
---memcmp
0.95% 699 db-write [.] walFindFrame.constprop.0 - -
|
---walFindFrame.constprop.0
0.95% 698 db-write [.] moveToChild - -
|
---moveToChild
|
--0.56%--0xfdd00007f040000
0.90% 662 db-write [.] moveToRoot - -
|
---moveToRoot
0.88% 646 db-write [k] xas_load - -
|
---xas_load
|
--0.87%--filemap_get_read_batch
filemap_get_pages
filemap_read
generic_file_read_iter
ext4_file_read_iter
vfs_read
__x64_sys_pread64
x64_sys_call
do_syscall_64
entry_SYSCALL_64_after_hwframe
sccp
0.84% 615 db-write [.] sqlite3VdbeSerialGet - -
|
---sqlite3VdbeSerialGet
0.73% 535 db-write [k] filemap_read - -
|
---filemap_read
|
--0.71%--generic_file_read_iter
ext4_file_read_iter
vfs_read
__x64_sys_pread64
x64_sys_call
do_syscall_64
entry_SYSCALL_64_after_hwframe
sccp
0.73% 533 db-write [.] vdbeCompareMemString - -
|
---vdbeCompareMemString
0.72% 528 db-write [.] btreeParseCellPtrIndex - -
|
---btreeParseCellPtrIndex
0x75d555b872d8
0.68% 499 db-write [.] readDbPage - -
|
---readDbPage
|
--0.67%--0xa02000100010500
0x200
0.64% 470 db-write [.] sqlite3VdbeFinishMoveto - -
|
---sqlite3VdbeFinishMoveto
0
0.55% 401 db-write [.] decodeFlags - -
|
---decodeFlags
0.50% 365 db-write [k] xas_descend - -
0.49% 362 db-write [k] aa_file_perm - -
0.48% 351 db-write [.] sqlite3MemCompare - -
0.48% 348 db-write [k] __fsnotify_parent - -
0.47% 346 db-write [.] btreeInitPage - -
0.45% 330 db-write [k] rw_verify_area - -
0.45% 327 db-write [.] unixRead - -
0.41% 303 db-write [.] sqlite3PagerUnrefNotNull - -
0.27% 197 db-write [k] xas_start - -
0.25% 186 db-write [.] binCollFunc - -
0.22% 163 db-write [k] vfs_read - -
0.14% 99 db-write [.] btreeNext.constprop.0 - -
0.11% 82 db-write [k] ext4_file_read_iter - -
0.11% 79 db-write [k] apparmor_file_permission - -
0.10% 76 db-write [k] fput - -
0.08% 61 db-write [.] moveToLeftmost - -
0.08% 56 db-write [k] atime_needs_update - -
0.08% 55 db-write [k] filemap_get_pages - -
0.07% 50 db-write [k] copy_page_to_iter - -
0.07% 49 db-write [k] __x64_sys_pread64 - -
0.06% 46 db-write [k] _copy_to_iter - -
0.05% 37 db-write [k] __xas_next - -
0.05% 33 db-write [.] walHashGet - -
0.04% 32 db-write [k] __rcu_read_lock - -
0.04% 29 db-write [k] __rcu_read_unlock - -
0.04% 29 db-write [k] current_time - -
0.04% 28 db-write [k] security_file_permission - -
0.04% 27 db-write [k] ktime_get_coarse_real_ts64 - -
0.03% 24 db-read-1 [k] rep_movs_alternative - -
0.03% 24 db-write [k] folio_mark_accessed - -
0.03% 24 db-write [k] generic_file_read_iter - -
0.03% 24 lws-main [.] remove_subscription_from_kind_index.part.0 - -
0.03% 22 db-write [k] __x86_indirect_thunk_rax - -
0.03% 21 db-read-1 [.] sqlite3VdbeExec - -
0.03% 21 db-write [.] pcacheFetchFinishWithInit - -
0.03% 20 db-read-1 [k] do_syscall_64 - -
0.03% 19 db-write [.] btreeParseCellAdjustSizeForOverflow.isra.0 - -
0.03% 19 lws-main [k] sock_poll - -
0.02% 18 db-write [k] touch_atime - -
0.02% 16 db-write [k] __cond_resched - -
0.02% 15 db-read-1 [.] pcache1Fetch - -
0.02% 15 db-write [k] syscall_exit_to_user_mode_prepare - -
0.02% 15 db-write [k] x64_sys_call - -
0.02% 14 db-read-1 [.] sccp - -
0.02% 13 db-read-1 [.] __libc_malloc_impl - -
0.02% 13 db-read-1 [k] filemap_get_read_batch - -
0.02% 12 db-read-1 [k] __fdget - -
0.02% 12 lws-main [k] _raw_spin_unlock_irqrestore - -
0.02% 11 db-write [k] handle_softirqs - -
0.02% 11 lws-main [.] has_subscriptions_for_kind - -
0.01% 10 db-read-1 [.] sqlite3BtreeTableMoveto - -
0.01% 10 lws-main [.] __libc_malloc_impl - -
0.01% 9 db-read-1 [.] pread - -
0.01% 9 lws-main [.] sccp - -
0.01% 8 db-read-1 [.] pcache1FetchStage2 - -
0.01% 8 db-write [.] __syscall_ret - -
0.01% 8 lws-main [.] cJSON_IsNumber - -
0.01% 8 lws-main [.] memcpy - -
0.01% 7 db-read-1 [k] do_user_addr_fault - -
0.01% 7 db-read-1 [.] sqlite3GetVarint - -
0.01% 7 db-write [k] _raw_spin_unlock_irqrestore - -
0.01% 7 db-write [k] make_vfsuid - -
0.01% 6 db-read-1 [k] _raw_spin_unlock_irqrestore - -
0.01% 6 db-read-1 [k] clear_page_orig - -
0.01% 6 db-read-1 [k] xas_load - -
0.01% 6 db-write [k] _raw_spin_unlock_irq - -
0.01% 5 db-read-1 [k] __fsnotify_parent - -
0.01% 5 db-read-1 [.] getAndInitPage - -
0.01% 5 db-read-1 [.] walFindFrame.constprop.0 - -
0.01% 5 db-read-1 [k] xas_descend - -
0.01% 5 db-write [k] clear_page_orig - -
0.01% 5 db-write [k] finish_task_switch.isra.0 - -
0.01% 5 db-write [k] iowrite16 - -
0.01% 5 db-write [k] make_vfsgid - -
0.01% 5 db-write [.] sqlite3BtreeIndexMoveto - -
0.01% 5 lws-main [k] __fdget - -
0.01% 4 db-read-1 [.] __pthread_mutex_timedlock - -
0.01% 4 db-read-1 [k] aa_file_perm - -
0.01% 4 db-read-1 [.] btreeParseCellPtr - -
0.01% 4 db-read-1 [.] memcpy - -
0.01% 4 db-read-1 [.] sqlite3VdbeRecordCompareWithSkip - -
0.01% 4 db-write [k] do_user_addr_fault - -
0.01% 4 db-write [k] syscall_exit_to_user_mode - -
0.01% 4 event-worker [.] secp256k1_fe_mul_inner - -
0.01% 4 lws-main [.] _lws_plat_service_tsi.part.0 - -
0.01% 4 lws-main [.] alloc_slot - -
0.01% 4 lws-main [k] fput - -
0.01% 4 lws-main [.] strlen - -
0.01% 4 lws-main [k] tcp_poll - -
0.00% 3 db-read-1 [.] __pthread_mutex_lock - -
0.00% 3 db-read-1 [k] blk_cgroup_congested - -
0.00% 3 db-read-1 [k] filemap_read - -
0.00% 3 db-read-1 [.] getCellInfo - -
0.00% 3 db-read-1 [.] sqlite3PcacheRelease - -
0.00% 3 db-read-1 [.] whereScanNext - -
0.00% 3 db-write [k] ___slab_alloc - -
0.00% 3 db-write [k] __lruvec_stat_mod_folio - -
0.00% 3 db-write [k] __next_zones_zonelist - -
0.00% 3 db-write [k] smp_call_function_many_cond - -
0.00% 3 db-write [.] sqlite3DbMallocRawNN - -
0.00% 3 db-write [.] strlen - -
0.00% 3 db-write [.] yy_reduce.isra.0 - -
0.00% 3 lws-main [.] cJSON_IsArray - -
0.00% 3 lws-main [k] do_syscall_64 - -
0.00% 3 lws-main [k] finish_task_switch.isra.0 - -
0.00% 3 lws-main [.] get_config_value_from_table - -
0.00% 2 db-read-1 [.] __libc_free - -
0.00% 2 db-read-1 [k] __lruvec_stat_mod_folio - -
0.00% 2 db-read-1 [.] __pthread_mutex_unlock - -
0.00% 2 db-read-1 [k] __rcu_read_lock - -
0.00% 2 db-read-1 [.] btreeInitPage - -
0.00% 2 db-read-1 [.] decodeFlags - -
0.00% 2 db-read-1 [k] down_write - -
0.00% 2 db-read-1 [.] moveToRoot - -
0.00% 2 db-read-1 [k] native_write_msr - -
0.00% 2 db-read-1 [.] pcache1Unpin - -
0.00% 2 db-read-1 [.] propagateConstantExprRewrite - -
0.00% 2 db-read-1 [.] readDbPage - -
0.00% 2 db-read-1 [.] sqlite3BtreeIndexMoveto - -
0.00% 2 db-read-1 [.] sqlite3BtreeNext.isra.0 - -
0.00% 2 db-read-1 [.] sqlite3WhereBegin - -
0.00% 2 db-read-1 [.] unixRead - -
0.00% 2 db-read-1 [.] whereLoopAddBtreeIndex - -
0.00% 2 db-read-1 [k] xas_start - -
0.00% 2 db-write [k] inode_to_bdi - -
0.00% 2 db-write [k] kmem_cache_alloc - -
0.00% 2 db-write [k] native_write_msr - -
0.00% 2 db-write [k] pvclock_clocksource_read_nowd - -
0.00% 2 db-write [.] pwrite - -
0.00% 2 db-write [.] sqlite3WalCheckpoint - -
0.00% 2 lws-main [.] __libc_free - -
0.00% 2 lws-main [.] __lock - -
0.00% 2 lws-main [.] __unlock - -
0.00% 2 lws-main [k] __x86_indirect_thunk_rax - -
0.00% 2 lws-main [.] broadcast_event_to_subscriptions - -
0.00% 2 lws-main [.] decfloat - -
0.00% 2 lws-main [k] do_user_addr_fault - -
0.00% 2 lws-main [k] eventfd_poll - -
0.00% 2 lws-main [k] nft_do_chain - -
0.00% 2 lws-main [.] nostr_relay_callback - -
0.00% 2 lws-main [.] print_value - -
0.00% 2 lws-main [k] pvclock_clocksource_read_nowd - -
0.00% 2 lws-main [.] strcmp - -
0.00% 2 lws-main [k] tcp_in_window - -
0.00% 1 db-read-1 [k] ___slab_alloc - -
0.00% 1 db-read-1 [k] __anon_vma_interval_tree_remove - -
0.00% 1 db-read-1 [k] __raw_callee_save___pv_queued_spin_unlock - -
0.00% 1 db-read-1 [k] __rcu_read_unlock - -
0.00% 1 db-read-1 [.] __timedwait_cp - -
0.00% 1 db-read-1 [k] __tlb_remove_page_size - -
0.00% 1 db-read-1 [.] allocateCursor - -
0.00% 1 db-read-1 [k] anon_vma_interval_tree_insert - -
0.00% 1 db-read-1 [.] binCollFunc - -
0.00% 1 db-read-1 [.] btreeBeginTrans - -
0.00% 1 db-read-1 [.] btreeParseCellPtrIndex - -
0.00% 1 db-read-1 [k] current_obj_cgroup - -
0.00% 1 db-read-1 [k] do_vmi_align_munmap - -
0.00% 1 db-read-1 [.] exprDup - -
0.00% 1 db-read-1 [.] exprNodeIsConstant - -
0.00% 1 db-read-1 [k] finish_task_switch.isra.0 - -
0.00% 1 db-read-1 [k] folio_add_lru - -
0.00% 1 db-read-1 [k] folio_add_new_anon_rmap - -
0.00% 1 db-read-1 [.] free_req_payload_main.part.0 - -
0.00% 1 db-read-1 [k] futex_q_lock - -
0.00% 1 db-read-1 [.] getPageNormal - -
0.00% 1 db-read-1 [k] get_mem_cgroup_from_mm - -
0.00% 1 db-read-1 [.] get_meta - -
0.00% 1 db-read-1 [k] handle_softirqs - -
0.00% 1 db-read-1 [k] hugepage_pmd_enabled - -
0.00% 1 db-read-1 [.] indexCellCompare.isra.0 - -
0.00% 1 db-read-1 [k] kmem_cache_alloc - -
0.00% 1 db-read-1 [k] kmem_cache_free - -
0.00% 1 db-read-1 [k] lock_vma_under_rcu - -
0.00% 1 db-read-1 [k] make_vfsuid - -
0.00% 1 db-read-1 [k] mas_walk - -
0.00% 1 db-read-1 [k] mem_cgroup_commit_charge - -
0.00% 1 db-read-1 [.] memcmp - -
0.00% 1 db-read-1 [k] obj_cgroup_uncharge_pages - -
0.00% 1 db-read-1 [k] post_alloc_hook - -
0.00% 1 db-read-1 [.] pthreadMutexLeave - -
0.00% 1 db-read-1 [k] rcu_nocb_unlock_irqrestore.part.0 - -
0.00% 1 db-read-1 [.] resolveP2Values - -
0.00% 1 db-read-1 [k] rw_verify_area - -
0.00% 1 db-read-1 [k] smp_call_function_many_cond - -
0.00% 1 db-read-1 [.] sqlite3CodeRhsOfIN - -
0.00% 1 db-read-1 [.] sqlite3DbMallocRawNN - -
0.00% 1 db-read-1 [.] sqlite3DbNNFreeNN - -
0.00% 1 db-read-1 [.] sqlite3ExprIfFalse - -
0.00% 1 db-read-1 [.] sqlite3GetVarint32 - -
0.00% 1 db-read-1 [.] sqlite3KeyInfoAlloc - -
0.00% 1 db-read-1 [.] sqlite3MemCompare - -
0.00% 1 db-read-1 [.] sqlite3Select - -
0.00% 1 db-read-1 [.] sqlite3SelectPopWith - -
0.00% 1 db-read-1 [.] sqlite3SrcListShiftJoinType.isra.0 - -
0.00% 1 db-read-1 [.] sqlite3VdbeFinishMoveto - -
0.00% 1 db-read-1 [.] sqlite3VdbeFreeCursorNN - -
0.00% 1 db-read-1 [.] sqlite3VdbeMemFromBtree - -
0.00% 1 db-read-1 [.] sqlite3VdbeSerialGet - -
0.00% 1 db-read-1 [.] sqlite3WalkSelect - -
0.00% 1 db-read-1 [.] sqlite3WhereExprUsageNN - -
0.00% 1 db-read-1 [k] uncharge_batch - -
0.00% 1 db-read-1 [.] unixShmBarrier - -
0.00% 1 db-read-1 [k] update_blocked_averages - -
0.00% 1 db-read-1 [k] vm_area_dup - -
0.00% 1 db-read-1 [k] vm_unmapped_area - -
0.00% 1 db-read-1 [k] vma_adjust_trans_huge - -
0.00% 1 db-read-1 [.] whereLoopFindLesser - -
0.00% 1 db-read-1 [.] wherePathSolver.isra.0 - -
0.00% 1 db-read-1 [.] yy_reduce.isra.0 - -
0.00% 1 db-read-1 [k] zap_pte_range - -
0.00% 1 db-write [k] __block_commit_write - -
0.00% 1 db-write [.] __libc_free - -
0.00% 1 db-write [.] __libc_malloc_impl - -
0.00% 1 db-write [k] __memcg_slab_post_alloc_hook - -
0.00% 1 db-write [k] __mmap_region - -
0.00% 1 db-write [k] __mod_memcg_lruvec_state - -
0.00% 1 db-write [.] __pthread_mutex_unlock - -
0.00% 1 db-write [k] __pv_queued_spin_lock_slowpath - -
0.00% 1 db-write [k] __radix_tree_lookup - -
0.00% 1 db-write [k] __rb_insert_augmented - -
0.00% 1 db-write [k] __sg_alloc_table - -
0.00% 1 db-write [k] __slab_free - -
0.00% 1 db-write [k] __sock_wfree - -
0.00% 1 db-write [k] __tlb_remove_page_size - -
0.00% 1 db-write [k] _raw_read_lock - -
0.00% 1 db-write [k] _raw_spin_lock - -
0.00% 1 db-write [.] accessPayload - -
0.00% 1 db-write [.] applyAffinity - -
0.00% 1 db-write [k] balance_dirty_pages_ratelimited_flags - -
0.00% 1 db-write [k] blk_finish_plug - -
0.00% 1 db-write [k] blkcg_set_ioprio - -
0.00% 1 db-write [k] count_memcg_events.constprop.0 - -
0.00% 1 db-write [k] crc32_body - -
0.00% 1 db-write [.] dbReallocFinish - -
0.00% 1 db-write [k] ext4_da_map_blocks.constprop.0 - -
0.00% 1 db-write [k] ext4_mb_mark_context - -
0.00% 1 db-write [k] ext4_readahead - -
0.00% 1 db-write [k] ext4_sb_block_valid - -
0.00% 1 db-write [k] fault_in_readable - -
0.00% 1 db-write [k] file_modified - -
0.00% 1 db-write [k] filemap_get_entry - -
0.00% 1 db-write [k] flock64_to_posix_lock - -
0.00% 1 db-write [k] folio_add_lru - -
0.00% 1 db-write [k] folio_add_new_anon_rmap - -
0.00% 1 db-write [k] folio_mapping - -
0.00% 1 db-write [k] futex_q_lock - -
0.00% 1 db-write [.] getDigits - -
0.00% 1 db-write [k] get_mem_cgroup_from_mm - -
0.00% 1 db-write [.] get_meta - -
0.00% 1 db-write [k] get_pfnblock_flags_mask - -
0.00% 1 db-write [k] get_unmapped_area - -
0.00% 1 db-write [k] handle_mm_fault - -
0.00% 1 db-write [k] jbd2_journal_dirty_metadata - -
0.00% 1 db-write [k] kfree - -
0.00% 1 db-write [k] kmem_cache_free - -
0.00% 1 db-write [k] lock_vma_under_rcu - -
0.00% 1 db-write [k] lruvec_stat_mod_folio - -
0.00% 1 db-write [k] mas_leaf_max_gap - -
0.00% 1 db-write [k] mem_cgroup_commit_charge - -
0.00% 1 db-write [k] mem_cgroup_flush_stats.part.0 - -
0.00% 1 db-write [k] mempool_alloc_slab - -
0.00% 1 db-write [k] mod_delayed_work_on - -
0.00% 1 db-write [k] net_rx_action - -
0.00% 1 db-write [k] nf_conntrack_tcp_packet - -
0.00% 1 db-write [.] pagerUnlockAndRollback - -
0.00% 1 db-write [.] pcache1Free - -
0.00% 1 db-write [k] perf_event_mmap - -
0.00% 1 db-write [k] perf_event_mmap_event - -
0.00% 1 db-write [k] pmd_val - -
0.00% 1 db-write [.] pthreadMutexAlloc - -
0.00% 1 db-write [k] qdisc_pkt_len_init - -
0.00% 1 db-write [.] queue_pop - -
0.00% 1 db-write [.] releaseMemArray.part.0 - -
0.00% 1 db-write [k] release_pages - -
0.00% 1 db-write [k] scsi_mq_get_budget - -
0.00% 1 db-write [.] seekAndWriteFd - -
0.00% 1 db-write [k] sk_forced_mem_schedule - -
0.00% 1 db-write [k] slab_update_freelist.isra.0 - -
0.00% 1 db-write [.] sqlite3AtoF - -
0.00% 1 db-write [.] sqlite3BeginWriteOperation - -
0.00% 1 db-write [.] sqlite3ExprDeleteNN - -
0.00% 1 db-write [.] sqlite3GenerateRowDelete - -
0.00% 1 db-write [.] sqlite3GetToken - -
0.00% 1 db-write [.] sqlite3GetVarint32 - -
0.00% 1 db-write [.] sqlite3KeyInfoOfIndex - -
0.00% 1 db-write [.] sqlite3LocateTable - -
0.00% 1 db-write [.] sqlite3OpenTableAndIndices - -
0.00% 1 db-write [.] sqlite3RunParser - -
0.00% 1 db-write [.] sqlite3TriggerList.isra.0 - -
0.00% 1 db-write [.] sqlite3Update.constprop.0 - -
0.00% 1 db-write [.] sqlite3VdbeFreeCursorNN - -
0.00% 1 db-write [k] update_blocked_averages - -
0.00% 1 db-write [k] userfaultfd_unmap_complete - -
0.00% 1 db-write [k] virtscsi_queuecommand - -
0.00% 1 db-write [k] vm_unmapped_area - -
0.00% 1 db-write [.] walChecksumBytes - -
0.00% 1 db-write [.] walIndexAppend - -
0.00% 1 db-write [.] walMerge - -
0.00% 1 db-write [k] xas_find - -
0.00% 1 db-write [k] zap_pte_range - -
0.00% 1 event-worker [k] __fdget_raw - -
0.00% 1 event-worker [k] _raw_spin_unlock_irq - -
0.00% 1 event-worker [k] iowrite16 - -
0.00% 1 event-worker [.] memchr - -
0.00% 1 event-worker [.] print_value - -
0.00% 1 event-worker [.] sqlite3AuthCheck - -
0.00% 1 event-worker [.] sqlite3KeyInfoAlloc - -
0.00% 1 lws-main [k] __alloc_pages - -
0.00% 1 lws-main [k] __alloc_skb - -
0.00% 1 lws-main [k] __free_pages - -
0.00% 1 lws-main [k] __mmap_region - -
0.00% 1 lws-main [k] __mod_timer - -
0.00% 1 lws-main [k] __pollwait - -
0.00% 1 lws-main [.] __pthread_mutex_unlock - -
0.00% 1 lws-main [k] __pv_queued_spin_lock_slowpath - -
0.00% 1 lws-main [k] __raw_spin_lock_irqsave - -
0.00% 1 lws-main [k] __rcu_read_unlock - -
0.00% 1 lws-main [k] __sock_wfree - -
0.00% 1 lws-main [k] __sys_sendto - -
0.00% 1 lws-main [k] __tcp_transmit_skb - -
0.00% 1 lws-main [.] __vdso_clock_gettime - -
0.00% 1 lws-main [k] __x64_sys_sendto - -
0.00% 1 lws-main [k] _raw_spin_lock - -
0.00% 1 lws-main [k] apparmor_file_permission - -
0.00% 1 lws-main [k] arch_get_unmapped_area_topdown - -
0.00% 1 lws-main [.] cJSON_Delete - -
0.00% 1 lws-main [.] cJSON_Duplicate_rec - -
0.00% 1 lws-main [.] cJSON_GetNumberValue - -
0.00% 1 lws-main [k] call_rcu - -
0.00% 1 lws-main [k] clear_page_orig - -
0.00% 1 lws-main [k] cond_accept_memory - -
0.00% 1 lws-main [.] copysignl - -
0.00% 1 lws-main [k] do_poll.constprop.0 - -
0.00% 1 lws-main [k] do_sys_poll - -
0.00% 1 lws-main [k] find_exception - -
0.00% 1 lws-main [k] futex_q_lock - -
0.00% 1 lws-main [k] get_l4proto - -
0.00% 1 lws-main [.] get_meta - -
0.00% 1 lws-main [k] get_page_from_freelist - -
0.00% 1 lws-main [.] handle_req_message - -
0.00% 1 lws-main [.] ip_ban_cleanup - -
0.00% 1 lws-main [k] ip_finish_output - -
0.00% 1 lws-main [k] ip_rcv_core - -
0.00% 1 lws-main [k] ipv4_conntrack_defrag - -
0.00% 1 lws-main [k] kmem_cache_free - -
0.00% 1 lws-main [k] kvm_clock_get_cycles - -
0.00% 1 lws-main [.] lws_check_utf8 - -
0.00% 1 lws-main [.] lws_handle_POLLOUT_event - -
0.00% 1 lws-main [.] lws_parse_ws - -
0.00% 1 lws-main [.] lws_set_timeout - -
0.00% 1 lws-main [.] lws_ssl_capable_read_no_ssl - -
0.00% 1 lws-main [.] lws_tls_fake_POLLIN_for_buffered - -
0.00% 1 lws-main [.] lws_write - -
0.00% 1 lws-main [k] mas_ascend - -
0.00% 1 lws-main [k] mas_wr_append - -
0.00% 1 lws-main [k] mod_memcg_state - -
0.00% 1 lws-main [k] native_write_msr - -
0.00% 1 lws-main [k] nf_confirm - -
0.00% 1 lws-main [.] nontrivial_free - -
0.00% 1 lws-main [.] parse_string - -
0.00% 1 lws-main [k] perf_iterate_ctx - -
0.00% 1 lws-main [k] poll_select_set_timeout - -
0.00% 1 lws-main [.] printf_core - -
0.00% 1 lws-main [k] refill_stock - -
0.00% 1 lws-main [.] refresh_hot_config_if_needed - -
0.00% 1 lws-main [k] rep_movs_alternative - -
0.00% 1 lws-main [.] rops_handle_POLLIN_ws - -
0.00% 1 lws-main [k] sk_forced_mem_schedule - -
0.00% 1 lws-main [k] skb_copy_bits - -
0.00% 1 lws-main [k] skb_page_frag_refill - -
0.00% 1 lws-main [k] skb_release_data - -
0.00% 1 lws-main [k] tcp_ack_update_window.isra.0 - -
0.00% 1 lws-main [k] tcp_chrono_stop - -
0.00% 1 lws-main [k] tcp_current_mss - -
0.00% 1 lws-main [k] tcp_data_queue - -
0.00% 1 lws-main [k] tcp_sendmsg_locked - -
0.00% 1 lws-main [k] tcp_v4_rcv - -
0.00% 1 lws-main [k] tcp_write_xmit - -
0.00% 1 lws-main [k] up_read - -
0.00% 1 lws-main [.] validate_filter_array - -
0.00% 1 lws-main [.] validate_filter_values - -
0.00% 1 lws-main [.] vfprintf - -
#
# (Cannot load tips.txt file, please install perf!)
#
@@ -0,0 +1,5 @@
Profiling PID: 2245961
CLK_TCK=100
Starting perf record in background...
perf_pid=2246179
Done. Artifacts in /tmp/c_relay_thread_profile_20260402_012527
@@ -0,0 +1,49 @@
timestamp,tid,thread_name,cpu_pct,delta_ticks,total_ticks
1775093128,2245961,lws-main,0.00,0,36
1775093128,2245969,db-read-1,0.00,0,26
1775093128,2245970,db-write,0.00,0,540
1775093128,2245971,event-worker,0.00,0,0
1775093133,2245961,lws-main,0.20,1,37
1775093133,2245969,db-read-1,0.00,0,26
1775093133,2245970,db-write,0.20,1,541
1775093133,2245971,event-worker,0.00,0,0
1775093138,2245961,lws-main,0.20,1,38
1775093138,2245969,db-read-1,0.20,1,27
1775093138,2245970,db-write,27.60,138,679
1775093138,2245971,event-worker,0.00,0,0
1775093143,2245961,lws-main,0.00,0,38
1775093143,2245969,db-read-1,0.00,0,27
1775093143,2245970,db-write,0.00,0,679
1775093143,2245971,event-worker,0.00,0,0
1775093148,2245961,lws-main,0.00,0,38
1775093148,2245969,db-read-1,0.60,3,30
1775093148,2245970,db-write,27.40,137,816
1775093148,2245971,event-worker,0.00,0,0
1775093153,2245961,lws-main,0.20,1,39
1775093153,2245969,db-read-1,0.20,1,31
1775093153,2245970,db-write,67.20,336,1152
1775093153,2245971,event-worker,0.00,0,0
1775093158,2245961,lws-main,0.20,1,40
1775093158,2245969,db-read-1,0.20,1,32
1775093158,2245970,db-write,40.00,200,1352
1775093158,2245971,event-worker,0.00,0,0
1775093163,2245961,lws-main,0.00,0,40
1775093163,2245969,db-read-1,0.20,1,33
1775093163,2245970,db-write,27.00,135,1487
1775093163,2245971,event-worker,0.00,0,0
1775093168,2245961,lws-main,0.20,1,41
1775093168,2245969,db-read-1,0.00,0,33
1775093168,2245970,db-write,26.60,133,1620
1775093168,2245971,event-worker,0.00,0,0
1775093173,2245961,lws-main,0.00,0,41
1775093173,2245969,db-read-1,0.00,0,33
1775093173,2245970,db-write,0.00,0,1620
1775093173,2245971,event-worker,0.00,0,0
1775093178,2245961,lws-main,0.20,1,42
1775093178,2245969,db-read-1,0.00,0,33
1775093178,2245970,db-write,64.40,322,1942
1775093178,2245971,event-worker,0.00,0,0
1775093183,2245961,lws-main,0.00,0,42
1775093183,2245969,db-read-1,0.40,2,35
1775093183,2245970,db-write,56.40,282,2224
1775093183,2245971,event-worker,0.00,0,0
1 timestamp tid thread_name cpu_pct delta_ticks total_ticks
2 1775093128 2245961 lws-main 0.00 0 36
3 1775093128 2245969 db-read-1 0.00 0 26
4 1775093128 2245970 db-write 0.00 0 540
5 1775093128 2245971 event-worker 0.00 0 0
6 1775093133 2245961 lws-main 0.20 1 37
7 1775093133 2245969 db-read-1 0.00 0 26
8 1775093133 2245970 db-write 0.20 1 541
9 1775093133 2245971 event-worker 0.00 0 0
10 1775093138 2245961 lws-main 0.20 1 38
11 1775093138 2245969 db-read-1 0.20 1 27
12 1775093138 2245970 db-write 27.60 138 679
13 1775093138 2245971 event-worker 0.00 0 0
14 1775093143 2245961 lws-main 0.00 0 38
15 1775093143 2245969 db-read-1 0.00 0 27
16 1775093143 2245970 db-write 0.00 0 679
17 1775093143 2245971 event-worker 0.00 0 0
18 1775093148 2245961 lws-main 0.00 0 38
19 1775093148 2245969 db-read-1 0.60 3 30
20 1775093148 2245970 db-write 27.40 137 816
21 1775093148 2245971 event-worker 0.00 0 0
22 1775093153 2245961 lws-main 0.20 1 39
23 1775093153 2245969 db-read-1 0.20 1 31
24 1775093153 2245970 db-write 67.20 336 1152
25 1775093153 2245971 event-worker 0.00 0 0
26 1775093158 2245961 lws-main 0.20 1 40
27 1775093158 2245969 db-read-1 0.20 1 32
28 1775093158 2245970 db-write 40.00 200 1352
29 1775093158 2245971 event-worker 0.00 0 0
30 1775093163 2245961 lws-main 0.00 0 40
31 1775093163 2245969 db-read-1 0.20 1 33
32 1775093163 2245970 db-write 27.00 135 1487
33 1775093163 2245971 event-worker 0.00 0 0
34 1775093168 2245961 lws-main 0.20 1 41
35 1775093168 2245969 db-read-1 0.00 0 33
36 1775093168 2245970 db-write 26.60 133 1620
37 1775093168 2245971 event-worker 0.00 0 0
38 1775093173 2245961 lws-main 0.00 0 41
39 1775093173 2245969 db-read-1 0.00 0 33
40 1775093173 2245970 db-write 0.00 0 1620
41 1775093173 2245971 event-worker 0.00 0 0
42 1775093178 2245961 lws-main 0.20 1 42
43 1775093178 2245969 db-read-1 0.00 0 33
44 1775093178 2245970 db-write 64.40 322 1942
45 1775093178 2245971 event-worker 0.00 0 0
46 1775093183 2245961 lws-main 0.00 0 42
47 1775093183 2245969 db-read-1 0.40 2 35
48 1775093183 2245970 db-write 56.40 282 2224
49 1775093183 2245971 event-worker 0.00 0 0
@@ -0,0 +1,11 @@
==========================================
Thread CPU Summary (avg + max over run)
==========================================
Run timestamp (UTC): 20260402_012527
Duration: 60s, Interval: 5s
2245970 db-write 28.07 67.20
2245969 db-read-1 0.15 0.60
2245961 lws-main 0.10 0.20
2245971 event-worker 0.00 0.00
TID THREAD AVG_CPU% MAX_CPU%