Compare commits
10 Commits
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
1fcb9f7c91 | ||
|
|
f272264960 | ||
|
|
cb3171b390 | ||
|
|
03f036d60d | ||
|
|
9b35f463ae | ||
|
|
7b74486519 | ||
|
|
b276b44ded | ||
|
|
3792649ed9 | ||
|
|
5f08956605 | ||
|
|
643d89ed7b |
1
.gitignore
vendored
1
.gitignore
vendored
@@ -11,4 +11,3 @@ copy_executable_local.sh
|
|||||||
nostr_login_lite/
|
nostr_login_lite/
|
||||||
style_guide/
|
style_guide/
|
||||||
nostr-tools
|
nostr-tools
|
||||||
|
|
||||||
|
|||||||
612
STATIC_MUSL_GUIDE.md
Normal file
612
STATIC_MUSL_GUIDE.md
Normal file
@@ -0,0 +1,612 @@
|
|||||||
|
# Static MUSL Build Guide for C Programs
|
||||||
|
|
||||||
|
## Overview
|
||||||
|
|
||||||
|
This guide explains how to build truly portable static binaries using Alpine Linux and MUSL libc. These binaries have **zero runtime dependencies** and work on any Linux distribution without modification.
|
||||||
|
|
||||||
|
This guide is specifically tailored for C programs that use:
|
||||||
|
- **nostr_core_lib** - Nostr protocol implementation
|
||||||
|
- **nostr_login_lite** - Nostr authentication library
|
||||||
|
- Common dependencies: libwebsockets, OpenSSL, SQLite, curl, secp256k1
|
||||||
|
|
||||||
|
## Why MUSL Static Binaries?
|
||||||
|
|
||||||
|
### Advantages Over glibc
|
||||||
|
|
||||||
|
| Feature | MUSL Static | glibc Static | glibc Dynamic |
|
||||||
|
|---------|-------------|--------------|---------------|
|
||||||
|
| **Portability** | ✓ Any Linux | ⚠ glibc only | ✗ Requires matching libs |
|
||||||
|
| **Binary Size** | ~7-10 MB | ~12-15 MB | ~2-3 MB |
|
||||||
|
| **Dependencies** | None | NSS libs | Many system libs |
|
||||||
|
| **Deployment** | Single file | Single file + NSS | Binary + libraries |
|
||||||
|
| **Compatibility** | Universal | glibc version issues | Library version hell |
|
||||||
|
|
||||||
|
### Key Benefits
|
||||||
|
|
||||||
|
1. **True Portability**: Works on Alpine, Ubuntu, Debian, CentOS, Arch, etc.
|
||||||
|
2. **No Library Hell**: No `GLIBC_2.XX not found` errors
|
||||||
|
3. **Simple Deployment**: Just copy one file
|
||||||
|
4. **Reproducible Builds**: Same Docker image = same binary
|
||||||
|
5. **Security**: No dependency on system libraries with vulnerabilities
|
||||||
|
|
||||||
|
## Quick Start
|
||||||
|
|
||||||
|
### Prerequisites
|
||||||
|
|
||||||
|
- Docker installed and running
|
||||||
|
- Your C project with source code
|
||||||
|
- Internet connection for downloading dependencies
|
||||||
|
|
||||||
|
### Basic Build Process
|
||||||
|
|
||||||
|
```bash
|
||||||
|
# 1. Copy the Dockerfile template (see below)
|
||||||
|
cp /path/to/c-relay/Dockerfile.alpine-musl ./Dockerfile.static
|
||||||
|
|
||||||
|
# 2. Customize for your project (see Customization section)
|
||||||
|
vim Dockerfile.static
|
||||||
|
|
||||||
|
# 3. Build the static binary
|
||||||
|
docker build --platform linux/amd64 -f Dockerfile.static -t my-app-builder .
|
||||||
|
|
||||||
|
# 4. Extract the binary
|
||||||
|
docker create --name temp-container my-app-builder
|
||||||
|
docker cp temp-container:/build/my_app_static ./my_app_static
|
||||||
|
docker rm temp-container
|
||||||
|
|
||||||
|
# 5. Verify it's static
|
||||||
|
ldd ./my_app_static # Should show "not a dynamic executable"
|
||||||
|
```
|
||||||
|
|
||||||
|
## Dockerfile Template
|
||||||
|
|
||||||
|
Here's a complete Dockerfile template you can customize for your project:
|
||||||
|
|
||||||
|
```dockerfile
|
||||||
|
# Alpine-based MUSL static binary builder
|
||||||
|
# Produces truly portable binaries with zero runtime dependencies
|
||||||
|
|
||||||
|
FROM alpine:3.19 AS builder
|
||||||
|
|
||||||
|
# Install build dependencies
|
||||||
|
RUN apk add --no-cache \
|
||||||
|
build-base \
|
||||||
|
musl-dev \
|
||||||
|
git \
|
||||||
|
cmake \
|
||||||
|
pkgconfig \
|
||||||
|
autoconf \
|
||||||
|
automake \
|
||||||
|
libtool \
|
||||||
|
openssl-dev \
|
||||||
|
openssl-libs-static \
|
||||||
|
zlib-dev \
|
||||||
|
zlib-static \
|
||||||
|
curl-dev \
|
||||||
|
curl-static \
|
||||||
|
sqlite-dev \
|
||||||
|
sqlite-static \
|
||||||
|
linux-headers \
|
||||||
|
wget \
|
||||||
|
bash
|
||||||
|
|
||||||
|
WORKDIR /build
|
||||||
|
|
||||||
|
# Build libsecp256k1 static (required for Nostr)
|
||||||
|
RUN cd /tmp && \
|
||||||
|
git clone https://github.com/bitcoin-core/secp256k1.git && \
|
||||||
|
cd secp256k1 && \
|
||||||
|
./autogen.sh && \
|
||||||
|
./configure --enable-static --disable-shared --prefix=/usr \
|
||||||
|
CFLAGS="-fPIC" && \
|
||||||
|
make -j$(nproc) && \
|
||||||
|
make install && \
|
||||||
|
rm -rf /tmp/secp256k1
|
||||||
|
|
||||||
|
# Build libwebsockets static (if needed for WebSocket support)
|
||||||
|
RUN cd /tmp && \
|
||||||
|
git clone --depth 1 --branch v4.3.3 https://github.com/warmcat/libwebsockets.git && \
|
||||||
|
cd libwebsockets && \
|
||||||
|
mkdir build && cd build && \
|
||||||
|
cmake .. \
|
||||||
|
-DLWS_WITH_STATIC=ON \
|
||||||
|
-DLWS_WITH_SHARED=OFF \
|
||||||
|
-DLWS_WITH_SSL=ON \
|
||||||
|
-DLWS_WITHOUT_TESTAPPS=ON \
|
||||||
|
-DLWS_WITHOUT_TEST_SERVER=ON \
|
||||||
|
-DLWS_WITHOUT_TEST_CLIENT=ON \
|
||||||
|
-DLWS_WITHOUT_TEST_PING=ON \
|
||||||
|
-DLWS_WITH_HTTP2=OFF \
|
||||||
|
-DLWS_WITH_LIBUV=OFF \
|
||||||
|
-DLWS_WITH_LIBEVENT=OFF \
|
||||||
|
-DLWS_IPV6=ON \
|
||||||
|
-DCMAKE_BUILD_TYPE=Release \
|
||||||
|
-DCMAKE_INSTALL_PREFIX=/usr \
|
||||||
|
-DCMAKE_C_FLAGS="-fPIC" && \
|
||||||
|
make -j$(nproc) && \
|
||||||
|
make install && \
|
||||||
|
rm -rf /tmp/libwebsockets
|
||||||
|
|
||||||
|
# Copy git configuration for submodules
|
||||||
|
COPY .gitmodules /build/.gitmodules
|
||||||
|
COPY .git /build/.git
|
||||||
|
|
||||||
|
# Initialize submodules
|
||||||
|
RUN git submodule update --init --recursive
|
||||||
|
|
||||||
|
# Copy and build nostr_core_lib
|
||||||
|
COPY nostr_core_lib /build/nostr_core_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 && \
|
||||||
|
rm -f *.o *.a 2>/dev/null || true && \
|
||||||
|
./build.sh --nips=1,6,13,17,19,44,59
|
||||||
|
|
||||||
|
# Copy and build nostr_login_lite (if used)
|
||||||
|
# COPY nostr_login_lite /build/nostr_login_lite/
|
||||||
|
# RUN cd nostr_login_lite && make static
|
||||||
|
|
||||||
|
# Copy your application source
|
||||||
|
COPY src/ /build/src/
|
||||||
|
COPY Makefile /build/Makefile
|
||||||
|
|
||||||
|
# Build your application with full static linking
|
||||||
|
RUN gcc -static -O2 -Wall -Wextra -std=c99 \
|
||||||
|
-U_FORTIFY_SOURCE -D_FORTIFY_SOURCE=0 \
|
||||||
|
-I. -Inostr_core_lib -Inostr_core_lib/nostr_core \
|
||||||
|
-Inostr_core_lib/cjson -Inostr_core_lib/nostr_websocket \
|
||||||
|
src/*.c \
|
||||||
|
-o /build/my_app_static \
|
||||||
|
nostr_core_lib/libnostr_core_x64.a \
|
||||||
|
-lwebsockets -lssl -lcrypto -lsqlite3 -lsecp256k1 \
|
||||||
|
-lcurl -lz -lpthread -lm -ldl && \
|
||||||
|
strip /build/my_app_static
|
||||||
|
|
||||||
|
# Verify it's truly static
|
||||||
|
RUN echo "=== Binary Information ===" && \
|
||||||
|
file /build/my_app_static && \
|
||||||
|
ls -lh /build/my_app_static && \
|
||||||
|
echo "=== Checking for dynamic dependencies ===" && \
|
||||||
|
(ldd /build/my_app_static 2>&1 || echo "Binary is static")
|
||||||
|
|
||||||
|
# Output stage - just the binary
|
||||||
|
FROM scratch AS output
|
||||||
|
COPY --from=builder /build/my_app_static /my_app_static
|
||||||
|
```
|
||||||
|
|
||||||
|
## Customization Guide
|
||||||
|
|
||||||
|
### 1. Adjust Dependencies
|
||||||
|
|
||||||
|
**Add dependencies** by modifying the `apk add` section:
|
||||||
|
|
||||||
|
```dockerfile
|
||||||
|
RUN apk add --no-cache \
|
||||||
|
build-base \
|
||||||
|
musl-dev \
|
||||||
|
# Add your dependencies here:
|
||||||
|
libpng-dev \
|
||||||
|
libpng-static \
|
||||||
|
libjpeg-turbo-dev \
|
||||||
|
libjpeg-turbo-static
|
||||||
|
```
|
||||||
|
|
||||||
|
**Remove unused dependencies** to speed up builds:
|
||||||
|
- Remove `libwebsockets` section if you don't need WebSocket support
|
||||||
|
- Remove `sqlite` if you don't use databases
|
||||||
|
- Remove `curl` if you don't make HTTP requests
|
||||||
|
|
||||||
|
### 2. Configure nostr_core_lib NIPs
|
||||||
|
|
||||||
|
Specify which NIPs your application needs:
|
||||||
|
|
||||||
|
```bash
|
||||||
|
./build.sh --nips=1,6,19 # Minimal: Basic protocol, keys, bech32
|
||||||
|
./build.sh --nips=1,6,13,17,19,44,59 # Full: All common NIPs
|
||||||
|
./build.sh --nips=all # Everything available
|
||||||
|
```
|
||||||
|
|
||||||
|
**Common NIP combinations:**
|
||||||
|
- **Basic client**: `1,6,19` (events, keys, bech32)
|
||||||
|
- **With encryption**: `1,6,19,44` (add modern encryption)
|
||||||
|
- **With DMs**: `1,6,17,19,44,59` (add private messages)
|
||||||
|
- **Relay/server**: `1,6,13,17,19,42,44,59` (add PoW, auth)
|
||||||
|
|
||||||
|
### 3. Modify Compilation Flags
|
||||||
|
|
||||||
|
**For your application:**
|
||||||
|
|
||||||
|
```dockerfile
|
||||||
|
RUN gcc -static -O2 -Wall -Wextra -std=c99 \
|
||||||
|
-U_FORTIFY_SOURCE -D_FORTIFY_SOURCE=0 \ # REQUIRED for MUSL
|
||||||
|
-I. -Inostr_core_lib \ # Include paths
|
||||||
|
src/*.c \ # Your source files
|
||||||
|
-o /build/my_app_static \ # Output binary
|
||||||
|
nostr_core_lib/libnostr_core_x64.a \ # Nostr library
|
||||||
|
-lwebsockets -lssl -lcrypto \ # Link libraries
|
||||||
|
-lsqlite3 -lsecp256k1 -lcurl \
|
||||||
|
-lz -lpthread -lm -ldl
|
||||||
|
```
|
||||||
|
|
||||||
|
**Debug build** (with symbols, no optimization):
|
||||||
|
|
||||||
|
```dockerfile
|
||||||
|
RUN gcc -static -g -O0 -DDEBUG \
|
||||||
|
-U_FORTIFY_SOURCE -D_FORTIFY_SOURCE=0 \
|
||||||
|
# ... rest of flags
|
||||||
|
```
|
||||||
|
|
||||||
|
### 4. Multi-Architecture Support
|
||||||
|
|
||||||
|
Build for different architectures:
|
||||||
|
|
||||||
|
```bash
|
||||||
|
# x86_64 (Intel/AMD)
|
||||||
|
docker build --platform linux/amd64 -f Dockerfile.static -t my-app-x86 .
|
||||||
|
|
||||||
|
# ARM64 (Apple Silicon, Raspberry Pi 4+)
|
||||||
|
docker build --platform linux/arm64 -f Dockerfile.static -t my-app-arm64 .
|
||||||
|
```
|
||||||
|
|
||||||
|
## Build Script Template
|
||||||
|
|
||||||
|
Create a `build_static.sh` script for convenience:
|
||||||
|
|
||||||
|
```bash
|
||||||
|
#!/bin/bash
|
||||||
|
set -e
|
||||||
|
|
||||||
|
SCRIPT_DIR="$(cd "$(dirname "${BASH_SOURCE[0]}")" && pwd)"
|
||||||
|
BUILD_DIR="$SCRIPT_DIR/build"
|
||||||
|
DOCKERFILE="$SCRIPT_DIR/Dockerfile.static"
|
||||||
|
|
||||||
|
# Detect architecture
|
||||||
|
ARCH=$(uname -m)
|
||||||
|
case "$ARCH" in
|
||||||
|
x86_64)
|
||||||
|
PLATFORM="linux/amd64"
|
||||||
|
OUTPUT_NAME="my_app_static_x86_64"
|
||||||
|
;;
|
||||||
|
aarch64|arm64)
|
||||||
|
PLATFORM="linux/arm64"
|
||||||
|
OUTPUT_NAME="my_app_static_arm64"
|
||||||
|
;;
|
||||||
|
*)
|
||||||
|
echo "Unknown architecture: $ARCH"
|
||||||
|
exit 1
|
||||||
|
;;
|
||||||
|
esac
|
||||||
|
|
||||||
|
echo "Building for platform: $PLATFORM"
|
||||||
|
mkdir -p "$BUILD_DIR"
|
||||||
|
|
||||||
|
# Build Docker image
|
||||||
|
docker build \
|
||||||
|
--platform "$PLATFORM" \
|
||||||
|
-f "$DOCKERFILE" \
|
||||||
|
-t my-app-builder:latest \
|
||||||
|
--progress=plain \
|
||||||
|
.
|
||||||
|
|
||||||
|
# Extract binary
|
||||||
|
CONTAINER_ID=$(docker create my-app-builder:latest)
|
||||||
|
docker cp "$CONTAINER_ID:/build/my_app_static" "$BUILD_DIR/$OUTPUT_NAME"
|
||||||
|
docker rm "$CONTAINER_ID"
|
||||||
|
|
||||||
|
chmod +x "$BUILD_DIR/$OUTPUT_NAME"
|
||||||
|
|
||||||
|
echo "✓ Build complete: $BUILD_DIR/$OUTPUT_NAME"
|
||||||
|
echo "✓ Size: $(du -h "$BUILD_DIR/$OUTPUT_NAME" | cut -f1)"
|
||||||
|
|
||||||
|
# Verify
|
||||||
|
if ldd "$BUILD_DIR/$OUTPUT_NAME" 2>&1 | grep -q "not a dynamic executable"; then
|
||||||
|
echo "✓ Binary is fully static"
|
||||||
|
else
|
||||||
|
echo "⚠ Warning: Binary may have dynamic dependencies"
|
||||||
|
fi
|
||||||
|
```
|
||||||
|
|
||||||
|
Make it executable:
|
||||||
|
|
||||||
|
```bash
|
||||||
|
chmod +x build_static.sh
|
||||||
|
./build_static.sh
|
||||||
|
```
|
||||||
|
|
||||||
|
## Common Issues and Solutions
|
||||||
|
|
||||||
|
### Issue 1: Fortification Errors
|
||||||
|
|
||||||
|
**Error:**
|
||||||
|
```
|
||||||
|
undefined reference to '__snprintf_chk'
|
||||||
|
undefined reference to '__fprintf_chk'
|
||||||
|
```
|
||||||
|
|
||||||
|
**Cause**: GCC's `-O2` enables fortification by default, which uses glibc-specific functions.
|
||||||
|
|
||||||
|
**Solution**: Add these flags to **all** compilation commands:
|
||||||
|
```bash
|
||||||
|
-U_FORTIFY_SOURCE -D_FORTIFY_SOURCE=0
|
||||||
|
```
|
||||||
|
|
||||||
|
This must be applied to:
|
||||||
|
1. nostr_core_lib build.sh
|
||||||
|
2. Your application compilation
|
||||||
|
3. Any other libraries you build
|
||||||
|
|
||||||
|
### Issue 2: Missing Symbols from nostr_core_lib
|
||||||
|
|
||||||
|
**Error:**
|
||||||
|
```
|
||||||
|
undefined reference to 'nostr_create_event'
|
||||||
|
undefined reference to 'nostr_sign_event'
|
||||||
|
```
|
||||||
|
|
||||||
|
**Cause**: Required NIPs not included in nostr_core_lib build.
|
||||||
|
|
||||||
|
**Solution**: Add missing NIPs:
|
||||||
|
```bash
|
||||||
|
./build.sh --nips=1,6,19 # Add the NIPs you need
|
||||||
|
```
|
||||||
|
|
||||||
|
### Issue 3: Docker Permission Denied
|
||||||
|
|
||||||
|
**Error:**
|
||||||
|
```
|
||||||
|
permission denied while trying to connect to the Docker daemon socket
|
||||||
|
```
|
||||||
|
|
||||||
|
**Solution**:
|
||||||
|
```bash
|
||||||
|
sudo usermod -aG docker $USER
|
||||||
|
newgrp docker # Or logout and login
|
||||||
|
```
|
||||||
|
|
||||||
|
### Issue 4: Binary Won't Run on Target System
|
||||||
|
|
||||||
|
**Checks**:
|
||||||
|
```bash
|
||||||
|
# 1. Verify it's static
|
||||||
|
ldd my_app_static # Should show "not a dynamic executable"
|
||||||
|
|
||||||
|
# 2. Check architecture
|
||||||
|
file my_app_static # Should match target system
|
||||||
|
|
||||||
|
# 3. Test on different distributions
|
||||||
|
docker run --rm -v $(pwd):/app alpine:latest /app/my_app_static --version
|
||||||
|
docker run --rm -v $(pwd):/app ubuntu:latest /app/my_app_static --version
|
||||||
|
```
|
||||||
|
|
||||||
|
## Project Structure Example
|
||||||
|
|
||||||
|
Organize your project for easy static builds:
|
||||||
|
|
||||||
|
```
|
||||||
|
my-nostr-app/
|
||||||
|
├── src/
|
||||||
|
│ ├── main.c
|
||||||
|
│ ├── handlers.c
|
||||||
|
│ └── utils.c
|
||||||
|
├── nostr_core_lib/ # Git submodule
|
||||||
|
├── nostr_login_lite/ # Git submodule (if used)
|
||||||
|
├── Dockerfile.static # Static build Dockerfile
|
||||||
|
├── build_static.sh # Build script
|
||||||
|
├── Makefile # Regular build
|
||||||
|
└── README.md
|
||||||
|
```
|
||||||
|
|
||||||
|
### Makefile Integration
|
||||||
|
|
||||||
|
Add static build targets to your Makefile:
|
||||||
|
|
||||||
|
```makefile
|
||||||
|
# Regular dynamic build
|
||||||
|
all: my_app
|
||||||
|
|
||||||
|
my_app: src/*.c
|
||||||
|
gcc -O2 src/*.c -o my_app \
|
||||||
|
nostr_core_lib/libnostr_core_x64.a \
|
||||||
|
-lssl -lcrypto -lsecp256k1 -lz -lpthread -lm
|
||||||
|
|
||||||
|
# Static MUSL build via Docker
|
||||||
|
static:
|
||||||
|
./build_static.sh
|
||||||
|
|
||||||
|
# Clean
|
||||||
|
clean:
|
||||||
|
rm -f my_app build/my_app_static_*
|
||||||
|
|
||||||
|
.PHONY: all static clean
|
||||||
|
```
|
||||||
|
|
||||||
|
## Deployment
|
||||||
|
|
||||||
|
### Single Binary Deployment
|
||||||
|
|
||||||
|
```bash
|
||||||
|
# Copy to server
|
||||||
|
scp build/my_app_static_x86_64 user@server:/opt/my-app/
|
||||||
|
|
||||||
|
# Run (no dependencies needed!)
|
||||||
|
ssh user@server
|
||||||
|
/opt/my-app/my_app_static_x86_64
|
||||||
|
```
|
||||||
|
|
||||||
|
### SystemD Service
|
||||||
|
|
||||||
|
```ini
|
||||||
|
[Unit]
|
||||||
|
Description=My Nostr Application
|
||||||
|
After=network.target
|
||||||
|
|
||||||
|
[Service]
|
||||||
|
Type=simple
|
||||||
|
User=myapp
|
||||||
|
WorkingDirectory=/opt/my-app
|
||||||
|
ExecStart=/opt/my-app/my_app_static_x86_64
|
||||||
|
Restart=always
|
||||||
|
RestartSec=5
|
||||||
|
|
||||||
|
[Install]
|
||||||
|
WantedBy=multi-user.target
|
||||||
|
```
|
||||||
|
|
||||||
|
### Docker Container (Minimal)
|
||||||
|
|
||||||
|
```dockerfile
|
||||||
|
FROM scratch
|
||||||
|
COPY my_app_static_x86_64 /app
|
||||||
|
ENTRYPOINT ["/app"]
|
||||||
|
```
|
||||||
|
|
||||||
|
Build and run:
|
||||||
|
```bash
|
||||||
|
docker build -t my-app:latest .
|
||||||
|
docker run --rm my-app:latest --help
|
||||||
|
```
|
||||||
|
|
||||||
|
## Reusing c-relay Files
|
||||||
|
|
||||||
|
You can directly copy these files from c-relay:
|
||||||
|
|
||||||
|
### 1. Dockerfile.alpine-musl
|
||||||
|
```bash
|
||||||
|
cp /path/to/c-relay/Dockerfile.alpine-musl ./Dockerfile.static
|
||||||
|
```
|
||||||
|
|
||||||
|
Then customize:
|
||||||
|
- Change binary name (line 125)
|
||||||
|
- Adjust source files (line 122-124)
|
||||||
|
- Modify include paths (line 120-121)
|
||||||
|
|
||||||
|
### 2. build_static.sh
|
||||||
|
```bash
|
||||||
|
cp /path/to/c-relay/build_static.sh ./
|
||||||
|
```
|
||||||
|
|
||||||
|
Then customize:
|
||||||
|
- Change `OUTPUT_NAME` variable (lines 66, 70)
|
||||||
|
- Update Docker image name (line 98)
|
||||||
|
- Modify verification commands (lines 180-184)
|
||||||
|
|
||||||
|
### 3. .dockerignore (Optional)
|
||||||
|
```bash
|
||||||
|
cp /path/to/c-relay/.dockerignore ./
|
||||||
|
```
|
||||||
|
|
||||||
|
Helps speed up Docker builds by excluding unnecessary files.
|
||||||
|
|
||||||
|
## Best Practices
|
||||||
|
|
||||||
|
1. **Version Control**: Commit your Dockerfile and build script
|
||||||
|
2. **Tag Builds**: Include git commit hash in binary version
|
||||||
|
3. **Test Thoroughly**: Verify on multiple distributions
|
||||||
|
4. **Document Dependencies**: List required NIPs and libraries
|
||||||
|
5. **Automate**: Use CI/CD to build on every commit
|
||||||
|
6. **Archive Binaries**: Keep old versions for rollback
|
||||||
|
|
||||||
|
## Performance Comparison
|
||||||
|
|
||||||
|
| Metric | MUSL Static | glibc Dynamic |
|
||||||
|
|--------|-------------|---------------|
|
||||||
|
| Binary Size | 7-10 MB | 2-3 MB + libs |
|
||||||
|
| Startup Time | ~50ms | ~40ms |
|
||||||
|
| Memory Usage | Similar | Similar |
|
||||||
|
| Portability | ✓ Universal | ✗ System-dependent |
|
||||||
|
| Deployment | Single file | Binary + libraries |
|
||||||
|
|
||||||
|
## References
|
||||||
|
|
||||||
|
- [MUSL libc](https://musl.libc.org/)
|
||||||
|
- [Alpine Linux](https://alpinelinux.org/)
|
||||||
|
- [nostr_core_lib](https://github.com/chebizarro/nostr_core_lib)
|
||||||
|
- [Static Linking Best Practices](https://www.musl-libc.org/faq.html)
|
||||||
|
- [c-relay Implementation](./docs/musl_static_build.md)
|
||||||
|
|
||||||
|
## Example: Minimal Nostr Client
|
||||||
|
|
||||||
|
Here's a complete example of building a minimal Nostr client:
|
||||||
|
|
||||||
|
```c
|
||||||
|
// minimal_client.c
|
||||||
|
#include "nostr_core/nostr_core.h"
|
||||||
|
#include <stdio.h>
|
||||||
|
|
||||||
|
int main() {
|
||||||
|
// Generate keypair
|
||||||
|
char nsec[64], npub[64];
|
||||||
|
nostr_generate_keypair(nsec, npub);
|
||||||
|
|
||||||
|
printf("Generated keypair:\n");
|
||||||
|
printf("Private key (nsec): %s\n", nsec);
|
||||||
|
printf("Public key (npub): %s\n", npub);
|
||||||
|
|
||||||
|
// Create event
|
||||||
|
cJSON *event = nostr_create_event(1, "Hello, Nostr!", NULL);
|
||||||
|
nostr_sign_event(event, nsec);
|
||||||
|
|
||||||
|
char *json = cJSON_Print(event);
|
||||||
|
printf("\nSigned event:\n%s\n", json);
|
||||||
|
|
||||||
|
free(json);
|
||||||
|
cJSON_Delete(event);
|
||||||
|
return 0;
|
||||||
|
}
|
||||||
|
```
|
||||||
|
|
||||||
|
**Dockerfile.static:**
|
||||||
|
```dockerfile
|
||||||
|
FROM alpine:3.19 AS builder
|
||||||
|
RUN apk add --no-cache build-base musl-dev git autoconf automake libtool \
|
||||||
|
openssl-dev openssl-libs-static zlib-dev zlib-static
|
||||||
|
|
||||||
|
WORKDIR /build
|
||||||
|
|
||||||
|
# Build secp256k1
|
||||||
|
RUN cd /tmp && git clone https://github.com/bitcoin-core/secp256k1.git && \
|
||||||
|
cd secp256k1 && ./autogen.sh && \
|
||||||
|
./configure --enable-static --disable-shared --prefix=/usr CFLAGS="-fPIC" && \
|
||||||
|
make -j$(nproc) && make install
|
||||||
|
|
||||||
|
# Copy and build nostr_core_lib
|
||||||
|
COPY nostr_core_lib /build/nostr_core_lib/
|
||||||
|
RUN cd nostr_core_lib && \
|
||||||
|
sed -i 's/CFLAGS="-Wall/CFLAGS="-U_FORTIFY_SOURCE -D_FORTIFY_SOURCE=0 -Wall/' build.sh && \
|
||||||
|
./build.sh --nips=1,6,19
|
||||||
|
|
||||||
|
# Build application
|
||||||
|
COPY minimal_client.c /build/
|
||||||
|
RUN gcc -static -O2 -Wall -std=c99 \
|
||||||
|
-U_FORTIFY_SOURCE -D_FORTIFY_SOURCE=0 \
|
||||||
|
-Inostr_core_lib -Inostr_core_lib/nostr_core -Inostr_core_lib/cjson \
|
||||||
|
minimal_client.c -o /build/minimal_client_static \
|
||||||
|
nostr_core_lib/libnostr_core_x64.a \
|
||||||
|
-lssl -lcrypto -lsecp256k1 -lz -lpthread -lm -ldl && \
|
||||||
|
strip /build/minimal_client_static
|
||||||
|
|
||||||
|
FROM scratch
|
||||||
|
COPY --from=builder /build/minimal_client_static /minimal_client_static
|
||||||
|
```
|
||||||
|
|
||||||
|
**Build and run:**
|
||||||
|
```bash
|
||||||
|
docker build -f Dockerfile.static -t minimal-client .
|
||||||
|
docker create --name temp minimal-client
|
||||||
|
docker cp temp:/minimal_client_static ./
|
||||||
|
docker rm temp
|
||||||
|
|
||||||
|
./minimal_client_static
|
||||||
|
```
|
||||||
|
|
||||||
|
## Conclusion
|
||||||
|
|
||||||
|
Static MUSL binaries provide the best portability for C applications. While they're slightly larger than dynamic binaries, the benefits of zero dependencies and universal compatibility make them ideal for:
|
||||||
|
|
||||||
|
- Server deployments across different Linux distributions
|
||||||
|
- Embedded systems and IoT devices
|
||||||
|
- Docker containers (FROM scratch)
|
||||||
|
- Distribution to users without dependency management
|
||||||
|
- Long-term archival and reproducibility
|
||||||
|
|
||||||
|
Follow this guide to create portable, self-contained binaries for your Nostr applications!
|
||||||
@@ -81,6 +81,29 @@ echo "Building for platform: $PLATFORM"
|
|||||||
echo "Output binary: $OUTPUT_NAME"
|
echo "Output binary: $OUTPUT_NAME"
|
||||||
echo ""
|
echo ""
|
||||||
|
|
||||||
|
# Check if Alpine base image is cached
|
||||||
|
echo "Checking for cached Alpine Docker image..."
|
||||||
|
if ! docker images alpine:3.19 --format "{{.Repository}}:{{.Tag}}" | grep -q "alpine:3.19"; then
|
||||||
|
echo "⚠ Alpine 3.19 image not found in cache"
|
||||||
|
echo "Attempting to pull Alpine 3.19 image..."
|
||||||
|
if ! docker pull alpine:3.19; then
|
||||||
|
echo ""
|
||||||
|
echo "ERROR: Failed to pull Alpine 3.19 image"
|
||||||
|
echo "This is required for the static build."
|
||||||
|
echo ""
|
||||||
|
echo "Possible solutions:"
|
||||||
|
echo " 1. Check your internet connection"
|
||||||
|
echo " 2. Try again later (Docker Hub may be temporarily unavailable)"
|
||||||
|
echo " 3. If you have IPv6 issues, disable IPv6 for Docker"
|
||||||
|
echo ""
|
||||||
|
exit 1
|
||||||
|
fi
|
||||||
|
echo "✓ Alpine 3.19 image pulled successfully"
|
||||||
|
else
|
||||||
|
echo "✓ Alpine 3.19 image found in cache"
|
||||||
|
fi
|
||||||
|
echo ""
|
||||||
|
|
||||||
# Build the Docker image
|
# Build the Docker image
|
||||||
echo "=========================================="
|
echo "=========================================="
|
||||||
echo "Step 1: Building Alpine Docker image"
|
echo "Step 1: Building Alpine Docker image"
|
||||||
|
|||||||
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
@@ -3,17 +3,17 @@
|
|||||||
# Copy the binary to the deployment location
|
# Copy the binary to the deployment location
|
||||||
cp build/c_relay_x86 ~/Storage/c_relay/crelay
|
cp build/c_relay_x86 ~/Storage/c_relay/crelay
|
||||||
|
|
||||||
# Copy the local service file to systemd
|
# Copy the service file to systemd (use the main service file)
|
||||||
# sudo cp systemd/c-relay-local.service /etc/systemd/system/
|
sudo cp systemd/c-relay.service /etc/systemd/system/c-relay-local.service
|
||||||
|
|
||||||
# Reload systemd daemon to pick up the new service
|
# Reload systemd daemon to pick up the new service
|
||||||
# sudo systemctl daemon-reload
|
sudo systemctl daemon-reload
|
||||||
|
|
||||||
# Enable the service (if not already enabled)
|
# Enable the service (if not already enabled)
|
||||||
# sudo systemctl enable c-relay-local.service
|
sudo systemctl enable c-relay-local.service
|
||||||
|
|
||||||
# Restart the service
|
# Restart the service
|
||||||
# sudo systemctl restart c-relay-local.service
|
sudo systemctl restart c-relay-local.service
|
||||||
|
|
||||||
# Show service status
|
# Show service status
|
||||||
# sudo systemctl status c-relay-local.service --no-pager -l
|
sudo systemctl status c-relay-local.service --no-pager -l
|
||||||
|
|||||||
532
docs/subscription_cleanup_simplified.md
Normal file
532
docs/subscription_cleanup_simplified.md
Normal file
@@ -0,0 +1,532 @@
|
|||||||
|
# Subscription Cleanup - Simplified Design
|
||||||
|
|
||||||
|
## Problem Summary
|
||||||
|
|
||||||
|
The c-relay Nostr relay experienced severe performance degradation (90-100% CPU) due to subscription accumulation in the database. Investigation revealed **323,644 orphaned subscriptions** that were never properly closed when WebSocket connections dropped.
|
||||||
|
|
||||||
|
## Solution: Two-Component Approach
|
||||||
|
|
||||||
|
This simplified design focuses on two pragmatic solutions that align with Nostr's stateless design:
|
||||||
|
|
||||||
|
1. **Startup Cleanup**: Close all subscriptions on relay restart
|
||||||
|
2. **Connection Age Limit**: Disconnect clients after a configurable time period
|
||||||
|
|
||||||
|
Both solutions force clients to reconnect and re-establish subscriptions, which is standard Nostr behavior.
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## Component 1: Startup Cleanup
|
||||||
|
|
||||||
|
### Purpose
|
||||||
|
Ensure clean state on every relay restart by closing all subscriptions in the database.
|
||||||
|
|
||||||
|
### Implementation
|
||||||
|
|
||||||
|
**File:** [`src/subscriptions.c`](src/subscriptions.c)
|
||||||
|
|
||||||
|
**New Function:**
|
||||||
|
```c
|
||||||
|
void cleanup_all_subscriptions_on_startup(void) {
|
||||||
|
if (!g_db) {
|
||||||
|
DEBUG_ERROR("Database not initialized for startup cleanup");
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
DEBUG_LOG("Startup cleanup: Marking all active subscriptions as disconnected");
|
||||||
|
|
||||||
|
// Mark all 'created' subscriptions as disconnected
|
||||||
|
const char* update_sql =
|
||||||
|
"UPDATE subscriptions "
|
||||||
|
"SET ended_at = strftime('%s', 'now') "
|
||||||
|
"WHERE event_type = 'created' AND ended_at IS NULL";
|
||||||
|
|
||||||
|
sqlite3_stmt* stmt;
|
||||||
|
int rc = sqlite3_prepare_v2(g_db, update_sql, -1, &stmt, NULL);
|
||||||
|
if (rc != SQLITE_OK) {
|
||||||
|
DEBUG_ERROR("Failed to prepare startup cleanup query: %s", sqlite3_errmsg(g_db));
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
rc = sqlite3_step(stmt);
|
||||||
|
int updated_count = sqlite3_changes(g_db);
|
||||||
|
sqlite3_finalize(stmt);
|
||||||
|
|
||||||
|
if (updated_count > 0) {
|
||||||
|
// Log a single 'disconnected' event for the startup cleanup
|
||||||
|
const char* insert_sql =
|
||||||
|
"INSERT INTO subscriptions (subscription_id, wsi_pointer, client_ip, event_type) "
|
||||||
|
"VALUES ('startup_cleanup', '', 'system', 'disconnected')";
|
||||||
|
|
||||||
|
rc = sqlite3_prepare_v2(g_db, insert_sql, -1, &stmt, NULL);
|
||||||
|
if (rc == SQLITE_OK) {
|
||||||
|
sqlite3_step(stmt);
|
||||||
|
sqlite3_finalize(stmt);
|
||||||
|
}
|
||||||
|
|
||||||
|
DEBUG_LOG("Startup cleanup: Marked %d subscriptions as disconnected", updated_count);
|
||||||
|
} else {
|
||||||
|
DEBUG_LOG("Startup cleanup: No active subscriptions found");
|
||||||
|
}
|
||||||
|
}
|
||||||
|
```
|
||||||
|
|
||||||
|
**Integration Point:** [`src/main.c:1810`](src/main.c:1810)
|
||||||
|
|
||||||
|
```c
|
||||||
|
// Initialize subscription manager mutexes
|
||||||
|
if (pthread_mutex_init(&g_subscription_manager.subscriptions_lock, NULL) != 0) {
|
||||||
|
DEBUG_ERROR("Failed to initialize subscriptions mutex");
|
||||||
|
sqlite3_close(g_db);
|
||||||
|
return 1;
|
||||||
|
}
|
||||||
|
|
||||||
|
if (pthread_mutex_init(&g_subscription_manager.ip_tracking_lock, NULL) != 0) {
|
||||||
|
DEBUG_ERROR("Failed to initialize IP tracking mutex");
|
||||||
|
pthread_mutex_destroy(&g_subscription_manager.subscriptions_lock);
|
||||||
|
sqlite3_close(g_db);
|
||||||
|
return 1;
|
||||||
|
}
|
||||||
|
|
||||||
|
// **NEW: Startup cleanup - close all subscriptions**
|
||||||
|
cleanup_all_subscriptions_on_startup();
|
||||||
|
|
||||||
|
// Start WebSocket relay server
|
||||||
|
DEBUG_LOG("Starting WebSocket relay server...");
|
||||||
|
if (start_websocket_relay(port_override, strict_port) != 0) {
|
||||||
|
DEBUG_ERROR("Failed to start WebSocket relay");
|
||||||
|
// ... cleanup code
|
||||||
|
}
|
||||||
|
```
|
||||||
|
|
||||||
|
### Benefits
|
||||||
|
- **Immediate relief**: Restart relay to fix subscription issues
|
||||||
|
- **Clean slate**: Every restart starts with zero active subscriptions
|
||||||
|
- **Simple**: Single SQL UPDATE statement
|
||||||
|
- **Nostr-aligned**: Clients are designed to reconnect after relay restart
|
||||||
|
- **No configuration needed**: Always runs on startup
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## Component 2: Connection Age Limit
|
||||||
|
|
||||||
|
### Purpose
|
||||||
|
Automatically disconnect clients after a configurable time period, forcing them to reconnect and re-establish subscriptions.
|
||||||
|
|
||||||
|
### Why Disconnect Instead of Just Closing Subscriptions?
|
||||||
|
|
||||||
|
**Option 1: Send CLOSED message (keep connection)**
|
||||||
|
- ❌ Not all clients handle `CLOSED` messages properly
|
||||||
|
- ❌ Silent failure - client thinks it's subscribed but isn't
|
||||||
|
- ❌ Partial cleanup - connection still consumes resources
|
||||||
|
- ❌ More complex to implement
|
||||||
|
|
||||||
|
**Option 2: Disconnect client entirely (force reconnection)** ✅
|
||||||
|
- ✅ Universal compatibility - all clients handle WebSocket reconnection
|
||||||
|
- ✅ Complete resource cleanup (memory, file descriptors, etc.)
|
||||||
|
- ✅ Simple implementation - single operation
|
||||||
|
- ✅ Well-tested code path (same as network interruptions)
|
||||||
|
- ✅ Forces re-authentication if needed
|
||||||
|
|
||||||
|
### Implementation
|
||||||
|
|
||||||
|
**File:** [`src/websockets.c`](src/websockets.c)
|
||||||
|
|
||||||
|
**New Function:**
|
||||||
|
```c
|
||||||
|
/**
|
||||||
|
* Check connection age and disconnect clients that have been connected too long.
|
||||||
|
* This forces clients to reconnect and re-establish subscriptions.
|
||||||
|
*
|
||||||
|
* Uses libwebsockets' lws_vhost_foreach_wsi() to iterate through all active
|
||||||
|
* connections and checks their connection_established timestamp from per_session_data.
|
||||||
|
*/
|
||||||
|
void check_connection_age(int max_connection_seconds) {
|
||||||
|
if (max_connection_seconds <= 0 || !ws_context) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
time_t now = time(NULL);
|
||||||
|
time_t cutoff = now - max_connection_seconds;
|
||||||
|
|
||||||
|
// Get the default vhost
|
||||||
|
struct lws_vhost *vhost = lws_get_vhost_by_name(ws_context, "default");
|
||||||
|
if (!vhost) {
|
||||||
|
DEBUG_ERROR("Failed to get vhost for connection age check");
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
// Iterate through all active WebSocket connections
|
||||||
|
// Note: lws_vhost_foreach_wsi() calls our callback for each connection
|
||||||
|
struct lws *wsi = NULL;
|
||||||
|
while ((wsi = lws_vhost_foreach_wsi(vhost, wsi)) != NULL) {
|
||||||
|
// Get per-session data which contains connection_established timestamp
|
||||||
|
struct per_session_data *pss = (struct per_session_data *)lws_wsi_user(wsi);
|
||||||
|
|
||||||
|
if (pss && pss->connection_established > 0) {
|
||||||
|
// Check if connection is older than cutoff
|
||||||
|
if (pss->connection_established < cutoff) {
|
||||||
|
// Connection is too old - close it
|
||||||
|
long age_seconds = now - pss->connection_established;
|
||||||
|
|
||||||
|
DEBUG_LOG("Closing connection from %s (age: %lds, limit: %ds)",
|
||||||
|
pss->client_ip, age_seconds, max_connection_seconds);
|
||||||
|
|
||||||
|
// Close with normal status and reason message
|
||||||
|
lws_close_reason(wsi, LWS_CLOSE_STATUS_NORMAL,
|
||||||
|
(unsigned char *)"connection age limit", 21);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
```
|
||||||
|
|
||||||
|
**Key Implementation Details:**
|
||||||
|
|
||||||
|
1. **No database needed**: Active connections are tracked by libwebsockets itself
|
||||||
|
2. **Uses existing timestamp**: `pss->connection_established` is already set on line 456 of websockets.c
|
||||||
|
3. **Built-in iterator**: `lws_vhost_foreach_wsi()` safely iterates through all active connections
|
||||||
|
4. **Per-session data**: Each connection's `per_session_data` is accessible via `lws_wsi_user()`
|
||||||
|
5. **Safe closure**: `lws_close_reason()` properly closes the WebSocket with a status code and message
|
||||||
|
|
||||||
|
**Integration Point:** [`src/websockets.c:2176`](src/websockets.c:2176) - in existing event loop
|
||||||
|
|
||||||
|
```c
|
||||||
|
// Main event loop with proper signal handling
|
||||||
|
while (g_server_running && !g_shutdown_flag) {
|
||||||
|
int result = lws_service(ws_context, 1000);
|
||||||
|
|
||||||
|
if (result < 0) {
|
||||||
|
DEBUG_ERROR("libwebsockets service error");
|
||||||
|
break;
|
||||||
|
}
|
||||||
|
|
||||||
|
// Check if it's time to post status update
|
||||||
|
time_t current_time = time(NULL);
|
||||||
|
int status_post_hours = get_config_int("kind_1_status_posts_hours", 0);
|
||||||
|
|
||||||
|
if (status_post_hours > 0) {
|
||||||
|
int seconds_interval = status_post_hours * 3600;
|
||||||
|
if (current_time - last_status_post_time >= seconds_interval) {
|
||||||
|
last_status_post_time = current_time;
|
||||||
|
generate_and_post_status_event();
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// **NEW: Check for connection age limit**
|
||||||
|
int max_connection_seconds = get_config_int("max_connection_seconds", 86400); // Default 24 hours
|
||||||
|
if (max_connection_seconds > 0) {
|
||||||
|
check_connection_age(max_connection_seconds);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
```
|
||||||
|
|
||||||
|
### Configuration
|
||||||
|
|
||||||
|
**Parameter:** `max_connection_seconds`
|
||||||
|
- **Default:** `86400` (24 hours)
|
||||||
|
- **Range:** `0` = disabled, `>0` = disconnect after X seconds
|
||||||
|
- **Units:** Seconds (for consistency with other time-based configs)
|
||||||
|
|
||||||
|
**Example configurations:**
|
||||||
|
```json
|
||||||
|
{
|
||||||
|
"max_connection_seconds": 86400 // 86400 seconds = 24 hours (default)
|
||||||
|
}
|
||||||
|
```
|
||||||
|
|
||||||
|
```json
|
||||||
|
{
|
||||||
|
"max_connection_seconds": 43200 // 43200 seconds = 12 hours
|
||||||
|
}
|
||||||
|
```
|
||||||
|
|
||||||
|
```json
|
||||||
|
{
|
||||||
|
"max_connection_seconds": 3600 // 3600 seconds = 1 hour
|
||||||
|
}
|
||||||
|
```
|
||||||
|
|
||||||
|
```json
|
||||||
|
{
|
||||||
|
"max_connection_seconds": 0 // Disabled
|
||||||
|
}
|
||||||
|
```
|
||||||
|
|
||||||
|
### Client Behavior
|
||||||
|
|
||||||
|
When disconnected due to age limit, clients will:
|
||||||
|
1. Detect WebSocket closure
|
||||||
|
2. Wait briefly (exponential backoff)
|
||||||
|
3. Reconnect to relay
|
||||||
|
4. Re-authenticate if needed (NIP-42)
|
||||||
|
5. Re-establish all subscriptions
|
||||||
|
6. Resume normal operation
|
||||||
|
|
||||||
|
This is **exactly what happens** during network interruptions, so it's a well-tested code path in all Nostr clients.
|
||||||
|
|
||||||
|
### Benefits
|
||||||
|
- **No new threads**: Uses existing event loop
|
||||||
|
- **Minimal overhead**: Check runs once per second (same as `lws_service`)
|
||||||
|
- **Simple implementation**: Iterate through active connections
|
||||||
|
- **Consistent pattern**: Matches existing status post checking
|
||||||
|
- **Universal compatibility**: All clients handle reconnection
|
||||||
|
- **Complete cleanup**: Frees all resources associated with connection
|
||||||
|
- **Configurable**: Can be adjusted per relay needs or disabled entirely
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## Implementation Plan
|
||||||
|
|
||||||
|
### Phase 1: Startup Cleanup (1-2 hours)
|
||||||
|
|
||||||
|
1. **Add `cleanup_all_subscriptions_on_startup()` function**
|
||||||
|
- File: [`src/subscriptions.c`](src/subscriptions.c)
|
||||||
|
- SQL UPDATE to mark all active subscriptions as disconnected
|
||||||
|
- Add logging for cleanup count
|
||||||
|
|
||||||
|
2. **Integrate in main()**
|
||||||
|
- File: [`src/main.c:1810`](src/main.c:1810)
|
||||||
|
- Call after mutex initialization, before WebSocket server start
|
||||||
|
|
||||||
|
3. **Test**
|
||||||
|
- Create subscriptions in database
|
||||||
|
- Restart relay
|
||||||
|
- Verify all subscriptions marked as disconnected
|
||||||
|
- Verify `active_subscriptions_log` shows 0 subscriptions
|
||||||
|
|
||||||
|
**Estimated Time:** 1-2 hours
|
||||||
|
|
||||||
|
### Phase 2: Connection Age Limit (2-3 hours)
|
||||||
|
|
||||||
|
1. **Add `check_connection_age()` function**
|
||||||
|
- File: [`src/websockets.c`](src/websockets.c)
|
||||||
|
- Iterate through active connections
|
||||||
|
- Close connections older than limit
|
||||||
|
|
||||||
|
2. **Integrate in event loop**
|
||||||
|
- File: [`src/websockets.c:2176`](src/websockets.c:2176)
|
||||||
|
- Add check after status post check
|
||||||
|
- Use same pattern as status posts
|
||||||
|
|
||||||
|
3. **Add configuration parameter**
|
||||||
|
- Add `max_connection_seconds` to default config
|
||||||
|
- Default: 86400 (24 hours)
|
||||||
|
|
||||||
|
4. **Test**
|
||||||
|
- Connect client
|
||||||
|
- Wait for timeout (or reduce timeout for testing)
|
||||||
|
- Verify client disconnected
|
||||||
|
- Verify client reconnects automatically
|
||||||
|
- Verify subscriptions re-established
|
||||||
|
|
||||||
|
**Estimated Time:** 2-3 hours
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## Testing Strategy
|
||||||
|
|
||||||
|
### Startup Cleanup Tests
|
||||||
|
|
||||||
|
```bash
|
||||||
|
# Test 1: Clean startup with existing subscriptions
|
||||||
|
- Create 100 active subscriptions in database
|
||||||
|
- Restart relay
|
||||||
|
- Verify all subscriptions marked as disconnected
|
||||||
|
- Verify active_subscriptions_log shows 0 subscriptions
|
||||||
|
|
||||||
|
# Test 2: Clean startup with no subscriptions
|
||||||
|
- Start relay with empty database
|
||||||
|
- Verify no errors
|
||||||
|
- Verify startup cleanup logs "No active subscriptions found"
|
||||||
|
|
||||||
|
# Test 3: Clients reconnect after restart
|
||||||
|
- Create subscriptions before restart
|
||||||
|
- Restart relay
|
||||||
|
- Connect clients and create new subscriptions
|
||||||
|
- Verify new subscriptions tracked correctly
|
||||||
|
```
|
||||||
|
|
||||||
|
### Connection Age Limit Tests
|
||||||
|
|
||||||
|
```bash
|
||||||
|
# Test 1: Connection disconnected after timeout
|
||||||
|
- Set max_connection_seconds to 60 (for testing)
|
||||||
|
- Connect client
|
||||||
|
- Wait 61 seconds
|
||||||
|
- Verify client disconnected
|
||||||
|
- Verify client reconnects automatically
|
||||||
|
|
||||||
|
# Test 2: Subscriptions re-established after reconnection
|
||||||
|
- Connect client with subscriptions
|
||||||
|
- Wait for timeout
|
||||||
|
- Verify client reconnects
|
||||||
|
- Verify subscriptions re-established
|
||||||
|
- Verify events still delivered
|
||||||
|
|
||||||
|
# Test 3: Disabled when set to 0
|
||||||
|
- Set max_connection_seconds to 0
|
||||||
|
- Connect client
|
||||||
|
- Wait extended period
|
||||||
|
- Verify client NOT disconnected
|
||||||
|
```
|
||||||
|
|
||||||
|
### Integration Tests
|
||||||
|
|
||||||
|
```bash
|
||||||
|
# Test 1: Combined behavior
|
||||||
|
- Start relay (startup cleanup runs)
|
||||||
|
- Connect multiple clients
|
||||||
|
- Create subscriptions
|
||||||
|
- Wait for connection timeout
|
||||||
|
- Verify clients reconnect
|
||||||
|
- Restart relay
|
||||||
|
- Verify clean state
|
||||||
|
|
||||||
|
# Test 2: Load test
|
||||||
|
- Connect 100 clients
|
||||||
|
- Each creates 5 subscriptions
|
||||||
|
- Wait for connection timeout
|
||||||
|
- Verify all clients reconnect
|
||||||
|
- Verify all subscriptions re-established
|
||||||
|
- Monitor CPU usage (should remain low)
|
||||||
|
```
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## Success Criteria
|
||||||
|
|
||||||
|
### Component 1: Startup Cleanup
|
||||||
|
- ✅ Relay starts with zero active subscriptions
|
||||||
|
- ✅ All previous subscriptions marked as disconnected on startup
|
||||||
|
- ✅ Clients successfully reconnect and re-establish subscriptions
|
||||||
|
- ✅ Relay restart can be used as emergency fix for subscription issues
|
||||||
|
- ✅ No errors during startup cleanup process
|
||||||
|
|
||||||
|
### Component 2: Connection Age Limit
|
||||||
|
- ✅ Clients disconnected after configured time period
|
||||||
|
- ✅ Clients automatically reconnect
|
||||||
|
- ✅ Subscriptions re-established after reconnection
|
||||||
|
- ✅ No impact on relay performance
|
||||||
|
- ✅ Configuration parameter works correctly (including disabled state)
|
||||||
|
|
||||||
|
### Overall Success
|
||||||
|
- ✅ CPU usage remains low (<10%)
|
||||||
|
- ✅ No orphaned subscriptions accumulate
|
||||||
|
- ✅ Database size remains stable
|
||||||
|
- ✅ No manual intervention required
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## Configuration Reference
|
||||||
|
|
||||||
|
**New Configuration Parameters:**
|
||||||
|
|
||||||
|
```json
|
||||||
|
{
|
||||||
|
"max_connection_seconds": 86400
|
||||||
|
}
|
||||||
|
```
|
||||||
|
|
||||||
|
**Recommended Settings:**
|
||||||
|
|
||||||
|
- **Production:**
|
||||||
|
- `max_connection_seconds: 86400` (24 hours)
|
||||||
|
|
||||||
|
- **Development:**
|
||||||
|
- `max_connection_seconds: 3600` (1 hour for faster testing)
|
||||||
|
|
||||||
|
- **High-traffic:**
|
||||||
|
- `max_connection_seconds: 43200` (12 hours)
|
||||||
|
|
||||||
|
- **Disabled:**
|
||||||
|
- `max_connection_seconds: 0`
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## Rollback Plan
|
||||||
|
|
||||||
|
If issues arise after deployment:
|
||||||
|
|
||||||
|
1. **Disable connection age limit:**
|
||||||
|
- Set `max_connection_seconds: 0` in config
|
||||||
|
- Restart relay
|
||||||
|
- Monitor for stability
|
||||||
|
|
||||||
|
2. **Revert code changes:**
|
||||||
|
- Remove `check_connection_age()` call from event loop
|
||||||
|
- Remove `cleanup_all_subscriptions_on_startup()` call from main
|
||||||
|
- Restart relay
|
||||||
|
|
||||||
|
3. **Database cleanup (if needed):**
|
||||||
|
- Manually clean up orphaned subscriptions using SQL:
|
||||||
|
```sql
|
||||||
|
UPDATE subscriptions
|
||||||
|
SET ended_at = strftime('%s', 'now')
|
||||||
|
WHERE event_type = 'created' AND ended_at IS NULL;
|
||||||
|
```
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## Comparison with Original Design
|
||||||
|
|
||||||
|
### Original Design (5 Components)
|
||||||
|
1. Startup cleanup
|
||||||
|
2. Fix WebSocket disconnection logging
|
||||||
|
3. Enhance subscription removal with reason parameter
|
||||||
|
4. Periodic cleanup task (background thread)
|
||||||
|
5. Optimize database VIEW
|
||||||
|
6. Subscription expiration (optional)
|
||||||
|
|
||||||
|
### Simplified Design (2 Components)
|
||||||
|
1. Startup cleanup
|
||||||
|
2. Connection age limit
|
||||||
|
|
||||||
|
### Why Simplified is Better
|
||||||
|
|
||||||
|
**Advantages:**
|
||||||
|
- **Simpler**: 2 components vs 5-6 components
|
||||||
|
- **Faster to implement**: 3-5 hours vs 11-17 hours
|
||||||
|
- **Easier to maintain**: Less code, fewer moving parts
|
||||||
|
- **More reliable**: Fewer potential failure points
|
||||||
|
- **Nostr-aligned**: Leverages client reconnection behavior
|
||||||
|
- **No new threads**: Uses existing event loop
|
||||||
|
- **Universal compatibility**: All clients handle reconnection
|
||||||
|
|
||||||
|
**What We're Not Losing:**
|
||||||
|
- Startup cleanup is identical in both designs
|
||||||
|
- Connection age limit achieves the same goal as periodic cleanup + expiration
|
||||||
|
- Disconnection forces complete cleanup (better than just logging)
|
||||||
|
- Database VIEW optimization not needed if subscriptions don't accumulate
|
||||||
|
|
||||||
|
**Trade-offs:**
|
||||||
|
- Less granular logging (but simpler)
|
||||||
|
- No historical subscription analytics (but cleaner database)
|
||||||
|
- Clients must reconnect periodically (but this is standard Nostr behavior)
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## Conclusion
|
||||||
|
|
||||||
|
This simplified design solves the subscription accumulation problem with two pragmatic solutions:
|
||||||
|
|
||||||
|
1. **Startup cleanup** ensures every relay restart starts with a clean slate
|
||||||
|
2. **Connection age limit** prevents long-term accumulation by forcing periodic reconnection
|
||||||
|
|
||||||
|
Both solutions align with Nostr's stateless design where clients are expected to handle reconnection. The implementation is simple, maintainable, and leverages existing code patterns.
|
||||||
|
|
||||||
|
**Key Benefits:**
|
||||||
|
- ✅ Solves the root problem (subscription accumulation)
|
||||||
|
- ✅ Simple to implement (3-5 hours total)
|
||||||
|
- ✅ Easy to maintain (minimal code)
|
||||||
|
- ✅ Universal compatibility (all clients handle reconnection)
|
||||||
|
- ✅ No new threads or background tasks
|
||||||
|
- ✅ Configurable and can be disabled if needed
|
||||||
|
- ✅ Relay restart as emergency fix
|
||||||
|
|
||||||
|
**Next Steps:**
|
||||||
|
1. Implement Component 1 (Startup Cleanup)
|
||||||
|
2. Test thoroughly
|
||||||
|
3. Implement Component 2 (Connection Age Limit)
|
||||||
|
4. Test thoroughly
|
||||||
|
5. Deploy to production
|
||||||
|
6. Monitor CPU usage and subscription counts
|
||||||
1
nips
1
nips
Submodule nips deleted from 8c45ff5d96
@@ -32,7 +32,7 @@ You're all set up now - just wait for the next crash and then run the coredumpct
|
|||||||
|
|
||||||
|
|
||||||
--------------------------
|
--------------------------
|
||||||
|
DEBUGGING
|
||||||
|
|
||||||
Even simpler: Use this one-liner
|
Even simpler: Use this one-liner
|
||||||
# Start relay and immediately attach gdb
|
# Start relay and immediately attach gdb
|
||||||
@@ -89,3 +89,4 @@ sudo ufw delete allow 8888/tcp
|
|||||||
lsof -i :7777
|
lsof -i :7777
|
||||||
kill $(lsof -t -i :7777)
|
kill $(lsof -t -i :7777)
|
||||||
kill -9 $(lsof -t -i :7777)
|
kill -9 $(lsof -t -i :7777)
|
||||||
|
|
||||||
|
|||||||
BIN
screenshots/2025-11-11_11-57.png
Normal file
BIN
screenshots/2025-11-11_11-57.png
Normal file
Binary file not shown.
|
After Width: | Height: | Size: 738 KiB |
BIN
screenshots/2025-11-13_15-53.png
Normal file
BIN
screenshots/2025-11-13_15-53.png
Normal file
Binary file not shown.
|
After Width: | Height: | Size: 2.2 MiB |
BIN
screenshots/2025-11-13_15-53_1.png
Normal file
BIN
screenshots/2025-11-13_15-53_1.png
Normal file
Binary file not shown.
|
After Width: | Height: | Size: 1.3 MiB |
BIN
screenshots/raffles.png
Normal file
BIN
screenshots/raffles.png
Normal file
Binary file not shown.
|
After Width: | Height: | Size: 296 KiB |
@@ -239,7 +239,7 @@ cJSON* query_subscription_details(void) {
|
|||||||
const char* sql =
|
const char* sql =
|
||||||
"SELECT * "
|
"SELECT * "
|
||||||
"FROM active_subscriptions_log "
|
"FROM active_subscriptions_log "
|
||||||
"ORDER BY created_at DESC LIMIT 100";
|
"ORDER BY created_at DESC";
|
||||||
|
|
||||||
// DEBUG: Log the query results for debugging subscription_details
|
// DEBUG: Log the query results for debugging subscription_details
|
||||||
DEBUG_LOG("=== SUBSCRIPTION_DETAILS QUERY DEBUG ===");
|
DEBUG_LOG("=== SUBSCRIPTION_DETAILS QUERY DEBUG ===");
|
||||||
|
|||||||
@@ -9,6 +9,9 @@
|
|||||||
#ifdef VERSION
|
#ifdef VERSION
|
||||||
#undef VERSION
|
#undef VERSION
|
||||||
#endif
|
#endif
|
||||||
|
#ifdef VERSION_MAJOR
|
||||||
|
#undef VERSION_MAJOR
|
||||||
|
#endif
|
||||||
#ifdef VERSION_MINOR
|
#ifdef VERSION_MINOR
|
||||||
#undef VERSION_MINOR
|
#undef VERSION_MINOR
|
||||||
#endif
|
#endif
|
||||||
|
|||||||
@@ -65,6 +65,9 @@ static const struct {
|
|||||||
{"max_total_subscriptions", "5000"},
|
{"max_total_subscriptions", "5000"},
|
||||||
{"max_filters_per_subscription", "10"},
|
{"max_filters_per_subscription", "10"},
|
||||||
|
|
||||||
|
// Connection Management
|
||||||
|
{"max_connection_seconds", "86400"}, // 24 hours (0 = disabled)
|
||||||
|
|
||||||
// Event Processing Limits
|
// Event Processing Limits
|
||||||
{"max_event_tags", "100"},
|
{"max_event_tags", "100"},
|
||||||
{"max_content_length", "8196"},
|
{"max_content_length", "8196"},
|
||||||
|
|||||||
@@ -1807,7 +1807,8 @@ int main(int argc, char* argv[]) {
|
|||||||
return 1;
|
return 1;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// Cleanup orphaned subscriptions from previous runs
|
||||||
|
cleanup_all_subscriptions_on_startup();
|
||||||
|
|
||||||
// Start WebSocket Nostr relay server (port from CLI override or configuration)
|
// 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
|
int result = start_websocket_relay(cli_options.port_override, cli_options.strict_port); // Use CLI port override if specified, otherwise config
|
||||||
|
|||||||
12
src/main.h
12
src/main.h
@@ -10,10 +10,14 @@
|
|||||||
#define MAIN_H
|
#define MAIN_H
|
||||||
|
|
||||||
// Version information (auto-updated by build system)
|
// Version information (auto-updated by build system)
|
||||||
#define VERSION_MAJOR 0
|
#define VERSION_MAJOR 1
|
||||||
#define VERSION_MINOR 8
|
#define VERSION_MINOR 0
|
||||||
#define VERSION_PATCH 6
|
#define VERSION_PATCH 9
|
||||||
#define VERSION "v0.8.6"
|
#define VERSION "v1.0.9"
|
||||||
|
|
||||||
|
// Avoid VERSION_MAJOR redefinition warning from nostr_core_lib
|
||||||
|
#undef VERSION_MAJOR
|
||||||
|
#define VERSION_MAJOR 1
|
||||||
|
|
||||||
// Relay metadata (authoritative source for NIP-11 information)
|
// Relay metadata (authoritative source for NIP-11 information)
|
||||||
#define RELAY_NAME "C-Relay"
|
#define RELAY_NAME "C-Relay"
|
||||||
|
|||||||
@@ -1,11 +1,11 @@
|
|||||||
/* Embedded SQL Schema for C Nostr Relay
|
/* Embedded SQL Schema for C Nostr Relay
|
||||||
* Schema Version: 8
|
* Schema Version: 9
|
||||||
*/
|
*/
|
||||||
#ifndef SQL_SCHEMA_H
|
#ifndef SQL_SCHEMA_H
|
||||||
#define SQL_SCHEMA_H
|
#define SQL_SCHEMA_H
|
||||||
|
|
||||||
/* Schema version constant */
|
/* Schema version constant */
|
||||||
#define EMBEDDED_SCHEMA_VERSION "8"
|
#define EMBEDDED_SCHEMA_VERSION "9"
|
||||||
|
|
||||||
/* Embedded SQL schema as C string literal */
|
/* Embedded SQL schema as C string literal */
|
||||||
static const char* const EMBEDDED_SCHEMA_SQL =
|
static const char* const EMBEDDED_SCHEMA_SQL =
|
||||||
@@ -14,7 +14,7 @@ static const char* const EMBEDDED_SCHEMA_SQL =
|
|||||||
-- Configuration system using config table\n\
|
-- Configuration system using config table\n\
|
||||||
\n\
|
\n\
|
||||||
-- Schema version tracking\n\
|
-- Schema version tracking\n\
|
||||||
PRAGMA user_version = 8;\n\
|
PRAGMA user_version = 9;\n\
|
||||||
\n\
|
\n\
|
||||||
-- Enable foreign key support\n\
|
-- Enable foreign key support\n\
|
||||||
PRAGMA foreign_keys = ON;\n\
|
PRAGMA foreign_keys = ON;\n\
|
||||||
@@ -57,8 +57,8 @@ CREATE TABLE schema_info (\n\
|
|||||||
\n\
|
\n\
|
||||||
-- Insert schema metadata\n\
|
-- Insert schema metadata\n\
|
||||||
INSERT INTO schema_info (key, value) VALUES\n\
|
INSERT INTO schema_info (key, value) VALUES\n\
|
||||||
('version', '8'),\n\
|
('version', '9'),\n\
|
||||||
('description', 'Hybrid Nostr relay schema with subscription deduplication support'),\n\
|
('description', 'Hybrid Nostr relay schema with fixed active_subscriptions_log view'),\n\
|
||||||
('created_at', strftime('%s', 'now'));\n\
|
('created_at', strftime('%s', 'now'));\n\
|
||||||
\n\
|
\n\
|
||||||
-- Helper views for common queries\n\
|
-- Helper views for common queries\n\
|
||||||
@@ -236,21 +236,16 @@ ORDER BY date DESC;\n\
|
|||||||
-- View for current active subscriptions (from log perspective)\n\
|
-- View for current active subscriptions (from log perspective)\n\
|
||||||
CREATE VIEW active_subscriptions_log AS\n\
|
CREATE VIEW active_subscriptions_log AS\n\
|
||||||
SELECT\n\
|
SELECT\n\
|
||||||
s.subscription_id,\n\
|
subscription_id,\n\
|
||||||
s.client_ip,\n\
|
client_ip,\n\
|
||||||
s.filter_json,\n\
|
filter_json,\n\
|
||||||
s.events_sent,\n\
|
events_sent,\n\
|
||||||
s.created_at,\n\
|
created_at,\n\
|
||||||
(strftime('%s', 'now') - s.created_at) as duration_seconds,\n\
|
(strftime('%s', 'now') - created_at) as duration_seconds,\n\
|
||||||
s.wsi_pointer\n\
|
wsi_pointer\n\
|
||||||
FROM subscriptions s\n\
|
FROM subscriptions\n\
|
||||||
WHERE s.event_type = 'created'\n\
|
WHERE event_type = 'created'\n\
|
||||||
AND NOT EXISTS (\n\
|
AND ended_at IS NULL;\n\
|
||||||
SELECT 1 FROM subscriptions s2\n\
|
|
||||||
WHERE s2.subscription_id = s.subscription_id\n\
|
|
||||||
AND s2.wsi_pointer = s.wsi_pointer\n\
|
|
||||||
AND s2.event_type IN ('closed', 'expired', 'disconnected')\n\
|
|
||||||
);\n\
|
|
||||||
\n\
|
\n\
|
||||||
-- Database Statistics Views for Admin API\n\
|
-- Database Statistics Views for Admin API\n\
|
||||||
-- Event kinds distribution view\n\
|
-- Event kinds distribution view\n\
|
||||||
|
|||||||
@@ -999,6 +999,44 @@ void update_subscription_events_sent(const char* sub_id, int events_sent) {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// Cleanup all subscriptions on startup
|
||||||
|
void cleanup_all_subscriptions_on_startup(void) {
|
||||||
|
if (!g_db) {
|
||||||
|
DEBUG_ERROR("Database not available for startup cleanup");
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
DEBUG_LOG("Performing startup subscription cleanup");
|
||||||
|
|
||||||
|
// Mark all active subscriptions as disconnected
|
||||||
|
const char* sql =
|
||||||
|
"UPDATE subscriptions "
|
||||||
|
"SET ended_at = strftime('%s', 'now') "
|
||||||
|
"WHERE event_type = 'created' AND ended_at IS NULL";
|
||||||
|
|
||||||
|
sqlite3_stmt* stmt;
|
||||||
|
int rc = sqlite3_prepare_v2(g_db, sql, -1, &stmt, NULL);
|
||||||
|
if (rc != SQLITE_OK) {
|
||||||
|
DEBUG_ERROR("Failed to prepare startup cleanup query");
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
rc = sqlite3_step(stmt);
|
||||||
|
int changes = sqlite3_changes(g_db);
|
||||||
|
sqlite3_finalize(stmt);
|
||||||
|
|
||||||
|
if (rc != SQLITE_DONE) {
|
||||||
|
DEBUG_ERROR("Failed to execute startup cleanup");
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
if (changes > 0) {
|
||||||
|
DEBUG_LOG("Startup cleanup: marked %d orphaned subscriptions as disconnected", changes);
|
||||||
|
} else {
|
||||||
|
DEBUG_LOG("Startup cleanup: no orphaned subscriptions found");
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
///////////////////////////////////////////////////////////////////////////////////////
|
///////////////////////////////////////////////////////////////////////////////////////
|
||||||
///////////////////////////////////////////////////////////////////////////////////////
|
///////////////////////////////////////////////////////////////////////////////////////
|
||||||
|
|||||||
@@ -120,4 +120,7 @@ void update_subscription_events_sent(const char* sub_id, int events_sent);
|
|||||||
// Subscription query functions
|
// Subscription query functions
|
||||||
int has_subscriptions_for_kind(int event_kind);
|
int has_subscriptions_for_kind(int event_kind);
|
||||||
|
|
||||||
|
// Startup cleanup function
|
||||||
|
void cleanup_all_subscriptions_on_startup(void);
|
||||||
|
|
||||||
#endif // SUBSCRIPTIONS_H
|
#endif // SUBSCRIPTIONS_H
|
||||||
977
src/websockets.c
977
src/websockets.c
File diff suppressed because it is too large
Load Diff
@@ -21,10 +21,10 @@
|
|||||||
|
|
||||||
// Filter validation constants
|
// Filter validation constants
|
||||||
#define MAX_FILTERS_PER_REQUEST 10
|
#define MAX_FILTERS_PER_REQUEST 10
|
||||||
#define MAX_AUTHORS_PER_FILTER 100
|
#define MAX_AUTHORS_PER_FILTER 1000
|
||||||
#define MAX_IDS_PER_FILTER 100
|
#define MAX_IDS_PER_FILTER 1000
|
||||||
#define MAX_KINDS_PER_FILTER 50
|
#define MAX_KINDS_PER_FILTER 500
|
||||||
#define MAX_TAG_VALUES_PER_FILTER 100
|
#define MAX_TAG_VALUES_PER_FILTER 1000
|
||||||
#define MAX_KIND_VALUE 65535
|
#define MAX_KIND_VALUE 65535
|
||||||
#define MAX_TIMESTAMP_VALUE 2147483647 // Max 32-bit signed int
|
#define MAX_TIMESTAMP_VALUE 2147483647 // Max 32-bit signed int
|
||||||
#define MAX_LIMIT_VALUE 5000
|
#define MAX_LIMIT_VALUE 5000
|
||||||
@@ -73,6 +73,12 @@ struct per_session_data {
|
|||||||
struct message_queue_node* message_queue_tail; // Tail of message queue
|
struct message_queue_node* message_queue_tail; // Tail of message queue
|
||||||
int message_queue_count; // Number of messages in queue
|
int message_queue_count; // Number of messages in queue
|
||||||
int writeable_requested; // Flag: 1 if writeable callback requested
|
int writeable_requested; // Flag: 1 if writeable callback requested
|
||||||
|
|
||||||
|
// Message reassembly for handling fragmented WebSocket messages
|
||||||
|
char* reassembly_buffer; // Buffer for accumulating message fragments (NULL when not reassembling)
|
||||||
|
size_t reassembly_size; // Current size of accumulated data
|
||||||
|
size_t reassembly_capacity; // Allocated capacity of reassembly buffer
|
||||||
|
int reassembly_active; // Flag: 1 if currently reassembling a message
|
||||||
};
|
};
|
||||||
|
|
||||||
// NIP-11 HTTP session data structure for managing buffer lifetime
|
// NIP-11 HTTP session data structure for managing buffer lifetime
|
||||||
|
|||||||
5
tests/.test_keys.txt
Normal file
5
tests/.test_keys.txt
Normal file
@@ -0,0 +1,5 @@
|
|||||||
|
# Test key configuration (from make_and_restart_relay.sh -t)
|
||||||
|
ADMIN_PRIVATE_KEY="aaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaa"
|
||||||
|
ADMIN_PUBLIC_KEY="6a04ab98d9e4774ad806e302dddeb63bea16b5cb5f223ee77478e861bb583eb3"
|
||||||
|
RELAY_PUBLIC_KEY="4f355bdcb7cc0af728ef3cceb9615d90684bb5b2ca5f859ab0f0b704075871aa"
|
||||||
|
RELAY_URL="ws://localhost:8888"
|
||||||
295
tests/subscription_cleanup_test.sh
Executable file
295
tests/subscription_cleanup_test.sh
Executable file
@@ -0,0 +1,295 @@
|
|||||||
|
#!/bin/bash
|
||||||
|
|
||||||
|
# Subscription Cleanup Testing Suite for C-Relay
|
||||||
|
# Tests startup cleanup and connection age limit features
|
||||||
|
|
||||||
|
set -e
|
||||||
|
|
||||||
|
# Load test keys
|
||||||
|
source "$(dirname "$0")/.test_keys.txt"
|
||||||
|
|
||||||
|
# Configuration
|
||||||
|
RELAY_HOST="127.0.0.1"
|
||||||
|
RELAY_PORT="8888"
|
||||||
|
RELAY_URL="ws://${RELAY_HOST}:${RELAY_PORT}"
|
||||||
|
|
||||||
|
# Colors for output
|
||||||
|
RED='\033[0;31m'
|
||||||
|
GREEN='\033[0;32m'
|
||||||
|
YELLOW='\033[1;33m'
|
||||||
|
BLUE='\033[0;34m'
|
||||||
|
NC='\033[0m' # No Color
|
||||||
|
|
||||||
|
# Test counters
|
||||||
|
TOTAL_TESTS=0
|
||||||
|
PASSED_TESTS=0
|
||||||
|
FAILED_TESTS=0
|
||||||
|
|
||||||
|
echo -e "${BLUE}========================================${NC}"
|
||||||
|
echo -e "${BLUE}Subscription Cleanup Test Suite${NC}"
|
||||||
|
echo -e "${BLUE}========================================${NC}"
|
||||||
|
echo ""
|
||||||
|
|
||||||
|
# Function to print test header
|
||||||
|
print_test_header() {
|
||||||
|
echo -e "${BLUE}=== Test $1: $2 ===${NC}"
|
||||||
|
}
|
||||||
|
|
||||||
|
# Function to print test result
|
||||||
|
print_result() {
|
||||||
|
local status=$1
|
||||||
|
local message=$2
|
||||||
|
|
||||||
|
TOTAL_TESTS=$((TOTAL_TESTS + 1))
|
||||||
|
|
||||||
|
if [ "$status" = "PASS" ]; then
|
||||||
|
echo -e "${GREEN}[PASS]${NC} $message"
|
||||||
|
PASSED_TESTS=$((PASSED_TESTS + 1))
|
||||||
|
elif [ "$status" = "FAIL" ]; then
|
||||||
|
echo -e "${RED}[FAIL]${NC} $message"
|
||||||
|
FAILED_TESTS=$((FAILED_TESTS + 1))
|
||||||
|
else
|
||||||
|
echo -e "${YELLOW}[WARN]${NC} $message"
|
||||||
|
PASSED_TESTS=$((PASSED_TESTS + 1))
|
||||||
|
fi
|
||||||
|
}
|
||||||
|
|
||||||
|
# Function to check if relay is running
|
||||||
|
check_relay_running() {
|
||||||
|
# Send a simple REQ and check for EOSE response
|
||||||
|
local response=$(echo '["REQ","ping",{}]' | timeout 2 websocat -n1 "$RELAY_URL" 2>/dev/null)
|
||||||
|
if echo "$response" | grep -q "EOSE\|EVENT"; then
|
||||||
|
return 0
|
||||||
|
else
|
||||||
|
return 1
|
||||||
|
fi
|
||||||
|
}
|
||||||
|
|
||||||
|
# Function to create a subscription
|
||||||
|
create_subscription() {
|
||||||
|
local sub_id=$1
|
||||||
|
local filter=${2:-"{}"}
|
||||||
|
|
||||||
|
echo "[\"REQ\",\"$sub_id\",$filter]" | timeout 5 websocat -n1 "$RELAY_URL" 2>/dev/null || echo "TIMEOUT"
|
||||||
|
}
|
||||||
|
|
||||||
|
# Function to close a subscription
|
||||||
|
close_subscription() {
|
||||||
|
local sub_id=$1
|
||||||
|
|
||||||
|
echo "[\"CLOSE\",\"$sub_id\"]" | timeout 5 websocat -n1 "$RELAY_URL" 2>/dev/null || echo "TIMEOUT"
|
||||||
|
}
|
||||||
|
|
||||||
|
# Function to query subscription count from database
|
||||||
|
get_subscription_count() {
|
||||||
|
local db_file=$(find . -name "*.db" -type f 2>/dev/null | head -1)
|
||||||
|
|
||||||
|
if [ -z "$db_file" ]; then
|
||||||
|
echo "0"
|
||||||
|
return
|
||||||
|
fi
|
||||||
|
|
||||||
|
sqlite3 "$db_file" "SELECT COUNT(*) FROM subscriptions WHERE event_type='created' AND ended_at IS NULL;" 2>/dev/null || echo "0"
|
||||||
|
}
|
||||||
|
|
||||||
|
# Test 1: Basic Connectivity
|
||||||
|
print_test_header "1" "Basic Connectivity"
|
||||||
|
|
||||||
|
if check_relay_running; then
|
||||||
|
print_result "PASS" "Relay is running and accepting connections"
|
||||||
|
else
|
||||||
|
print_result "FAIL" "Cannot connect to relay at $RELAY_URL"
|
||||||
|
echo ""
|
||||||
|
echo -e "${RED}ERROR: Relay must be running for tests to proceed${NC}"
|
||||||
|
exit 1
|
||||||
|
fi
|
||||||
|
echo ""
|
||||||
|
|
||||||
|
# Test 2: Create Multiple Subscriptions
|
||||||
|
print_test_header "2" "Create Multiple Subscriptions"
|
||||||
|
|
||||||
|
echo "[INFO] Creating 5 test subscriptions..."
|
||||||
|
for i in {1..5}; do
|
||||||
|
response=$(create_subscription "cleanup_test_$i")
|
||||||
|
if echo "$response" | grep -q "EOSE"; then
|
||||||
|
echo "[INFO] Subscription cleanup_test_$i created successfully"
|
||||||
|
else
|
||||||
|
print_result "WARN" "Subscription cleanup_test_$i may not have been created: $response"
|
||||||
|
fi
|
||||||
|
done
|
||||||
|
|
||||||
|
# Give subscriptions time to be logged
|
||||||
|
sleep 2
|
||||||
|
|
||||||
|
# Check subscription count in database
|
||||||
|
active_subs=$(get_subscription_count)
|
||||||
|
echo "[INFO] Active subscriptions in database: $active_subs"
|
||||||
|
|
||||||
|
if [ "$active_subs" -ge 5 ]; then
|
||||||
|
print_result "PASS" "Multiple subscriptions created and logged ($active_subs active)"
|
||||||
|
else
|
||||||
|
print_result "WARN" "Expected at least 5 subscriptions, found $active_subs"
|
||||||
|
fi
|
||||||
|
echo ""
|
||||||
|
|
||||||
|
# Test 3: Simulate Orphaned Subscriptions (disconnect without CLOSE)
|
||||||
|
print_test_header "3" "Simulate Orphaned Subscriptions"
|
||||||
|
|
||||||
|
echo "[INFO] Creating subscriptions and disconnecting abruptly..."
|
||||||
|
|
||||||
|
# Create subscriptions in background and kill the connection
|
||||||
|
for i in {6..10}; do
|
||||||
|
(echo "[\"REQ\",\"orphan_test_$i\",{}]" | timeout 2 websocat "$RELAY_URL" &>/dev/null) &
|
||||||
|
pid=$!
|
||||||
|
sleep 0.5
|
||||||
|
kill -9 $pid 2>/dev/null || true
|
||||||
|
done
|
||||||
|
|
||||||
|
sleep 2
|
||||||
|
|
||||||
|
orphaned_subs=$(get_subscription_count)
|
||||||
|
echo "[INFO] Subscriptions after abrupt disconnects: $orphaned_subs"
|
||||||
|
|
||||||
|
if [ "$orphaned_subs" -gt "$active_subs" ]; then
|
||||||
|
print_result "PASS" "Orphaned subscriptions detected ($orphaned_subs total, was $active_subs)"
|
||||||
|
else
|
||||||
|
print_result "WARN" "No increase in orphaned subscriptions detected"
|
||||||
|
fi
|
||||||
|
echo ""
|
||||||
|
|
||||||
|
# Test 4: Startup Cleanup (requires relay restart)
|
||||||
|
print_test_header "4" "Startup Cleanup Feature"
|
||||||
|
|
||||||
|
echo "[INFO] This test requires relay restart to verify startup cleanup"
|
||||||
|
echo "[INFO] Current orphaned subscriptions: $orphaned_subs"
|
||||||
|
echo ""
|
||||||
|
echo -e "${YELLOW}[ACTION REQUIRED]${NC} Please restart the relay now with:"
|
||||||
|
echo " ./make_and_restart_relay.sh"
|
||||||
|
echo ""
|
||||||
|
echo -n "Press Enter after relay has restarted to continue..."
|
||||||
|
read
|
||||||
|
|
||||||
|
# Wait for relay to be ready
|
||||||
|
echo "[INFO] Waiting for relay to be ready..."
|
||||||
|
sleep 3
|
||||||
|
|
||||||
|
if ! check_relay_running; then
|
||||||
|
print_result "FAIL" "Relay not responding after restart"
|
||||||
|
exit 1
|
||||||
|
fi
|
||||||
|
|
||||||
|
# Check if orphaned subscriptions were cleaned up
|
||||||
|
cleaned_subs=$(get_subscription_count)
|
||||||
|
echo "[INFO] Active subscriptions after restart: $cleaned_subs"
|
||||||
|
|
||||||
|
if [ "$cleaned_subs" -eq 0 ]; then
|
||||||
|
print_result "PASS" "Startup cleanup removed all orphaned subscriptions"
|
||||||
|
elif [ "$cleaned_subs" -lt "$orphaned_subs" ]; then
|
||||||
|
print_result "PASS" "Startup cleanup reduced orphaned subscriptions (from $orphaned_subs to $cleaned_subs)"
|
||||||
|
else
|
||||||
|
print_result "FAIL" "Startup cleanup did not reduce orphaned subscriptions"
|
||||||
|
fi
|
||||||
|
echo ""
|
||||||
|
|
||||||
|
# Test 5: Connection Age Limit (requires configuration)
|
||||||
|
print_test_header "5" "Connection Age Limit Feature"
|
||||||
|
|
||||||
|
echo "[INFO] Testing connection age limit feature..."
|
||||||
|
echo "[INFO] Default max_connection_seconds is 86400 (24 hours)"
|
||||||
|
echo ""
|
||||||
|
echo -e "${YELLOW}[INFO]${NC} To test connection age limit with shorter timeout:"
|
||||||
|
echo " 1. Set max_connection_seconds to 60 (1 minute) via admin event"
|
||||||
|
echo " 2. Create a subscription and wait 61 seconds"
|
||||||
|
echo " 3. Connection should be automatically closed"
|
||||||
|
echo ""
|
||||||
|
echo "[INFO] For this test, we'll verify the feature is enabled in config"
|
||||||
|
|
||||||
|
# Create a test subscription to verify connection works
|
||||||
|
response=$(create_subscription "age_test_1")
|
||||||
|
if echo "$response" | grep -q "EOSE"; then
|
||||||
|
print_result "PASS" "Connection age limit feature is operational (subscription created)"
|
||||||
|
else
|
||||||
|
print_result "WARN" "Could not verify connection age limit feature"
|
||||||
|
fi
|
||||||
|
echo ""
|
||||||
|
|
||||||
|
# Test 6: Verify Client Reconnection
|
||||||
|
print_test_header "6" "Client Reconnection After Cleanup"
|
||||||
|
|
||||||
|
echo "[INFO] Testing that clients can reconnect after cleanup..."
|
||||||
|
|
||||||
|
# Create a subscription
|
||||||
|
response=$(create_subscription "reconnect_test_1")
|
||||||
|
if echo "$response" | grep -q "EOSE"; then
|
||||||
|
echo "[INFO] First connection successful"
|
||||||
|
|
||||||
|
# Close and reconnect
|
||||||
|
sleep 1
|
||||||
|
response=$(create_subscription "reconnect_test_2")
|
||||||
|
if echo "$response" | grep -q "EOSE"; then
|
||||||
|
print_result "PASS" "Client can reconnect and create new subscriptions"
|
||||||
|
else
|
||||||
|
print_result "FAIL" "Client reconnection failed"
|
||||||
|
fi
|
||||||
|
else
|
||||||
|
print_result "FAIL" "Initial connection failed"
|
||||||
|
fi
|
||||||
|
echo ""
|
||||||
|
|
||||||
|
# Test 7: Verify Disabled State (max_connection_seconds = 0)
|
||||||
|
print_test_header "7" "Verify Feature Can Be Disabled"
|
||||||
|
|
||||||
|
echo "[INFO] Connection age limit can be disabled by setting max_connection_seconds=0"
|
||||||
|
echo "[INFO] When disabled, connections remain open indefinitely"
|
||||||
|
echo "[INFO] This is the recommended setting for most relays"
|
||||||
|
|
||||||
|
# Create a long-lived subscription
|
||||||
|
response=$(create_subscription "disabled_test_1")
|
||||||
|
if echo "$response" | grep -q "EOSE"; then
|
||||||
|
print_result "PASS" "Subscriptions work normally when feature is disabled/default"
|
||||||
|
else
|
||||||
|
print_result "WARN" "Could not verify disabled state"
|
||||||
|
fi
|
||||||
|
echo ""
|
||||||
|
|
||||||
|
# Test 8: Database Integrity Check
|
||||||
|
print_test_header "8" "Database Integrity After Cleanup"
|
||||||
|
|
||||||
|
echo "[INFO] Checking database integrity..."
|
||||||
|
|
||||||
|
db_file=$(find . -name "*.db" -type f 2>/dev/null | head -1)
|
||||||
|
if [ -n "$db_file" ]; then
|
||||||
|
# Check if database is accessible
|
||||||
|
if sqlite3 "$db_file" "PRAGMA integrity_check;" 2>/dev/null | grep -q "ok"; then
|
||||||
|
print_result "PASS" "Database integrity check passed"
|
||||||
|
else
|
||||||
|
print_result "FAIL" "Database integrity check failed"
|
||||||
|
fi
|
||||||
|
|
||||||
|
# Check subscription table structure
|
||||||
|
if sqlite3 "$db_file" "SELECT COUNT(*) FROM subscriptions;" &>/dev/null; then
|
||||||
|
print_result "PASS" "Subscription table is accessible"
|
||||||
|
else
|
||||||
|
print_result "FAIL" "Subscription table is not accessible"
|
||||||
|
fi
|
||||||
|
else
|
||||||
|
print_result "WARN" "No database file found"
|
||||||
|
fi
|
||||||
|
echo ""
|
||||||
|
|
||||||
|
# Final Summary
|
||||||
|
echo -e "${BLUE}========================================${NC}"
|
||||||
|
echo -e "${BLUE}Test Summary${NC}"
|
||||||
|
echo -e "${BLUE}========================================${NC}"
|
||||||
|
echo "Total Tests: $TOTAL_TESTS"
|
||||||
|
echo -e "${GREEN}Passed: $PASSED_TESTS${NC}"
|
||||||
|
echo -e "${RED}Failed: $FAILED_TESTS${NC}"
|
||||||
|
echo ""
|
||||||
|
|
||||||
|
if [ $FAILED_TESTS -eq 0 ]; then
|
||||||
|
echo -e "${GREEN}All tests passed!${NC}"
|
||||||
|
exit 0
|
||||||
|
else
|
||||||
|
echo -e "${RED}Some tests failed. Please review the output above.${NC}"
|
||||||
|
exit 1
|
||||||
|
fi
|
||||||
747
tests/test_requests.mjs
Normal file
747
tests/test_requests.mjs
Normal file
@@ -0,0 +1,747 @@
|
|||||||
|
|
||||||
|
/**
|
||||||
|
* Nostr Relay Pubkey Filter Test
|
||||||
|
* Tests how many pubkeys different relays can handle in a single filter request
|
||||||
|
*/
|
||||||
|
|
||||||
|
import { WebSocket } from 'ws';
|
||||||
|
|
||||||
|
// Configuration
|
||||||
|
const RELAYS = [
|
||||||
|
// "wss://relay.laantungir.net"
|
||||||
|
"ws://127.0.0.1:8888"
|
||||||
|
];
|
||||||
|
|
||||||
|
// Test parameters
|
||||||
|
const STEP_SIZE = 25; // Increment pubkey count by 25 each test
|
||||||
|
const MAX_PUBKEYS = 500; // Maximum pubkeys to test
|
||||||
|
const EVENT_KIND = 1; // Kind 1 = text notes
|
||||||
|
const EVENT_LIMIT = 2; // Only request 2 events per test
|
||||||
|
|
||||||
|
// Generate test pubkey arrays of increasing sizes
|
||||||
|
function generateTestPubkeyArrays() {
|
||||||
|
const testArrays = [];
|
||||||
|
for (let count = STEP_SIZE; count <= MAX_PUBKEYS; count += STEP_SIZE) {
|
||||||
|
testArrays.push(PUBKEYS.slice(0, count));
|
||||||
|
}
|
||||||
|
return testArrays;
|
||||||
|
}
|
||||||
|
|
||||||
|
const PUBKEYS = [
|
||||||
|
"85080d3bad70ccdcd7f74c29a44f55bb85cbcd3dd0cbb957da1d215bdb931204",
|
||||||
|
"82341f882b6eabcd2ba7f1ef90aad961cf074af15b9ef44a09f9d2a8fbfbe6a2",
|
||||||
|
"916b7aca250f43b9f842faccc831db4d155088632a8c27c0d140f2043331ba57",
|
||||||
|
"2645caf5706a31767c921532975a079f85950e1006bd5065f5dd0213e6848a96",
|
||||||
|
"8fe3f243e91121818107875d51bca4f3fcf543437aa9715150ec8036358939c5",
|
||||||
|
"83e818dfbeccea56b0f551576b3fd39a7a50e1d8159343500368fa085ccd964b",
|
||||||
|
"a341f45ff9758f570a21b000c17d4e53a3a497c8397f26c0e6d61e5acffc7a98",
|
||||||
|
"e88a691e98d9987c964521dff60025f60700378a4879180dcbbb4a5027850411",
|
||||||
|
"2cde0e02bda47eaeeed65e341619cc5f2afce990164669da4e1e5989180a96b9",
|
||||||
|
"edcd20558f17d99327d841e4582f9b006331ac4010806efa020ef0d40078e6da",
|
||||||
|
"34d2f5274f1958fcd2cb2463dabeaddf8a21f84ace4241da888023bf05cc8095",
|
||||||
|
"c48b5cced5ada74db078df6b00fa53fc1139d73bf0ed16de325d52220211dbd5",
|
||||||
|
"04c915daefee38317fa734444acee390a8269fe5810b2241e5e6dd343dfbecc9",
|
||||||
|
"e33fe65f1fde44c6dc17eeb38fdad0fceaf1cae8722084332ed1e32496291d42",
|
||||||
|
"1306edd66f1da374adc417cf884bbcff57c6399656236c1f872ee10403c01b2d",
|
||||||
|
"eaf27aa104833bcd16f671488b01d65f6da30163b5848aea99677cc947dd00aa",
|
||||||
|
"472f440f29ef996e92a186b8d320ff180c855903882e59d50de1b8bd5669301e",
|
||||||
|
"be1d89794bf92de5dd64c1e60f6a2c70c140abac9932418fee30c5c637fe9479",
|
||||||
|
"c49d52a573366792b9a6e4851587c28042fb24fa5625c6d67b8c95c8751aca15",
|
||||||
|
"c037a6897df86bfd4df5496ca7e2318992b4766897fb18fbd1d347a4f4459f5e",
|
||||||
|
"e41e883f1ef62485a074c1a1fa1d0a092a5d678ad49bedc2f955ab5e305ba94e",
|
||||||
|
"020f2d21ae09bf35fcdfb65decf1478b846f5f728ab30c5eaabcd6d081a81c3e",
|
||||||
|
"e2f28c1ac6dff5a7b755635af4c8436d2fec89b888a9d9548a51b2c63f779555",
|
||||||
|
"29fbc05acee671fb579182ca33b0e41b455bb1f9564b90a3d8f2f39dee3f2779",
|
||||||
|
"090254801a7e8e5085b02e711622f0dfa1a85503493af246aa42af08f5e4d2df",
|
||||||
|
"3bf0c63fcb93463407af97a5e5ee64fa883d107ef9e558472c4eb9aaaefa459d",
|
||||||
|
"6b0d4c8d9dc59e110d380b0429a02891f1341a0fa2ba1b1cf83a3db4d47e3964",
|
||||||
|
"b0b8fbd9578ac23e782d97a32b7b3a72cda0760761359bd65661d42752b4090a",
|
||||||
|
"b7996c183e036df27802945b80bbdc8b0bf5971b6621a86bf3569c332117f07d",
|
||||||
|
"1833ee04459feb2ca4ae690d5f31269ad488c69e5fe903a42b532c677c4a8170",
|
||||||
|
"4adb4ff2dc72bbf1f6da19fc109008a25013c837cf712016972fad015b19513f",
|
||||||
|
"c4eabae1be3cf657bc1855ee05e69de9f059cb7a059227168b80b89761cbc4e0",
|
||||||
|
"368f4e0027fd223fdb69b6ec6e1c06d1f027a611b1ed38eeb32493eb2878bb35",
|
||||||
|
"703e26b4f8bc0fa57f99d815dbb75b086012acc24fc557befa310f5aa08d1898",
|
||||||
|
"50d94fc2d8580c682b071a542f8b1e31a200b0508bab95a33bef0855df281d63",
|
||||||
|
"6e1534f56fc9e937e06237c8ba4b5662bcacc4e1a3cfab9c16d89390bec4fca3",
|
||||||
|
"a5e93aef8e820cbc7ab7b6205f854b87aed4b48c5f6b30fbbeba5c99e40dcf3f",
|
||||||
|
"1989034e56b8f606c724f45a12ce84a11841621aaf7182a1f6564380b9c4276b",
|
||||||
|
"19fefd7f39c96d2ff76f87f7627ae79145bc971d8ab23205005939a5a913bc2f",
|
||||||
|
"6e468422dfb74a5738702a8823b9b28168abab8655faacb6853cd0ee15deee93",
|
||||||
|
"a3b0ce5d70d0db22885706b2b1f144c6864a7e4828acff3f8f01ca6b3f54ad15",
|
||||||
|
"aef0d6b212827f3ba1de6189613e6d4824f181f567b1205273c16895fdaf0b23",
|
||||||
|
"826e9f895b81ab41a4522268b249e68d02ca81608def562a493cee35ffc5c759",
|
||||||
|
"460c25e682fda7832b52d1f22d3d22b3176d972f60dcdc3212ed8c92ef85065c",
|
||||||
|
"e1055729d51e037b3c14e8c56e2c79c22183385d94aadb32e5dc88092cd0fef4",
|
||||||
|
"27f211f4542fd89d673cfad15b6d838cc5d525615aae8695ed1dcebc39b2dadb",
|
||||||
|
"eab0e756d32b80bcd464f3d844b8040303075a13eabc3599a762c9ac7ab91f4f",
|
||||||
|
"32e1827635450ebb3c5a7d12c1f8e7b2b514439ac10a67eef3d9fd9c5c68e245",
|
||||||
|
"63fe6318dc58583cfe16810f86dd09e18bfd76aabc24a0081ce2856f330504ed",
|
||||||
|
"00000000827ffaa94bfea288c3dfce4422c794fbb96625b6b31e9049f729d700",
|
||||||
|
"7fa56f5d6962ab1e3cd424e758c3002b8665f7b0d8dcee9fe9e288d7751ac194",
|
||||||
|
"22aa81510ee63fe2b16cae16e0921f78e9ba9882e2868e7e63ad6d08ae9b5954",
|
||||||
|
"175f568d77fb0cb7400f0ddd8aed1738cd797532b314ef053a1669d4dba7433a",
|
||||||
|
"6c535d95a8659b234d5a0805034f5f0a67e3c0ceffcc459f61f680fe944424bf",
|
||||||
|
"9579444852221038dcba34512257b66a1c6e5bdb4339b6794826d4024b3e4ce9",
|
||||||
|
"58c741aa630c2da35a56a77c1d05381908bd10504fdd2d8b43f725efa6d23196",
|
||||||
|
"b8e6bf46e109314616fe24e6c7e265791a5f2f4ec95ae8aa15d7107ad250dc63",
|
||||||
|
"84dee6e676e5bb67b4ad4e042cf70cbd8681155db535942fcc6a0533858a7240",
|
||||||
|
"387519cafd325668ecffe59577f37238638da4cf2d985b82f932fc81d33da1e8",
|
||||||
|
"b9e76546ba06456ed301d9e52bc49fa48e70a6bf2282be7a1ae72947612023dc",
|
||||||
|
"4d62dd5e6ac55ae2405940f59f6f030a994ec2b3ecc5556c8dc542cce20e46dd",
|
||||||
|
"9c612f8b770f0e3fd35cdac2bc57fcee8561e560504ea25c8b9eff8e03512b3e",
|
||||||
|
"3eeb3de14ec5c48c6c4c9ff80908c4186170eabb74b2a6705a7db9f9922cd61e",
|
||||||
|
"51d7f1b736d1958fa56f113e82a27987a3aca4f0e6d237fa8fc369cc1608c5c0",
|
||||||
|
"c2622c916d9b90e10a81b2ba67b19bdfc5d6be26c25756d1f990d3785ce1361b",
|
||||||
|
"b111d517452f9ef015e16d60ae623a6b66af63024eec941b0653bfee0dd667d4",
|
||||||
|
"d897efcd971f8e5eae08c86b7da66f89b30e761a4a86ac41d907425d15b630fe",
|
||||||
|
"9dea27855974a08fceb48c40fab8432c1a8e3a53a1da22a1ad568595d6010649",
|
||||||
|
"47b630bbcdfa88b1c85f84aa3b68fe6c0102b651ba5d9a23cbd2d07b4f6eecc1",
|
||||||
|
"eb0dc09a61fdfc0df5db1f20c7fc7d83f00c690580fea2e5bac8f99c13f65065",
|
||||||
|
"5c0775b1ae0a5140da9599aa9cd1c5beea55c2d55a5d681808525eb5fce37b32",
|
||||||
|
"b474e6999980aa9e8c9dd6e3720fb03136bfa05aba5fab1634dc0bd8767d412f",
|
||||||
|
"759f7abf05ca710bf2c8da7ad7a9a7df6d0c85db7b2217da524e94e3627b2fbd",
|
||||||
|
"060e7c6ed0dbeb9c8cdc61445ee38b9b08d899d6b617e28064b0916e243ddddb",
|
||||||
|
"f728d9e6e7048358e70930f5ca64b097770d989ccd86854fe618eda9c8a38106",
|
||||||
|
"bf2376e17ba4ec269d10fcc996a4746b451152be9031fa48e74553dde5526bce",
|
||||||
|
"b99dbca0184a32ce55904cb267b22e434823c97f418f36daf5d2dff0dd7b5c27",
|
||||||
|
"c7dccba4fe4426a7b1ea239a5637ba40fab9862c8c86b3330fe65e9f667435f6",
|
||||||
|
"ad46db12ee250a108756ab4f0f3007b04d7e699f45eac3ab696077296219d207",
|
||||||
|
"59fbee7369df7713dbbfa9bbdb0892c62eba929232615c6ff2787da384cb770f",
|
||||||
|
"d7f0e3917c466f1e2233e9624fbd6d4bd1392dbcfcaf3574f457569d496cb731",
|
||||||
|
"e9e4276490374a0daf7759fd5f475deff6ffb9b0fc5fa98c902b5f4b2fe3bba2",
|
||||||
|
"6f35047caf7432fc0ab54a28fed6c82e7b58230bf98302bf18350ff71e10430a",
|
||||||
|
"fdd5e8f6ae0db817be0b71da20498c1806968d8a6459559c249f322fa73464a7",
|
||||||
|
"883fea4c071fda4406d2b66be21cb1edaf45a3e058050d6201ecf1d3596bbc39",
|
||||||
|
"a1808558470389142e297d4729e081ab8bdff1ab50d0ebe22ffa78958f7a6ab7",
|
||||||
|
"330fb1431ff9d8c250706bbcdc016d5495a3f744e047a408173e92ae7ee42dac",
|
||||||
|
"a4cb51f4618cfcd16b2d3171c466179bed8e197c43b8598823b04de266cef110",
|
||||||
|
"9c163c7351f8832b08b56cbb2e095960d1c5060dd6b0e461e813f0f07459119e",
|
||||||
|
"0a722ca20e1ccff0adfdc8c2abb097957f0e0bf32db18c4281f031756d50eb8d",
|
||||||
|
"5cad82c898ee66013711945d687f7d9549f645a0118467dae2f5e274c598d6ff",
|
||||||
|
"03b593ef3d95102b54bdff77728131a7c3bdfe9007b0b92cd7c4ad4a0021de25",
|
||||||
|
"d0debf9fb12def81f43d7c69429bb784812ac1e4d2d53a202db6aac7ea4b466c",
|
||||||
|
"60d53675f07dee9e7d77910efa44682d87cb532313ba66b8f4449d649172296b",
|
||||||
|
"d3ab33199eb48c6f785072b4a66a8e57814e35d31375cca8c3ceeecc171f30ba",
|
||||||
|
"772bd267dffbff318d1a89f257c3371410111a8b89571dbbefa77af6bfa179f3",
|
||||||
|
"11b9a89404dbf3034e7e1886ba9dc4c6d376f239a118271bd2ec567a889850ce",
|
||||||
|
"0497384b57b43c107a778870462901bf68e0e8583b32e2816563543c059784a4",
|
||||||
|
"5d9ba2c5ee0e86e2c4477b145eb301f2df06063a19f4c4ab9042bd347188ec8e",
|
||||||
|
"5683ffc7ff8a732565135aad56cdff94ebacd9a616d1313aea8ad48a446bfe99",
|
||||||
|
"3004d45a0ab6352c61a62586a57c50f11591416c29db1143367a4f0623b491ca",
|
||||||
|
"b24e32ee9a1c18f2771b53345ed8dbc55b59cbe958e5a165dc01704c3aaa6196",
|
||||||
|
"0a2df905acd5b5be3214a84cb2d4f61b0efb4d9bf05739d51112252504959688",
|
||||||
|
"95361a2b42a26c22bac3b6b6ba4c5cac4d36906eb0cfb98268681c45a301c518",
|
||||||
|
"b07d216f2f0422ec0252dd81a6513b8d0b0c7ef85291fbf5a85ef23f8df78fa7",
|
||||||
|
"064de2497ce621aee2a5b4b926a08b1ca01bce9da85b0c714e883e119375140c",
|
||||||
|
"5a8e581f16a012e24d2a640152ad562058cb065e1df28e907c1bfa82c150c8ba",
|
||||||
|
"a36bdc7952e973b31cb32d4ce3ce21447db66c3149c1b7a3d2450f77f9c7e8f9",
|
||||||
|
"e03cfe011d81424bb60a12e9eb0cb0c9c688c34712c3794c0752e0718b369ef2",
|
||||||
|
"2edbcea694d164629854a52583458fd6d965b161e3c48b57d3aff01940558884",
|
||||||
|
"b9003833fabff271d0782e030be61b7ec38ce7d45a1b9a869fbdb34b9e2d2000",
|
||||||
|
"4379e76bfa76a80b8db9ea759211d90bb3e67b2202f8880cc4f5ffe2065061ad",
|
||||||
|
"76c71aae3a491f1d9eec47cba17e229cda4113a0bbb6e6ae1776d7643e29cafa",
|
||||||
|
"d307643547703537dfdef811c3dea96f1f9e84c8249e200353425924a9908cf8",
|
||||||
|
"da0cc82154bdf4ce8bf417eaa2d2fa99aa65c96c77867d6656fccdbf8e781b18",
|
||||||
|
"3511ad63cd9ad760780044b7c815ee55e8e00722b5de271c47ff29367653456c",
|
||||||
|
"f9acb0b034c4c1177e985f14639f317ef0fedee7657c060b146ee790024317ec",
|
||||||
|
"0c371f5ed95076613443e8331c4b60828ed67bcdefaa1698fb5ce9d7b3285ffb",
|
||||||
|
"ee11a5dff40c19a555f41fe42b48f00e618c91225622ae37b6c2bb67b76c4e49",
|
||||||
|
"053935081a69624466034446eda3374d905652ddbf8217c88708182687a33066",
|
||||||
|
"a305cc8926861bdde5c71bbb6fd394bb4cea6ef5f5f86402b249fc5ceb0ce220",
|
||||||
|
"03a6e50be223dbb49e282764388f6f2ca8826eae8c5a427aa82bb1b61e51d5e6",
|
||||||
|
"a197639863cf175adc96348382a73b4a4a361c6b2e6fc1de61a14244a2f926a1",
|
||||||
|
"3ca7ca157b5975ace02225caf99fdce43f11207c072cb4899c80a414a9c7539d",
|
||||||
|
"02d9f5676fffc339ffe94dfab38bebe21ce117c6f1509d9922a82d454f420da2",
|
||||||
|
"08634a74c9d14479b462389b307695815f9a189e8fb6e058b92e18bd3f537405",
|
||||||
|
"ec7de4aa8758ba9e09a8c89d2757a1fa0e2cc61c20b757af52ae058931c1a33f",
|
||||||
|
"2250f69694c2a43929e77e5de0f6a61ae5e37a1ee6d6a3baef1706ed9901248b",
|
||||||
|
"a9434ee165ed01b286becfc2771ef1705d3537d051b387288898cc00d5c885be",
|
||||||
|
"bd625f1b8c49a79f075f3ebd2d111ff625504cf2ad12442fd70d191dd2f4a562",
|
||||||
|
"25e5c82273a271cb1a840d0060391a0bf4965cafeb029d5ab55350b418953fbb",
|
||||||
|
"42224859763652914db53052103f0b744df79dfc4efef7e950fc0802fc3df3c5",
|
||||||
|
"11d0b66747887ba9a6d34b23eb31287374b45b1a1b161eac54cb183c53e00ef7",
|
||||||
|
"2544cfcb89d7c2f8d3a31ea2ed386ac5189a18f484672436580eec215f9b039c",
|
||||||
|
"d4338b7c3306491cfdf54914d1a52b80a965685f7361311eae5f3eaff1d23a5b",
|
||||||
|
"c43e382ee4835010b9fad18e0a6f50f1ae143b98e089b8bb974232fce4d1f295",
|
||||||
|
"92de68b21302fa2137b1cbba7259b8ba967b535a05c6d2b0847d9f35ff3cf56a",
|
||||||
|
"55f04590674f3648f4cdc9dc8ce32da2a282074cd0b020596ee033d12d385185",
|
||||||
|
"2af01e0d6bd1b9fbb9e3d43157d64590fb27dcfbcabe28784a5832e17befb87b",
|
||||||
|
"35b23cd02d2d75e55cee38fdee26bc82f1d15d3c9580800b04b0da2edb7517ea",
|
||||||
|
"7e0c255fd3d0f9b48789a944baf19bf42c205a9c55199805eb13573b32137488",
|
||||||
|
"ee0e01eb17fc6cb4cd2d9300d2f8945b51056514f156c6bc6d491b74496d161a",
|
||||||
|
"cbc5ef6b01cbd1ffa2cb95a954f04c385a936c1a86e1bb9ccdf2cf0f4ebeaccb",
|
||||||
|
"ec6e36d5c9eb874f1db4253ef02377f7cc70697cda40fbfb24ded6b5d14cce4c",
|
||||||
|
"2779f3d9f42c7dee17f0e6bcdcf89a8f9d592d19e3b1bbd27ef1cffd1a7f98d1",
|
||||||
|
"976713246c36db1a4364f917b98633cbe0805d46af880f6b50a505d4eb32ed47",
|
||||||
|
"8766a54ef9a170b3860bc66fd655abb24b5fda75d7d7ff362f44442fbdeb47b9",
|
||||||
|
"ea2e3c814d08a378f8a5b8faecb2884d05855975c5ca4b5c25e2d6f936286f14",
|
||||||
|
"e2ccf7cf20403f3f2a4a55b328f0de3be38558a7d5f33632fdaaefc726c1c8eb",
|
||||||
|
"07eced8b63b883cedbd8520bdb3303bf9c2b37c2c7921ca5c59f64e0f79ad2a6",
|
||||||
|
"532d830dffe09c13e75e8b145c825718fc12b0003f61d61e9077721c7fff93cb",
|
||||||
|
"1afe0c74e3d7784eba93a5e3fa554a6eeb01928d12739ae8ba4832786808e36d",
|
||||||
|
"c708943ea349519dcf56b2a5c138fd9ed064ad65ddecae6394eabd87a62f1770",
|
||||||
|
"ccaa58e37c99c85bc5e754028a718bd46485e5d3cb3345691ecab83c755d48cc",
|
||||||
|
"5b0e8da6fdfba663038690b37d216d8345a623cc33e111afd0f738ed7792bc54",
|
||||||
|
"f2c96c97f6419a538f84cf3fa72e2194605e1848096e6e5170cce5b76799d400",
|
||||||
|
"aa55a479ad6934d0fd78f3dbd88515cd1ca0d7a110812e711380d59df7598935",
|
||||||
|
"bd9eb657c25b4f6cda68871ce26259d1f9bc62420487e3224905b674a710a45a",
|
||||||
|
"69a80567e79b6b9bc7282ad595512df0b804784616bedb623c122fad420a2635",
|
||||||
|
"fa984bd7dbb282f07e16e7ae87b26a2a7b9b90b7246a44771f0cf5ae58018f52",
|
||||||
|
"df173277182f3155d37b330211ba1de4a81500c02d195e964f91be774ec96708",
|
||||||
|
"675b84fe75e216ab947c7438ee519ca7775376ddf05dadfba6278bd012e1d728",
|
||||||
|
"91c9a5e1a9744114c6fe2d61ae4de82629eaaa0fb52f48288093c7e7e036f832",
|
||||||
|
"24ebb6b58d0b984a965b76f82ce9eff8795cc95085a4d09dedc56949ed596ada",
|
||||||
|
"bb1cf5250435ff475cd8b32acb23e3ee7bbe8fc38f6951704b4798513947672c",
|
||||||
|
"922945779f93fd0b3759f1157e3d9fa20f3fd24c4b8f2bcf520cacf649af776d",
|
||||||
|
"3d842afecd5e293f28b6627933704a3fb8ce153aa91d790ab11f6a752d44a42d",
|
||||||
|
"e8d67c435a4a59304e1414280e952efe17be4254fca27916bf63f9f73e54aba4",
|
||||||
|
"c1fc7771f5fa418fd3ac49221a18f19b42ccb7a663da8f04cbbf6c08c80d20b1",
|
||||||
|
"8eee8f5a002e533e9f9ffef14c713da449c23f56f4415e7995552075a02d1d37",
|
||||||
|
"c998a5739f04f7fff202c54962aa5782b34ecb10d6f915bdfdd7582963bf9171",
|
||||||
|
"a536ab1f7f3c0133baadbdf472b1ac7ad4b774ed432c1989284193572788bca0",
|
||||||
|
"c9b19ffcd43e6a5f23b3d27106ce19e4ad2df89ba1031dd4617f1b591e108965",
|
||||||
|
"e6ee5b449c220defea6373b8a7e147cabd67c2bdb5016886bf6096a3c7435a61",
|
||||||
|
"a619cf1a888a73211bbf32e0c438319f23e91444d45d7bc88816ed5fcb7e8fa3",
|
||||||
|
"56a6b75373c8f7b93c53bcae86d8ffbaba9f2a1b38122054fcdb7f3bf645b727",
|
||||||
|
"89bfe407c647eb1888871f756516bb1906254fba3132d516ce9099614e37d10c",
|
||||||
|
"b7ed68b062de6b4a12e51fd5285c1e1e0ed0e5128cda93ab11b4150b55ed32fc",
|
||||||
|
"4657dfe8965be8980a93072bcfb5e59a65124406db0f819215ee78ba47934b3e",
|
||||||
|
"d61f3bc5b3eb4400efdae6169a5c17cabf3246b514361de939ce4a1a0da6ef4a",
|
||||||
|
"58ead82fa15b550094f7f5fe4804e0fe75b779dbef2e9b20511eccd69e6d08f9",
|
||||||
|
"fcf6fee0e959c7195dadc5f36fe5a873003b389e7033293b06057c821fcbc9c5",
|
||||||
|
"6681268ace4748d41a4cfcc1e64006fb935bbc359782b3d9611f64d51c6752d9",
|
||||||
|
"e76450df94f84c1c0b71677a45d75b7918f0b786113c2d038e6ab8841b99f276",
|
||||||
|
"a44dbc9aaa357176a7d4f5c3106846ea096b66de0b50ee39aff54baab6c4bf4b",
|
||||||
|
"281e109d2a2899bb0555cf0c3a69b24b3debd61885ca29ef39b95b632be25fe7",
|
||||||
|
"5be6446aa8a31c11b3b453bf8dafc9b346ff328d1fa11a0fa02a1e6461f6a9b1",
|
||||||
|
"e1ff3bfdd4e40315959b08b4fcc8245eaa514637e1d4ec2ae166b743341be1af",
|
||||||
|
"0d6c8388dcb049b8dd4fc8d3d8c3bb93de3da90ba828e4f09c8ad0f346488a33",
|
||||||
|
"9be0be0e64d38a29a9cec9a5c8ef5d873c2bfa5362a4b558da5ff69bc3cbb81e",
|
||||||
|
"c48e29f04b482cc01ca1f9ef8c86ef8318c059e0e9353235162f080f26e14c11",
|
||||||
|
"4c7f826edf647462f744b3f16d485f53c797eabdb21cc8a7bb0713283b88e629",
|
||||||
|
"d1621db4d91b23180707b8d4eb9b599fa8ec1dfc2453793a1f83878bd4bbc9d8",
|
||||||
|
"4b74667f89358cd582ad82b16a2d24d5bfcb89ac4b1347ee80e5674a13ba78b2",
|
||||||
|
"83d999a148625c3d2bb819af3064c0f6a12d7da88f68b2c69221f3a746171d19",
|
||||||
|
"b6494a74d18a2dfa3f80ced9fadae35807716fce1071e4de19e2d746b6d87606",
|
||||||
|
"b9c411db4036219e3dfcbe28d60e550b46cce86260fcf2c65d281258e437556f",
|
||||||
|
"2590201e2919a8aa6568c88900192aa54ef00e6c0974a5b0432f52614a841ec8",
|
||||||
|
"c15a5a65986e7ab4134dee3ab85254da5c5d4b04e78b4f16c82837192d355185",
|
||||||
|
"dab6c6065c439b9bafb0b0f1ff5a0c68273bce5c1959a4158ad6a70851f507b6",
|
||||||
|
"baf27a4cc4da49913e7fdecc951fd3b971c9279959af62b02b761a043c33384c",
|
||||||
|
"6c237d8b3b120251c38c230c06d9e48f0d3017657c5b65c8c36112eb15c52aeb",
|
||||||
|
"f173040998481bcb2534a53433eafb8d6ea4c7b0e1fc64572830471fe43fc77d",
|
||||||
|
"36732cc35fe56185af1b11160a393d6c73a1fe41ddf1184c10394c28ca5d627b",
|
||||||
|
"126103bfddc8df256b6e0abfd7f3797c80dcc4ea88f7c2f87dd4104220b4d65f",
|
||||||
|
"457e17b7ea97a845a0d1fa8feda9976596678e3a8af46dc6671d40e050ce857d",
|
||||||
|
"1739d937dc8c0c7370aa27585938c119e25c41f6c441a5d34c6d38503e3136ef",
|
||||||
|
"b676ded7c768d66a757aa3967b1243d90bf57afb09d1044d3219d8d424e4aea0",
|
||||||
|
"33bd77e5394520747faae1394a4af5fa47f404389676375b6dc7be865ed81452",
|
||||||
|
"fe7f6bc6f7338b76bbf80db402ade65953e20b2f23e66e898204b63cc42539a3",
|
||||||
|
"4f83ef69228b3e09b0abc11ded9e6b85319c0b7fef1a044b8ee9970e38441817",
|
||||||
|
"4523be58d395b1b196a9b8c82b038b6895cb02b683d0c253a955068dba1facd0",
|
||||||
|
"d8f38b894b42f7008305cebf17b48925654f22b180c5861b81141f80ccf72848",
|
||||||
|
"9c557e253213c127a86e333ff01c9f12f63091957efafd878d220a0e2cb1193e",
|
||||||
|
"9eab64e92219ccedb15ea9b75ababaa4ae831451019394e0e3336390c3a742d8",
|
||||||
|
"43dedbafef3748c3f9146b961c9b87a3f5cdb1ccb50b4f5890e408702a27a506",
|
||||||
|
"17717ad4d20e2a425cda0a2195624a0a4a73c4f6975f16b1593fc87fa46f2d58",
|
||||||
|
"ee6ea13ab9fe5c4a68eaf9b1a34fe014a66b40117c50ee2a614f4cda959b6e74",
|
||||||
|
"4d023ce9dfd75a7f3075b8e8e084008be17a1f750c63b5de721e6ef883adc765",
|
||||||
|
"d91191e30e00444b942c0e82cad470b32af171764c2275bee0bd99377efd4075",
|
||||||
|
"4eb88310d6b4ed95c6d66a395b3d3cf559b85faec8f7691dafd405a92e055d6d",
|
||||||
|
"0aeb0814c99a13df50643ca27b831a92aaae6366f54e9c276166347aa037d63a",
|
||||||
|
"16f1a0100d4cfffbcc4230e8e0e4290cc5849c1adc64d6653fda07c031b1074b",
|
||||||
|
"148d1366a5e4672b1321adf00321778f86a2371a4bdbe99133f28df0b3d32fa1",
|
||||||
|
"7076f6592de184f9e912c617c46e5e83bad91d3b7f88b7b54cc73bf8ca493321",
|
||||||
|
"dea51494fec5947d27ca659b73dd281ff5bdba3f89f5da1977a731ad0c22e725",
|
||||||
|
"aa97e3d392b97c21327081cdb3cb674dfa8c9c663493db43799a4079555ad1b1",
|
||||||
|
"b7dfbebf760efb2c756b3cae22baafdbbdf55abb782f85e93b4db804d5cba7e3",
|
||||||
|
"ca696d7edb4a86be7c7d9872bd2f9a44440cf8e2de7853536cbb3a60ae89641f",
|
||||||
|
"ff27d01cb1e56fb58580306c7ba76bb037bf211c5b573c56e4e70ca858755af0",
|
||||||
|
"58dece9ff68d021afe76d549b9e48e6cb7b06a5c14cdf45332c8ed7321d6f211",
|
||||||
|
"78688c1f371e7b923d95368c9298cca06c1ec0a89ea897aa181bd60091121fea",
|
||||||
|
"9e8dd91d21e867dec464868a8d1f4a27c0e113c53e32f2bec0a7c6e25ad2e9d5",
|
||||||
|
"6d028aa49aa1f584b3d35aee9fcee8e3c0d81108114289aa046a7969b21eb5f5",
|
||||||
|
"2c470abbac95a49cd0ed5b3b9e628ffda1dbb03c14caba1a225a9b8bf1dc9d5f",
|
||||||
|
"9d7af6946b320b3ba6b4d386de2b2cf3f8ac52fdcb63f3343d1a8362693a3ce5",
|
||||||
|
"d7a4345c3ead1ea7a34bd6aae43c63cbd81941d9ba019fe972843e5ce78e3187",
|
||||||
|
"00f471f6312ce408f7eb56592a2b6c6b5f54ac2967c77f4c1498903b598e1b16",
|
||||||
|
"41e9e2c8398583b90204f8e35c2a4c036aeebac6d05dbdc3e7fb44a1d6bd65a2",
|
||||||
|
"0d978064b9054c023111926050d983573dac2aff16bb8a7497fde8ad725357c0",
|
||||||
|
"507f5bf8367c1883f115ddf9ee95f79ea693c720eb5a5a8718443a99fa308954",
|
||||||
|
"9a090f86adf9fbdc37de2a14745e73d6e1fd096d3da0670b6795ce5ad3cfeea3",
|
||||||
|
"f2b7c5787424c8f9cf6c4480eb99f4a3770cc06337a4f0d1b109ba849b464193",
|
||||||
|
"6ffe93bb72d4ac788fd8be2dadf5bb4a2f14a330d530b0e68bd733c9744c6619",
|
||||||
|
"3ebc74907d1f928f209ef210e872cac033eaf3ff89e6853286d45d91e351ef9e",
|
||||||
|
"da56c54b5e6749d65ad038c196478794af94e4fa5a4efdc20b49981e4ec566c3",
|
||||||
|
"266815e0c9210dfa324c6cba3573b14bee49da4209a9456f9484e5106cd408a5",
|
||||||
|
"5b705e6cb602425c019202dd070a0c009b040ac19960eeef2d8a8fab25c1efe5",
|
||||||
|
"9ec7a778167afb1d30c4833de9322da0c08ba71a69e1911d5578d3144bb56437",
|
||||||
|
"e771af0b05c8e95fcdf6feb3500544d2fb1ccd384788e9f490bb3ee28e8ed66f",
|
||||||
|
"40b9c85fffeafc1cadf8c30a4e5c88660ff6e4971a0dc723d5ab674b5e61b451",
|
||||||
|
"efe5d120df0cc290fa748727fb45ac487caad346d4f2293ab069e8f01fc51981",
|
||||||
|
"408f636bd26fcc5f29889033b447cb2411f60ab1b8a5fc8cb3842dab758fdeb5",
|
||||||
|
"d8a2c33f2e2ff3a9d4ff2a5593f3d5a59e9167fa5ded063d0e49891776611e0c",
|
||||||
|
"02a11d1545114ab63c29958093c91b9f88618e56fee037b9d2fabcff32f62ea9",
|
||||||
|
"1bbd7fdf68eaf5c19446c3aaf63b39dd4a8e33548bc96f6bd239a4124d8f229e",
|
||||||
|
"726a1e261cc6474674e8285e3951b3bb139be9a773d1acf49dc868db861a1c11",
|
||||||
|
"167e7fe01a76b6bec9d2a9b196b18c72e150e985fbeb46ee651869e7b4032785",
|
||||||
|
"c88f94f0a391b9aaa1ffefd645253b1a968b0a422a876ea48920a95d45c33f47",
|
||||||
|
"cd169bd8fbd5179e2a8d498ffc31d3ae0e40825ff2b8a85ea359c4455a107ca8",
|
||||||
|
"fd38f135ef675eac5e93d5b2a738c41777c250188031caf1dcf07b1687a1fe49",
|
||||||
|
"6b4c612991132cf4c6c390dceaae75041b9954ba4f9c465aca70beb350815a57",
|
||||||
|
"8fec426247845bdd26f36ae4f737508c15dbec07d43ce18f8c136ab9e35ac212",
|
||||||
|
"af551accea482000bdccb34bd3c521558e1f353773a3caed83a147921c369ea1",
|
||||||
|
"a664a4973cd23e9f3b35a62429f7671aba2c2ae68c03313913b5c2d77269d906",
|
||||||
|
"18f54af1e10c5bb7a35468b0f62b295d12347903c9f95738d065c84bef1402ef",
|
||||||
|
"be39043cc12efbddfee564d95da751a71df6c139e2def45c431cadeb4a573ca3",
|
||||||
|
"01ddee289b1a2e90874ca3428a7a414764a6cad1abfaa985c201e7aada16d38c",
|
||||||
|
"da25cf7b457bddb6b7bc8e1b0146c0fa85373807d6efdac955199fd01fd53c1f",
|
||||||
|
"ec380784d96b93d404166a6bf1a778227a94a02bdf499f94b5f48f61f5b1350f",
|
||||||
|
"6538925ebfb661f418d8c7d074bee2e8afd778701dd89070c2da936d571e55c3",
|
||||||
|
"9edd72eb23222c969379d90d60ec82891b7c827188bb28510a863f59cb697b0a",
|
||||||
|
"09222857afceb23c66c99fc93d8e5ebda6d7aad901eb38af73c508f117685012",
|
||||||
|
"744ecc9a119a92da88b1f448b4030cdbc2fec5c37ea06ebdd026e742b002af7f",
|
||||||
|
"f531a8672baa2415b271e866dbe11fb08640f6c0e5d98f918bd0308e7169b5b7",
|
||||||
|
"44dc1c2db9c3fbd7bee9257eceb52be3cf8c40baf7b63f46e56b58a131c74f0b",
|
||||||
|
"89e14be49ed0073da83b678279cd29ba5ad86cf000b6a3d1a4c3dc4aa4fdd02c",
|
||||||
|
"8fb140b4e8ddef97ce4b821d247278a1a4353362623f64021484b372f948000c",
|
||||||
|
"72f9755501e1a4464f7277d86120f67e7f7ec3a84ef6813cc7606bf5e0870ff3",
|
||||||
|
"3d99feac152027ede63326aa4f43d4ca88e4cd27296b96fe18c55d496a8f6340",
|
||||||
|
"2540d50aeb9be889c3bd050c9cc849b57b156a2759b48084a83db59aa9056eb4",
|
||||||
|
"b66be78da89991544a05c3a2b63da1d15eefe8e9a1bb6a4369f8616865bd6b7c",
|
||||||
|
"2f5de0003db84ecd5449128350c66c7fb63e9d02b250d84af84f463e2f9bcef1",
|
||||||
|
"2045369fc115b138d1438f98d3c29916986c9fde6b8203f7ff8699f0faee1c93",
|
||||||
|
"ae1008d23930b776c18092f6eab41e4b09fcf3f03f3641b1b4e6ee3aa166d760",
|
||||||
|
"1ec454734dcbf6fe54901ce25c0c7c6bca5edd89443416761fadc321d38df139",
|
||||||
|
"b2d670de53b27691c0c3400225b65c35a26d06093bcc41f48ffc71e0907f9d4a",
|
||||||
|
"ac3f6afe17593f61810513dac9a1e544e87b9ce91b27d37b88ec58fbaa9014aa",
|
||||||
|
"d1f3c71639ae3bba17ffc6c8deb1fdb3a56506b3492213d033528cc291523704",
|
||||||
|
"6b4a29bbd43d1d0eeead384f512dbb591ce9407d27dba48ad54b00d9d2e1972b",
|
||||||
|
"84de08882b6e36705cf6592ee58e632dd6e092dd61c13192fc80cbbc0cbc82cc",
|
||||||
|
"d3d74124ddfb5bdc61b8f18d17c3335bbb4f8c71182a35ee27314a49a4eb7b1d",
|
||||||
|
"a008def15796fba9a0d6fab04e8fd57089285d9fd505da5a83fe8aad57a3564d",
|
||||||
|
"eb7246eb8e26b0c48dd4f9c2a822a0f4d5c84138937195090932b61a2d756051",
|
||||||
|
"683211bd155c7b764e4b99ba263a151d81209be7a566a2bb1971dc1bbd3b715e",
|
||||||
|
"5468bceeb74ce35cb4173dcc9974bddac9e894a74bf3d44f9ca8b7554605c9ed",
|
||||||
|
"78ce6faa72264387284e647ba6938995735ec8c7d5c5a65737e55130f026307d",
|
||||||
|
"2754fc862d6bc0b7c3971046612d942563d181c187a391e180ed6b00f80e7e5b",
|
||||||
|
"f1725586a402c06aec818d1478a45aaa0dc16c7a9c4869d97c350336d16f8e43",
|
||||||
|
"6a359852238dc902aed19fbbf6a055f9abf21c1ca8915d1c4e27f50df2f290d9",
|
||||||
|
"9e4954853fca260cecf983f098e5204c68b2bdfebde91f1f7b25c10b566d50f8",
|
||||||
|
"3356de61b39647931ce8b2140b2bab837e0810c0ef515bbe92de0248040b8bdd",
|
||||||
|
"3e294d2fd339bb16a5403a86e3664947dd408c4d87a0066524f8a573ae53ca8e",
|
||||||
|
"21335073401a310cc9179fe3a77e9666710cfdf630dfd840f972c183a244b1ad",
|
||||||
|
"987096ef8a2853fea1a31b0ed5276503da291536f167bbf7f3f991c9f05d6d7f",
|
||||||
|
"7a78fbfec68c2b3ab6084f1f808321ba3b5ea47502c41115902013e648e76288",
|
||||||
|
"c12a2bcb002fd74b4d342f9b942c24c44cc46d5ed39201245a8b6f4162e7efce",
|
||||||
|
"8867bed93e89c93d0d8ac98b2443c5554799edb9190346946b12e03f13664450",
|
||||||
|
"9b61cd02adac4b18fbcc06237e7469b07e276faf6ec4ecb34b030c2e385892a0",
|
||||||
|
"0463223adf38df9a22a7fb07999a638fdd42d8437573e0bf19c43e013b14d673",
|
||||||
|
"9989500413fb756d8437912cc32be0730dbe1bfc6b5d2eef759e1456c239f905",
|
||||||
|
"17e2889fba01021d048a13fd0ba108ad31c38326295460c21e69c43fa8fbe515",
|
||||||
|
"6c6c253fe26a5b2abf440124e35dcaa39e891cd28274431ba49da5c11d89747d",
|
||||||
|
"9d065f84c0cba7b0ef86f5d2d155e6ce01178a8a33e194f9999b7497b1b2201b",
|
||||||
|
"5ffb8e1b6b629c0e34a013f9298ebb0759b98a3d24029916321d5eb4255b6735",
|
||||||
|
"3fc5f8553abd753ac47967c4c468cfd08e8cb9dee71b79e12d5adab205bc04d3",
|
||||||
|
"ff82c8b53aa53a9705200690b91c572e2e4918f1a88de5d923ac06fa4560fa19",
|
||||||
|
"4d4ab737e2fbb5af0fd590b4b7e8c6fe76d3a02a9791ef7fdacf601f9e50fad8",
|
||||||
|
"5eca50a04afaefe55659fb74810b42654e2268c1acca6e53801b9862db74a83a",
|
||||||
|
"d700fc10d457eeae4f02eb04d715a054837e68a2e2d010971382c5e1016dc99e",
|
||||||
|
"af321973db865bb33fbc50a4de67fc0e6808d812c6e4dfd9cbc2fd50275b1dfd",
|
||||||
|
"bbf923aa9246065f88c40c7d9bf61cccc0ff3fcff065a8cb2ff4cfbb62088f1e",
|
||||||
|
"268b948b5aab4bab0e5430ee49e3cff11776cf183df93b32159f9670ed541495",
|
||||||
|
"3d2e51508699f98f0f2bdbe7a45b673c687fe6420f466dc296d90b908d51d594",
|
||||||
|
"4d4fb5ff0afb8c04e6c6e03f51281b664576f985e5bc34a3a7ee310a1e821f47",
|
||||||
|
"9b12847f3d28bf8850ebc03f8d495a1ae8f9a2c86dbda295c90556619a3ee831",
|
||||||
|
"733c5427f55ceba01a0f6607ab0fd11832bbb27d7db17b570e7eb7b68a081d9a",
|
||||||
|
"4bc7982c4ee4078b2ada5340ae673f18d3b6a664b1f97e8d6799e6074cb5c39d",
|
||||||
|
"afa0f26dbf3e674630d1cd6499e86c14f316cd4f78c6ab73bb85b00aa9c50a57",
|
||||||
|
"c301f13372c8f0d9bc8186d874fa45fa33aede13e66f4187a3bd22ee41c95b2a",
|
||||||
|
"548a29f145187fc97689f8ae67944627723c315c163b0dbb88842e50c681d7ca",
|
||||||
|
"d0a1ffb8761b974cec4a3be8cbcb2e96a7090dcf465ffeac839aa4ca20c9a59e",
|
||||||
|
"faaf47af27e3de06e83f346fc6ccea0aabfc7520d82ffe90c48dfcd740c69caa",
|
||||||
|
"3eacaa768326d7dce80f6ee17ada199bebe7eb3c1a60b39b14e0a58bbac66fe4",
|
||||||
|
"7f5237e9f77a22c4a89624c7ac31cae797d8ac4144b02493890d54fee7399bcd",
|
||||||
|
"d84517802a434757c56ae8642bffb4d26e5ade0712053750215680f5896e579b",
|
||||||
|
"bdb96ad31ac6af123c7683c55775ee2138da0f8f011e3994d56a27270e692575",
|
||||||
|
"aab1b0caf13b9bd26a62cf8b3b20f9bfaa0e56f3ec42196a00fedf432e07d739",
|
||||||
|
"c230edd34ca5c8318bf4592ac056cde37519d395c0904c37ea1c650b8ad4a712",
|
||||||
|
"ce41c1698a8c042218bc586f0b9ec8d5bffa3dcbcea09bd59db9d0d92c3fc0b4",
|
||||||
|
"b9a537523bba2fcdae857d90d8a760de4f2139c9f90d986f747ce7d0ec0d173d",
|
||||||
|
"1a6e0aeff1dba7ba121fbeb33bf3162901495df3fcb4e4a40423e1c10edf0dca",
|
||||||
|
"21b419102da8fc0ba90484aec934bf55b7abcf75eedb39124e8d75e491f41a5e",
|
||||||
|
"2183e94758481d0f124fbd93c56ccaa45e7e545ceeb8d52848f98253f497b975",
|
||||||
|
"e3f98bfb9cbeb7563a139983602e50f616cb7ebb06c3295b8ee377328f051206",
|
||||||
|
"b5b9b84d1723994d06013606227fb5b91f9de8820b04cf848d1dccc23d054f39",
|
||||||
|
"07adfda9c5adc80881bb2a5220f6e3181e0c043b90fa115c4f183464022968e6",
|
||||||
|
"facdaf1ce758bdf04cdf1a1fa32a3564a608d4abc2481a286ffc178f86953ef0",
|
||||||
|
"efc83f01c8fb309df2c8866b8c7924cc8b6f0580afdde1d6e16e2b6107c2862c",
|
||||||
|
"52b4a076bcbbbdc3a1aefa3735816cf74993b1b8db202b01c883c58be7fad8bd",
|
||||||
|
"c6f7077f1699d50cf92a9652bfebffac05fc6842b9ee391089d959b8ad5d48fd",
|
||||||
|
"e7424ad457e512fdf4764a56bf6d428a06a13a1006af1fb8e0fe32f6d03265c7",
|
||||||
|
"27797bd4e5ee52db0a197668c92b9a3e7e237e1f9fa73a10c38d731c294cfc9a",
|
||||||
|
"7bdef7bdebb8721f77927d0e77c66059360fa62371fdf15f3add93923a613229",
|
||||||
|
"3335d373e6c1b5bc669b4b1220c08728ea8ce622e5a7cfeeb4c0001d91ded1de",
|
||||||
|
"645681b9d067b1a362c4bee8ddff987d2466d49905c26cb8fec5e6fb73af5c84",
|
||||||
|
"06b7819d7f1c7f5472118266ed7bca8785dceae09e36ea3a4af665c6d1d8327c",
|
||||||
|
"7a6b8c7de171955c214ded7e35cc782cd6dddfd141abb1929c632f69348e6f49",
|
||||||
|
"eb882b0bb659bf72235020a0b884c4a7d817e0af3903715736b146188b1d0868",
|
||||||
|
"2ae6c71323a225ecfa8cf655600ebbe12b1019ff36bf02726d82d095aab29729",
|
||||||
|
"c2f85a06279a7bfa7f2477a3cee907990231a13d17b54524738504bd12e0c86c",
|
||||||
|
"f1b911af1c7a56073e3b83ba7eaa681467040e0fbbdd265445aa80e65c274c22",
|
||||||
|
"a54c2ae6ec6ac06b4d7b45c483eab86ac226b8ecfa99163ef7cc000da9b40895",
|
||||||
|
"bbc73cae41502ddad7a4112586dcaf4422810d60aa4b57c637ccd1a746b07844",
|
||||||
|
"218238431393959d6c8617a3bd899303a96609b44a644e973891038a7de8622d",
|
||||||
|
"59cacbd83ad5c54ad91dacf51a49c06e0bef730ac0e7c235a6f6fa29b9230f02",
|
||||||
|
"ba80990666ef0b6f4ba5059347beb13242921e54669e680064ca755256a1e3a6",
|
||||||
|
"031db544f3158556508b321db59afd62c5bb1592021e5dfd9ff87dca0ad27d8c",
|
||||||
|
"ee85604f8ec6e4e24f8eaf2a624d042ebd431dae448fe11779adcfb6bb78575e",
|
||||||
|
"266ee74062e8dae0aeddfcd0f72725107598efaa80c1a7176d6ee6dd302bce4c",
|
||||||
|
"b83a28b7e4e5d20bd960c5faeb6625f95529166b8bdb045d42634a2f35919450",
|
||||||
|
"dbe0605a9c73172bad7523a327b236d55ea4b634e80e78a9013db791f8fd5b2c",
|
||||||
|
"1e53e900c3bbc5ead295215efe27b2c8d5fbd15fb3dd810da3063674cb7213b2",
|
||||||
|
"832a2b3cef4b1754c4a7572964a44db64d19edf627ec45179b519d0a5eae8199",
|
||||||
|
"4c800257a588a82849d049817c2bdaad984b25a45ad9f6dad66e47d3b47e3b2f",
|
||||||
|
"3743244390be53473a7e3b3b8d04dce83f6c9514b81a997fb3b123c072ef9f78",
|
||||||
|
"f96c3d76497074c4c83a7b3823380e77dc73d5a9494fd2e053e4a1453e17824b",
|
||||||
|
"d04ecf33a303a59852fdb681ed8b412201ba85d8d2199aec73cb62681d62aa90",
|
||||||
|
"0cca6201658d5d98239c1511ef402562ff7d72446fb201a8d1857c39e369c9fa",
|
||||||
|
"61066504617ee79387021e18c89fb79d1ddbc3e7bff19cf2298f40466f8715e9",
|
||||||
|
"7adb520c3ac7cb6dc8253508df0ce1d975da49fefda9b5c956744a049d230ace",
|
||||||
|
"7579076d9aff0a4cfdefa7e2045f2486c7e5d8bc63bfc6b45397233e1bbfcb19",
|
||||||
|
"9ec078ef9ca31e1bdbb97175dde1cb00bf9f7225e6f622ccc8d367302e220497",
|
||||||
|
"93e174736c4719f80627854aca8b67efd0b59558c8ece267a8eccbbd2e7c5535",
|
||||||
|
"e62f419a0e16607b96ff10ecb00249af7d4b69c7d121e4b04130c61cc998c32e",
|
||||||
|
"171ddd43dab1af0d1fb14029287152a4c89296890e0607cf5e7ba73c73fdf1a5",
|
||||||
|
"604e96e099936a104883958b040b47672e0f048c98ac793f37ffe4c720279eb2",
|
||||||
|
"7726c437ccf791f6ded97dbac1846e62019e5fbd24f42e9db2f640f231c3c09a",
|
||||||
|
"1bf9f239dca1636149bc2f3fc334077ae959ea9607cacf945ef8f8bb227dc5e1",
|
||||||
|
"fcd818454002a6c47a980393f0549ac6e629d28d5688114bb60d831b5c1832a7",
|
||||||
|
"56172b53f730750b40e63c501b16068dd96a245e7f0551675c0fec9817ee96e0",
|
||||||
|
"260d3a820b7f8de20f4972725999b1af88b0cc5554ca38f9681c8d657e043cc3",
|
||||||
|
"9ba8c688f091ca48de2b0f9bc998e3bc36a0092149f9201767da592849777f1c",
|
||||||
|
"61594d714aa94fe551f604123578c4a6592145f4228ad8601224b1b89ce401b0",
|
||||||
|
"416ca193aa5448b8cca1f09642807765cc0ee299609f972df0614cfb8ea2f2b1",
|
||||||
|
"9b6d95b76a01191a4c778185681ed7f3bced2fffa8e41516ec78240b213285f5",
|
||||||
|
"ee0304bae0d4679bb34347ce3b1b80482262b9812bd0c0d5e19a5e2445043b75",
|
||||||
|
"8027a1877f39e603dafc63279e004b4ed9df861d18ce81d9c43c7d7135da8f65",
|
||||||
|
"42b409ff9b261a985227b1ab92707e706777ac14de24654d7e05f0501b37e003",
|
||||||
|
"99097983b74c70800b182abc6f64046ab70407e9cabcd6cf570a38ada9ef75d5",
|
||||||
|
"de8ca7a6b3f7314e91921d4dc5e915fb7bc2bd32129ea6966322effa48050c4c",
|
||||||
|
"dcb302978215f54f33c3d2d7157ef69fd5058cf488fc43dd75c32b5dcaf47e7a",
|
||||||
|
"7c765d407d3a9d5ea117cb8b8699628560787fc084a0c76afaa449bfbd121d84",
|
||||||
|
"06639a386c9c1014217622ccbcf40908c4f1a0c33e23f8d6d68f4abf655f8f71",
|
||||||
|
"59f1b5faf29904fe94a6a042e2d82d80d68fc16ad7651eba90a8de39f63f8fe8",
|
||||||
|
"174398550d1468a41b98a09f496c38d3694feadef0f0073fd557610384bafb10",
|
||||||
|
"00d52016bd7e4aae8bf8eaa23f42276b649fe557483b5d7684702633dd0fd944",
|
||||||
|
"9a39bf837c868d61ed8cce6a4c7a0eb96f5e5bcc082ad6afdd5496cb614a23fb",
|
||||||
|
"74ffc51cc30150cf79b6cb316d3a15cf332ab29a38fec9eb484ab1551d6d1856",
|
||||||
|
"97c70a44366a6535c145b333f973ea86dfdc2d7a99da618c40c64705ad98e322",
|
||||||
|
"6c6e3e05e1c9d2aae0ed2431544aea411771dd9d81017539af0fd818b2389f28",
|
||||||
|
"23a2cf63ec81e65561acafc655898d2fd0ef190084653fa452413f75e5a3d5bc",
|
||||||
|
"f1f9b0996d4ff1bf75e79e4cc8577c89eb633e68415c7faf74cf17a07bf80bd8",
|
||||||
|
"e3aefda887252a72cee3578d33b2dcd90e9fe53b8bed6347ef5e26f74211adbb",
|
||||||
|
"6b4ec98f02e647e01440b473bbd92a7fae21e01b6aa6c65e32db94a36092272e",
|
||||||
|
"623ed218de81311783656783d6ce690b521a89c4dc09f28962e5bfd4fa549249",
|
||||||
|
"9ce71f1506ccf4b99f234af49bd6202be883a80f95a155c6e9a1c36fd7e780c7",
|
||||||
|
"139fcc6bb304b2879974c59cda61d86d7816ad4ac0f38ee7a724df488060e65d",
|
||||||
|
"0e8c41eb946657188ea6f2aac36c25e393fff4d4149a83679220d66595ff0faa",
|
||||||
|
"59ffbe1fc829decf90655438bd2df3a7b746ef4a04634d4ee9e280bb6ce5f14e",
|
||||||
|
"e4f695f05bb05b231255ccce3d471b8d79c64a65bccc014662d27f0f7e921092",
|
||||||
|
"39cc53c9e3f7d4980b21bea5ebc8a5b9cdf7fa6539430b5a826e8ad527168656",
|
||||||
|
"685fb49563864326e78df461468795b7e47849a27e713281cd8bb75c0547936d",
|
||||||
|
"05e4649832dfb8d1bfa81ea7cbf1c92c4f1cd5052bfc8d5465ba744aa6fa5eb8",
|
||||||
|
"e5cece49ae3fc2c81f50c8e7a93a5fb1e1585380c467e4822234b64a94add617",
|
||||||
|
"dda028cd1b806b4d494cc7f2789b6c2bd7e3c28ff6a267d03acc5ac6e69a05e0",
|
||||||
|
"046c436b2a525059867b40c81e469b6d83001442fc65312c87a7ce7abeb022ff",
|
||||||
|
"676ffea2ec31426a906d7795d7ebae2ba5e61f0b9fa815995b4a299dd085d510",
|
||||||
|
"15b5cf6cdf4fd1c02f28bcce0f197cafae4c8c7c66a3e2e23af9fe610875315e",
|
||||||
|
"c0fb5367cfcb803c5383f98e26524bed9176e6211588f53ec63fe6079cbfd3df",
|
||||||
|
"7ab00f596b0286b77f78af567ee1be2536feee41daee67bd872f1480b7aa65b9",
|
||||||
|
"e8d66519e43b1214ac68f9f2bdbc4386d41ac66b20c5a260b9b04102784074e9",
|
||||||
|
"e6618db6961dc7b91478e0fa78c4c1b6699009981526693bd5e273972550860c",
|
||||||
|
"b1e1185884a6d14bbfce3899cb53e8183adde642f264d0ff4f1745371e06134c",
|
||||||
|
"cae5b7ea348afefc4c102bb7b125c4928f114739a27b877c6bcfbe5a79280384",
|
||||||
|
"ecbe372132a9323b813eeb48f8dfcedaeca00e2887af181b063c6cfa13ed8ea1",
|
||||||
|
"52387c6b99cc42aac51916b08b7b51d2baddfc19f2ba08d82a48432849dbdfb2",
|
||||||
|
"3c39a7b53dec9ac85acf08b267637a9841e6df7b7b0f5e2ac56a8cf107de37da",
|
||||||
|
"f8e6c64342f1e052480630e27e1016dce35fc3a614e60434fef4aa2503328ca9",
|
||||||
|
"fd0266485777bd73e97c7c37f520c83c82e362fe4c25a6be93f3380083d4646b",
|
||||||
|
"433e80c14ff7b8e16e179ccec35f55833df7dd5a5a063d23117b4b01b6f97170",
|
||||||
|
"b7c6f6915cfa9a62fff6a1f02604de88c23c6c6c6d1b8f62c7cc10749f307e81",
|
||||||
|
"ddf03aca85ade039e6742d5bef3df352df199d0d31e22b9858e7eda85cb3bbbe",
|
||||||
|
"d36e8083fa7b36daee646cb8b3f99feaa3d89e5a396508741f003e21ac0b6bec",
|
||||||
|
"ec79b568bdea63ca6091f5b84b0c639c10a0919e175fa09a4de3154f82906f25",
|
||||||
|
"8cd2d0f8310f7009e94f50231870756cb39ba68f37506044910e2f71482b1788",
|
||||||
|
"0eef96197f5c6be3859b6817e6a5736685856c416e29a2925bd5a15b2a57c8b1",
|
||||||
|
"04918dfc36c93e7db6cc0d60f37e1522f1c36b64d3f4b424c532d7c595febbc5",
|
||||||
|
"c8383d81dd24406745b68409be40d6721c301029464067fcc50a25ddf9139549",
|
||||||
|
"3b3a42d34cf0a1402d18d536c9d2ac2eb1c6019a9153be57084c8165d192e325",
|
||||||
|
"da18e9860040f3bf493876fc16b1a912ae5a6f6fa8d5159c3de2b8233a0d9851",
|
||||||
|
"e3fc673fc5f99cc554d0ff47756795647d25cb6e6658f912d114ae6429d35d35",
|
||||||
|
"3aa5817273c3b2f94f491840e0472f049d0f10009e23de63006166bca9b36ea3",
|
||||||
|
"bbb5dda0e15567979f0543407bdc2033d6f0bbb30f72512a981cfdb2f09e2747",
|
||||||
|
"1096f6be0a4d7f0ecc2df4ed2c8683f143efc81eeba3ece6daadd2fca74c7ecc",
|
||||||
|
"d76726da1b64e8679d8b6e66facf551ba96f2612de5a171fac818ee85ce3e5fe",
|
||||||
|
"27487c9600b16b24a1bfb0519cfe4a5d1ad84959e3cce5d6d7a99d48660a1f78",
|
||||||
|
"5d3ab876c206a37ad3b094e20bfc3941df3fa21a15ac8ea76d6918473789669a",
|
||||||
|
"6b1b8dac34ffc61d464dfeef00e4a84a604e172ef6391fb629293d6f1666148c",
|
||||||
|
"6fb266012c3008303e54ae55140b46957e9978098401dda34f4d921a275bf8bb",
|
||||||
|
"53a91e3a64d1f658e983ac1e4f9e0c697f8f33e01d8debe439f4c1a92113f592",
|
||||||
|
"5082984480f3b27891840a2037512739149678efc2ac981ca8cd016d02304efd",
|
||||||
|
"7b849efa5604b58d50c419637b9873847dbf957081d526136c3a49b7357cd617",
|
||||||
|
"f53b9d91a8cd177fb4a1cf081a1b6d58759a381ef120a7c5a18c0e70cae80983",
|
||||||
|
"cfd7df62799a22e384a4ab5da8c4026c875b119d0f47c2716b20cdac9cc1f1a6",
|
||||||
|
"d83b5ef189df7e884627294b752969547814c3cfe38995cf207c040e03bbe7a4",
|
||||||
|
"96f652249b0946e1575d78a8bc7450123c8e64f1c56f6b2f93bc23fb249ed85a",
|
||||||
|
"d60bdad03468f5f8c85b1b10db977e310a5aafab33750dfadb37488b02bfc8d7",
|
||||||
|
"9839f160d893daae661c84168e07f46f0e1e9746feb8439a6d76738b4ad32eaa",
|
||||||
|
"453a656903a031395d450f318211a6ec54cd79049a851f92cd6702c65ff5f5bd",
|
||||||
|
"1634b87b5fcfd4a6c4ff2f2de17450ccce46f9abe0b02a71876c596ec165bfed",
|
||||||
|
"24480686b56234a240fd9827209b584847f3d4f9657f0d9a97aec5320a264acb",
|
||||||
|
"f4d1866e8599563c52ceeedf11c28b8567e465c6e9a91df92add535d57f02ab0",
|
||||||
|
"805b34f708837dfb3e7f05815ac5760564628b58d5a0ce839ccbb6ef3620fac3",
|
||||||
|
"659a74f6cfbc7c252c58d93452b9d9575e36c464aa6544c6375227c9166a6ed9",
|
||||||
|
"75d737c3472471029c44876b330d2284288a42779b591a2ed4daa1c6c07efaf7",
|
||||||
|
"dac1d8c5a9fe94f224e095b52577c33c2cc2b8f3a2d6ad9cbd46845af8c987f0",
|
||||||
|
"be7358c4fe50148cccafc02ea205d80145e253889aa3958daafa8637047c840e",
|
||||||
|
"30e8cbf1427c137fa60674a639431c19a9d6f4c07fd2959df83158e674fccbaa",
|
||||||
|
"7f573f55d875ce8edc528edf822949fd2ab9f9c65d914a40225663b0a697be07",
|
||||||
|
"781a1527055f74c1f70230f10384609b34548f8ab6a0a6caa74025827f9fdae5",
|
||||||
|
"d82a91e1013170b10ca7fa0ec800fd0dc6e9335b70c303dadba603fc36802b5f",
|
||||||
|
"a4237e420cdb0b3231d171fe879bcae37a2db7abf2f12a337b975337618c3ac2",
|
||||||
|
"7ff4d89f90845ac4d7a50a259163798e0f446e61d4c943cc89637beff567ad02",
|
||||||
|
"48dbb5e717a6221d64fd13ba12794bc28e5067ac1d7632ee9437d533772750df",
|
||||||
|
"efba340bd479176486e5a2281c97ac4a90fdcf86ec9c13a78c3182ab877cd19b",
|
||||||
|
"1021c8921548fa89abb4cc7e8668a3a8dcebae0a4c323ffeaf570438832d6993",
|
||||||
|
"c67cd3e1a83daa56cff16f635db2fdb9ed9619300298d4701a58e68e84098345",
|
||||||
|
"4ad6fa2d16e2a9b576c863b4cf7404a70d4dc320c0c447d10ad6ff58993eacc8",
|
||||||
|
"e568a76a4f8836be296d405eb41034260d55e2361e4b2ef88350a4003bbd5f9b",
|
||||||
|
"ebdee92945ef05283be0ac3de25787c81a6a58a10f568f9c6b61d9dd513adbad",
|
||||||
|
"6e8f3edfa28bfc8057d735794f76b697bcf18fb894a5a37a132693ebda31a464",
|
||||||
|
"576d23dc3db2056d208849462fee358cf9f0f3310a2c63cb6c267a4b9f5848f9",
|
||||||
|
"18905d0a5d623ab81a98ba98c582bd5f57f2506c6b808905fc599d5a0b229b08",
|
||||||
|
"a9046cc9175dc5a45fb93a2c890f9a8b18c707fa6d695771aab9300081d3e21a",
|
||||||
|
"7a69e5f62fcc20e81beea7461a945e6531f8c7944200d0b3cb4cc63556c44106",
|
||||||
|
"fd0266485777bd73e97c7c37f520c83c82e362fe4c25a6be93f3380083d4646b",
|
||||||
|
"4b29db7a76f3b4fbc0a4fffc092e41c14f1a1a975a462d87e82827af03719cb2",
|
||||||
|
"df1a6cb6c95a5bdd2a69e4fa921061da950fc0bb0b3529d04ca75e0c11f871df",
|
||||||
|
"08bfc00b7f72e015f45c326f486bec16e4d5236b70e44543f1c5e86a8e21c76a",
|
||||||
|
"1e908fbc1d131c17a87f32069f53f64f45c75f91a2f6d43f8aa6410974da5562",
|
||||||
|
"b3a737d014a7e75f44b0f5afbd752f9bcc2abe54f60dbbebc3681b6e16611967",
|
||||||
|
"d3052ca3e3d523b1ec80671eb1bba0517a2f522e195778dc83dd03a8d84a170e",
|
||||||
|
"b98ded4ceaea20790dbcb3c31400692009d34c7f9927c286835a99b7481a5c22",
|
||||||
|
"9e1e498420bc7c35f3e3a78d20045b4c8343986dae48739759bccb2a27e88c53",
|
||||||
|
"141d2053cb29535ad45aa9e865cdec492524f0ec0066496b98b7099daab5d658",
|
||||||
|
"8722c3843c85ddd6162a5cb506e1cb4d6ab0cafb966034f426e55a2ef89a345e",
|
||||||
|
"52dfd21724329920c5c95f5361464e468584136d30030eb29247a7fe6c2c6e36",
|
||||||
|
"d82a91e1013170b10ca7fa0ec800fd0dc6e9335b70c303dadba603fc36802b5f",
|
||||||
|
"6bbb7d71eaa2544215a877e136cd7f490f4625eb56459a0da856cc8296d5df30",
|
||||||
|
"1ebb28301aa1a48248d3723a0ea434bb7d4612ec920fa749583e3f41ce25849f",
|
||||||
|
"00000001505e7e48927046e9bbaa728b1f3b511227e2200c578d6e6bb0c77eb9",
|
||||||
|
"a01b5ba26421374250442e0d23f96e6a4bce429e0175cd0769ad8c585dd5a892",
|
||||||
|
"26d6a946675e603f8de4bf6f9cef442037b70c7eee170ff06ed7673fc34c98f1",
|
||||||
|
"1dd7992ea0ecbda7480ceed35748c3655691133c8c78af947fd24299db8f481f",
|
||||||
|
"cdee943cbb19c51ab847a66d5d774373aa9f63d287246bb59b0827fa5e637400",
|
||||||
|
"3d03c53608415b1d718c7786ee10bdb4e67bced32207e32880ee9e44301a19ec",
|
||||||
|
"170dc4045d6c06275b40bd39f68ca31dbb962094e9763ee460f8341bd40bebca",
|
||||||
|
"db1abbff170320730e5ace672ad7217161b8935afc1a896ae2fecf903c159932",
|
||||||
|
"b0ac2c26eabdb0e0a9b0d10fd1458ca73c575b19d65e13f0e7484cbee84038b3",
|
||||||
|
"c1e7fc21b4f9c199e6086e095639f0f16a4e4884544547ce8a653ed7b5b6c4a7",
|
||||||
|
"813c2662366a12f6337b951c048552fd3c4894e403cab701634dcd803786dc09",
|
||||||
|
"fd0bcf8cd1aee83fe75e6c0fdfc543845e5bc3f50d26d2d5e5c6d3fa521f98c0",
|
||||||
|
"45fd1955f590da87c1fd2edb99d17accf235ec3ebf0afe1d3306ade42693c6e9",
|
||||||
|
"27938497226683c701e2843c6db580e2f0e25f5a198f4c3397e3a0a27764215d",
|
||||||
|
"2321edfd415f9558605b4d7a7083c52624e8922ae86bb2ae359fbf829724111a",
|
||||||
|
"0461fcbecc4c3374439932d6b8f11269ccdb7cc973ad7a50ae362db135a474dd",
|
||||||
|
"1f8e182bf72d61cb78dcd6b96dd3be8b874b8881da6630757b6508970f67230c",
|
||||||
|
"c6603b0f1ccfec625d9c08b753e4f774eaf7d1cf2769223125b5fd4da728019e",
|
||||||
|
"296842eaaed9be5ae0668da09fe48aac0521c4af859ad547d93145e5ac34c17e",
|
||||||
|
"88f8707a45e825a13ed383332abe6e2f104ab44d877918be22550083a2b59e60",
|
||||||
|
"27a20b41a66b35d442302a50ca1baad72c2ed844c8d1224c9f6d50a12752084e",
|
||||||
|
"280e847ef0c82a2a7c4e877c91cd7567474c1431b815d27bbc1017e147d9d77c",
|
||||||
|
"ad9d42203fd2480ea2e5c4c64593a027708aebe2b02aa60bd7b1d666daa5b08d",
|
||||||
|
"bb0174ae21a6cac1a0a9c8b4ac6ebfda56ce51605c315b1824970bc275f7239a",
|
||||||
|
"edb470271297ac5a61f277f3cd14de54c67eb5ccd20ef0d9df29be18685bb004",
|
||||||
|
"9609b093450dd7e0afb389619acdaf2e6a0d6817c93552f3911e05b50ae73e3d",
|
||||||
|
"3e33fd7124f174fc535151937f8718634dd9d856143d4cefb5a10ddaf2f615c0",
|
||||||
|
"463555bb4b0f80fd1376fae628fabfaf7e5e31cd2741d80aa4d225c926bc287e",
|
||||||
|
"916cb5ff07d3b51cef7f6b6b7f5479b1001b401c0e82558ee1a22504c7d507c9",
|
||||||
|
"cc5f259f036683798e4a52071dbb97238702ffb6f0c85af6d273c8ddbe5c0afb",
|
||||||
|
"2ad91f1dca2dcd5fc89e7208d1e5059f0bac0870d63fc3bac21c7a9388fa18fd",
|
||||||
|
"8bf629b3d519a0f8a8390137a445c0eb2f5f2b4a8ed71151de898051e8006f13",
|
||||||
|
"94215f42a96335c87fcb9e881a0bbb62b9a795519e109cf5f9d2ef617681f622",
|
||||||
|
"bcbf9644d3f475d00eb9c6e467385ce16d4546c1a24222ccfa542bf776eaba95",
|
||||||
|
"ba5115c37b0f911e530ed6c487ccbd9b737da33fd4b88a9f590860378c06af62",
|
||||||
|
"609f186ca023d658c0fe019570472f59565c8be1dc163b1541fac9d90aa4e8af",
|
||||||
|
"4e5622b575cdbb4d5ded093e48c68cd3f724fad547142f0c1b0aec2f2b2a0b2e",
|
||||||
|
"4df7b43b3a4db4b99e3dbad6bd0f84226726efd63ae7e027f91acbd91b4dba48",
|
||||||
|
"bcbf9644d3f475d00eb9c6e467385ce16d4546c1a24222ccfa542bf776eaba95",
|
||||||
|
"3a06add309fd8419ea4d4e475e9c0dff5909c635d9769bf0728232f3a0683a84",
|
||||||
|
"d2384c4ac623eb9f15ae4cb32ee7a9b03e0202802d52f2395f2ee8f6433151aa",
|
||||||
|
"1d4cc828b657da8c7a101e8657365459b9dc74139bed5d35bd8295b00be2a1ae",
|
||||||
|
"76fcec0e0638351f1d0e0dc4ebaf6dd3d67404126d664547674070f3175273d9",
|
||||||
|
"6707c39e6c53ef945c5df29af78667dc941ed80094994bd264fd806a6e0a3230",
|
||||||
|
"80482e60178c2ce996da6d67577f56a2b2c47ccb1c84c81f2b7960637cb71b78",
|
||||||
|
"147784df719c09fad62bff0493a60b4f5dbbe8579e73f00d207350e8ffdfd65f",
|
||||||
|
"afc0295d2c6e0a1820c214c07312070a4070d52083163d7fe410fa02bf85d9d2",
|
||||||
|
"6a5e3cc17279cbdf051c06d96e3f843cdb296f351d8ca35a6a190c0ab90dbf9a",
|
||||||
|
"3b7fc823611f1aeaea63ee3bf69b25b8aa16ec6e81d1afc39026808fe194354f",
|
||||||
|
"d96fe9c5478d1bb36e9ec40cc678b0bf7ff67e017922a151f925640a8884f291",
|
||||||
|
"1d80e5588de010d137a67c42b03717595f5f510e73e42cfc48f31bae91844d59",
|
||||||
|
"06dde95f0268ce40128bf73ca6e85567b8567688ea52f24dcd5734e77c50f2d9",
|
||||||
|
"f683e87035f7ad4f44e0b98cfbd9537e16455a92cd38cefc4cb31db7557f5ef2",
|
||||||
|
"036533caa872376946d4e4fdea4c1a0441eda38ca2d9d9417bb36006cbaabf58",
|
||||||
|
"7cc328a08ddb2afdf9f9be77beff4c83489ff979721827d628a542f32a247c0e",
|
||||||
|
"f240be2b684f85cc81566f2081386af81d7427ea86250c8bde6b7a8500c761ba",
|
||||||
|
"fb61b93d864e4f0eba766bb8556f2dc0262e8e985012e29ba28508dd52067d98",
|
||||||
|
"0e52122d1eb95cdd8ba5f65815f7d1c9125a8c14d82989eae52ab369eea6c7e4",
|
||||||
|
"04dcaf2552801937d1c20b69adf89646f21b53c17906271d22c7be9bcadb96c0",
|
||||||
|
"0ee827a36e8bb0cfc483cf1872781182c4a16c58acba3ae2d7b155e0370e93b8",
|
||||||
|
"adc14fa3ad590856dd8b80815d367f7c1e6735ad00fd98a86d002fbe9fb535e1",
|
||||||
|
"2bc9be7569515701581d5d765422a17ee3500d8e1f4e7aa53f6be86ae6ba274d",
|
||||||
|
"9a21569255d0a3a9e75f1de2e4c883c9be2e5615887f22b2ecf6b1813bcd587d",
|
||||||
|
"3f3ff7adb39159c42c0aa16d53c0483bfbfad57df22a9e9e9364a306741eb2cf",
|
||||||
|
"e9aa50decff01f2cec1ec2b2e0b34332cf9c92cafdac5a7cc0881a6d26b59854",
|
||||||
|
"c7eda660a6bc8270530e82b4a7712acdea2e31dc0a56f8dc955ac009efd97c86",
|
||||||
|
"787338757fc25d65cd929394d5e7713cf43638e8d259e8dcf5c73b834eb851f2",
|
||||||
|
"7e8ffe414a53ce18a85536eda74d3cbda0da6a98d4fe6c514948002472178c95",
|
||||||
|
"6a04ab98d9e4774ad806e302dddeb63bea16b5cb5f223ee77478e861bb583eb3",
|
||||||
|
"0c371f5ed95076613443e8331c4b60828ed67bcdefaa1698fb5ce9d7b3285ffb",
|
||||||
|
"daa41bedb68591363bf4407f687cb9789cc543ed024bb77c22d2c84d88f54153",
|
||||||
|
"12ee03d11684a125dd87be879c28190415be3f3b1eca6b4ed743bd74ffd880e6",
|
||||||
|
"00dfb20815a71e24572394efdfbf772e56a507921b8287201ab8937496ee8e6d",
|
||||||
|
"94a6a78a5aebbba40bd1aaa2234810132c2d8004bb9177616c413d3c0ddf320e"
|
||||||
|
];
|
||||||
|
const PUBKEY_ARRAYS = generateTestPubkeyArrays();
|
||||||
|
|
||||||
|
// Main testing function
|
||||||
|
async function testRelayPubkeyLimits() {
|
||||||
|
console.log('Starting Nostr Relay Pubkey Filter Test');
|
||||||
|
console.log(`Testing ${RELAYS.length} relays with up to ${MAX_PUBKEYS} pubkeys`);
|
||||||
|
console.log(`Incrementing by ${STEP_SIZE} pubkeys per test, requesting ${EVENT_LIMIT} events per test\n`);
|
||||||
|
|
||||||
|
const results = {};
|
||||||
|
|
||||||
|
// Initialize results for each relay
|
||||||
|
for (const relayUrl of RELAYS) {
|
||||||
|
results[relayUrl] = {
|
||||||
|
maxPubkeys: 0,
|
||||||
|
failures: 0,
|
||||||
|
lastSuccess: 0
|
||||||
|
};
|
||||||
|
}
|
||||||
|
|
||||||
|
// Test each pubkey array size
|
||||||
|
for (const pubkeyArray of PUBKEY_ARRAYS) {
|
||||||
|
const pubkeyCount = pubkeyArray.length;
|
||||||
|
console.log(`\nTesting with ${pubkeyCount} pubkeys...`);
|
||||||
|
|
||||||
|
// Test each relay with this pubkey count
|
||||||
|
for (const relayUrl of RELAYS) {
|
||||||
|
if (results[relayUrl].failures >= 2) {
|
||||||
|
console.log(` ${relayUrl}: Skipping (already failed 2 times)`);
|
||||||
|
continue;
|
||||||
|
}
|
||||||
|
|
||||||
|
try {
|
||||||
|
const success = await testRelayWithPubkeys(relayUrl, pubkeyArray);
|
||||||
|
if (success) {
|
||||||
|
results[relayUrl].maxPubkeys = pubkeyCount;
|
||||||
|
results[relayUrl].lastSuccess = pubkeyCount;
|
||||||
|
results[relayUrl].failures = 0; // Reset failures on success
|
||||||
|
console.log(` ${relayUrl}: ✓ Success`);
|
||||||
|
} else {
|
||||||
|
results[relayUrl].failures++;
|
||||||
|
console.log(` ${relayUrl}: ✗ Failed (${results[relayUrl].failures}/2)`);
|
||||||
|
}
|
||||||
|
} catch (error) {
|
||||||
|
results[relayUrl].failures++;
|
||||||
|
console.log(` ${relayUrl}: ✗ Error (${results[relayUrl].failures}/2): ${error.message}`);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// Print final results
|
||||||
|
console.log('\n=== FINAL RESULTS ===');
|
||||||
|
for (const relayUrl of RELAYS) {
|
||||||
|
const result = results[relayUrl];
|
||||||
|
console.log(`${relayUrl}: ${result.maxPubkeys} pubkeys (last success: ${result.lastSuccess})`);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// Test a single relay with a specific pubkey array
|
||||||
|
async function testRelayWithPubkeys(relayUrl, pubkeys) {
|
||||||
|
return new Promise((resolve) => {
|
||||||
|
const ws = new WebSocket(relayUrl);
|
||||||
|
let receivedEOSE = false;
|
||||||
|
let subscriptionId = 'test_' + Math.random().toString(36).substr(2, 9);
|
||||||
|
let timeoutId;
|
||||||
|
|
||||||
|
const cleanup = () => {
|
||||||
|
clearTimeout(timeoutId);
|
||||||
|
ws.close();
|
||||||
|
};
|
||||||
|
|
||||||
|
ws.on('open', () => {
|
||||||
|
console.log(`Connected to ${relayUrl}`);
|
||||||
|
// Send subscription request
|
||||||
|
const filter = {
|
||||||
|
kinds: [EVENT_KIND],
|
||||||
|
authors: pubkeys,
|
||||||
|
limit: EVENT_LIMIT
|
||||||
|
};
|
||||||
|
const subscriptionMessage = ['REQ', subscriptionId, filter];
|
||||||
|
const messageString = JSON.stringify(subscriptionMessage);
|
||||||
|
const messageSize = Buffer.byteLength(messageString, 'utf8');
|
||||||
|
console.log(`Sending request with ${pubkeys.length} pubkeys (${messageSize} bytes)`);
|
||||||
|
ws.send(messageString);
|
||||||
|
|
||||||
|
// Set timeout for EOSE response
|
||||||
|
timeoutId = setTimeout(() => {
|
||||||
|
if (!receivedEOSE) {
|
||||||
|
console.log('Timeout waiting for EOSE');
|
||||||
|
cleanup();
|
||||||
|
resolve(false);
|
||||||
|
}
|
||||||
|
}, 10000); // 10 second timeout
|
||||||
|
});
|
||||||
|
|
||||||
|
ws.on('message', (data) => {
|
||||||
|
try {
|
||||||
|
const message = JSON.parse(data.toString());
|
||||||
|
if (message[0] === 'EVENT' && message[1] === subscriptionId) {
|
||||||
|
// Skip event content, just log that we received an event
|
||||||
|
console.log(`Received EVENT for subscription ${subscriptionId}`);
|
||||||
|
} else if (message[0] === 'EOSE' && message[1] === subscriptionId) {
|
||||||
|
console.log(`Received EOSE: ${JSON.stringify(message)}`);
|
||||||
|
receivedEOSE = true;
|
||||||
|
cleanup();
|
||||||
|
resolve(true);
|
||||||
|
} else {
|
||||||
|
console.log(`Received other message: ${JSON.stringify(message)}`);
|
||||||
|
}
|
||||||
|
} catch (e) {
|
||||||
|
console.log(`Received non-JSON: ${data.toString()}`);
|
||||||
|
}
|
||||||
|
});
|
||||||
|
|
||||||
|
ws.on('error', (error) => {
|
||||||
|
console.log(`WebSocket error: ${error.message}`);
|
||||||
|
cleanup();
|
||||||
|
resolve(false);
|
||||||
|
});
|
||||||
|
|
||||||
|
// Overall connection timeout
|
||||||
|
setTimeout(() => {
|
||||||
|
if (!receivedEOSE) {
|
||||||
|
cleanup();
|
||||||
|
resolve(false);
|
||||||
|
}
|
||||||
|
}, 15000); // 15 second total timeout
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
// Run the test
|
||||||
|
testRelayPubkeyLimits().catch(console.error);
|
||||||
Reference in New Issue
Block a user