Compare commits
4 Commits
| Author | SHA1 | Date | |
|---|---|---|---|
|
5fbe131755
|
|||
|
8757b41dd9
|
|||
|
1810c8bef3
|
|||
|
fad39ec201
|
@@ -133,7 +133,12 @@
|
||||
"Bash(ssh relay1:*)",
|
||||
"Bash(done)",
|
||||
"Bash(go run:*)",
|
||||
"Bash(go doc:*)"
|
||||
"Bash(go doc:*)",
|
||||
"Bash(/tmp/orly-test help:*)",
|
||||
"Bash(go version:*)",
|
||||
"Bash(ss:*)",
|
||||
"Bash(CGO_ENABLED=0 go clean:*)",
|
||||
"Bash(CGO_ENABLED=0 timeout 30 go test:*)"
|
||||
],
|
||||
"deny": [],
|
||||
"ask": []
|
||||
|
||||
@@ -4,6 +4,7 @@ test-build
|
||||
*.exe
|
||||
*.dll
|
||||
*.so
|
||||
!libsecp256k1.so
|
||||
*.dylib
|
||||
|
||||
# Test files
|
||||
|
||||
@@ -65,7 +65,7 @@ The workflow uses standard Gitea Actions environment variables:
|
||||
- **Solution**: Verify `GITEA_TOKEN` secret is set correctly with appropriate permissions
|
||||
|
||||
**Issue**: Go version not found
|
||||
- **Solution**: The workflow downloads Go 1.25.0 directly from go.dev, ensure the runner has internet access
|
||||
- **Solution**: The workflow downloads Go 1.25.3 directly from go.dev, ensure the runner has internet access
|
||||
|
||||
### Customization
|
||||
|
||||
|
||||
@@ -35,11 +35,11 @@ jobs:
|
||||
|
||||
- name: Set up Go
|
||||
run: |
|
||||
echo "Setting up Go 1.25.0..."
|
||||
echo "Setting up Go 1.25.3..."
|
||||
cd /tmp
|
||||
wget -q https://go.dev/dl/go1.25.0.linux-amd64.tar.gz
|
||||
wget -q https://go.dev/dl/go1.25.3.linux-amd64.tar.gz
|
||||
sudo rm -rf /usr/local/go
|
||||
sudo tar -C /usr/local -xzf go1.25.0.linux-amd64.tar.gz
|
||||
sudo tar -C /usr/local -xzf go1.25.3.linux-amd64.tar.gz
|
||||
export PATH=/usr/local/go/bin:$PATH
|
||||
go version
|
||||
|
||||
@@ -76,9 +76,7 @@ jobs:
|
||||
export PATH=/usr/local/go/bin:$PATH
|
||||
cd ${GITHUB_WORKSPACE}
|
||||
echo "Running tests..."
|
||||
# Download libsecp256k1.so from nostr repository
|
||||
echo "Downloading libsecp256k1.so from nostr repository..."
|
||||
wget -q https://git.mleku.dev/mleku/nostr/raw/branch/main/crypto/p8k/libsecp256k1.so -O libsecp256k1.so
|
||||
# libsecp256k1.so is included in the repository
|
||||
chmod +x libsecp256k1.so
|
||||
# Set LD_LIBRARY_PATH so tests can find the library
|
||||
export LD_LIBRARY_PATH=${GITHUB_WORKSPACE}:${LD_LIBRARY_PATH}
|
||||
@@ -96,9 +94,8 @@ jobs:
|
||||
# Create directory for binaries
|
||||
mkdir -p release-binaries
|
||||
|
||||
# Download the pre-compiled libsecp256k1.so for Linux AMD64 from nostr repository
|
||||
echo "Downloading libsecp256k1.so from nostr repository..."
|
||||
wget -q https://git.mleku.dev/mleku/nostr/raw/branch/main/crypto/p8k/libsecp256k1.so -O release-binaries/libsecp256k1-linux-amd64.so
|
||||
# Copy libsecp256k1.so from repository to release binaries
|
||||
cp libsecp256k1.so release-binaries/libsecp256k1-linux-amd64.so
|
||||
chmod +x release-binaries/libsecp256k1-linux-amd64.so
|
||||
|
||||
# Build for Linux AMD64 (pure Go + purego dynamic loading)
|
||||
|
||||
30
CLAUDE.md
30
CLAUDE.md
@@ -59,10 +59,8 @@ cd app/web && bun run dev
|
||||
# Or manually with purego setup
|
||||
CGO_ENABLED=0 go test ./...
|
||||
|
||||
# Note: libsecp256k1.so is automatically downloaded by test.sh if needed
|
||||
# It can also be manually downloaded from the nostr repository:
|
||||
# wget https://git.mleku.dev/mleku/nostr/raw/branch/main/crypto/p8k/libsecp256k1.so
|
||||
# export LD_LIBRARY_PATH="${LD_LIBRARY_PATH:+$LD_LIBRARY_PATH:}$(pwd)"
|
||||
# Note: libsecp256k1.so is included in the repository root
|
||||
# Set LD_LIBRARY_PATH to use it: export LD_LIBRARY_PATH="${LD_LIBRARY_PATH:+$LD_LIBRARY_PATH:}$(pwd)"
|
||||
```
|
||||
|
||||
### Run Specific Package Tests
|
||||
@@ -210,7 +208,7 @@ export ORLY_DB_INDEX_CACHE_MB=256 # Index cache size
|
||||
- Schnorr signature operations (NIP-01)
|
||||
- ECDH for encrypted DMs (NIP-04, NIP-44)
|
||||
- Public key recovery from signatures
|
||||
- `libsecp256k1.so` - Downloaded from nostr repository at runtime/build time
|
||||
- `libsecp256k1.so` - Included in repository root for runtime loading
|
||||
- Key derivation and conversion utilities
|
||||
- SIMD-accelerated SHA256 using minio/sha256-simd
|
||||
- SIMD-accelerated hex encoding using templexxx/xhex
|
||||
@@ -259,8 +257,7 @@ export ORLY_DB_INDEX_CACHE_MB=256 # Index cache size
|
||||
- All builds use `CGO_ENABLED=0`
|
||||
- The p8k crypto library (from `git.mleku.dev/mleku/nostr`) uses `github.com/ebitengine/purego` to dynamically load `libsecp256k1.so` at runtime
|
||||
- This avoids CGO complexity while maintaining C library performance
|
||||
- `libsecp256k1.so` is automatically downloaded by build/test scripts from the nostr repository
|
||||
- Manual download: `wget https://git.mleku.dev/mleku/nostr/raw/branch/main/crypto/p8k/libsecp256k1.so`
|
||||
- `libsecp256k1.so` is included in the repository root
|
||||
- Library must be in `LD_LIBRARY_PATH` or same directory as binary for runtime loading
|
||||
|
||||
**Database Backend Selection:**
|
||||
@@ -310,6 +307,23 @@ export ORLY_DB_INDEX_CACHE_MB=256 # Index cache size
|
||||
- External packages (e.g., `app/`) should ONLY use public API methods, never access internal fields
|
||||
- **DO NOT** change unexported fields to exported when fixing bugs - this breaks the domain boundary
|
||||
|
||||
**Binary-Optimized Tag Storage (IMPORTANT):**
|
||||
- The nostr library (`git.mleku.dev/mleku/nostr/encoders/tag`) uses binary optimization for `e` and `p` tags
|
||||
- When events are unmarshaled from JSON, 64-character hex values in e/p tags are converted to 33-byte binary format (32 bytes hash + null terminator)
|
||||
- **DO NOT** use `tag.Value()` directly for e/p tags - it returns raw bytes which may be binary, not hex
|
||||
- **ALWAYS** use these methods instead:
|
||||
- `tag.ValueHex()` - Returns hex string regardless of storage format (handles both binary and hex)
|
||||
- `tag.ValueBinary()` - Returns 32-byte binary if stored in binary format, nil otherwise
|
||||
- Example pattern for comparing pubkeys:
|
||||
```go
|
||||
// CORRECT: Use ValueHex() for hex decoding
|
||||
pt, err := hex.Dec(string(pTag.ValueHex()))
|
||||
|
||||
// WRONG: Value() may return binary bytes, not hex
|
||||
pt, err := hex.Dec(string(pTag.Value())) // Will fail for binary-encoded tags!
|
||||
```
|
||||
- This optimization saves memory and enables faster comparisons in the database layer
|
||||
|
||||
## Development Workflow
|
||||
|
||||
### Making Changes to Web UI
|
||||
@@ -370,7 +384,7 @@ export ORLY_PPROF_PATH=/tmp/profiles
|
||||
```
|
||||
|
||||
This script:
|
||||
1. Installs Go 1.25.0 if needed
|
||||
1. Installs Go 1.25.3 if needed
|
||||
2. Builds relay with embedded web UI
|
||||
3. Installs to `~/.local/bin/orly`
|
||||
4. Creates systemd service
|
||||
|
||||
27
Dockerfile
27
Dockerfile
@@ -1,10 +1,11 @@
|
||||
# Multi-stage Dockerfile for ORLY relay
|
||||
|
||||
# Stage 1: Build stage
|
||||
FROM golang:1.21-alpine AS builder
|
||||
# Use Debian-based Go image to match runtime stage (avoids musl/glibc linker mismatch)
|
||||
FROM golang:1.25-bookworm AS builder
|
||||
|
||||
# Install build dependencies
|
||||
RUN apk add --no-cache git make
|
||||
RUN apt-get update && apt-get install -y --no-install-recommends git make && rm -rf /var/lib/apt/lists/*
|
||||
|
||||
# Set working directory
|
||||
WORKDIR /build
|
||||
@@ -20,28 +21,26 @@ COPY . .
|
||||
RUN CGO_ENABLED=0 GOOS=linux GOARCH=amd64 go build -o orly -ldflags="-w -s" .
|
||||
|
||||
# Stage 2: Runtime stage
|
||||
FROM alpine:latest
|
||||
# Use Debian slim instead of Alpine because Debian's libsecp256k1 includes
|
||||
# Schnorr signatures (secp256k1_schnorrsig_*) and ECDH which Nostr requires.
|
||||
# Alpine's libsecp256k1 is built without these modules.
|
||||
FROM debian:bookworm-slim
|
||||
|
||||
# Install runtime dependencies
|
||||
RUN apk add --no-cache ca-certificates curl wget
|
||||
RUN apt-get update && \
|
||||
apt-get install -y --no-install-recommends ca-certificates curl libsecp256k1-1 && \
|
||||
rm -rf /var/lib/apt/lists/*
|
||||
|
||||
# Create app user
|
||||
RUN addgroup -g 1000 orly && \
|
||||
adduser -D -u 1000 -G orly orly
|
||||
RUN groupadd -g 1000 orly && \
|
||||
useradd -m -u 1000 -g orly orly
|
||||
|
||||
# Set working directory
|
||||
WORKDIR /app
|
||||
|
||||
# Copy binary from builder
|
||||
# Copy binary (libsecp256k1.so.1 is already installed via apt)
|
||||
COPY --from=builder /build/orly /app/orly
|
||||
|
||||
# Download libsecp256k1.so from nostr repository (optional for performance)
|
||||
RUN wget -q https://git.mleku.dev/mleku/nostr/raw/branch/main/crypto/p8k/libsecp256k1.so \
|
||||
-O /app/libsecp256k1.so || echo "Warning: libsecp256k1.so download failed (optional)"
|
||||
|
||||
# Set library path
|
||||
ENV LD_LIBRARY_PATH=/app
|
||||
|
||||
# Create data directory
|
||||
RUN mkdir -p /data && chown -R orly:orly /data /app
|
||||
|
||||
|
||||
@@ -1,9 +1,10 @@
|
||||
# Dockerfile for relay-tester
|
||||
|
||||
FROM golang:1.21-alpine AS builder
|
||||
# Use Debian-based Go image to match runtime stage (avoids musl/glibc linker mismatch)
|
||||
FROM golang:1.25-bookworm AS builder
|
||||
|
||||
# Install build dependencies
|
||||
RUN apk add --no-cache git
|
||||
RUN apt-get update && apt-get install -y --no-install-recommends git && rm -rf /var/lib/apt/lists/*
|
||||
|
||||
# Set working directory
|
||||
WORKDIR /build
|
||||
@@ -19,12 +20,19 @@ COPY . .
|
||||
RUN CGO_ENABLED=0 GOOS=linux GOARCH=amd64 go build -o relay-tester ./cmd/relay-tester
|
||||
|
||||
# Runtime stage
|
||||
FROM alpine:latest
|
||||
# Use Debian slim instead of Alpine because Debian's libsecp256k1 includes
|
||||
# Schnorr signatures (secp256k1_schnorrsig_*) and ECDH which Nostr requires.
|
||||
# Alpine's libsecp256k1 is built without these modules.
|
||||
FROM debian:bookworm-slim
|
||||
|
||||
RUN apk add --no-cache ca-certificates
|
||||
# Install runtime dependencies
|
||||
RUN apt-get update && \
|
||||
apt-get install -y --no-install-recommends ca-certificates libsecp256k1-1 && \
|
||||
rm -rf /var/lib/apt/lists/*
|
||||
|
||||
WORKDIR /app
|
||||
|
||||
# Copy binary (libsecp256k1.so.1 is already installed via apt)
|
||||
COPY --from=builder /build/relay-tester /app/relay-tester
|
||||
|
||||
# Default relay URL (can be overridden)
|
||||
|
||||
@@ -68,6 +68,11 @@ type C struct {
|
||||
// Spider settings
|
||||
SpiderMode string `env:"ORLY_SPIDER_MODE" default:"none" usage:"spider mode for syncing events: none, follows"`
|
||||
|
||||
// Directory Spider settings
|
||||
DirectorySpiderEnabled bool `env:"ORLY_DIRECTORY_SPIDER" default:"false" usage:"enable directory spider for metadata sync (kinds 0, 3, 10000, 10002)"`
|
||||
DirectorySpiderInterval time.Duration `env:"ORLY_DIRECTORY_SPIDER_INTERVAL" default:"24h" usage:"how often to run directory spider"`
|
||||
DirectorySpiderMaxHops int `env:"ORLY_DIRECTORY_SPIDER_HOPS" default:"3" usage:"maximum hops for relay discovery from seed users"`
|
||||
|
||||
PolicyEnabled bool `env:"ORLY_POLICY_ENABLED" default:"false" usage:"enable policy-based event processing (configuration found in $HOME/.config/ORLY/policy.json)"`
|
||||
|
||||
// NIP-43 Relay Access Metadata and Requests
|
||||
@@ -88,6 +93,10 @@ type C struct {
|
||||
|
||||
// Cluster replication configuration
|
||||
ClusterPropagatePrivilegedEvents bool `env:"ORLY_CLUSTER_PROPAGATE_PRIVILEGED_EVENTS" default:"true" usage:"propagate privileged events (DMs, gift wraps, etc.) to relay peers for replication"`
|
||||
|
||||
// ServeMode is set programmatically by the 'serve' subcommand to grant full owner
|
||||
// access to all users (no env tag - internal use only)
|
||||
ServeMode bool
|
||||
}
|
||||
|
||||
// New creates and initializes a new configuration object for the relay
|
||||
@@ -193,6 +202,21 @@ func IdentityRequested() (requested bool) {
|
||||
return
|
||||
}
|
||||
|
||||
// ServeRequested checks if the first command line argument is "serve" and returns
|
||||
// whether the relay should start in ephemeral serve mode with RAM-based storage.
|
||||
//
|
||||
// Return Values
|
||||
// - requested: true if the 'serve' subcommand was provided, false otherwise.
|
||||
func ServeRequested() (requested bool) {
|
||||
if len(os.Args) > 1 {
|
||||
switch strings.ToLower(os.Args[1]) {
|
||||
case "serve":
|
||||
requested = true
|
||||
}
|
||||
}
|
||||
return
|
||||
}
|
||||
|
||||
// KV is a key/value pair.
|
||||
type KV struct{ Key, Value string }
|
||||
|
||||
@@ -324,10 +348,14 @@ func PrintHelp(cfg *C, printer io.Writer) {
|
||||
)
|
||||
_, _ = fmt.Fprintf(
|
||||
printer,
|
||||
`Usage: %s [env|help]
|
||||
`Usage: %s [env|help|identity|serve]
|
||||
|
||||
- env: print environment variables configuring %s
|
||||
- help: print this help text
|
||||
- identity: print the relay identity secret and public key
|
||||
- serve: start ephemeral relay with RAM-based storage at /dev/shm/orlyserve
|
||||
listening on 0.0.0.0:10547 with 'none' ACL mode (open relay)
|
||||
useful for testing and benchmarking
|
||||
|
||||
`,
|
||||
cfg.AppName, cfg.AppName,
|
||||
|
||||
@@ -142,19 +142,26 @@ func (l *Listener) HandleDelete(env *eventenvelope.Submission) (err error) {
|
||||
// if e tags are found, delete them if the author is signer, or one of
|
||||
// the owners is signer
|
||||
if utils.FastEqual(t.Key(), []byte("e")) {
|
||||
val := t.Value()
|
||||
if len(val) == 0 {
|
||||
log.W.F("HandleDelete: empty e-tag value")
|
||||
continue
|
||||
}
|
||||
log.I.F("HandleDelete: processing e-tag with value: %s", string(val))
|
||||
// First try binary format (optimized storage for e-tags)
|
||||
var dst []byte
|
||||
if b, e := hex.Dec(string(val)); chk.E(e) {
|
||||
log.E.F("HandleDelete: failed to decode hex event ID %s: %v", string(val), e)
|
||||
continue
|
||||
if binVal := t.ValueBinary(); binVal != nil {
|
||||
dst = binVal
|
||||
log.I.F("HandleDelete: processing binary e-tag event ID: %0x", dst)
|
||||
} else {
|
||||
dst = b
|
||||
log.I.F("HandleDelete: decoded event ID: %0x", dst)
|
||||
// Fall back to hex decoding for non-binary values
|
||||
val := t.Value()
|
||||
if len(val) == 0 {
|
||||
log.W.F("HandleDelete: empty e-tag value")
|
||||
continue
|
||||
}
|
||||
log.I.F("HandleDelete: processing e-tag with value: %s", string(val))
|
||||
if b, e := hex.Dec(string(val)); chk.E(e) {
|
||||
log.E.F("HandleDelete: failed to decode hex event ID %s: %v", string(val), e)
|
||||
continue
|
||||
} else {
|
||||
dst = b
|
||||
log.I.F("HandleDelete: decoded event ID: %0x", dst)
|
||||
}
|
||||
}
|
||||
f := &filter.F{
|
||||
Ids: tag.NewFromBytesSlice(dst),
|
||||
@@ -164,7 +171,7 @@ func (l *Listener) HandleDelete(env *eventenvelope.Submission) (err error) {
|
||||
log.E.F("HandleDelete: failed to get serials from filter: %v", err)
|
||||
continue
|
||||
}
|
||||
log.I.F("HandleDelete: found %d serials for event ID %s", len(sers), string(val))
|
||||
log.I.F("HandleDelete: found %d serials for event ID %0x", len(sers), dst)
|
||||
// if found, delete them
|
||||
if len(sers) > 0 {
|
||||
// there should be only one event per serial, so we can just
|
||||
|
||||
44
app/main.go
44
app/main.go
@@ -141,6 +141,44 @@ func Run(
|
||||
}
|
||||
}
|
||||
|
||||
// Initialize directory spider if enabled (only for Badger backend)
|
||||
if badgerDB, ok := db.(*database.D); ok && cfg.DirectorySpiderEnabled {
|
||||
if l.directorySpider, err = spider.NewDirectorySpider(
|
||||
ctx,
|
||||
badgerDB,
|
||||
l.publishers,
|
||||
cfg.DirectorySpiderInterval,
|
||||
cfg.DirectorySpiderMaxHops,
|
||||
); chk.E(err) {
|
||||
log.E.F("failed to create directory spider: %v", err)
|
||||
} else {
|
||||
// Set up callback to get seed pubkeys (whitelisted users)
|
||||
l.directorySpider.SetSeedCallback(func() [][]byte {
|
||||
var pubkeys [][]byte
|
||||
// Get followed pubkeys from follows ACL if available
|
||||
for _, aclInstance := range acl.Registry.ACL {
|
||||
if aclInstance.Type() == "follows" {
|
||||
if follows, ok := aclInstance.(*acl.Follows); ok {
|
||||
pubkeys = append(pubkeys, follows.GetFollowedPubkeys()...)
|
||||
}
|
||||
}
|
||||
}
|
||||
// Fall back to admin keys if no follows ACL
|
||||
if len(pubkeys) == 0 {
|
||||
pubkeys = adminKeys
|
||||
}
|
||||
return pubkeys
|
||||
})
|
||||
|
||||
if err = l.directorySpider.Start(); chk.E(err) {
|
||||
log.E.F("failed to start directory spider: %v", err)
|
||||
} else {
|
||||
log.I.F("directory spider started (interval: %v, max hops: %d)",
|
||||
cfg.DirectorySpiderInterval, cfg.DirectorySpiderMaxHops)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// Initialize relay group manager (only for Badger backend)
|
||||
if badgerDB, ok := db.(*database.D); ok {
|
||||
l.relayGroupMgr = dsync.NewRelayGroupManager(badgerDB, cfg.RelayGroupAdmins)
|
||||
@@ -360,6 +398,12 @@ func Run(
|
||||
log.I.F("spider manager stopped")
|
||||
}
|
||||
|
||||
// Stop directory spider if running
|
||||
if l.directorySpider != nil {
|
||||
l.directorySpider.Stop()
|
||||
log.I.F("directory spider stopped")
|
||||
}
|
||||
|
||||
// Create shutdown context with timeout
|
||||
shutdownCtx, cancelShutdown := context.WithTimeout(context.Background(), 10*time.Second)
|
||||
defer cancelShutdown()
|
||||
|
||||
@@ -54,9 +54,18 @@ func testPrivilegedEventFiltering(events event.S, authedPubkey []byte, aclMode s
|
||||
// Check p tags
|
||||
pTags := ev.Tags.GetAll([]byte("p"))
|
||||
for _, pTag := range pTags {
|
||||
var pt []byte
|
||||
var err error
|
||||
if pt, err = hex.Dec(string(pTag.Value())); err != nil {
|
||||
// First try binary format (optimized storage)
|
||||
if pt := pTag.ValueBinary(); pt != nil {
|
||||
if bytes.Equal(pt, authedPubkey) {
|
||||
authorized = true
|
||||
break
|
||||
}
|
||||
continue
|
||||
}
|
||||
// Fall back to hex decoding for non-binary values
|
||||
// Use ValueHex() which handles both binary and hex storage formats
|
||||
pt, err := hex.Dec(string(pTag.ValueHex()))
|
||||
if err != nil {
|
||||
continue
|
||||
}
|
||||
if bytes.Equal(pt, authedPubkey) {
|
||||
|
||||
@@ -48,17 +48,22 @@ type Server struct {
|
||||
challengeMutex sync.RWMutex
|
||||
challenges map[string][]byte
|
||||
|
||||
paymentProcessor *PaymentProcessor
|
||||
sprocketManager *SprocketManager
|
||||
policyManager *policy.P
|
||||
spiderManager *spider.Spider
|
||||
syncManager *dsync.Manager
|
||||
relayGroupMgr *dsync.RelayGroupManager
|
||||
clusterManager *dsync.ClusterManager
|
||||
blossomServer *blossom.Server
|
||||
InviteManager *nip43.InviteManager
|
||||
cfg *config.C
|
||||
db database.Database // Changed from *database.D to interface
|
||||
// Message processing pause mutex for policy/follow list updates
|
||||
// Use RLock() for normal message processing, Lock() for updates
|
||||
messagePauseMutex sync.RWMutex
|
||||
|
||||
paymentProcessor *PaymentProcessor
|
||||
sprocketManager *SprocketManager
|
||||
policyManager *policy.P
|
||||
spiderManager *spider.Spider
|
||||
directorySpider *spider.DirectorySpider
|
||||
syncManager *dsync.Manager
|
||||
relayGroupMgr *dsync.RelayGroupManager
|
||||
clusterManager *dsync.ClusterManager
|
||||
blossomServer *blossom.Server
|
||||
InviteManager *nip43.InviteManager
|
||||
cfg *config.C
|
||||
db database.Database // Changed from *database.D to interface
|
||||
}
|
||||
|
||||
// isIPBlacklisted checks if an IP address is blacklisted using the managed ACL system
|
||||
@@ -1135,3 +1140,32 @@ func (s *Server) updatePeerAdminACL(peerPubkey []byte) {
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// =============================================================================
|
||||
// Message Processing Pause/Resume for Policy and Follow List Updates
|
||||
// =============================================================================
|
||||
|
||||
// PauseMessageProcessing acquires an exclusive lock to pause all message processing.
|
||||
// This should be called before updating policy configuration or follow lists.
|
||||
// Call ResumeMessageProcessing to release the lock after updates are complete.
|
||||
func (s *Server) PauseMessageProcessing() {
|
||||
s.messagePauseMutex.Lock()
|
||||
}
|
||||
|
||||
// ResumeMessageProcessing releases the exclusive lock to resume message processing.
|
||||
// This should be called after policy configuration or follow list updates are complete.
|
||||
func (s *Server) ResumeMessageProcessing() {
|
||||
s.messagePauseMutex.Unlock()
|
||||
}
|
||||
|
||||
// AcquireMessageProcessingLock acquires a read lock for normal message processing.
|
||||
// This allows concurrent message processing while blocking during policy updates.
|
||||
// Call ReleaseMessageProcessingLock when message processing is complete.
|
||||
func (s *Server) AcquireMessageProcessingLock() {
|
||||
s.messagePauseMutex.RLock()
|
||||
}
|
||||
|
||||
// ReleaseMessageProcessingLock releases the read lock after message processing.
|
||||
func (s *Server) ReleaseMessageProcessingLock() {
|
||||
s.messagePauseMutex.RUnlock()
|
||||
}
|
||||
|
||||
@@ -1,22 +1,11 @@
|
||||
# Dockerfile for benchmark runner
|
||||
FROM golang:1.25-alpine AS builder
|
||||
# Uses pure Go build with purego for dynamic libsecp256k1 loading
|
||||
|
||||
# Install build dependencies including libsecp256k1 build requirements
|
||||
RUN apk add --no-cache git ca-certificates gcc musl-dev autoconf automake libtool make
|
||||
# Use Debian-based Go image to match runtime stage (avoids musl/glibc linker mismatch)
|
||||
FROM golang:1.25-bookworm AS builder
|
||||
|
||||
# Build libsecp256k1 EARLY - this layer will be cached unless secp256k1 version changes
|
||||
# Using specific version tag and parallel builds for faster compilation
|
||||
RUN cd /tmp && \
|
||||
git clone https://github.com/bitcoin-core/secp256k1.git && \
|
||||
cd secp256k1 && \
|
||||
git checkout v0.6.0 && \
|
||||
git submodule init && \
|
||||
git submodule update && \
|
||||
./autogen.sh && \
|
||||
./configure --enable-module-recovery --enable-module-ecdh --enable-module-schnorrsig --enable-module-extrakeys && \
|
||||
make -j$(nproc) && \
|
||||
make install && \
|
||||
cd /tmp && rm -rf secp256k1
|
||||
# Install build dependencies (no secp256k1 build needed)
|
||||
RUN apt-get update && apt-get install -y --no-install-recommends git ca-certificates && rm -rf /var/lib/apt/lists/*
|
||||
|
||||
# Set working directory
|
||||
WORKDIR /build
|
||||
@@ -28,27 +17,25 @@ RUN go mod download
|
||||
# Copy source code
|
||||
COPY . .
|
||||
|
||||
# Build the benchmark tool with CGO enabled
|
||||
RUN CGO_ENABLED=1 GOOS=linux go build -a -o benchmark ./cmd/benchmark
|
||||
|
||||
# Copy libsecp256k1.so if available
|
||||
RUN if [ -f pkg/crypto/p8k/libsecp256k1.so ]; then \
|
||||
cp pkg/crypto/p8k/libsecp256k1.so /build/; \
|
||||
fi
|
||||
# Build the benchmark tool with CGO disabled (uses purego for crypto)
|
||||
RUN CGO_ENABLED=0 GOOS=linux GOARCH=amd64 go build -o benchmark ./cmd/benchmark
|
||||
|
||||
# Final stage
|
||||
FROM alpine:latest
|
||||
# Use Debian slim instead of Alpine because Debian's libsecp256k1 includes
|
||||
# Schnorr signatures (secp256k1_schnorrsig_*) and ECDH which Nostr requires.
|
||||
# Alpine's libsecp256k1 is built without these modules.
|
||||
FROM debian:bookworm-slim
|
||||
|
||||
# Install runtime dependencies including libsecp256k1
|
||||
RUN apk --no-cache add ca-certificates curl wget libsecp256k1
|
||||
# Install runtime dependencies
|
||||
RUN apt-get update && \
|
||||
apt-get install -y --no-install-recommends ca-certificates curl libsecp256k1-1 && \
|
||||
rm -rf /var/lib/apt/lists/*
|
||||
|
||||
WORKDIR /app
|
||||
|
||||
# Copy benchmark binary
|
||||
# Copy benchmark binary (libsecp256k1.so.1 is already installed via apt)
|
||||
COPY --from=builder /build/benchmark /app/benchmark
|
||||
|
||||
# libsecp256k1 is already installed system-wide via apk
|
||||
|
||||
# Copy benchmark runner script
|
||||
COPY cmd/benchmark/benchmark-runner.sh /app/benchmark-runner
|
||||
|
||||
@@ -56,13 +43,10 @@ COPY cmd/benchmark/benchmark-runner.sh /app/benchmark-runner
|
||||
RUN chmod +x /app/benchmark-runner
|
||||
|
||||
# Create runtime user and reports directory owned by uid 1000
|
||||
RUN adduser -u 1000 -D appuser && \
|
||||
RUN useradd -m -u 1000 appuser && \
|
||||
mkdir -p /reports && \
|
||||
chown -R 1000:1000 /app /reports
|
||||
|
||||
# Set library path
|
||||
ENV LD_LIBRARY_PATH=/app:/usr/local/lib:/usr/lib
|
||||
|
||||
# Environment variables
|
||||
ENV BENCHMARK_EVENTS=50000
|
||||
ENV BENCHMARK_WORKERS=24
|
||||
@@ -72,4 +56,4 @@ ENV BENCHMARK_DURATION=60s
|
||||
USER 1000:1000
|
||||
|
||||
# Run the benchmark runner
|
||||
CMD ["/app/benchmark-runner"]
|
||||
CMD ["/app/benchmark-runner"]
|
||||
|
||||
@@ -1,75 +1,51 @@
|
||||
# Dockerfile for next.orly.dev relay
|
||||
FROM ubuntu:22.04 as builder
|
||||
# Dockerfile for next.orly.dev relay (benchmark version)
|
||||
# Uses pure Go build with purego for dynamic libsecp256k1 loading
|
||||
|
||||
# Set environment variables
|
||||
ARG GOLANG_VERSION=1.22.5
|
||||
# Stage 1: Build stage
|
||||
# Use Debian-based Go image to match runtime stage (avoids musl/glibc linker mismatch)
|
||||
FROM golang:1.25-bookworm AS builder
|
||||
|
||||
# Update package list and install ALL dependencies in one layer
|
||||
RUN apt-get update && \
|
||||
apt-get install -y wget ca-certificates build-essential autoconf libtool git && \
|
||||
rm -rf /var/lib/apt/lists/*
|
||||
|
||||
# Download and install Go binary
|
||||
RUN wget https://go.dev/dl/go${GOLANG_VERSION}.linux-amd64.tar.gz && \
|
||||
rm -rf /usr/local/go && \
|
||||
tar -C /usr/local -xzf go${GOLANG_VERSION}.linux-amd64.tar.gz && \
|
||||
rm go${GOLANG_VERSION}.linux-amd64.tar.gz
|
||||
|
||||
# Set PATH environment variable
|
||||
ENV PATH="/usr/local/go/bin:${PATH}"
|
||||
|
||||
# Verify installation
|
||||
RUN go version
|
||||
|
||||
# Build secp256k1 EARLY - this layer will be cached unless secp256k1 version changes
|
||||
RUN cd /tmp && \
|
||||
rm -rf secp256k1 && \
|
||||
git clone https://github.com/bitcoin-core/secp256k1.git && \
|
||||
cd secp256k1 && \
|
||||
git checkout v0.6.0 && \
|
||||
git submodule init && \
|
||||
git submodule update && \
|
||||
./autogen.sh && \
|
||||
./configure --enable-module-schnorrsig --enable-module-ecdh --prefix=/usr && \
|
||||
make -j$(nproc) && \
|
||||
make install && \
|
||||
cd /tmp && rm -rf secp256k1
|
||||
# Install build dependencies
|
||||
RUN apt-get update && apt-get install -y --no-install-recommends git make && rm -rf /var/lib/apt/lists/*
|
||||
|
||||
# Set working directory
|
||||
WORKDIR /build
|
||||
|
||||
# Copy go modules AFTER secp256k1 build - this allows module cache to be reused
|
||||
# Copy go mod files first for better layer caching
|
||||
COPY go.mod go.sum ./
|
||||
RUN go mod download
|
||||
|
||||
# Copy source code LAST - this is the most frequently changing layer
|
||||
# Copy source code
|
||||
COPY . .
|
||||
|
||||
# Build the relay (libsecp256k1 installed via make install to /usr/lib)
|
||||
RUN CGO_ENABLED=1 GOOS=linux go build -gcflags "all=-N -l" -o relay .
|
||||
# Build the relay with CGO disabled (uses purego for crypto)
|
||||
# Include debug symbols for profiling
|
||||
RUN CGO_ENABLED=0 GOOS=linux GOARCH=amd64 go build -gcflags "all=-N -l" -o relay .
|
||||
|
||||
# Create non-root user (uid 1000) for runtime in builder stage (used by analyzer)
|
||||
RUN useradd -u 1000 -m -s /bin/bash appuser && \
|
||||
RUN useradd -m -u 1000 appuser && \
|
||||
chown -R 1000:1000 /build
|
||||
# Switch to uid 1000 for any subsequent runtime use of this stage
|
||||
USER 1000:1000
|
||||
|
||||
# Final stage
|
||||
FROM ubuntu:22.04
|
||||
# Use Debian slim instead of Alpine because Debian's libsecp256k1 includes
|
||||
# Schnorr signatures (secp256k1_schnorrsig_*) and ECDH which Nostr requires.
|
||||
# Alpine's libsecp256k1 is built without these modules.
|
||||
FROM debian:bookworm-slim
|
||||
|
||||
# Install runtime dependencies
|
||||
RUN apt-get update && apt-get install -y ca-certificates curl libsecp256k1-0 libsecp256k1-dev && rm -rf /var/lib/apt/lists/* && \
|
||||
ln -sf /usr/lib/x86_64-linux-gnu/libsecp256k1.so.0 /usr/lib/x86_64-linux-gnu/libsecp256k1.so.5
|
||||
RUN apt-get update && \
|
||||
apt-get install -y --no-install-recommends ca-certificates curl libsecp256k1-1 && \
|
||||
rm -rf /var/lib/apt/lists/*
|
||||
|
||||
WORKDIR /app
|
||||
|
||||
# Copy binary from builder
|
||||
# Copy binary (libsecp256k1.so.1 is already installed via apt)
|
||||
COPY --from=builder /build/relay /app/relay
|
||||
|
||||
# libsecp256k1 is already installed system-wide in the final stage via apt-get install libsecp256k1-0
|
||||
|
||||
# Create runtime user and writable directories
|
||||
RUN useradd -u 1000 -m -s /bin/bash appuser && \
|
||||
RUN useradd -m -u 1000 appuser && \
|
||||
mkdir -p /data /profiles /app && \
|
||||
chown -R 1000:1000 /data /profiles /app
|
||||
|
||||
@@ -96,4 +72,4 @@ HEALTHCHECK --interval=30s --timeout=10s --start-period=40s --retries=3 \
|
||||
USER 1000:1000
|
||||
|
||||
# Run the relay
|
||||
CMD ["/app/relay"]
|
||||
CMD ["/app/relay"]
|
||||
|
||||
77
cmd/benchmark/docker-compose.ramdisk.yml
Normal file
77
cmd/benchmark/docker-compose.ramdisk.yml
Normal file
@@ -0,0 +1,77 @@
|
||||
# Docker Compose override file for ramdisk-based benchmarks
|
||||
# Uses /dev/shm (tmpfs) for all database storage to eliminate disk I/O bottlenecks
|
||||
# and measure raw relay performance.
|
||||
#
|
||||
# Usage: docker compose -f docker-compose.yml -f docker-compose.ramdisk.yml up
|
||||
# Or via run-benchmark.sh --ramdisk
|
||||
|
||||
version: "3.8"
|
||||
|
||||
services:
|
||||
# Next.orly.dev relay with Badger
|
||||
next-orly-badger:
|
||||
volumes:
|
||||
- /dev/shm/benchmark/next-orly-badger:/data
|
||||
|
||||
# Next.orly.dev relay with DGraph
|
||||
next-orly-dgraph:
|
||||
volumes:
|
||||
- /dev/shm/benchmark/next-orly-dgraph:/data
|
||||
|
||||
# DGraph Zero - cluster coordinator
|
||||
dgraph-zero:
|
||||
volumes:
|
||||
- /dev/shm/benchmark/dgraph-zero:/data
|
||||
|
||||
# DGraph Alpha - data node
|
||||
dgraph-alpha:
|
||||
volumes:
|
||||
- /dev/shm/benchmark/dgraph-alpha:/data
|
||||
|
||||
# Next.orly.dev relay with Neo4j
|
||||
next-orly-neo4j:
|
||||
volumes:
|
||||
- /dev/shm/benchmark/next-orly-neo4j:/data
|
||||
|
||||
# Neo4j database
|
||||
neo4j:
|
||||
volumes:
|
||||
- /dev/shm/benchmark/neo4j:/data
|
||||
- /dev/shm/benchmark/neo4j-logs:/logs
|
||||
|
||||
# Khatru with SQLite
|
||||
khatru-sqlite:
|
||||
volumes:
|
||||
- /dev/shm/benchmark/khatru-sqlite:/data
|
||||
|
||||
# Khatru with Badger
|
||||
khatru-badger:
|
||||
volumes:
|
||||
- /dev/shm/benchmark/khatru-badger:/data
|
||||
|
||||
# Relayer basic example
|
||||
relayer-basic:
|
||||
volumes:
|
||||
- /dev/shm/benchmark/relayer-basic:/data
|
||||
|
||||
# Strfry
|
||||
strfry:
|
||||
volumes:
|
||||
- /dev/shm/benchmark/strfry:/data
|
||||
- ./configs/strfry.conf:/etc/strfry.conf
|
||||
|
||||
# Nostr-rs-relay
|
||||
nostr-rs-relay:
|
||||
volumes:
|
||||
- /dev/shm/benchmark/nostr-rs-relay:/data
|
||||
- ./configs/config.toml:/app/config.toml
|
||||
|
||||
# Rely-SQLite relay
|
||||
rely-sqlite:
|
||||
volumes:
|
||||
- /dev/shm/benchmark/rely-sqlite:/data
|
||||
|
||||
# PostgreSQL for relayer-basic
|
||||
postgres:
|
||||
volumes:
|
||||
- /dev/shm/benchmark/postgres:/var/lib/postgresql/data
|
||||
194
cmd/benchmark/reports/run_20251126_073410/aggregate_report.txt
Normal file
194
cmd/benchmark/reports/run_20251126_073410/aggregate_report.txt
Normal file
@@ -0,0 +1,194 @@
|
||||
================================================================
|
||||
NOSTR RELAY BENCHMARK AGGREGATE REPORT
|
||||
================================================================
|
||||
Generated: 2025-11-26T08:04:35+00:00
|
||||
Benchmark Configuration:
|
||||
Events per test: 50000
|
||||
Concurrent workers: 24
|
||||
Test duration: 60s
|
||||
|
||||
Relays tested: 9
|
||||
|
||||
================================================================
|
||||
SUMMARY BY RELAY
|
||||
================================================================
|
||||
|
||||
Relay: rely-sqlite
|
||||
----------------------------------------
|
||||
Status: COMPLETED
|
||||
Events/sec: 16298.40
|
||||
Events/sec: 6150.97
|
||||
Events/sec: 16298.40
|
||||
Success Rate: 100.0%
|
||||
Success Rate: 100.0%
|
||||
Success Rate: 100.0%
|
||||
Avg Latency: 1.360569ms
|
||||
Bottom 10% Avg Latency: 746.704µs
|
||||
Avg Latency: 1.411735ms
|
||||
P95 Latency: 2.160818ms
|
||||
P95 Latency: 2.29313ms
|
||||
P95 Latency: 916.446µs
|
||||
|
||||
Relay: next-orly-badger
|
||||
----------------------------------------
|
||||
Status: COMPLETED
|
||||
Events/sec: 16698.91
|
||||
Events/sec: 6011.59
|
||||
Events/sec: 16698.91
|
||||
Success Rate: 100.0%
|
||||
Success Rate: 100.0%
|
||||
Success Rate: 100.0%
|
||||
Avg Latency: 1.331911ms
|
||||
Bottom 10% Avg Latency: 766.682µs
|
||||
Avg Latency: 1.496861ms
|
||||
P95 Latency: 2.019719ms
|
||||
P95 Latency: 2.715024ms
|
||||
P95 Latency: 914.112µs
|
||||
|
||||
Relay: next-orly-dgraph
|
||||
----------------------------------------
|
||||
Status: COMPLETED
|
||||
Events/sec: 14573.58
|
||||
Events/sec: 6072.22
|
||||
Events/sec: 14573.58
|
||||
Success Rate: 100.0%
|
||||
Success Rate: 100.0%
|
||||
Success Rate: 100.0%
|
||||
Avg Latency: 1.571025ms
|
||||
Bottom 10% Avg Latency: 802.953µs
|
||||
Avg Latency: 1.454825ms
|
||||
P95 Latency: 2.610305ms
|
||||
P95 Latency: 2.541414ms
|
||||
P95 Latency: 902.751µs
|
||||
|
||||
Relay: next-orly-neo4j
|
||||
----------------------------------------
|
||||
Status: COMPLETED
|
||||
Events/sec: 16594.60
|
||||
Events/sec: 6139.73
|
||||
Events/sec: 16594.60
|
||||
Success Rate: 100.0%
|
||||
Success Rate: 100.0%
|
||||
Success Rate: 100.0%
|
||||
Avg Latency: 1.341265ms
|
||||
Bottom 10% Avg Latency: 760.268µs
|
||||
Avg Latency: 1.417529ms
|
||||
P95 Latency: 2.068012ms
|
||||
P95 Latency: 2.279114ms
|
||||
P95 Latency: 893.313µs
|
||||
|
||||
Relay: khatru-sqlite
|
||||
----------------------------------------
|
||||
Status: COMPLETED
|
||||
Events/sec: 16775.48
|
||||
Events/sec: 6077.32
|
||||
Events/sec: 16775.48
|
||||
Success Rate: 100.0%
|
||||
Success Rate: 100.0%
|
||||
Success Rate: 100.0%
|
||||
Avg Latency: 1.316097ms
|
||||
Bottom 10% Avg Latency: 743.925µs
|
||||
Avg Latency: 1.448816ms
|
||||
P95 Latency: 2.019999ms
|
||||
P95 Latency: 2.415349ms
|
||||
P95 Latency: 915.807µs
|
||||
|
||||
Relay: khatru-badger
|
||||
----------------------------------------
|
||||
Status: COMPLETED
|
||||
Events/sec: 14573.64
|
||||
Events/sec: 6123.62
|
||||
Events/sec: 14573.64
|
||||
Success Rate: 100.0%
|
||||
Success Rate: 100.0%
|
||||
Success Rate: 100.0%
|
||||
Avg Latency: 1.582659ms
|
||||
Bottom 10% Avg Latency: 849.196µs
|
||||
Avg Latency: 1.42045ms
|
||||
P95 Latency: 2.584156ms
|
||||
P95 Latency: 2.297743ms
|
||||
P95 Latency: 911.2µs
|
||||
|
||||
Relay: relayer-basic
|
||||
----------------------------------------
|
||||
Status: COMPLETED
|
||||
Events/sec: 16103.85
|
||||
Events/sec: 6038.31
|
||||
Events/sec: 16103.85
|
||||
Success Rate: 100.0%
|
||||
Success Rate: 100.0%
|
||||
Success Rate: 100.0%
|
||||
Avg Latency: 1.401051ms
|
||||
Bottom 10% Avg Latency: 788.805µs
|
||||
Avg Latency: 1.501362ms
|
||||
P95 Latency: 2.187347ms
|
||||
P95 Latency: 2.477719ms
|
||||
P95 Latency: 920.8µs
|
||||
|
||||
Relay: strfry
|
||||
----------------------------------------
|
||||
Status: COMPLETED
|
||||
Events/sec: 16207.30
|
||||
Events/sec: 6075.12
|
||||
Events/sec: 16207.30
|
||||
Success Rate: 100.0%
|
||||
Success Rate: 100.0%
|
||||
Success Rate: 100.0%
|
||||
Avg Latency: 1.381579ms
|
||||
Bottom 10% Avg Latency: 760.474µs
|
||||
Avg Latency: 1.45496ms
|
||||
P95 Latency: 2.15555ms
|
||||
P95 Latency: 2.414222ms
|
||||
P95 Latency: 907.647µs
|
||||
|
||||
Relay: nostr-rs-relay
|
||||
----------------------------------------
|
||||
Status: COMPLETED
|
||||
Events/sec: 15751.45
|
||||
Events/sec: 6163.36
|
||||
Events/sec: 15751.45
|
||||
Success Rate: 100.0%
|
||||
Success Rate: 100.0%
|
||||
Success Rate: 100.0%
|
||||
Avg Latency: 1.442411ms
|
||||
Bottom 10% Avg Latency: 812.222µs
|
||||
Avg Latency: 1.414472ms
|
||||
P95 Latency: 2.22848ms
|
||||
P95 Latency: 2.267184ms
|
||||
P95 Latency: 921.434µs
|
||||
|
||||
|
||||
================================================================
|
||||
DETAILED RESULTS
|
||||
================================================================
|
||||
|
||||
Individual relay reports are available in:
|
||||
- /reports/run_20251126_073410/khatru-badger_results.txt
|
||||
- /reports/run_20251126_073410/khatru-sqlite_results.txt
|
||||
- /reports/run_20251126_073410/next-orly-badger_results.txt
|
||||
- /reports/run_20251126_073410/next-orly-dgraph_results.txt
|
||||
- /reports/run_20251126_073410/next-orly-neo4j_results.txt
|
||||
- /reports/run_20251126_073410/nostr-rs-relay_results.txt
|
||||
- /reports/run_20251126_073410/relayer-basic_results.txt
|
||||
- /reports/run_20251126_073410/rely-sqlite_results.txt
|
||||
- /reports/run_20251126_073410/strfry_results.txt
|
||||
|
||||
================================================================
|
||||
BENCHMARK COMPARISON TABLE
|
||||
================================================================
|
||||
|
||||
Relay Status Peak Tput/s Avg Latency Success Rate
|
||||
---- ------ ----------- ----------- ------------
|
||||
rely-sqlite OK 16298.40 1.360569ms 100.0%
|
||||
next-orly-badger OK 16698.91 1.331911ms 100.0%
|
||||
next-orly-dgraph OK 14573.58 1.571025ms 100.0%
|
||||
next-orly-neo4j OK 16594.60 1.341265ms 100.0%
|
||||
khatru-sqlite OK 16775.48 1.316097ms 100.0%
|
||||
khatru-badger OK 14573.64 1.582659ms 100.0%
|
||||
relayer-basic OK 16103.85 1.401051ms 100.0%
|
||||
strfry OK 16207.30 1.381579ms 100.0%
|
||||
nostr-rs-relay OK 15751.45 1.442411ms 100.0%
|
||||
|
||||
================================================================
|
||||
End of Report
|
||||
================================================================
|
||||
@@ -0,0 +1,197 @@
|
||||
Starting Nostr Relay Benchmark (Badger Backend)
|
||||
Data Directory: /tmp/benchmark_khatru-badger_8
|
||||
Events: 50000, Workers: 24, Duration: 1m0s
|
||||
1764143463950443ℹ️ migrating to version 1... /build/pkg/database/migrations.go:66
|
||||
1764143463950524ℹ️ migrating to version 2... /build/pkg/database/migrations.go:73
|
||||
1764143463950554ℹ️ migrating to version 3... /build/pkg/database/migrations.go:80
|
||||
1764143463950562ℹ️ cleaning up ephemeral events (kinds 20000-29999)... /build/pkg/database/migrations.go:294
|
||||
1764143463950601ℹ️ cleaned up 0 ephemeral events from database /build/pkg/database/migrations.go:339
|
||||
1764143463950677ℹ️ migrating to version 4... /build/pkg/database/migrations.go:87
|
||||
1764143463950693ℹ️ converting events to optimized inline storage (Reiser4 optimization)... /build/pkg/database/migrations.go:347
|
||||
1764143463950707ℹ️ found 0 events to convert (0 regular, 0 replaceable, 0 addressable) /build/pkg/database/migrations.go:436
|
||||
1764143463950715ℹ️ migration complete: converted 0 events to optimized inline storage, deleted 0 old keys /build/pkg/database/migrations.go:545
|
||||
1764143463950741ℹ️ migrating to version 5... /build/pkg/database/migrations.go:94
|
||||
1764143463950748ℹ️ re-encoding events with optimized tag binary format... /build/pkg/database/migrations.go:552
|
||||
1764143463950772ℹ️ found 0 events with e/p tags to re-encode /build/pkg/database/migrations.go:639
|
||||
1764143463950779ℹ️ no events need re-encoding /build/pkg/database/migrations.go:642
|
||||
|
||||
╔════════════════════════════════════════════════════════╗
|
||||
║ BADGER BACKEND BENCHMARK SUITE ║
|
||||
╚════════════════════════════════════════════════════════╝
|
||||
|
||||
=== Starting Badger benchmark ===
|
||||
RunPeakThroughputTest (Badger)..
|
||||
|
||||
=== Peak Throughput Test ===
|
||||
2025/11/26 07:51:03 INFO: Successfully loaded embedded libsecp256k1 v5.0.0 from /tmp/orly-libsecp256k1/libsecp256k1.so
|
||||
Events saved: 50000/50000 (100.0%), errors: 0
|
||||
Duration: 3.430851381s
|
||||
Events/sec: 14573.64
|
||||
Avg latency: 1.582659ms
|
||||
P90 latency: 2.208413ms
|
||||
P95 latency: 2.584156ms
|
||||
P99 latency: 3.989364ms
|
||||
Bottom 10% Avg latency: 849.196µs
|
||||
Wiping database between tests...
|
||||
RunBurstPatternTest (Badger)..
|
||||
|
||||
=== Burst Pattern Test ===
|
||||
Burst completed: 5000 events in 327.135579ms
|
||||
Burst completed: 5000 events in 347.321999ms
|
||||
Burst completed: 5000 events in 293.638919ms
|
||||
Burst completed: 5000 events in 315.213974ms
|
||||
Burst completed: 5000 events in 293.822691ms
|
||||
Burst completed: 5000 events in 393.17551ms
|
||||
Burst completed: 5000 events in 317.689223ms
|
||||
Burst completed: 5000 events in 283.629668ms
|
||||
Burst completed: 5000 events in 306.891378ms
|
||||
Burst completed: 5000 events in 281.684719ms
|
||||
Burst test completed: 50000 events in 8.165107452s, errors: 0
|
||||
Events/sec: 6123.62
|
||||
Wiping database between tests...
|
||||
RunMixedReadWriteTest (Badger)..
|
||||
|
||||
=== Mixed Read/Write Test ===
|
||||
Generating 1000 unique synthetic events (minimum 300 bytes each)...
|
||||
Generated 1000 events:
|
||||
Average content size: 312 bytes
|
||||
All events are unique (incremental timestamps)
|
||||
All events are properly signed
|
||||
|
||||
Pre-populating database for read tests...
|
||||
Generating 50000 unique synthetic events (minimum 300 bytes each)...
|
||||
Generated 50000 events:
|
||||
Average content size: 314 bytes
|
||||
All events are unique (incremental timestamps)
|
||||
All events are properly signed
|
||||
|
||||
Mixed test completed: 25000 writes, 25000 reads in 24.414376807s
|
||||
Combined ops/sec: 2047.97
|
||||
Wiping database between tests...
|
||||
RunQueryTest (Badger)..
|
||||
|
||||
=== Query Test ===
|
||||
Generating 10000 unique synthetic events (minimum 300 bytes each)...
|
||||
Generated 10000 events:
|
||||
Average content size: 313 bytes
|
||||
All events are unique (incremental timestamps)
|
||||
All events are properly signed
|
||||
|
||||
Pre-populating database with 10000 events for query tests...
|
||||
Query test completed: 367781 queries in 1m0.004424256s
|
||||
Queries/sec: 6129.23
|
||||
Avg query latency: 1.861418ms
|
||||
P95 query latency: 7.652288ms
|
||||
P99 query latency: 11.670769ms
|
||||
Wiping database between tests...
|
||||
RunConcurrentQueryStoreTest (Badger)..
|
||||
|
||||
=== Concurrent Query/Store Test ===
|
||||
Generating 5000 unique synthetic events (minimum 300 bytes each)...
|
||||
Generated 5000 events:
|
||||
Average content size: 313 bytes
|
||||
All events are unique (incremental timestamps)
|
||||
All events are properly signed
|
||||
|
||||
Pre-populating database with 5000 events for concurrent query/store test...
|
||||
Generating 50000 unique synthetic events (minimum 300 bytes each)...
|
||||
Generated 50000 events:
|
||||
Average content size: 314 bytes
|
||||
All events are unique (incremental timestamps)
|
||||
All events are properly signed
|
||||
|
||||
Concurrent test completed: 307708 operations (257708 queries, 50000 writes) in 1m0.003628582s
|
||||
Operations/sec: 5128.16
|
||||
Avg latency: 1.520953ms
|
||||
Avg query latency: 1.503959ms
|
||||
Avg write latency: 1.608546ms
|
||||
P95 latency: 3.958904ms
|
||||
P99 latency: 6.227011ms
|
||||
|
||||
=== Badger benchmark completed ===
|
||||
|
||||
|
||||
================================================================================
|
||||
BENCHMARK REPORT
|
||||
================================================================================
|
||||
|
||||
Test: Peak Throughput
|
||||
Duration: 3.430851381s
|
||||
Total Events: 50000
|
||||
Events/sec: 14573.64
|
||||
Success Rate: 100.0%
|
||||
Concurrent Workers: 24
|
||||
Memory Used: 102 MB
|
||||
Avg Latency: 1.582659ms
|
||||
P90 Latency: 2.208413ms
|
||||
P95 Latency: 2.584156ms
|
||||
P99 Latency: 3.989364ms
|
||||
Bottom 10% Avg Latency: 849.196µs
|
||||
----------------------------------------
|
||||
|
||||
Test: Burst Pattern
|
||||
Duration: 8.165107452s
|
||||
Total Events: 50000
|
||||
Events/sec: 6123.62
|
||||
Success Rate: 100.0%
|
||||
Concurrent Workers: 24
|
||||
Memory Used: 211 MB
|
||||
Avg Latency: 1.42045ms
|
||||
P90 Latency: 1.976894ms
|
||||
P95 Latency: 2.297743ms
|
||||
P99 Latency: 3.397761ms
|
||||
Bottom 10% Avg Latency: 671.897µs
|
||||
----------------------------------------
|
||||
|
||||
Test: Mixed Read/Write
|
||||
Duration: 24.414376807s
|
||||
Total Events: 50000
|
||||
Events/sec: 2047.97
|
||||
Success Rate: 100.0%
|
||||
Concurrent Workers: 24
|
||||
Memory Used: 134 MB
|
||||
Avg Latency: 390.225µs
|
||||
P90 Latency: 811.651µs
|
||||
P95 Latency: 911.2µs
|
||||
P99 Latency: 1.140536ms
|
||||
Bottom 10% Avg Latency: 1.056491ms
|
||||
----------------------------------------
|
||||
|
||||
Test: Query Performance
|
||||
Duration: 1m0.004424256s
|
||||
Total Events: 367781
|
||||
Events/sec: 6129.23
|
||||
Success Rate: 100.0%
|
||||
Concurrent Workers: 24
|
||||
Memory Used: 113 MB
|
||||
Avg Latency: 1.861418ms
|
||||
P90 Latency: 5.800639ms
|
||||
P95 Latency: 7.652288ms
|
||||
P99 Latency: 11.670769ms
|
||||
Bottom 10% Avg Latency: 8.426888ms
|
||||
----------------------------------------
|
||||
|
||||
Test: Concurrent Query/Store
|
||||
Duration: 1m0.003628582s
|
||||
Total Events: 307708
|
||||
Events/sec: 5128.16
|
||||
Success Rate: 100.0%
|
||||
Concurrent Workers: 24
|
||||
Memory Used: 100 MB
|
||||
Avg Latency: 1.520953ms
|
||||
P90 Latency: 3.075583ms
|
||||
P95 Latency: 3.958904ms
|
||||
P99 Latency: 6.227011ms
|
||||
Bottom 10% Avg Latency: 4.506519ms
|
||||
----------------------------------------
|
||||
|
||||
Report saved to: /tmp/benchmark_khatru-badger_8/benchmark_report.txt
|
||||
AsciiDoc report saved to: /tmp/benchmark_khatru-badger_8/benchmark_report.adoc
|
||||
|
||||
RELAY_NAME: khatru-badger
|
||||
RELAY_URL: ws://khatru-badger:3334
|
||||
TEST_TIMESTAMP: 2025-11-26T07:54:21+00:00
|
||||
BENCHMARK_CONFIG:
|
||||
Events: 50000
|
||||
Workers: 24
|
||||
Duration: 60s
|
||||
@@ -0,0 +1,197 @@
|
||||
Starting Nostr Relay Benchmark (Badger Backend)
|
||||
Data Directory: /tmp/benchmark_khatru-sqlite_8
|
||||
Events: 50000, Workers: 24, Duration: 1m0s
|
||||
1764143261406084ℹ️ migrating to version 1... /build/pkg/database/migrations.go:66
|
||||
1764143261406169ℹ️ migrating to version 2... /build/pkg/database/migrations.go:73
|
||||
1764143261406201ℹ️ migrating to version 3... /build/pkg/database/migrations.go:80
|
||||
1764143261406210ℹ️ cleaning up ephemeral events (kinds 20000-29999)... /build/pkg/database/migrations.go:294
|
||||
1764143261406219ℹ️ cleaned up 0 ephemeral events from database /build/pkg/database/migrations.go:339
|
||||
1764143261406234ℹ️ migrating to version 4... /build/pkg/database/migrations.go:87
|
||||
1764143261406240ℹ️ converting events to optimized inline storage (Reiser4 optimization)... /build/pkg/database/migrations.go:347
|
||||
1764143261406256ℹ️ found 0 events to convert (0 regular, 0 replaceable, 0 addressable) /build/pkg/database/migrations.go:436
|
||||
1764143261406263ℹ️ migration complete: converted 0 events to optimized inline storage, deleted 0 old keys /build/pkg/database/migrations.go:545
|
||||
1764143261406285ℹ️ migrating to version 5... /build/pkg/database/migrations.go:94
|
||||
1764143261406291ℹ️ re-encoding events with optimized tag binary format... /build/pkg/database/migrations.go:552
|
||||
1764143261406310ℹ️ found 0 events with e/p tags to re-encode /build/pkg/database/migrations.go:639
|
||||
1764143261406315ℹ️ no events need re-encoding /build/pkg/database/migrations.go:642
|
||||
|
||||
╔════════════════════════════════════════════════════════╗
|
||||
║ BADGER BACKEND BENCHMARK SUITE ║
|
||||
╚════════════════════════════════════════════════════════╝
|
||||
|
||||
=== Starting Badger benchmark ===
|
||||
RunPeakThroughputTest (Badger)..
|
||||
|
||||
=== Peak Throughput Test ===
|
||||
2025/11/26 07:47:41 INFO: Successfully loaded embedded libsecp256k1 v5.0.0 from /tmp/orly-libsecp256k1/libsecp256k1.so
|
||||
Events saved: 50000/50000 (100.0%), errors: 0
|
||||
Duration: 2.980541518s
|
||||
Events/sec: 16775.48
|
||||
Avg latency: 1.316097ms
|
||||
P90 latency: 1.75215ms
|
||||
P95 latency: 2.019999ms
|
||||
P99 latency: 2.884086ms
|
||||
Bottom 10% Avg latency: 743.925µs
|
||||
Wiping database between tests...
|
||||
RunBurstPatternTest (Badger)..
|
||||
|
||||
=== Burst Pattern Test ===
|
||||
Burst completed: 5000 events in 294.559368ms
|
||||
Burst completed: 5000 events in 338.351868ms
|
||||
Burst completed: 5000 events in 289.64343ms
|
||||
Burst completed: 5000 events in 418.427743ms
|
||||
Burst completed: 5000 events in 337.294837ms
|
||||
Burst completed: 5000 events in 359.624702ms
|
||||
Burst completed: 5000 events in 307.791949ms
|
||||
Burst completed: 5000 events in 284.861295ms
|
||||
Burst completed: 5000 events in 314.638569ms
|
||||
Burst completed: 5000 events in 274.271908ms
|
||||
Burst test completed: 50000 events in 8.227316527s, errors: 0
|
||||
Events/sec: 6077.32
|
||||
Wiping database between tests...
|
||||
RunMixedReadWriteTest (Badger)..
|
||||
|
||||
=== Mixed Read/Write Test ===
|
||||
Generating 1000 unique synthetic events (minimum 300 bytes each)...
|
||||
Generated 1000 events:
|
||||
Average content size: 312 bytes
|
||||
All events are unique (incremental timestamps)
|
||||
All events are properly signed
|
||||
|
||||
Pre-populating database for read tests...
|
||||
Generating 50000 unique synthetic events (minimum 300 bytes each)...
|
||||
Generated 50000 events:
|
||||
Average content size: 314 bytes
|
||||
All events are unique (incremental timestamps)
|
||||
All events are properly signed
|
||||
|
||||
Mixed test completed: 25000 writes, 25000 reads in 24.361629597s
|
||||
Combined ops/sec: 2052.41
|
||||
Wiping database between tests...
|
||||
RunQueryTest (Badger)..
|
||||
|
||||
=== Query Test ===
|
||||
Generating 10000 unique synthetic events (minimum 300 bytes each)...
|
||||
Generated 10000 events:
|
||||
Average content size: 313 bytes
|
||||
All events are unique (incremental timestamps)
|
||||
All events are properly signed
|
||||
|
||||
Pre-populating database with 10000 events for query tests...
|
||||
Query test completed: 369485 queries in 1m0.007598809s
|
||||
Queries/sec: 6157.30
|
||||
Avg query latency: 1.851496ms
|
||||
P95 query latency: 7.629059ms
|
||||
P99 query latency: 11.579084ms
|
||||
Wiping database between tests...
|
||||
RunConcurrentQueryStoreTest (Badger)..
|
||||
|
||||
=== Concurrent Query/Store Test ===
|
||||
Generating 5000 unique synthetic events (minimum 300 bytes each)...
|
||||
Generated 5000 events:
|
||||
Average content size: 313 bytes
|
||||
All events are unique (incremental timestamps)
|
||||
All events are properly signed
|
||||
|
||||
Pre-populating database with 5000 events for concurrent query/store test...
|
||||
Generating 50000 unique synthetic events (minimum 300 bytes each)...
|
||||
Generated 50000 events:
|
||||
Average content size: 314 bytes
|
||||
All events are unique (incremental timestamps)
|
||||
All events are properly signed
|
||||
|
||||
Concurrent test completed: 307591 operations (257591 queries, 50000 writes) in 1m0.003842232s
|
||||
Operations/sec: 5126.19
|
||||
Avg latency: 1.567905ms
|
||||
Avg query latency: 1.520146ms
|
||||
Avg write latency: 1.813947ms
|
||||
P95 latency: 4.080054ms
|
||||
P99 latency: 7.252873ms
|
||||
|
||||
=== Badger benchmark completed ===
|
||||
|
||||
|
||||
================================================================================
|
||||
BENCHMARK REPORT
|
||||
================================================================================
|
||||
|
||||
Test: Peak Throughput
|
||||
Duration: 2.980541518s
|
||||
Total Events: 50000
|
||||
Events/sec: 16775.48
|
||||
Success Rate: 100.0%
|
||||
Concurrent Workers: 24
|
||||
Memory Used: 205 MB
|
||||
Avg Latency: 1.316097ms
|
||||
P90 Latency: 1.75215ms
|
||||
P95 Latency: 2.019999ms
|
||||
P99 Latency: 2.884086ms
|
||||
Bottom 10% Avg Latency: 743.925µs
|
||||
----------------------------------------
|
||||
|
||||
Test: Burst Pattern
|
||||
Duration: 8.227316527s
|
||||
Total Events: 50000
|
||||
Events/sec: 6077.32
|
||||
Success Rate: 100.0%
|
||||
Concurrent Workers: 24
|
||||
Memory Used: 206 MB
|
||||
Avg Latency: 1.448816ms
|
||||
P90 Latency: 2.065115ms
|
||||
P95 Latency: 2.415349ms
|
||||
P99 Latency: 3.441514ms
|
||||
Bottom 10% Avg Latency: 642.527µs
|
||||
----------------------------------------
|
||||
|
||||
Test: Mixed Read/Write
|
||||
Duration: 24.361629597s
|
||||
Total Events: 50000
|
||||
Events/sec: 2052.41
|
||||
Success Rate: 100.0%
|
||||
Concurrent Workers: 24
|
||||
Memory Used: 170 MB
|
||||
Avg Latency: 395.815µs
|
||||
P90 Latency: 821.619µs
|
||||
P95 Latency: 915.807µs
|
||||
P99 Latency: 1.137015ms
|
||||
Bottom 10% Avg Latency: 1.044106ms
|
||||
----------------------------------------
|
||||
|
||||
Test: Query Performance
|
||||
Duration: 1m0.007598809s
|
||||
Total Events: 369485
|
||||
Events/sec: 6157.30
|
||||
Success Rate: 100.0%
|
||||
Concurrent Workers: 24
|
||||
Memory Used: 97 MB
|
||||
Avg Latency: 1.851496ms
|
||||
P90 Latency: 5.786274ms
|
||||
P95 Latency: 7.629059ms
|
||||
P99 Latency: 11.579084ms
|
||||
Bottom 10% Avg Latency: 8.382865ms
|
||||
----------------------------------------
|
||||
|
||||
Test: Concurrent Query/Store
|
||||
Duration: 1m0.003842232s
|
||||
Total Events: 307591
|
||||
Events/sec: 5126.19
|
||||
Success Rate: 100.0%
|
||||
Concurrent Workers: 24
|
||||
Memory Used: 143 MB
|
||||
Avg Latency: 1.567905ms
|
||||
P90 Latency: 3.141841ms
|
||||
P95 Latency: 4.080054ms
|
||||
P99 Latency: 7.252873ms
|
||||
Bottom 10% Avg Latency: 4.875018ms
|
||||
----------------------------------------
|
||||
|
||||
Report saved to: /tmp/benchmark_khatru-sqlite_8/benchmark_report.txt
|
||||
AsciiDoc report saved to: /tmp/benchmark_khatru-sqlite_8/benchmark_report.adoc
|
||||
|
||||
RELAY_NAME: khatru-sqlite
|
||||
RELAY_URL: ws://khatru-sqlite:3334
|
||||
TEST_TIMESTAMP: 2025-11-26T07:50:58+00:00
|
||||
BENCHMARK_CONFIG:
|
||||
Events: 50000
|
||||
Workers: 24
|
||||
Duration: 60s
|
||||
@@ -0,0 +1,197 @@
|
||||
Starting Nostr Relay Benchmark (Badger Backend)
|
||||
Data Directory: /tmp/benchmark_next-orly-badger_8
|
||||
Events: 50000, Workers: 24, Duration: 1m0s
|
||||
1764142653240629ℹ️ migrating to version 1... /build/pkg/database/migrations.go:66
|
||||
1764142653240705ℹ️ migrating to version 2... /build/pkg/database/migrations.go:73
|
||||
1764142653240726ℹ️ migrating to version 3... /build/pkg/database/migrations.go:80
|
||||
1764142653240732ℹ️ cleaning up ephemeral events (kinds 20000-29999)... /build/pkg/database/migrations.go:294
|
||||
1764142653240742ℹ️ cleaned up 0 ephemeral events from database /build/pkg/database/migrations.go:339
|
||||
1764142653240754ℹ️ migrating to version 4... /build/pkg/database/migrations.go:87
|
||||
1764142653240759ℹ️ converting events to optimized inline storage (Reiser4 optimization)... /build/pkg/database/migrations.go:347
|
||||
1764142653240772ℹ️ found 0 events to convert (0 regular, 0 replaceable, 0 addressable) /build/pkg/database/migrations.go:436
|
||||
1764142653240777ℹ️ migration complete: converted 0 events to optimized inline storage, deleted 0 old keys /build/pkg/database/migrations.go:545
|
||||
1764142653240794ℹ️ migrating to version 5... /build/pkg/database/migrations.go:94
|
||||
1764142653240799ℹ️ re-encoding events with optimized tag binary format... /build/pkg/database/migrations.go:552
|
||||
1764142653240815ℹ️ found 0 events with e/p tags to re-encode /build/pkg/database/migrations.go:639
|
||||
1764142653240820ℹ️ no events need re-encoding /build/pkg/database/migrations.go:642
|
||||
|
||||
╔════════════════════════════════════════════════════════╗
|
||||
║ BADGER BACKEND BENCHMARK SUITE ║
|
||||
╚════════════════════════════════════════════════════════╝
|
||||
|
||||
=== Starting Badger benchmark ===
|
||||
RunPeakThroughputTest (Badger)..
|
||||
|
||||
=== Peak Throughput Test ===
|
||||
2025/11/26 07:37:33 INFO: Successfully loaded embedded libsecp256k1 v5.0.0 from /tmp/orly-libsecp256k1/libsecp256k1.so
|
||||
Events saved: 50000/50000 (100.0%), errors: 0
|
||||
Duration: 2.994207496s
|
||||
Events/sec: 16698.91
|
||||
Avg latency: 1.331911ms
|
||||
P90 latency: 1.752681ms
|
||||
P95 latency: 2.019719ms
|
||||
P99 latency: 2.937258ms
|
||||
Bottom 10% Avg latency: 766.682µs
|
||||
Wiping database between tests...
|
||||
RunBurstPatternTest (Badger)..
|
||||
|
||||
=== Burst Pattern Test ===
|
||||
Burst completed: 5000 events in 296.493381ms
|
||||
Burst completed: 5000 events in 346.037614ms
|
||||
Burst completed: 5000 events in 295.42219ms
|
||||
Burst completed: 5000 events in 310.553567ms
|
||||
Burst completed: 5000 events in 290.939907ms
|
||||
Burst completed: 5000 events in 586.599699ms
|
||||
Burst completed: 5000 events in 331.078074ms
|
||||
Burst completed: 5000 events in 266.026786ms
|
||||
Burst completed: 5000 events in 305.143046ms
|
||||
Burst completed: 5000 events in 283.61665ms
|
||||
Burst test completed: 50000 events in 8.317273769s, errors: 0
|
||||
Events/sec: 6011.59
|
||||
Wiping database between tests...
|
||||
RunMixedReadWriteTest (Badger)..
|
||||
|
||||
=== Mixed Read/Write Test ===
|
||||
Generating 1000 unique synthetic events (minimum 300 bytes each)...
|
||||
Generated 1000 events:
|
||||
Average content size: 312 bytes
|
||||
All events are unique (incremental timestamps)
|
||||
All events are properly signed
|
||||
|
||||
Pre-populating database for read tests...
|
||||
Generating 50000 unique synthetic events (minimum 300 bytes each)...
|
||||
Generated 50000 events:
|
||||
Average content size: 314 bytes
|
||||
All events are unique (incremental timestamps)
|
||||
All events are properly signed
|
||||
|
||||
Mixed test completed: 25000 writes, 25000 reads in 24.376567267s
|
||||
Combined ops/sec: 2051.15
|
||||
Wiping database between tests...
|
||||
RunQueryTest (Badger)..
|
||||
|
||||
=== Query Test ===
|
||||
Generating 10000 unique synthetic events (minimum 300 bytes each)...
|
||||
Generated 10000 events:
|
||||
Average content size: 313 bytes
|
||||
All events are unique (incremental timestamps)
|
||||
All events are properly signed
|
||||
|
||||
Pre-populating database with 10000 events for query tests...
|
||||
Query test completed: 379823 queries in 1m0.005132427s
|
||||
Queries/sec: 6329.84
|
||||
Avg query latency: 1.793906ms
|
||||
P95 query latency: 7.34021ms
|
||||
P99 query latency: 11.188253ms
|
||||
Wiping database between tests...
|
||||
RunConcurrentQueryStoreTest (Badger)..
|
||||
|
||||
=== Concurrent Query/Store Test ===
|
||||
Generating 5000 unique synthetic events (minimum 300 bytes each)...
|
||||
Generated 5000 events:
|
||||
Average content size: 313 bytes
|
||||
All events are unique (incremental timestamps)
|
||||
All events are properly signed
|
||||
|
||||
Pre-populating database with 5000 events for concurrent query/store test...
|
||||
Generating 50000 unique synthetic events (minimum 300 bytes each)...
|
||||
Generated 50000 events:
|
||||
Average content size: 314 bytes
|
||||
All events are unique (incremental timestamps)
|
||||
All events are properly signed
|
||||
|
||||
Concurrent test completed: 311181 operations (261181 queries, 50000 writes) in 1m0.003287869s
|
||||
Operations/sec: 5186.07
|
||||
Avg latency: 1.534716ms
|
||||
Avg query latency: 1.48944ms
|
||||
Avg write latency: 1.771222ms
|
||||
P95 latency: 3.923748ms
|
||||
P99 latency: 6.879882ms
|
||||
|
||||
=== Badger benchmark completed ===
|
||||
|
||||
|
||||
================================================================================
|
||||
BENCHMARK REPORT
|
||||
================================================================================
|
||||
|
||||
Test: Peak Throughput
|
||||
Duration: 2.994207496s
|
||||
Total Events: 50000
|
||||
Events/sec: 16698.91
|
||||
Success Rate: 100.0%
|
||||
Concurrent Workers: 24
|
||||
Memory Used: 91 MB
|
||||
Avg Latency: 1.331911ms
|
||||
P90 Latency: 1.752681ms
|
||||
P95 Latency: 2.019719ms
|
||||
P99 Latency: 2.937258ms
|
||||
Bottom 10% Avg Latency: 766.682µs
|
||||
----------------------------------------
|
||||
|
||||
Test: Burst Pattern
|
||||
Duration: 8.317273769s
|
||||
Total Events: 50000
|
||||
Events/sec: 6011.59
|
||||
Success Rate: 100.0%
|
||||
Concurrent Workers: 24
|
||||
Memory Used: 204 MB
|
||||
Avg Latency: 1.496861ms
|
||||
P90 Latency: 2.150147ms
|
||||
P95 Latency: 2.715024ms
|
||||
P99 Latency: 5.496937ms
|
||||
Bottom 10% Avg Latency: 684.458µs
|
||||
----------------------------------------
|
||||
|
||||
Test: Mixed Read/Write
|
||||
Duration: 24.376567267s
|
||||
Total Events: 50000
|
||||
Events/sec: 2051.15
|
||||
Success Rate: 100.0%
|
||||
Concurrent Workers: 24
|
||||
Memory Used: 194 MB
|
||||
Avg Latency: 396.054µs
|
||||
P90 Latency: 819.913µs
|
||||
P95 Latency: 914.112µs
|
||||
P99 Latency: 1.134723ms
|
||||
Bottom 10% Avg Latency: 1.077234ms
|
||||
----------------------------------------
|
||||
|
||||
Test: Query Performance
|
||||
Duration: 1m0.005132427s
|
||||
Total Events: 379823
|
||||
Events/sec: 6329.84
|
||||
Success Rate: 100.0%
|
||||
Concurrent Workers: 24
|
||||
Memory Used: 96 MB
|
||||
Avg Latency: 1.793906ms
|
||||
P90 Latency: 5.558514ms
|
||||
P95 Latency: 7.34021ms
|
||||
P99 Latency: 11.188253ms
|
||||
Bottom 10% Avg Latency: 8.06994ms
|
||||
----------------------------------------
|
||||
|
||||
Test: Concurrent Query/Store
|
||||
Duration: 1m0.003287869s
|
||||
Total Events: 311181
|
||||
Events/sec: 5186.07
|
||||
Success Rate: 100.0%
|
||||
Concurrent Workers: 24
|
||||
Memory Used: 141 MB
|
||||
Avg Latency: 1.534716ms
|
||||
P90 Latency: 3.051195ms
|
||||
P95 Latency: 3.923748ms
|
||||
P99 Latency: 6.879882ms
|
||||
Bottom 10% Avg Latency: 4.67505ms
|
||||
----------------------------------------
|
||||
|
||||
Report saved to: /tmp/benchmark_next-orly-badger_8/benchmark_report.txt
|
||||
AsciiDoc report saved to: /tmp/benchmark_next-orly-badger_8/benchmark_report.adoc
|
||||
|
||||
RELAY_NAME: next-orly-badger
|
||||
RELAY_URL: ws://next-orly-badger:8080
|
||||
TEST_TIMESTAMP: 2025-11-26T07:40:50+00:00
|
||||
BENCHMARK_CONFIG:
|
||||
Events: 50000
|
||||
Workers: 24
|
||||
Duration: 60s
|
||||
@@ -0,0 +1,197 @@
|
||||
Starting Nostr Relay Benchmark (Badger Backend)
|
||||
Data Directory: /tmp/benchmark_next-orly-dgraph_8
|
||||
Events: 50000, Workers: 24, Duration: 1m0s
|
||||
1764142855890301ℹ️ migrating to version 1... /build/pkg/database/migrations.go:66
|
||||
1764142855890401ℹ️ migrating to version 2... /build/pkg/database/migrations.go:73
|
||||
1764142855890440ℹ️ migrating to version 3... /build/pkg/database/migrations.go:80
|
||||
1764142855890449ℹ️ cleaning up ephemeral events (kinds 20000-29999)... /build/pkg/database/migrations.go:294
|
||||
1764142855890460ℹ️ cleaned up 0 ephemeral events from database /build/pkg/database/migrations.go:339
|
||||
1764142855890476ℹ️ migrating to version 4... /build/pkg/database/migrations.go:87
|
||||
1764142855890481ℹ️ converting events to optimized inline storage (Reiser4 optimization)... /build/pkg/database/migrations.go:347
|
||||
1764142855890495ℹ️ found 0 events to convert (0 regular, 0 replaceable, 0 addressable) /build/pkg/database/migrations.go:436
|
||||
1764142855890504ℹ️ migration complete: converted 0 events to optimized inline storage, deleted 0 old keys /build/pkg/database/migrations.go:545
|
||||
1764142855890528ℹ️ migrating to version 5... /build/pkg/database/migrations.go:94
|
||||
1764142855890536ℹ️ re-encoding events with optimized tag binary format... /build/pkg/database/migrations.go:552
|
||||
1764142855890559ℹ️ found 0 events with e/p tags to re-encode /build/pkg/database/migrations.go:639
|
||||
1764142855890568ℹ️ no events need re-encoding /build/pkg/database/migrations.go:642
|
||||
|
||||
╔════════════════════════════════════════════════════════╗
|
||||
║ BADGER BACKEND BENCHMARK SUITE ║
|
||||
╚════════════════════════════════════════════════════════╝
|
||||
|
||||
=== Starting Badger benchmark ===
|
||||
RunPeakThroughputTest (Badger)..
|
||||
|
||||
=== Peak Throughput Test ===
|
||||
2025/11/26 07:40:55 INFO: Successfully loaded embedded libsecp256k1 v5.0.0 from /tmp/orly-libsecp256k1/libsecp256k1.so
|
||||
Events saved: 50000/50000 (100.0%), errors: 0
|
||||
Duration: 3.430865656s
|
||||
Events/sec: 14573.58
|
||||
Avg latency: 1.571025ms
|
||||
P90 latency: 2.249507ms
|
||||
P95 latency: 2.610305ms
|
||||
P99 latency: 3.786808ms
|
||||
Bottom 10% Avg latency: 802.953µs
|
||||
Wiping database between tests...
|
||||
RunBurstPatternTest (Badger)..
|
||||
|
||||
=== Burst Pattern Test ===
|
||||
Burst completed: 5000 events in 413.260391ms
|
||||
Burst completed: 5000 events in 416.696811ms
|
||||
Burst completed: 5000 events in 281.278288ms
|
||||
Burst completed: 5000 events in 305.471838ms
|
||||
Burst completed: 5000 events in 284.063576ms
|
||||
Burst completed: 5000 events in 366.197285ms
|
||||
Burst completed: 5000 events in 310.188337ms
|
||||
Burst completed: 5000 events in 270.424131ms
|
||||
Burst completed: 5000 events in 313.061864ms
|
||||
Burst completed: 5000 events in 268.841724ms
|
||||
Burst test completed: 50000 events in 8.234222191s, errors: 0
|
||||
Events/sec: 6072.22
|
||||
Wiping database between tests...
|
||||
RunMixedReadWriteTest (Badger)..
|
||||
|
||||
=== Mixed Read/Write Test ===
|
||||
Generating 1000 unique synthetic events (minimum 300 bytes each)...
|
||||
Generated 1000 events:
|
||||
Average content size: 312 bytes
|
||||
All events are unique (incremental timestamps)
|
||||
All events are properly signed
|
||||
|
||||
Pre-populating database for read tests...
|
||||
Generating 50000 unique synthetic events (minimum 300 bytes each)...
|
||||
Generated 50000 events:
|
||||
Average content size: 314 bytes
|
||||
All events are unique (incremental timestamps)
|
||||
All events are properly signed
|
||||
|
||||
Mixed test completed: 25000 writes, 25000 reads in 24.374242444s
|
||||
Combined ops/sec: 2051.35
|
||||
Wiping database between tests...
|
||||
RunQueryTest (Badger)..
|
||||
|
||||
=== Query Test ===
|
||||
Generating 10000 unique synthetic events (minimum 300 bytes each)...
|
||||
Generated 10000 events:
|
||||
Average content size: 313 bytes
|
||||
All events are unique (incremental timestamps)
|
||||
All events are properly signed
|
||||
|
||||
Pre-populating database with 10000 events for query tests...
|
||||
Query test completed: 363398 queries in 1m0.008386122s
|
||||
Queries/sec: 6055.79
|
||||
Avg query latency: 1.896628ms
|
||||
P95 query latency: 7.915977ms
|
||||
P99 query latency: 12.369055ms
|
||||
Wiping database between tests...
|
||||
RunConcurrentQueryStoreTest (Badger)..
|
||||
|
||||
=== Concurrent Query/Store Test ===
|
||||
Generating 5000 unique synthetic events (minimum 300 bytes each)...
|
||||
Generated 5000 events:
|
||||
Average content size: 313 bytes
|
||||
All events are unique (incremental timestamps)
|
||||
All events are properly signed
|
||||
|
||||
Pre-populating database with 5000 events for concurrent query/store test...
|
||||
Generating 50000 unique synthetic events (minimum 300 bytes each)...
|
||||
Generated 50000 events:
|
||||
Average content size: 314 bytes
|
||||
All events are unique (incremental timestamps)
|
||||
All events are properly signed
|
||||
|
||||
Concurrent test completed: 310491 operations (260491 queries, 50000 writes) in 1m0.002972174s
|
||||
Operations/sec: 5174.59
|
||||
Avg latency: 1.519446ms
|
||||
Avg query latency: 1.48579ms
|
||||
Avg write latency: 1.694789ms
|
||||
P95 latency: 3.910804ms
|
||||
P99 latency: 6.189507ms
|
||||
|
||||
=== Badger benchmark completed ===
|
||||
|
||||
|
||||
================================================================================
|
||||
BENCHMARK REPORT
|
||||
================================================================================
|
||||
|
||||
Test: Peak Throughput
|
||||
Duration: 3.430865656s
|
||||
Total Events: 50000
|
||||
Events/sec: 14573.58
|
||||
Success Rate: 100.0%
|
||||
Concurrent Workers: 24
|
||||
Memory Used: 128 MB
|
||||
Avg Latency: 1.571025ms
|
||||
P90 Latency: 2.249507ms
|
||||
P95 Latency: 2.610305ms
|
||||
P99 Latency: 3.786808ms
|
||||
Bottom 10% Avg Latency: 802.953µs
|
||||
----------------------------------------
|
||||
|
||||
Test: Burst Pattern
|
||||
Duration: 8.234222191s
|
||||
Total Events: 50000
|
||||
Events/sec: 6072.22
|
||||
Success Rate: 100.0%
|
||||
Concurrent Workers: 24
|
||||
Memory Used: 195 MB
|
||||
Avg Latency: 1.454825ms
|
||||
P90 Latency: 2.128246ms
|
||||
P95 Latency: 2.541414ms
|
||||
P99 Latency: 3.875045ms
|
||||
Bottom 10% Avg Latency: 688.084µs
|
||||
----------------------------------------
|
||||
|
||||
Test: Mixed Read/Write
|
||||
Duration: 24.374242444s
|
||||
Total Events: 50000
|
||||
Events/sec: 2051.35
|
||||
Success Rate: 100.0%
|
||||
Concurrent Workers: 24
|
||||
Memory Used: 125 MB
|
||||
Avg Latency: 390.403µs
|
||||
P90 Latency: 807.74µs
|
||||
P95 Latency: 902.751µs
|
||||
P99 Latency: 1.111889ms
|
||||
Bottom 10% Avg Latency: 1.037165ms
|
||||
----------------------------------------
|
||||
|
||||
Test: Query Performance
|
||||
Duration: 1m0.008386122s
|
||||
Total Events: 363398
|
||||
Events/sec: 6055.79
|
||||
Success Rate: 100.0%
|
||||
Concurrent Workers: 24
|
||||
Memory Used: 149 MB
|
||||
Avg Latency: 1.896628ms
|
||||
P90 Latency: 5.916526ms
|
||||
P95 Latency: 7.915977ms
|
||||
P99 Latency: 12.369055ms
|
||||
Bottom 10% Avg Latency: 8.802319ms
|
||||
----------------------------------------
|
||||
|
||||
Test: Concurrent Query/Store
|
||||
Duration: 1m0.002972174s
|
||||
Total Events: 310491
|
||||
Events/sec: 5174.59
|
||||
Success Rate: 100.0%
|
||||
Concurrent Workers: 24
|
||||
Memory Used: 156 MB
|
||||
Avg Latency: 1.519446ms
|
||||
P90 Latency: 3.03826ms
|
||||
P95 Latency: 3.910804ms
|
||||
P99 Latency: 6.189507ms
|
||||
Bottom 10% Avg Latency: 4.473046ms
|
||||
----------------------------------------
|
||||
|
||||
Report saved to: /tmp/benchmark_next-orly-dgraph_8/benchmark_report.txt
|
||||
AsciiDoc report saved to: /tmp/benchmark_next-orly-dgraph_8/benchmark_report.adoc
|
||||
|
||||
RELAY_NAME: next-orly-dgraph
|
||||
RELAY_URL: ws://next-orly-dgraph:8080
|
||||
TEST_TIMESTAMP: 2025-11-26T07:44:13+00:00
|
||||
BENCHMARK_CONFIG:
|
||||
Events: 50000
|
||||
Workers: 24
|
||||
Duration: 60s
|
||||
@@ -0,0 +1,197 @@
|
||||
Starting Nostr Relay Benchmark (Badger Backend)
|
||||
Data Directory: /tmp/benchmark_next-orly-neo4j_8
|
||||
Events: 50000, Workers: 24, Duration: 1m0s
|
||||
1764143058917148ℹ️ migrating to version 1... /build/pkg/database/migrations.go:66
|
||||
1764143058917210ℹ️ migrating to version 2... /build/pkg/database/migrations.go:73
|
||||
1764143058917229ℹ️ migrating to version 3... /build/pkg/database/migrations.go:80
|
||||
1764143058917234ℹ️ cleaning up ephemeral events (kinds 20000-29999)... /build/pkg/database/migrations.go:294
|
||||
1764143058917243ℹ️ cleaned up 0 ephemeral events from database /build/pkg/database/migrations.go:339
|
||||
1764143058917256ℹ️ migrating to version 4... /build/pkg/database/migrations.go:87
|
||||
1764143058917261ℹ️ converting events to optimized inline storage (Reiser4 optimization)... /build/pkg/database/migrations.go:347
|
||||
1764143058917274ℹ️ found 0 events to convert (0 regular, 0 replaceable, 0 addressable) /build/pkg/database/migrations.go:436
|
||||
1764143058917281ℹ️ migration complete: converted 0 events to optimized inline storage, deleted 0 old keys /build/pkg/database/migrations.go:545
|
||||
1764143058917296ℹ️ migrating to version 5... /build/pkg/database/migrations.go:94
|
||||
1764143058917301ℹ️ re-encoding events with optimized tag binary format... /build/pkg/database/migrations.go:552
|
||||
1764143058917316ℹ️ found 0 events with e/p tags to re-encode /build/pkg/database/migrations.go:639
|
||||
1764143058917321ℹ️ no events need re-encoding /build/pkg/database/migrations.go:642
|
||||
|
||||
╔════════════════════════════════════════════════════════╗
|
||||
║ BADGER BACKEND BENCHMARK SUITE ║
|
||||
╚════════════════════════════════════════════════════════╝
|
||||
|
||||
=== Starting Badger benchmark ===
|
||||
RunPeakThroughputTest (Badger)..
|
||||
|
||||
=== Peak Throughput Test ===
|
||||
2025/11/26 07:44:18 INFO: Successfully loaded embedded libsecp256k1 v5.0.0 from /tmp/orly-libsecp256k1/libsecp256k1.so
|
||||
Events saved: 50000/50000 (100.0%), errors: 0
|
||||
Duration: 3.013027595s
|
||||
Events/sec: 16594.60
|
||||
Avg latency: 1.341265ms
|
||||
P90 latency: 1.798828ms
|
||||
P95 latency: 2.068012ms
|
||||
P99 latency: 2.883646ms
|
||||
Bottom 10% Avg latency: 760.268µs
|
||||
Wiping database between tests...
|
||||
RunBurstPatternTest (Badger)..
|
||||
|
||||
=== Burst Pattern Test ===
|
||||
Burst completed: 5000 events in 286.776937ms
|
||||
Burst completed: 5000 events in 322.103436ms
|
||||
Burst completed: 5000 events in 287.074253ms
|
||||
Burst completed: 5000 events in 307.39847ms
|
||||
Burst completed: 5000 events in 289.282402ms
|
||||
Burst completed: 5000 events in 351.106806ms
|
||||
Burst completed: 5000 events in 307.616957ms
|
||||
Burst completed: 5000 events in 281.010206ms
|
||||
Burst completed: 5000 events in 387.29128ms
|
||||
Burst completed: 5000 events in 317.867754ms
|
||||
Burst test completed: 50000 events in 8.143674752s, errors: 0
|
||||
Events/sec: 6139.73
|
||||
Wiping database between tests...
|
||||
RunMixedReadWriteTest (Badger)..
|
||||
|
||||
=== Mixed Read/Write Test ===
|
||||
Generating 1000 unique synthetic events (minimum 300 bytes each)...
|
||||
Generated 1000 events:
|
||||
Average content size: 312 bytes
|
||||
All events are unique (incremental timestamps)
|
||||
All events are properly signed
|
||||
|
||||
Pre-populating database for read tests...
|
||||
Generating 50000 unique synthetic events (minimum 300 bytes each)...
|
||||
Generated 50000 events:
|
||||
Average content size: 314 bytes
|
||||
All events are unique (incremental timestamps)
|
||||
All events are properly signed
|
||||
|
||||
Mixed test completed: 25000 writes, 25000 reads in 24.392570025s
|
||||
Combined ops/sec: 2049.80
|
||||
Wiping database between tests...
|
||||
RunQueryTest (Badger)..
|
||||
|
||||
=== Query Test ===
|
||||
Generating 10000 unique synthetic events (minimum 300 bytes each)...
|
||||
Generated 10000 events:
|
||||
Average content size: 313 bytes
|
||||
All events are unique (incremental timestamps)
|
||||
All events are properly signed
|
||||
|
||||
Pre-populating database with 10000 events for query tests...
|
||||
Query test completed: 381354 queries in 1m0.004315541s
|
||||
Queries/sec: 6355.44
|
||||
Avg query latency: 1.774601ms
|
||||
P95 query latency: 7.270517ms
|
||||
P99 query latency: 11.058437ms
|
||||
Wiping database between tests...
|
||||
RunConcurrentQueryStoreTest (Badger)..
|
||||
|
||||
=== Concurrent Query/Store Test ===
|
||||
Generating 5000 unique synthetic events (minimum 300 bytes each)...
|
||||
Generated 5000 events:
|
||||
Average content size: 313 bytes
|
||||
All events are unique (incremental timestamps)
|
||||
All events are properly signed
|
||||
|
||||
Pre-populating database with 5000 events for concurrent query/store test...
|
||||
Generating 50000 unique synthetic events (minimum 300 bytes each)...
|
||||
Generated 50000 events:
|
||||
Average content size: 314 bytes
|
||||
All events are unique (incremental timestamps)
|
||||
All events are properly signed
|
||||
|
||||
Concurrent test completed: 311298 operations (261298 queries, 50000 writes) in 1m0.002804902s
|
||||
Operations/sec: 5188.06
|
||||
Avg latency: 1.525543ms
|
||||
Avg query latency: 1.487415ms
|
||||
Avg write latency: 1.724798ms
|
||||
P95 latency: 3.973942ms
|
||||
P99 latency: 6.346957ms
|
||||
|
||||
=== Badger benchmark completed ===
|
||||
|
||||
|
||||
================================================================================
|
||||
BENCHMARK REPORT
|
||||
================================================================================
|
||||
|
||||
Test: Peak Throughput
|
||||
Duration: 3.013027595s
|
||||
Total Events: 50000
|
||||
Events/sec: 16594.60
|
||||
Success Rate: 100.0%
|
||||
Concurrent Workers: 24
|
||||
Memory Used: 135 MB
|
||||
Avg Latency: 1.341265ms
|
||||
P90 Latency: 1.798828ms
|
||||
P95 Latency: 2.068012ms
|
||||
P99 Latency: 2.883646ms
|
||||
Bottom 10% Avg Latency: 760.268µs
|
||||
----------------------------------------
|
||||
|
||||
Test: Burst Pattern
|
||||
Duration: 8.143674752s
|
||||
Total Events: 50000
|
||||
Events/sec: 6139.73
|
||||
Success Rate: 100.0%
|
||||
Concurrent Workers: 24
|
||||
Memory Used: 210 MB
|
||||
Avg Latency: 1.417529ms
|
||||
P90 Latency: 1.96735ms
|
||||
P95 Latency: 2.279114ms
|
||||
P99 Latency: 3.319737ms
|
||||
Bottom 10% Avg Latency: 689.835µs
|
||||
----------------------------------------
|
||||
|
||||
Test: Mixed Read/Write
|
||||
Duration: 24.392570025s
|
||||
Total Events: 50000
|
||||
Events/sec: 2049.80
|
||||
Success Rate: 100.0%
|
||||
Concurrent Workers: 24
|
||||
Memory Used: 194 MB
|
||||
Avg Latency: 389.458µs
|
||||
P90 Latency: 807.449µs
|
||||
P95 Latency: 893.313µs
|
||||
P99 Latency: 1.078376ms
|
||||
Bottom 10% Avg Latency: 1.008354ms
|
||||
----------------------------------------
|
||||
|
||||
Test: Query Performance
|
||||
Duration: 1m0.004315541s
|
||||
Total Events: 381354
|
||||
Events/sec: 6355.44
|
||||
Success Rate: 100.0%
|
||||
Concurrent Workers: 24
|
||||
Memory Used: 149 MB
|
||||
Avg Latency: 1.774601ms
|
||||
P90 Latency: 5.479193ms
|
||||
P95 Latency: 7.270517ms
|
||||
P99 Latency: 11.058437ms
|
||||
Bottom 10% Avg Latency: 7.987ms
|
||||
----------------------------------------
|
||||
|
||||
Test: Concurrent Query/Store
|
||||
Duration: 1m0.002804902s
|
||||
Total Events: 311298
|
||||
Events/sec: 5188.06
|
||||
Success Rate: 100.0%
|
||||
Concurrent Workers: 24
|
||||
Memory Used: 91 MB
|
||||
Avg Latency: 1.525543ms
|
||||
P90 Latency: 3.063464ms
|
||||
P95 Latency: 3.973942ms
|
||||
P99 Latency: 6.346957ms
|
||||
Bottom 10% Avg Latency: 4.524119ms
|
||||
----------------------------------------
|
||||
|
||||
Report saved to: /tmp/benchmark_next-orly-neo4j_8/benchmark_report.txt
|
||||
AsciiDoc report saved to: /tmp/benchmark_next-orly-neo4j_8/benchmark_report.adoc
|
||||
|
||||
RELAY_NAME: next-orly-neo4j
|
||||
RELAY_URL: ws://next-orly-neo4j:8080
|
||||
TEST_TIMESTAMP: 2025-11-26T07:47:36+00:00
|
||||
BENCHMARK_CONFIG:
|
||||
Events: 50000
|
||||
Workers: 24
|
||||
Duration: 60s
|
||||
@@ -0,0 +1,197 @@
|
||||
Starting Nostr Relay Benchmark (Badger Backend)
|
||||
Data Directory: /tmp/benchmark_nostr-rs-relay_8
|
||||
Events: 50000, Workers: 24, Duration: 1m0s
|
||||
1764144072428228ℹ️ migrating to version 1... /build/pkg/database/migrations.go:66
|
||||
1764144072428311ℹ️ migrating to version 2... /build/pkg/database/migrations.go:73
|
||||
1764144072428332ℹ️ migrating to version 3... /build/pkg/database/migrations.go:80
|
||||
1764144072428337ℹ️ cleaning up ephemeral events (kinds 20000-29999)... /build/pkg/database/migrations.go:294
|
||||
1764144072428348ℹ️ cleaned up 0 ephemeral events from database /build/pkg/database/migrations.go:339
|
||||
1764144072428362ℹ️ migrating to version 4... /build/pkg/database/migrations.go:87
|
||||
1764144072428367ℹ️ converting events to optimized inline storage (Reiser4 optimization)... /build/pkg/database/migrations.go:347
|
||||
1764144072428382ℹ️ found 0 events to convert (0 regular, 0 replaceable, 0 addressable) /build/pkg/database/migrations.go:436
|
||||
1764144072428388ℹ️ migration complete: converted 0 events to optimized inline storage, deleted 0 old keys /build/pkg/database/migrations.go:545
|
||||
1764144072428403ℹ️ migrating to version 5... /build/pkg/database/migrations.go:94
|
||||
1764144072428407ℹ️ re-encoding events with optimized tag binary format... /build/pkg/database/migrations.go:552
|
||||
1764144072428461ℹ️ found 0 events with e/p tags to re-encode /build/pkg/database/migrations.go:639
|
||||
1764144072428504ℹ️ no events need re-encoding /build/pkg/database/migrations.go:642
|
||||
|
||||
╔════════════════════════════════════════════════════════╗
|
||||
║ BADGER BACKEND BENCHMARK SUITE ║
|
||||
╚════════════════════════════════════════════════════════╝
|
||||
|
||||
=== Starting Badger benchmark ===
|
||||
RunPeakThroughputTest (Badger)..
|
||||
|
||||
=== Peak Throughput Test ===
|
||||
2025/11/26 08:01:12 INFO: Successfully loaded embedded libsecp256k1 v5.0.0 from /tmp/orly-libsecp256k1/libsecp256k1.so
|
||||
Events saved: 50000/50000 (100.0%), errors: 0
|
||||
Duration: 3.174311581s
|
||||
Events/sec: 15751.45
|
||||
Avg latency: 1.442411ms
|
||||
P90 latency: 1.94422ms
|
||||
P95 latency: 2.22848ms
|
||||
P99 latency: 3.230197ms
|
||||
Bottom 10% Avg latency: 812.222µs
|
||||
Wiping database between tests...
|
||||
RunBurstPatternTest (Badger)..
|
||||
|
||||
=== Burst Pattern Test ===
|
||||
Burst completed: 5000 events in 307.983371ms
|
||||
Burst completed: 5000 events in 362.020748ms
|
||||
Burst completed: 5000 events in 287.762195ms
|
||||
Burst completed: 5000 events in 312.062236ms
|
||||
Burst completed: 5000 events in 293.876571ms
|
||||
Burst completed: 5000 events in 374.103253ms
|
||||
Burst completed: 5000 events in 310.909244ms
|
||||
Burst completed: 5000 events in 283.004205ms
|
||||
Burst completed: 5000 events in 298.739839ms
|
||||
Burst completed: 5000 events in 276.165042ms
|
||||
Burst test completed: 50000 events in 8.112460039s, errors: 0
|
||||
Events/sec: 6163.36
|
||||
Wiping database between tests...
|
||||
RunMixedReadWriteTest (Badger)..
|
||||
|
||||
=== Mixed Read/Write Test ===
|
||||
Generating 1000 unique synthetic events (minimum 300 bytes each)...
|
||||
Generated 1000 events:
|
||||
Average content size: 312 bytes
|
||||
All events are unique (incremental timestamps)
|
||||
All events are properly signed
|
||||
|
||||
Pre-populating database for read tests...
|
||||
Generating 50000 unique synthetic events (minimum 300 bytes each)...
|
||||
Generated 50000 events:
|
||||
Average content size: 314 bytes
|
||||
All events are unique (incremental timestamps)
|
||||
All events are properly signed
|
||||
|
||||
Mixed test completed: 25000 writes, 25000 reads in 24.41340672s
|
||||
Combined ops/sec: 2048.06
|
||||
Wiping database between tests...
|
||||
RunQueryTest (Badger)..
|
||||
|
||||
=== Query Test ===
|
||||
Generating 10000 unique synthetic events (minimum 300 bytes each)...
|
||||
Generated 10000 events:
|
||||
Average content size: 313 bytes
|
||||
All events are unique (incremental timestamps)
|
||||
All events are properly signed
|
||||
|
||||
Pre-populating database with 10000 events for query tests...
|
||||
Query test completed: 370248 queries in 1m0.004253098s
|
||||
Queries/sec: 6170.36
|
||||
Avg query latency: 1.845097ms
|
||||
P95 query latency: 7.60818ms
|
||||
P99 query latency: 11.65437ms
|
||||
Wiping database between tests...
|
||||
RunConcurrentQueryStoreTest (Badger)..
|
||||
|
||||
=== Concurrent Query/Store Test ===
|
||||
Generating 5000 unique synthetic events (minimum 300 bytes each)...
|
||||
Generated 5000 events:
|
||||
Average content size: 313 bytes
|
||||
All events are unique (incremental timestamps)
|
||||
All events are properly signed
|
||||
|
||||
Pre-populating database with 5000 events for concurrent query/store test...
|
||||
Generating 50000 unique synthetic events (minimum 300 bytes each)...
|
||||
Generated 50000 events:
|
||||
Average content size: 314 bytes
|
||||
All events are unique (incremental timestamps)
|
||||
All events are properly signed
|
||||
|
||||
Concurrent test completed: 309475 operations (259475 queries, 50000 writes) in 1m0.004403417s
|
||||
Operations/sec: 5157.54
|
||||
Avg latency: 1.523601ms
|
||||
Avg query latency: 1.501844ms
|
||||
Avg write latency: 1.63651ms
|
||||
P95 latency: 3.938186ms
|
||||
P99 latency: 6.342582ms
|
||||
|
||||
=== Badger benchmark completed ===
|
||||
|
||||
|
||||
================================================================================
|
||||
BENCHMARK REPORT
|
||||
================================================================================
|
||||
|
||||
Test: Peak Throughput
|
||||
Duration: 3.174311581s
|
||||
Total Events: 50000
|
||||
Events/sec: 15751.45
|
||||
Success Rate: 100.0%
|
||||
Concurrent Workers: 24
|
||||
Memory Used: 205 MB
|
||||
Avg Latency: 1.442411ms
|
||||
P90 Latency: 1.94422ms
|
||||
P95 Latency: 2.22848ms
|
||||
P99 Latency: 3.230197ms
|
||||
Bottom 10% Avg Latency: 812.222µs
|
||||
----------------------------------------
|
||||
|
||||
Test: Burst Pattern
|
||||
Duration: 8.112460039s
|
||||
Total Events: 50000
|
||||
Events/sec: 6163.36
|
||||
Success Rate: 100.0%
|
||||
Concurrent Workers: 24
|
||||
Memory Used: 254 MB
|
||||
Avg Latency: 1.414472ms
|
||||
P90 Latency: 1.957275ms
|
||||
P95 Latency: 2.267184ms
|
||||
P99 Latency: 3.19513ms
|
||||
Bottom 10% Avg Latency: 750.181µs
|
||||
----------------------------------------
|
||||
|
||||
Test: Mixed Read/Write
|
||||
Duration: 24.41340672s
|
||||
Total Events: 50000
|
||||
Events/sec: 2048.06
|
||||
Success Rate: 100.0%
|
||||
Concurrent Workers: 24
|
||||
Memory Used: 129 MB
|
||||
Avg Latency: 400.791µs
|
||||
P90 Latency: 826.182µs
|
||||
P95 Latency: 921.434µs
|
||||
P99 Latency: 1.143516ms
|
||||
Bottom 10% Avg Latency: 1.063808ms
|
||||
----------------------------------------
|
||||
|
||||
Test: Query Performance
|
||||
Duration: 1m0.004253098s
|
||||
Total Events: 370248
|
||||
Events/sec: 6170.36
|
||||
Success Rate: 100.0%
|
||||
Concurrent Workers: 24
|
||||
Memory Used: 156 MB
|
||||
Avg Latency: 1.845097ms
|
||||
P90 Latency: 5.757979ms
|
||||
P95 Latency: 7.60818ms
|
||||
P99 Latency: 11.65437ms
|
||||
Bottom 10% Avg Latency: 8.384135ms
|
||||
----------------------------------------
|
||||
|
||||
Test: Concurrent Query/Store
|
||||
Duration: 1m0.004403417s
|
||||
Total Events: 309475
|
||||
Events/sec: 5157.54
|
||||
Success Rate: 100.0%
|
||||
Concurrent Workers: 24
|
||||
Memory Used: 142 MB
|
||||
Avg Latency: 1.523601ms
|
||||
P90 Latency: 3.071867ms
|
||||
P95 Latency: 3.938186ms
|
||||
P99 Latency: 6.342582ms
|
||||
Bottom 10% Avg Latency: 4.516506ms
|
||||
----------------------------------------
|
||||
|
||||
Report saved to: /tmp/benchmark_nostr-rs-relay_8/benchmark_report.txt
|
||||
AsciiDoc report saved to: /tmp/benchmark_nostr-rs-relay_8/benchmark_report.adoc
|
||||
|
||||
RELAY_NAME: nostr-rs-relay
|
||||
RELAY_URL: ws://nostr-rs-relay:8080
|
||||
TEST_TIMESTAMP: 2025-11-26T08:04:30+00:00
|
||||
BENCHMARK_CONFIG:
|
||||
Events: 50000
|
||||
Workers: 24
|
||||
Duration: 60s
|
||||
@@ -0,0 +1,197 @@
|
||||
Starting Nostr Relay Benchmark (Badger Backend)
|
||||
Data Directory: /tmp/benchmark_relayer-basic_8
|
||||
Events: 50000, Workers: 24, Duration: 1m0s
|
||||
1764143666952973ℹ️ migrating to version 1... /build/pkg/database/migrations.go:66
|
||||
1764143666953030ℹ️ migrating to version 2... /build/pkg/database/migrations.go:73
|
||||
1764143666953049ℹ️ migrating to version 3... /build/pkg/database/migrations.go:80
|
||||
1764143666953055ℹ️ cleaning up ephemeral events (kinds 20000-29999)... /build/pkg/database/migrations.go:294
|
||||
1764143666953065ℹ️ cleaned up 0 ephemeral events from database /build/pkg/database/migrations.go:339
|
||||
1764143666953078ℹ️ migrating to version 4... /build/pkg/database/migrations.go:87
|
||||
1764143666953083ℹ️ converting events to optimized inline storage (Reiser4 optimization)... /build/pkg/database/migrations.go:347
|
||||
1764143666953094ℹ️ found 0 events to convert (0 regular, 0 replaceable, 0 addressable) /build/pkg/database/migrations.go:436
|
||||
1764143666953100ℹ️ migration complete: converted 0 events to optimized inline storage, deleted 0 old keys /build/pkg/database/migrations.go:545
|
||||
1764143666953114ℹ️ migrating to version 5... /build/pkg/database/migrations.go:94
|
||||
1764143666953119ℹ️ re-encoding events with optimized tag binary format... /build/pkg/database/migrations.go:552
|
||||
1764143666953134ℹ️ found 0 events with e/p tags to re-encode /build/pkg/database/migrations.go:639
|
||||
1764143666953141ℹ️ no events need re-encoding /build/pkg/database/migrations.go:642
|
||||
|
||||
╔════════════════════════════════════════════════════════╗
|
||||
║ BADGER BACKEND BENCHMARK SUITE ║
|
||||
╚════════════════════════════════════════════════════════╝
|
||||
|
||||
=== Starting Badger benchmark ===
|
||||
RunPeakThroughputTest (Badger)..
|
||||
|
||||
=== Peak Throughput Test ===
|
||||
2025/11/26 07:54:26 INFO: Successfully loaded embedded libsecp256k1 v5.0.0 from /tmp/orly-libsecp256k1/libsecp256k1.so
|
||||
Events saved: 50000/50000 (100.0%), errors: 0
|
||||
Duration: 3.104848253s
|
||||
Events/sec: 16103.85
|
||||
Avg latency: 1.401051ms
|
||||
P90 latency: 1.888349ms
|
||||
P95 latency: 2.187347ms
|
||||
P99 latency: 3.155266ms
|
||||
Bottom 10% Avg latency: 788.805µs
|
||||
Wiping database between tests...
|
||||
RunBurstPatternTest (Badger)..
|
||||
|
||||
=== Burst Pattern Test ===
|
||||
Burst completed: 5000 events in 309.873989ms
|
||||
Burst completed: 5000 events in 341.685521ms
|
||||
Burst completed: 5000 events in 289.850715ms
|
||||
Burst completed: 5000 events in 315.600908ms
|
||||
Burst completed: 5000 events in 288.702527ms
|
||||
Burst completed: 5000 events in 374.124316ms
|
||||
Burst completed: 5000 events in 312.291426ms
|
||||
Burst completed: 5000 events in 289.316359ms
|
||||
Burst completed: 5000 events in 420.327167ms
|
||||
Burst completed: 5000 events in 332.309838ms
|
||||
Burst test completed: 50000 events in 8.280469107s, errors: 0
|
||||
Events/sec: 6038.31
|
||||
Wiping database between tests...
|
||||
RunMixedReadWriteTest (Badger)..
|
||||
|
||||
=== Mixed Read/Write Test ===
|
||||
Generating 1000 unique synthetic events (minimum 300 bytes each)...
|
||||
Generated 1000 events:
|
||||
Average content size: 312 bytes
|
||||
All events are unique (incremental timestamps)
|
||||
All events are properly signed
|
||||
|
||||
Pre-populating database for read tests...
|
||||
Generating 50000 unique synthetic events (minimum 300 bytes each)...
|
||||
Generated 50000 events:
|
||||
Average content size: 314 bytes
|
||||
All events are unique (incremental timestamps)
|
||||
All events are properly signed
|
||||
|
||||
Mixed test completed: 25000 writes, 25000 reads in 24.499295481s
|
||||
Combined ops/sec: 2040.88
|
||||
Wiping database between tests...
|
||||
RunQueryTest (Badger)..
|
||||
|
||||
=== Query Test ===
|
||||
Generating 10000 unique synthetic events (minimum 300 bytes each)...
|
||||
Generated 10000 events:
|
||||
Average content size: 313 bytes
|
||||
All events are unique (incremental timestamps)
|
||||
All events are properly signed
|
||||
|
||||
Pre-populating database with 10000 events for query tests...
|
||||
Query test completed: 375154 queries in 1m0.004300893s
|
||||
Queries/sec: 6252.12
|
||||
Avg query latency: 1.804479ms
|
||||
P95 query latency: 7.361776ms
|
||||
P99 query latency: 11.303739ms
|
||||
Wiping database between tests...
|
||||
RunConcurrentQueryStoreTest (Badger)..
|
||||
|
||||
=== Concurrent Query/Store Test ===
|
||||
Generating 5000 unique synthetic events (minimum 300 bytes each)...
|
||||
Generated 5000 events:
|
||||
Average content size: 313 bytes
|
||||
All events are unique (incremental timestamps)
|
||||
All events are properly signed
|
||||
|
||||
Pre-populating database with 5000 events for concurrent query/store test...
|
||||
Generating 50000 unique synthetic events (minimum 300 bytes each)...
|
||||
Generated 50000 events:
|
||||
Average content size: 314 bytes
|
||||
All events are unique (incremental timestamps)
|
||||
All events are properly signed
|
||||
|
||||
Concurrent test completed: 306374 operations (256374 queries, 50000 writes) in 1m0.003786148s
|
||||
Operations/sec: 5105.91
|
||||
Avg latency: 1.576576ms
|
||||
Avg query latency: 1.528734ms
|
||||
Avg write latency: 1.821884ms
|
||||
P95 latency: 4.109035ms
|
||||
P99 latency: 6.61579ms
|
||||
|
||||
=== Badger benchmark completed ===
|
||||
|
||||
|
||||
================================================================================
|
||||
BENCHMARK REPORT
|
||||
================================================================================
|
||||
|
||||
Test: Peak Throughput
|
||||
Duration: 3.104848253s
|
||||
Total Events: 50000
|
||||
Events/sec: 16103.85
|
||||
Success Rate: 100.0%
|
||||
Concurrent Workers: 24
|
||||
Memory Used: 126 MB
|
||||
Avg Latency: 1.401051ms
|
||||
P90 Latency: 1.888349ms
|
||||
P95 Latency: 2.187347ms
|
||||
P99 Latency: 3.155266ms
|
||||
Bottom 10% Avg Latency: 788.805µs
|
||||
----------------------------------------
|
||||
|
||||
Test: Burst Pattern
|
||||
Duration: 8.280469107s
|
||||
Total Events: 50000
|
||||
Events/sec: 6038.31
|
||||
Success Rate: 100.0%
|
||||
Concurrent Workers: 24
|
||||
Memory Used: 253 MB
|
||||
Avg Latency: 1.501362ms
|
||||
P90 Latency: 2.126101ms
|
||||
P95 Latency: 2.477719ms
|
||||
P99 Latency: 3.656509ms
|
||||
Bottom 10% Avg Latency: 737.519µs
|
||||
----------------------------------------
|
||||
|
||||
Test: Mixed Read/Write
|
||||
Duration: 24.499295481s
|
||||
Total Events: 50000
|
||||
Events/sec: 2040.88
|
||||
Success Rate: 100.0%
|
||||
Concurrent Workers: 24
|
||||
Memory Used: 146 MB
|
||||
Avg Latency: 400.179µs
|
||||
P90 Latency: 824.427µs
|
||||
P95 Latency: 920.8µs
|
||||
P99 Latency: 1.163662ms
|
||||
Bottom 10% Avg Latency: 1.084633ms
|
||||
----------------------------------------
|
||||
|
||||
Test: Query Performance
|
||||
Duration: 1m0.004300893s
|
||||
Total Events: 375154
|
||||
Events/sec: 6252.12
|
||||
Success Rate: 100.0%
|
||||
Concurrent Workers: 24
|
||||
Memory Used: 144 MB
|
||||
Avg Latency: 1.804479ms
|
||||
P90 Latency: 5.607171ms
|
||||
P95 Latency: 7.361776ms
|
||||
P99 Latency: 11.303739ms
|
||||
Bottom 10% Avg Latency: 8.12332ms
|
||||
----------------------------------------
|
||||
|
||||
Test: Concurrent Query/Store
|
||||
Duration: 1m0.003786148s
|
||||
Total Events: 306374
|
||||
Events/sec: 5105.91
|
||||
Success Rate: 100.0%
|
||||
Concurrent Workers: 24
|
||||
Memory Used: 115 MB
|
||||
Avg Latency: 1.576576ms
|
||||
P90 Latency: 3.182483ms
|
||||
P95 Latency: 4.109035ms
|
||||
P99 Latency: 6.61579ms
|
||||
Bottom 10% Avg Latency: 4.720777ms
|
||||
----------------------------------------
|
||||
|
||||
Report saved to: /tmp/benchmark_relayer-basic_8/benchmark_report.txt
|
||||
AsciiDoc report saved to: /tmp/benchmark_relayer-basic_8/benchmark_report.adoc
|
||||
|
||||
RELAY_NAME: relayer-basic
|
||||
RELAY_URL: ws://relayer-basic:7447
|
||||
TEST_TIMESTAMP: 2025-11-26T07:57:44+00:00
|
||||
BENCHMARK_CONFIG:
|
||||
Events: 50000
|
||||
Workers: 24
|
||||
Duration: 60s
|
||||
@@ -0,0 +1,198 @@
|
||||
Starting Nostr Relay Benchmark (Badger Backend)
|
||||
Data Directory: /tmp/benchmark_rely-sqlite_8
|
||||
Events: 50000, Workers: 24, Duration: 1m0s
|
||||
1764142450497543ℹ️ migrating to version 1... /build/pkg/database/migrations.go:66
|
||||
1764142450497609ℹ️ migrating to version 2... /build/pkg/database/migrations.go:73
|
||||
1764142450497631ℹ️ migrating to version 3... /build/pkg/database/migrations.go:80
|
||||
1764142450497636ℹ️ cleaning up ephemeral events (kinds 20000-29999)... /build/pkg/database/migrations.go:294
|
||||
1764142450497646ℹ️ cleaned up 0 ephemeral events from database /build/pkg/database/migrations.go:339
|
||||
1764142450497688ℹ️ migrating to version 4... /build/pkg/database/migrations.go:87
|
||||
1764142450497694ℹ️ converting events to optimized inline storage (Reiser4 optimization)... /build/pkg/database/migrations.go:347
|
||||
1764142450497706ℹ️ found 0 events to convert (0 regular, 0 replaceable, 0 addressable) /build/pkg/database/migrations.go:436
|
||||
1764142450497711ℹ️ migration complete: converted 0 events to optimized inline storage, deleted 0 old keys /build/pkg/database/migrations.go:545
|
||||
1764142450497773ℹ️ migrating to version 5... /build/pkg/database/migrations.go:94
|
||||
1764142450497779ℹ️ re-encoding events with optimized tag binary format... /build/pkg/database/migrations.go:552
|
||||
1764142450497793ℹ️ found 0 events with e/p tags to re-encode /build/pkg/database/migrations.go:639
|
||||
1764142450497798ℹ️ no events need re-encoding /build/pkg/database/migrations.go:642
|
||||
|
||||
╔════════════════════════════════════════════════════════╗
|
||||
║ BADGER BACKEND BENCHMARK SUITE ║
|
||||
╚════════════════════════════════════════════════════════╝
|
||||
|
||||
=== Starting Badger benchmark ===
|
||||
RunPeakThroughputTest (Badger)..
|
||||
|
||||
=== Peak Throughput Test ===
|
||||
2025/11/26 07:34:10 INFO: Extracted embedded libsecp256k1 to /tmp/orly-libsecp256k1/libsecp256k1.so
|
||||
2025/11/26 07:34:10 INFO: Successfully loaded embedded libsecp256k1 v5.0.0 from /tmp/orly-libsecp256k1/libsecp256k1.so
|
||||
Events saved: 50000/50000 (100.0%), errors: 0
|
||||
Duration: 3.067785126s
|
||||
Events/sec: 16298.40
|
||||
Avg latency: 1.360569ms
|
||||
P90 latency: 1.819407ms
|
||||
P95 latency: 2.160818ms
|
||||
P99 latency: 3.606363ms
|
||||
Bottom 10% Avg latency: 746.704µs
|
||||
Wiping database between tests...
|
||||
RunBurstPatternTest (Badger)..
|
||||
|
||||
=== Burst Pattern Test ===
|
||||
Burst completed: 5000 events in 312.311304ms
|
||||
Burst completed: 5000 events in 359.334028ms
|
||||
Burst completed: 5000 events in 307.257652ms
|
||||
Burst completed: 5000 events in 318.240243ms
|
||||
Burst completed: 5000 events in 295.405906ms
|
||||
Burst completed: 5000 events in 369.690986ms
|
||||
Burst completed: 5000 events in 308.42646ms
|
||||
Burst completed: 5000 events in 267.313308ms
|
||||
Burst completed: 5000 events in 301.834829ms
|
||||
Burst completed: 5000 events in 282.800373ms
|
||||
Burst test completed: 50000 events in 8.128805288s, errors: 0
|
||||
Events/sec: 6150.97
|
||||
Wiping database between tests...
|
||||
RunMixedReadWriteTest (Badger)..
|
||||
|
||||
=== Mixed Read/Write Test ===
|
||||
Generating 1000 unique synthetic events (minimum 300 bytes each)...
|
||||
Generated 1000 events:
|
||||
Average content size: 312 bytes
|
||||
All events are unique (incremental timestamps)
|
||||
All events are properly signed
|
||||
|
||||
Pre-populating database for read tests...
|
||||
Generating 50000 unique synthetic events (minimum 300 bytes each)...
|
||||
Generated 50000 events:
|
||||
Average content size: 314 bytes
|
||||
All events are unique (incremental timestamps)
|
||||
All events are properly signed
|
||||
|
||||
Mixed test completed: 25000 writes, 25000 reads in 24.426575006s
|
||||
Combined ops/sec: 2046.95
|
||||
Wiping database between tests...
|
||||
RunQueryTest (Badger)..
|
||||
|
||||
=== Query Test ===
|
||||
Generating 10000 unique synthetic events (minimum 300 bytes each)...
|
||||
Generated 10000 events:
|
||||
Average content size: 313 bytes
|
||||
All events are unique (incremental timestamps)
|
||||
All events are properly signed
|
||||
|
||||
Pre-populating database with 10000 events for query tests...
|
||||
Query test completed: 369377 queries in 1m0.005034278s
|
||||
Queries/sec: 6155.77
|
||||
Avg query latency: 1.850212ms
|
||||
P95 query latency: 7.621476ms
|
||||
P99 query latency: 11.610958ms
|
||||
Wiping database between tests...
|
||||
RunConcurrentQueryStoreTest (Badger)..
|
||||
|
||||
=== Concurrent Query/Store Test ===
|
||||
Generating 5000 unique synthetic events (minimum 300 bytes each)...
|
||||
Generated 5000 events:
|
||||
Average content size: 313 bytes
|
||||
All events are unique (incremental timestamps)
|
||||
All events are properly signed
|
||||
|
||||
Pre-populating database with 5000 events for concurrent query/store test...
|
||||
Generating 50000 unique synthetic events (minimum 300 bytes each)...
|
||||
Generated 50000 events:
|
||||
Average content size: 314 bytes
|
||||
All events are unique (incremental timestamps)
|
||||
All events are properly signed
|
||||
|
||||
Concurrent test completed: 310678 operations (260678 queries, 50000 writes) in 1m0.003278222s
|
||||
Operations/sec: 5177.68
|
||||
Avg latency: 1.513088ms
|
||||
Avg query latency: 1.495086ms
|
||||
Avg write latency: 1.606937ms
|
||||
P95 latency: 3.92433ms
|
||||
P99 latency: 6.216487ms
|
||||
|
||||
=== Badger benchmark completed ===
|
||||
|
||||
|
||||
================================================================================
|
||||
BENCHMARK REPORT
|
||||
================================================================================
|
||||
|
||||
Test: Peak Throughput
|
||||
Duration: 3.067785126s
|
||||
Total Events: 50000
|
||||
Events/sec: 16298.40
|
||||
Success Rate: 100.0%
|
||||
Concurrent Workers: 24
|
||||
Memory Used: 89 MB
|
||||
Avg Latency: 1.360569ms
|
||||
P90 Latency: 1.819407ms
|
||||
P95 Latency: 2.160818ms
|
||||
P99 Latency: 3.606363ms
|
||||
Bottom 10% Avg Latency: 746.704µs
|
||||
----------------------------------------
|
||||
|
||||
Test: Burst Pattern
|
||||
Duration: 8.128805288s
|
||||
Total Events: 50000
|
||||
Events/sec: 6150.97
|
||||
Success Rate: 100.0%
|
||||
Concurrent Workers: 24
|
||||
Memory Used: 203 MB
|
||||
Avg Latency: 1.411735ms
|
||||
P90 Latency: 1.9936ms
|
||||
P95 Latency: 2.29313ms
|
||||
P99 Latency: 3.168238ms
|
||||
Bottom 10% Avg Latency: 711.036µs
|
||||
----------------------------------------
|
||||
|
||||
Test: Mixed Read/Write
|
||||
Duration: 24.426575006s
|
||||
Total Events: 50000
|
||||
Events/sec: 2046.95
|
||||
Success Rate: 100.0%
|
||||
Concurrent Workers: 24
|
||||
Memory Used: 127 MB
|
||||
Avg Latency: 401.18µs
|
||||
P90 Latency: 826.125µs
|
||||
P95 Latency: 916.446µs
|
||||
P99 Latency: 1.122669ms
|
||||
Bottom 10% Avg Latency: 1.080638ms
|
||||
----------------------------------------
|
||||
|
||||
Test: Query Performance
|
||||
Duration: 1m0.005034278s
|
||||
Total Events: 369377
|
||||
Events/sec: 6155.77
|
||||
Success Rate: 100.0%
|
||||
Concurrent Workers: 24
|
||||
Memory Used: 106 MB
|
||||
Avg Latency: 1.850212ms
|
||||
P90 Latency: 5.767292ms
|
||||
P95 Latency: 7.621476ms
|
||||
P99 Latency: 11.610958ms
|
||||
Bottom 10% Avg Latency: 8.365982ms
|
||||
----------------------------------------
|
||||
|
||||
Test: Concurrent Query/Store
|
||||
Duration: 1m0.003278222s
|
||||
Total Events: 310678
|
||||
Events/sec: 5177.68
|
||||
Success Rate: 100.0%
|
||||
Concurrent Workers: 24
|
||||
Memory Used: 133 MB
|
||||
Avg Latency: 1.513088ms
|
||||
P90 Latency: 3.049471ms
|
||||
P95 Latency: 3.92433ms
|
||||
P99 Latency: 6.216487ms
|
||||
Bottom 10% Avg Latency: 4.456235ms
|
||||
----------------------------------------
|
||||
|
||||
Report saved to: /tmp/benchmark_rely-sqlite_8/benchmark_report.txt
|
||||
AsciiDoc report saved to: /tmp/benchmark_rely-sqlite_8/benchmark_report.adoc
|
||||
|
||||
RELAY_NAME: rely-sqlite
|
||||
RELAY_URL: ws://rely-sqlite:3334
|
||||
TEST_TIMESTAMP: 2025-11-26T07:37:28+00:00
|
||||
BENCHMARK_CONFIG:
|
||||
Events: 50000
|
||||
Workers: 24
|
||||
Duration: 60s
|
||||
197
cmd/benchmark/reports/run_20251126_073410/strfry_results.txt
Normal file
197
cmd/benchmark/reports/run_20251126_073410/strfry_results.txt
Normal file
@@ -0,0 +1,197 @@
|
||||
Starting Nostr Relay Benchmark (Badger Backend)
|
||||
Data Directory: /tmp/benchmark_strfry_8
|
||||
Events: 50000, Workers: 24, Duration: 1m0s
|
||||
1764143869786425ℹ️ migrating to version 1... /build/pkg/database/migrations.go:66
|
||||
1764143869786498ℹ️ migrating to version 2... /build/pkg/database/migrations.go:73
|
||||
1764143869786524ℹ️ migrating to version 3... /build/pkg/database/migrations.go:80
|
||||
1764143869786530ℹ️ cleaning up ephemeral events (kinds 20000-29999)... /build/pkg/database/migrations.go:294
|
||||
1764143869786539ℹ️ cleaned up 0 ephemeral events from database /build/pkg/database/migrations.go:339
|
||||
1764143869786552ℹ️ migrating to version 4... /build/pkg/database/migrations.go:87
|
||||
1764143869786556ℹ️ converting events to optimized inline storage (Reiser4 optimization)... /build/pkg/database/migrations.go:347
|
||||
1764143869786565ℹ️ found 0 events to convert (0 regular, 0 replaceable, 0 addressable) /build/pkg/database/migrations.go:436
|
||||
1764143869786570ℹ️ migration complete: converted 0 events to optimized inline storage, deleted 0 old keys /build/pkg/database/migrations.go:545
|
||||
1764143869786584ℹ️ migrating to version 5... /build/pkg/database/migrations.go:94
|
||||
1764143869786589ℹ️ re-encoding events with optimized tag binary format... /build/pkg/database/migrations.go:552
|
||||
1764143869786604ℹ️ found 0 events with e/p tags to re-encode /build/pkg/database/migrations.go:639
|
||||
1764143869786609ℹ️ no events need re-encoding /build/pkg/database/migrations.go:642
|
||||
|
||||
╔════════════════════════════════════════════════════════╗
|
||||
║ BADGER BACKEND BENCHMARK SUITE ║
|
||||
╚════════════════════════════════════════════════════════╝
|
||||
|
||||
=== Starting Badger benchmark ===
|
||||
RunPeakThroughputTest (Badger)..
|
||||
|
||||
=== Peak Throughput Test ===
|
||||
2025/11/26 07:57:49 INFO: Successfully loaded embedded libsecp256k1 v5.0.0 from /tmp/orly-libsecp256k1/libsecp256k1.so
|
||||
Events saved: 50000/50000 (100.0%), errors: 0
|
||||
Duration: 3.085029825s
|
||||
Events/sec: 16207.30
|
||||
Avg latency: 1.381579ms
|
||||
P90 latency: 1.865718ms
|
||||
P95 latency: 2.15555ms
|
||||
P99 latency: 3.097841ms
|
||||
Bottom 10% Avg latency: 760.474µs
|
||||
Wiping database between tests...
|
||||
RunBurstPatternTest (Badger)..
|
||||
|
||||
=== Burst Pattern Test ===
|
||||
Burst completed: 5000 events in 307.173651ms
|
||||
Burst completed: 5000 events in 334.907841ms
|
||||
Burst completed: 5000 events in 290.888159ms
|
||||
Burst completed: 5000 events in 403.807089ms
|
||||
Burst completed: 5000 events in 327.956144ms
|
||||
Burst completed: 5000 events in 364.629959ms
|
||||
Burst completed: 5000 events in 328.780115ms
|
||||
Burst completed: 5000 events in 290.361314ms
|
||||
Burst completed: 5000 events in 304.825415ms
|
||||
Burst completed: 5000 events in 270.287065ms
|
||||
Burst test completed: 50000 events in 8.230287366s, errors: 0
|
||||
Events/sec: 6075.12
|
||||
Wiping database between tests...
|
||||
RunMixedReadWriteTest (Badger)..
|
||||
|
||||
=== Mixed Read/Write Test ===
|
||||
Generating 1000 unique synthetic events (minimum 300 bytes each)...
|
||||
Generated 1000 events:
|
||||
Average content size: 312 bytes
|
||||
All events are unique (incremental timestamps)
|
||||
All events are properly signed
|
||||
|
||||
Pre-populating database for read tests...
|
||||
Generating 50000 unique synthetic events (minimum 300 bytes each)...
|
||||
Generated 50000 events:
|
||||
Average content size: 314 bytes
|
||||
All events are unique (incremental timestamps)
|
||||
All events are properly signed
|
||||
|
||||
Mixed test completed: 25000 writes, 25000 reads in 24.348961585s
|
||||
Combined ops/sec: 2053.48
|
||||
Wiping database between tests...
|
||||
RunQueryTest (Badger)..
|
||||
|
||||
=== Query Test ===
|
||||
Generating 10000 unique synthetic events (minimum 300 bytes each)...
|
||||
Generated 10000 events:
|
||||
Average content size: 313 bytes
|
||||
All events are unique (incremental timestamps)
|
||||
All events are properly signed
|
||||
|
||||
Pre-populating database with 10000 events for query tests...
|
||||
Query test completed: 376537 queries in 1m0.004019885s
|
||||
Queries/sec: 6275.20
|
||||
Avg query latency: 1.80891ms
|
||||
P95 query latency: 7.432319ms
|
||||
P99 query latency: 11.306037ms
|
||||
Wiping database between tests...
|
||||
RunConcurrentQueryStoreTest (Badger)..
|
||||
|
||||
=== Concurrent Query/Store Test ===
|
||||
Generating 5000 unique synthetic events (minimum 300 bytes each)...
|
||||
Generated 5000 events:
|
||||
Average content size: 313 bytes
|
||||
All events are unique (incremental timestamps)
|
||||
All events are properly signed
|
||||
|
||||
Pre-populating database with 5000 events for concurrent query/store test...
|
||||
Generating 50000 unique synthetic events (minimum 300 bytes each)...
|
||||
Generated 50000 events:
|
||||
Average content size: 314 bytes
|
||||
All events are unique (incremental timestamps)
|
||||
All events are properly signed
|
||||
|
||||
Concurrent test completed: 310473 operations (260473 queries, 50000 writes) in 1m0.003152564s
|
||||
Operations/sec: 5174.28
|
||||
Avg latency: 1.532065ms
|
||||
Avg query latency: 1.496816ms
|
||||
Avg write latency: 1.715689ms
|
||||
P95 latency: 3.943934ms
|
||||
P99 latency: 6.631879ms
|
||||
|
||||
=== Badger benchmark completed ===
|
||||
|
||||
|
||||
================================================================================
|
||||
BENCHMARK REPORT
|
||||
================================================================================
|
||||
|
||||
Test: Peak Throughput
|
||||
Duration: 3.085029825s
|
||||
Total Events: 50000
|
||||
Events/sec: 16207.30
|
||||
Success Rate: 100.0%
|
||||
Concurrent Workers: 24
|
||||
Memory Used: 129 MB
|
||||
Avg Latency: 1.381579ms
|
||||
P90 Latency: 1.865718ms
|
||||
P95 Latency: 2.15555ms
|
||||
P99 Latency: 3.097841ms
|
||||
Bottom 10% Avg Latency: 760.474µs
|
||||
----------------------------------------
|
||||
|
||||
Test: Burst Pattern
|
||||
Duration: 8.230287366s
|
||||
Total Events: 50000
|
||||
Events/sec: 6075.12
|
||||
Success Rate: 100.0%
|
||||
Concurrent Workers: 24
|
||||
Memory Used: 254 MB
|
||||
Avg Latency: 1.45496ms
|
||||
P90 Latency: 2.073563ms
|
||||
P95 Latency: 2.414222ms
|
||||
P99 Latency: 3.497151ms
|
||||
Bottom 10% Avg Latency: 681.141µs
|
||||
----------------------------------------
|
||||
|
||||
Test: Mixed Read/Write
|
||||
Duration: 24.348961585s
|
||||
Total Events: 50000
|
||||
Events/sec: 2053.48
|
||||
Success Rate: 100.0%
|
||||
Concurrent Workers: 24
|
||||
Memory Used: 175 MB
|
||||
Avg Latency: 394.928µs
|
||||
P90 Latency: 814.769µs
|
||||
P95 Latency: 907.647µs
|
||||
P99 Latency: 1.116704ms
|
||||
Bottom 10% Avg Latency: 1.044591ms
|
||||
----------------------------------------
|
||||
|
||||
Test: Query Performance
|
||||
Duration: 1m0.004019885s
|
||||
Total Events: 376537
|
||||
Events/sec: 6275.20
|
||||
Success Rate: 100.0%
|
||||
Concurrent Workers: 24
|
||||
Memory Used: 138 MB
|
||||
Avg Latency: 1.80891ms
|
||||
P90 Latency: 5.616736ms
|
||||
P95 Latency: 7.432319ms
|
||||
P99 Latency: 11.306037ms
|
||||
Bottom 10% Avg Latency: 8.164604ms
|
||||
----------------------------------------
|
||||
|
||||
Test: Concurrent Query/Store
|
||||
Duration: 1m0.003152564s
|
||||
Total Events: 310473
|
||||
Events/sec: 5174.28
|
||||
Success Rate: 100.0%
|
||||
Concurrent Workers: 24
|
||||
Memory Used: 147 MB
|
||||
Avg Latency: 1.532065ms
|
||||
P90 Latency: 3.05393ms
|
||||
P95 Latency: 3.943934ms
|
||||
P99 Latency: 6.631879ms
|
||||
Bottom 10% Avg Latency: 4.619007ms
|
||||
----------------------------------------
|
||||
|
||||
Report saved to: /tmp/benchmark_strfry_8/benchmark_report.txt
|
||||
AsciiDoc report saved to: /tmp/benchmark_strfry_8/benchmark_report.adoc
|
||||
|
||||
RELAY_NAME: strfry
|
||||
RELAY_URL: ws://strfry:8080
|
||||
TEST_TIMESTAMP: 2025-11-26T08:01:07+00:00
|
||||
BENCHMARK_CONFIG:
|
||||
Events: 50000
|
||||
Workers: 24
|
||||
Duration: 60s
|
||||
@@ -1,9 +1,44 @@
|
||||
#!/bin/bash
|
||||
|
||||
# Wrapper script to run the benchmark suite and automatically shut down when complete
|
||||
#
|
||||
# Usage:
|
||||
# ./run-benchmark.sh # Use disk-based storage (default)
|
||||
# ./run-benchmark.sh --ramdisk # Use /dev/shm ramdisk for maximum performance
|
||||
|
||||
set -e
|
||||
|
||||
# Parse command line arguments
|
||||
USE_RAMDISK=false
|
||||
for arg in "$@"; do
|
||||
case $arg in
|
||||
--ramdisk)
|
||||
USE_RAMDISK=true
|
||||
shift
|
||||
;;
|
||||
--help|-h)
|
||||
echo "Usage: $0 [OPTIONS]"
|
||||
echo ""
|
||||
echo "Options:"
|
||||
echo " --ramdisk Use /dev/shm ramdisk storage instead of disk"
|
||||
echo " This eliminates disk I/O bottlenecks for accurate"
|
||||
echo " relay performance measurement."
|
||||
echo " --help, -h Show this help message"
|
||||
echo ""
|
||||
echo "Requirements for --ramdisk:"
|
||||
echo " - /dev/shm must be available (tmpfs mount)"
|
||||
echo " - At least 8GB available in /dev/shm recommended"
|
||||
echo " - Increase size with: sudo mount -o remount,size=16G /dev/shm"
|
||||
exit 0
|
||||
;;
|
||||
*)
|
||||
echo "Unknown option: $arg"
|
||||
echo "Use --help for usage information"
|
||||
exit 1
|
||||
;;
|
||||
esac
|
||||
done
|
||||
|
||||
# Determine docker-compose command
|
||||
if docker compose version &> /dev/null 2>&1; then
|
||||
DOCKER_COMPOSE="docker compose"
|
||||
@@ -11,43 +46,107 @@ else
|
||||
DOCKER_COMPOSE="docker-compose"
|
||||
fi
|
||||
|
||||
# Clean old data directories (may be owned by root from Docker)
|
||||
if [ -d "data" ]; then
|
||||
echo "Cleaning old data directories..."
|
||||
if ! rm -rf data/ 2>/dev/null; then
|
||||
# If normal rm fails (permission denied), provide clear instructions
|
||||
echo ""
|
||||
echo "ERROR: Cannot remove data directories due to permission issues."
|
||||
echo "This happens because Docker creates files as root."
|
||||
echo ""
|
||||
echo "Please run one of the following to clean up:"
|
||||
echo " sudo rm -rf data/"
|
||||
echo " sudo chown -R \$(id -u):\$(id -g) data/ && rm -rf data/"
|
||||
echo ""
|
||||
echo "Then run this script again."
|
||||
# Set data directory and compose files based on mode
|
||||
if [ "$USE_RAMDISK" = true ]; then
|
||||
DATA_BASE="/dev/shm/benchmark"
|
||||
COMPOSE_FILES="-f docker-compose.yml -f docker-compose.ramdisk.yml"
|
||||
|
||||
echo "======================================================"
|
||||
echo " RAMDISK BENCHMARK MODE"
|
||||
echo "======================================================"
|
||||
|
||||
# Check /dev/shm availability
|
||||
if [ ! -d "/dev/shm" ]; then
|
||||
echo "ERROR: /dev/shm is not available on this system."
|
||||
echo "This benchmark requires a tmpfs-mounted /dev/shm for RAM-based storage."
|
||||
exit 1
|
||||
fi
|
||||
|
||||
# Check available space in /dev/shm (need at least 8GB for benchmarks)
|
||||
SHM_AVAILABLE_KB=$(df /dev/shm | tail -1 | awk '{print $4}')
|
||||
SHM_AVAILABLE_GB=$((SHM_AVAILABLE_KB / 1024 / 1024))
|
||||
echo " Storage location: ${DATA_BASE}"
|
||||
echo " Available RAM: ${SHM_AVAILABLE_GB}GB"
|
||||
echo " This eliminates disk I/O bottlenecks for accurate"
|
||||
echo " relay performance measurement."
|
||||
echo "======================================================"
|
||||
echo ""
|
||||
|
||||
if [ "$SHM_AVAILABLE_KB" -lt 8388608 ]; then
|
||||
echo "WARNING: Less than 8GB available in /dev/shm (${SHM_AVAILABLE_GB}GB available)"
|
||||
echo "Benchmarks may fail if databases grow too large."
|
||||
echo "Consider increasing tmpfs size: sudo mount -o remount,size=16G /dev/shm"
|
||||
echo ""
|
||||
read -p "Continue anyway? [y/N] " -n 1 -r
|
||||
echo
|
||||
if [[ ! $REPLY =~ ^[Yy]$ ]]; then
|
||||
exit 1
|
||||
fi
|
||||
fi
|
||||
else
|
||||
DATA_BASE="./data"
|
||||
COMPOSE_FILES="-f docker-compose.yml"
|
||||
|
||||
echo "======================================================"
|
||||
echo " DISK-BASED BENCHMARK MODE (default)"
|
||||
echo "======================================================"
|
||||
echo " Storage location: ${DATA_BASE}"
|
||||
echo " Tip: Use --ramdisk for faster benchmarks without"
|
||||
echo " disk I/O bottlenecks."
|
||||
echo "======================================================"
|
||||
echo ""
|
||||
fi
|
||||
|
||||
# Clean old data directories (may be owned by root from Docker)
|
||||
if [ -d "${DATA_BASE}" ]; then
|
||||
echo "Cleaning old data directories at ${DATA_BASE}..."
|
||||
if ! rm -rf "${DATA_BASE}" 2>/dev/null; then
|
||||
# If normal rm fails (permission denied), try with sudo for ramdisk
|
||||
if [ "$USE_RAMDISK" = true ]; then
|
||||
echo "Need elevated permissions to clean ramdisk..."
|
||||
if ! sudo rm -rf "${DATA_BASE}" 2>/dev/null; then
|
||||
echo ""
|
||||
echo "ERROR: Cannot remove data directories."
|
||||
echo "Please run: sudo rm -rf ${DATA_BASE}"
|
||||
echo "Then run this script again."
|
||||
exit 1
|
||||
fi
|
||||
else
|
||||
# Provide clear instructions for disk-based mode
|
||||
echo ""
|
||||
echo "ERROR: Cannot remove data directories due to permission issues."
|
||||
echo "This happens because Docker creates files as root."
|
||||
echo ""
|
||||
echo "Please run one of the following to clean up:"
|
||||
echo " sudo rm -rf ${DATA_BASE}/"
|
||||
echo " sudo chown -R \$(id -u):\$(id -g) ${DATA_BASE}/ && rm -rf ${DATA_BASE}/"
|
||||
echo ""
|
||||
echo "Then run this script again."
|
||||
exit 1
|
||||
fi
|
||||
fi
|
||||
fi
|
||||
|
||||
# Stop any running containers from previous runs
|
||||
echo "Stopping any running containers..."
|
||||
$DOCKER_COMPOSE down 2>/dev/null || true
|
||||
$DOCKER_COMPOSE $COMPOSE_FILES down 2>/dev/null || true
|
||||
|
||||
# Create fresh data directories with correct permissions
|
||||
echo "Preparing data directories..."
|
||||
echo "Preparing data directories at ${DATA_BASE}..."
|
||||
|
||||
# Clean Neo4j data to prevent "already running" errors
|
||||
if [ -d "data/neo4j" ]; then
|
||||
echo "Cleaning Neo4j data directory..."
|
||||
rm -rf data/neo4j/*
|
||||
if [ "$USE_RAMDISK" = true ]; then
|
||||
# Create ramdisk directories
|
||||
mkdir -p "${DATA_BASE}"/{next-orly-badger,next-orly-dgraph,next-orly-neo4j,dgraph-zero,dgraph-alpha,neo4j,neo4j-logs,khatru-sqlite,khatru-badger,relayer-basic,strfry,nostr-rs-relay,rely-sqlite,postgres}
|
||||
chmod 777 "${DATA_BASE}"/{next-orly-badger,next-orly-dgraph,next-orly-neo4j,dgraph-zero,dgraph-alpha,neo4j,neo4j-logs,khatru-sqlite,khatru-badger,relayer-basic,strfry,nostr-rs-relay,rely-sqlite,postgres}
|
||||
else
|
||||
# Create disk directories (relative path)
|
||||
mkdir -p data/{next-orly-badger,next-orly-dgraph,next-orly-neo4j,dgraph-zero,dgraph-alpha,neo4j,neo4j-logs,khatru-sqlite,khatru-badger,relayer-basic,strfry,nostr-rs-relay,rely-sqlite,postgres}
|
||||
chmod 777 data/{next-orly-badger,next-orly-dgraph,next-orly-neo4j,dgraph-zero,dgraph-alpha,neo4j,neo4j-logs,khatru-sqlite,khatru-badger,relayer-basic,strfry,nostr-rs-relay,rely-sqlite,postgres}
|
||||
fi
|
||||
|
||||
mkdir -p data/{next-orly-badger,next-orly-dgraph,next-orly-neo4j,dgraph-zero,dgraph-alpha,neo4j,neo4j-logs,khatru-sqlite,khatru-badger,relayer-basic,strfry,nostr-rs-relay,rely-sqlite,postgres}
|
||||
chmod 777 data/{next-orly-badger,next-orly-dgraph,next-orly-neo4j,dgraph-zero,dgraph-alpha,neo4j,neo4j-logs,khatru-sqlite,khatru-badger,relayer-basic,strfry,nostr-rs-relay,rely-sqlite,postgres}
|
||||
|
||||
echo "Building fresh Docker images..."
|
||||
# Force rebuild to pick up latest code changes
|
||||
$DOCKER_COMPOSE build --no-cache benchmark-runner next-orly-badger next-orly-dgraph next-orly-neo4j rely-sqlite
|
||||
$DOCKER_COMPOSE $COMPOSE_FILES build --no-cache benchmark-runner next-orly-badger next-orly-dgraph next-orly-neo4j rely-sqlite
|
||||
|
||||
echo ""
|
||||
echo "Starting benchmark suite..."
|
||||
@@ -55,7 +154,22 @@ echo "This will automatically shut down all containers when the benchmark comple
|
||||
echo ""
|
||||
|
||||
# Run docker compose with flags to exit when benchmark-runner completes
|
||||
$DOCKER_COMPOSE up --exit-code-from benchmark-runner --abort-on-container-exit
|
||||
$DOCKER_COMPOSE $COMPOSE_FILES up --exit-code-from benchmark-runner --abort-on-container-exit
|
||||
|
||||
# Cleanup function
|
||||
cleanup() {
|
||||
echo ""
|
||||
echo "Cleaning up..."
|
||||
$DOCKER_COMPOSE $COMPOSE_FILES down 2>/dev/null || true
|
||||
|
||||
if [ "$USE_RAMDISK" = true ]; then
|
||||
echo "Cleaning ramdisk data at ${DATA_BASE}..."
|
||||
rm -rf "${DATA_BASE}" 2>/dev/null || sudo rm -rf "${DATA_BASE}" 2>/dev/null || true
|
||||
fi
|
||||
}
|
||||
|
||||
# Register cleanup on script exit
|
||||
trap cleanup EXIT
|
||||
|
||||
echo ""
|
||||
echo "Benchmark suite has completed and all containers have been stopped."
|
||||
|
||||
@@ -36,12 +36,12 @@ var (
|
||||
|
||||
// BlossomDescriptor represents a blob descriptor returned by the server
|
||||
type BlossomDescriptor struct {
|
||||
URL string `json:"url"`
|
||||
SHA256 string `json:"sha256"`
|
||||
Size int64 `json:"size"`
|
||||
Type string `json:"type,omitempty"`
|
||||
Uploaded int64 `json:"uploaded"`
|
||||
PublicKey string `json:"public_key,omitempty"`
|
||||
URL string `json:"url"`
|
||||
SHA256 string `json:"sha256"`
|
||||
Size int64 `json:"size"`
|
||||
Type string `json:"type,omitempty"`
|
||||
Uploaded int64 `json:"uploaded"`
|
||||
PublicKey string `json:"public_key,omitempty"`
|
||||
Tags [][]string `json:"tags,omitempty"`
|
||||
}
|
||||
|
||||
@@ -49,7 +49,7 @@ func main() {
|
||||
flag.Parse()
|
||||
|
||||
fmt.Println("🌸 Blossom Test Tool")
|
||||
fmt.Println("===================\n")
|
||||
fmt.Println("===================")
|
||||
|
||||
// Get or generate keypair (only if auth is enabled)
|
||||
var sec, pub []byte
|
||||
|
||||
@@ -1,54 +1,50 @@
|
||||
# Dockerfile for Stella's Nostr Relay (next.orly.dev)
|
||||
# Owner: npub1v30tsz9vw6ylpz63g0a702nj3xa26t3m7p5us8f2y2sd8v6cnsvq465zjx
|
||||
#
|
||||
# Build from repository root:
|
||||
# docker build -f contrib/stella/Dockerfile -t stella-relay .
|
||||
|
||||
FROM golang:alpine AS builder
|
||||
# Use Debian-based Go image to match runtime stage (avoids musl/glibc linker mismatch)
|
||||
FROM golang:1.25-bookworm AS builder
|
||||
|
||||
# Install build dependencies
|
||||
RUN apk add --no-cache \
|
||||
git \
|
||||
build-base \
|
||||
autoconf \
|
||||
automake \
|
||||
libtool \
|
||||
pkgconfig
|
||||
|
||||
# Install secp256k1 library from Alpine packages
|
||||
RUN apk add --no-cache libsecp256k1-dev
|
||||
RUN apt-get update && apt-get install -y --no-install-recommends git make && rm -rf /var/lib/apt/lists/*
|
||||
|
||||
# Set working directory
|
||||
WORKDIR /build
|
||||
|
||||
# Copy go modules first (for better caching)
|
||||
COPY ../../go.mod go.sum ./
|
||||
COPY go.mod go.sum ./
|
||||
RUN go mod download
|
||||
|
||||
# Copy source code
|
||||
COPY ../.. .
|
||||
COPY . .
|
||||
|
||||
# Build the relay with optimizations from v0.4.8
|
||||
RUN CGO_ENABLED=1 GOOS=linux go build -ldflags "-w -s" -o relay .
|
||||
# Build the relay with CGO disabled (uses purego for crypto)
|
||||
RUN CGO_ENABLED=0 GOOS=linux GOARCH=amd64 go build -ldflags "-w -s" -o relay .
|
||||
|
||||
# Create non-root user for security
|
||||
RUN adduser -D -u 1000 stella && \
|
||||
RUN useradd -m -u 1000 stella && \
|
||||
chown -R 1000:1000 /build
|
||||
|
||||
# Final stage - minimal runtime image
|
||||
FROM alpine:latest
|
||||
# Use Debian slim instead of Alpine because Debian's libsecp256k1 includes
|
||||
# Schnorr signatures (secp256k1_schnorrsig_*) and ECDH which Nostr requires.
|
||||
# Alpine's libsecp256k1 is built without these modules.
|
||||
FROM debian:bookworm-slim
|
||||
|
||||
# Install only runtime dependencies
|
||||
RUN apk add --no-cache \
|
||||
ca-certificates \
|
||||
curl \
|
||||
libsecp256k1 \
|
||||
libsecp256k1-dev
|
||||
# Install runtime dependencies
|
||||
RUN apt-get update && \
|
||||
apt-get install -y --no-install-recommends ca-certificates curl libsecp256k1-1 && \
|
||||
rm -rf /var/lib/apt/lists/*
|
||||
|
||||
WORKDIR /app
|
||||
|
||||
# Copy binary from builder
|
||||
# Copy binary (libsecp256k1.so.1 is already installed via apt)
|
||||
COPY --from=builder /build/relay /app/relay
|
||||
|
||||
# Create runtime user and directories
|
||||
RUN adduser -D -u 1000 stella && \
|
||||
RUN useradd -m -u 1000 stella && \
|
||||
mkdir -p /data /profiles /app && \
|
||||
chown -R 1000:1000 /data /profiles /app
|
||||
|
||||
|
||||
@@ -283,15 +283,13 @@ Dockerfiles simplified:
|
||||
FROM golang:1.25-alpine AS builder
|
||||
WORKDIR /build
|
||||
COPY . .
|
||||
RUN go build -ldflags "-s -w" -o orly .
|
||||
RUN CGO_ENABLED=0 go build -ldflags "-s -w" -o orly .
|
||||
|
||||
# Runtime can optionally include library
|
||||
# Runtime includes libsecp256k1.so from repository
|
||||
FROM alpine:latest
|
||||
RUN apk add --no-cache wget ca-certificates
|
||||
RUN apk add --no-cache ca-certificates
|
||||
COPY --from=builder /build/orly /app/orly
|
||||
# Download libsecp256k1.so from nostr repository (optional for performance)
|
||||
RUN wget -q https://git.mleku.dev/mleku/nostr/raw/branch/main/crypto/p8k/libsecp256k1.so \
|
||||
-O /app/libsecp256k1.so || echo "Warning: libsecp256k1.so download failed (optional)"
|
||||
COPY --from=builder /build/libsecp256k1.so /app/libsecp256k1.so
|
||||
ENV LD_LIBRARY_PATH=/app
|
||||
CMD ["/app/orly"]
|
||||
```
|
||||
|
||||
18
go.mod
18
go.mod
@@ -3,27 +3,23 @@ module next.orly.dev
|
||||
go 1.25.3
|
||||
|
||||
require (
|
||||
git.mleku.dev/mleku/nostr v1.0.2
|
||||
git.mleku.dev/mleku/nostr v1.0.3
|
||||
github.com/adrg/xdg v0.5.3
|
||||
github.com/davecgh/go-spew v1.1.1
|
||||
github.com/dgraph-io/badger/v4 v4.8.0
|
||||
github.com/dgraph-io/dgo/v230 v230.0.1
|
||||
github.com/ebitengine/purego v0.9.1
|
||||
github.com/gorilla/websocket v1.5.3
|
||||
github.com/kardianos/osext v0.0.0-20190222173326-2bc1f35cddc0
|
||||
github.com/klauspost/compress v1.18.1
|
||||
github.com/minio/sha256-simd v1.0.1
|
||||
github.com/nbd-wtf/go-nostr v0.52.0
|
||||
github.com/neo4j/neo4j-go-driver/v5 v5.28.4
|
||||
github.com/pkg/profile v1.7.0
|
||||
github.com/puzpuzpuz/xsync/v3 v3.5.1
|
||||
github.com/stretchr/testify v1.11.1
|
||||
github.com/templexxx/xhex v0.0.0-20200614015412-aed53437177b
|
||||
github.com/vertex-lab/nostr-sqlite v0.3.2
|
||||
go-simpler.org/env v0.12.0
|
||||
go.uber.org/atomic v1.11.0
|
||||
golang.org/x/crypto v0.45.0
|
||||
golang.org/x/exp v0.0.0-20251113190631-e25ba8c21ef6
|
||||
golang.org/x/lint v0.0.0-20241112194109-818c5a804067
|
||||
golang.org/x/net v0.47.0
|
||||
google.golang.org/grpc v1.76.0
|
||||
honnef.co/go/tools v0.6.1
|
||||
lol.mleku.dev v1.0.5
|
||||
@@ -40,10 +36,12 @@ require (
|
||||
github.com/cespare/xxhash/v2 v2.3.0 // indirect
|
||||
github.com/cloudwego/base64x v0.1.5 // indirect
|
||||
github.com/coder/websocket v1.8.12 // indirect
|
||||
github.com/davecgh/go-spew v1.1.1 // indirect
|
||||
github.com/decred/dcrd/crypto/blake256 v1.1.0 // indirect
|
||||
github.com/decred/dcrd/dcrec/secp256k1/v4 v4.4.0 // indirect
|
||||
github.com/dgraph-io/ristretto/v2 v2.3.0 // indirect
|
||||
github.com/dustin/go-humanize v1.0.1 // indirect
|
||||
github.com/ebitengine/purego v0.9.1 // indirect
|
||||
github.com/felixge/fgprof v0.9.5 // indirect
|
||||
github.com/go-logr/logr v1.4.3 // indirect
|
||||
github.com/go-logr/stdr v1.2.2 // indirect
|
||||
@@ -58,22 +56,24 @@ require (
|
||||
github.com/mattn/go-sqlite3 v1.14.32 // indirect
|
||||
github.com/modern-go/concurrent v0.0.0-20180306012644-bacd9c7ef1dd // indirect
|
||||
github.com/modern-go/reflect2 v1.0.2 // indirect
|
||||
github.com/nbd-wtf/go-nostr v0.52.0 // indirect
|
||||
github.com/pkg/errors v0.9.1 // indirect
|
||||
github.com/pmezard/go-difflib v1.0.0 // indirect
|
||||
github.com/puzpuzpuz/xsync/v3 v3.5.1 // indirect
|
||||
github.com/templexxx/cpu v0.1.1 // indirect
|
||||
github.com/templexxx/xhex v0.0.0-20200614015412-aed53437177b // indirect
|
||||
github.com/tidwall/gjson v1.18.0 // indirect
|
||||
github.com/tidwall/match v1.1.1 // indirect
|
||||
github.com/tidwall/pretty v1.2.1 // indirect
|
||||
github.com/twitchyliquid64/golang-asm v0.15.1 // indirect
|
||||
github.com/vertex-lab/nostr-sqlite v0.3.2 // indirect
|
||||
go.opentelemetry.io/auto/sdk v1.2.1 // indirect
|
||||
go.opentelemetry.io/otel v1.38.0 // indirect
|
||||
go.opentelemetry.io/otel/metric v1.38.0 // indirect
|
||||
go.opentelemetry.io/otel/trace v1.38.0 // indirect
|
||||
golang.org/x/arch v0.15.0 // indirect
|
||||
golang.org/x/exp v0.0.0-20251113190631-e25ba8c21ef6 // indirect
|
||||
golang.org/x/exp/typeparams v0.0.0-20251023183803-a4bb9ffd2546 // indirect
|
||||
golang.org/x/mod v0.30.0 // indirect
|
||||
golang.org/x/net v0.47.0 // indirect
|
||||
golang.org/x/sync v0.18.0 // indirect
|
||||
golang.org/x/sys v0.38.0 // indirect
|
||||
golang.org/x/text v0.31.0 // indirect
|
||||
|
||||
26
go.sum
26
go.sum
@@ -1,6 +1,6 @@
|
||||
cloud.google.com/go v0.26.0/go.mod h1:aQUYkXzVsufM+DwF1aE+0xfcU+56JwCaLick0ClmMTw=
|
||||
git.mleku.dev/mleku/nostr v1.0.2 h1:SbCUoja9baTOEybQdtTkUcJWWNMAMsVzI/OXh+ZuSKw=
|
||||
git.mleku.dev/mleku/nostr v1.0.2/go.mod h1:swI7bWLc7yU1jd7PLCCIrIcUR3Ug5O+GPvpub/w6eTY=
|
||||
git.mleku.dev/mleku/nostr v1.0.3 h1:dWpGVzIOrjeWVnDnrX039s2LvcfHwDIo47NyyO1CBbs=
|
||||
git.mleku.dev/mleku/nostr v1.0.3/go.mod h1:swI7bWLc7yU1jd7PLCCIrIcUR3Ug5O+GPvpub/w6eTY=
|
||||
github.com/BurntSushi/toml v0.3.1/go.mod h1:xHWCNGjB5oqiDr8zfno3MHue2Ht5sIBksp03qcyfWMU=
|
||||
github.com/BurntSushi/toml v1.5.0 h1:W5quZX/G/csjUnuI8SUYlsHs9M38FC7znL0lIO+DvMg=
|
||||
github.com/BurntSushi/toml v1.5.0/go.mod h1:ukJfTF/6rtPPRCnwkur4qwRxa8vTRFBF0uk2lLoLwho=
|
||||
@@ -8,7 +8,6 @@ github.com/ImVexed/fasturl v0.0.0-20230304231329-4e41488060f3 h1:ClzzXMDDuUbWfNN
|
||||
github.com/ImVexed/fasturl v0.0.0-20230304231329-4e41488060f3/go.mod h1:we0YA5CsBbH5+/NUzC/AlMmxaDtWlXeNsqrwXjTzmzA=
|
||||
github.com/adrg/xdg v0.5.3 h1:xRnxJXne7+oWDatRhR1JLnvuccuIeCoBu2rtuLqQB78=
|
||||
github.com/adrg/xdg v0.5.3/go.mod h1:nlTsY+NNiCBGCK2tpm09vRqfVzrc2fLmXGpBLF0zlTQ=
|
||||
github.com/btcsuite/btcd v0.24.2 h1:aLmxPguqxza+4ag8R1I2nnJjSu2iFn/kqtHTIImswcY=
|
||||
github.com/btcsuite/btcd/btcec/v2 v2.3.4 h1:3EJjcN70HCu/mwqlUsGK8GcNVyLVxFDlWurTXGPFfiQ=
|
||||
github.com/btcsuite/btcd/btcec/v2 v2.3.4/go.mod h1:zYzJ8etWJQIv1Ogk7OzpWjowwOdXY1W/17j2MW85J04=
|
||||
github.com/btcsuite/btcd/chaincfg/chainhash v1.1.0 h1:59Kx4K6lzOW5w6nFlA0v5+lk/6sjybR934QNHSJZPTQ=
|
||||
@@ -138,8 +137,6 @@ github.com/nbd-wtf/go-nostr v0.52.0/go.mod h1:4avYoc9mDGZ9wHsvCOhHH9vPzKucCfuYBt
|
||||
github.com/neo4j/neo4j-go-driver/v5 v5.28.4 h1:7toxehVcYkZbyxV4W3Ib9VcnyRBQPucF+VwNNmtSXi4=
|
||||
github.com/neo4j/neo4j-go-driver/v5 v5.28.4/go.mod h1:Vff8OwT7QpLm7L2yYr85XNWe9Rbqlbeb9asNXJTHO4k=
|
||||
github.com/orisano/pixelmatch v0.0.0-20220722002657-fb0b55479cde/go.mod h1:nZgzbfBr3hhjoZnS66nKrHmduYNpc34ny7RK4z5/HM0=
|
||||
github.com/pkg/errors v0.8.1 h1:iURUrRGxPUNPdy5/HRSm+Yj6okJ6UtLINN0Q9M4+h3I=
|
||||
github.com/pkg/errors v0.8.1/go.mod h1:bwawxfHBFNV+L2hUp1rHADufV3IMtnDRdf1r5NINEl0=
|
||||
github.com/pkg/errors v0.9.1 h1:FEBLx1zS214owpjy7qsBeixbURkuhQAwrK5UwLGTwt4=
|
||||
github.com/pkg/errors v0.9.1/go.mod h1:bwawxfHBFNV+L2hUp1rHADufV3IMtnDRdf1r5NINEl0=
|
||||
github.com/pkg/profile v1.7.0 h1:hnbDkaNWPCLMO9wGLdBFTIZvzDrDfBM2072E1S9gJkA=
|
||||
@@ -201,13 +198,9 @@ golang.org/x/arch v0.15.0/go.mod h1:JmwW7aLIoRUKgaTzhkiEFxvcEiQGyOg9BMonBJUS7EE=
|
||||
golang.org/x/crypto v0.0.0-20190308221718-c2843e01d9a2/go.mod h1:djNgcEr1/C05ACkg1iLfiJU5Ep61QUkGW8qpdssI0+w=
|
||||
golang.org/x/crypto v0.0.0-20191011191535-87dc89f01550/go.mod h1:yigFU9vqHzYiE8UmvKecakEJjdnWj3jj499lnFckfCI=
|
||||
golang.org/x/crypto v0.0.0-20200622213623-75b288015ac9/go.mod h1:LzIPMQfyMNhhGPhUkYOs5KpL4U8rLKemX1yGLhDgUto=
|
||||
golang.org/x/crypto v0.43.0 h1:dduJYIi3A3KOfdGOHX8AVZ/jGiyPa3IbBozJ5kNuE04=
|
||||
golang.org/x/crypto v0.43.0/go.mod h1:BFbav4mRNlXJL4wNeejLpWxB7wMbc79PdRGhWKncxR0=
|
||||
golang.org/x/crypto v0.45.0 h1:jMBrvKuj23MTlT0bQEOBcAE0mjg8mK9RXFhRH6nyF3Q=
|
||||
golang.org/x/crypto v0.45.0/go.mod h1:XTGrrkGJve7CYK7J8PEww4aY7gM3qMCElcJQ8n8JdX4=
|
||||
golang.org/x/exp v0.0.0-20190121172915-509febef88a4/go.mod h1:CJ0aWSM057203Lf6IL+f9T1iT9GByDxfZKAQTCR3kQA=
|
||||
golang.org/x/exp v0.0.0-20251023183803-a4bb9ffd2546 h1:mgKeJMpvi0yx/sU5GsxQ7p6s2wtOnGAHZWCHUM4KGzY=
|
||||
golang.org/x/exp v0.0.0-20251023183803-a4bb9ffd2546/go.mod h1:j/pmGrbnkbPtQfxEe5D0VQhZC6qKbfKifgD0oM7sR70=
|
||||
golang.org/x/exp v0.0.0-20251113190631-e25ba8c21ef6 h1:zfMcR1Cs4KNuomFFgGefv5N0czO2XZpUbxGUy8i8ug0=
|
||||
golang.org/x/exp v0.0.0-20251113190631-e25ba8c21ef6/go.mod h1:46edojNIoXTNOhySWIWdix628clX9ODXwPsQuG6hsK0=
|
||||
golang.org/x/exp/typeparams v0.0.0-20251023183803-a4bb9ffd2546 h1:HDjDiATsGqvuqvkDvgJjD1IgPrVekcSXVVE21JwvzGE=
|
||||
@@ -220,8 +213,7 @@ golang.org/x/lint v0.0.0-20241112194109-818c5a804067/go.mod h1:3xt1FjdF8hUf6vQPI
|
||||
golang.org/x/mod v0.1.1-0.20191105210325-c90efee705ee/go.mod h1:QqPTAvyqsEbceGzBzNggFXnrqF1CaUcvgkdR5Ot7KZg=
|
||||
golang.org/x/mod v0.2.0/go.mod h1:s0Qsj1ACt9ePp/hMypM3fl4fZqREWJwdYDEqhRiZZUA=
|
||||
golang.org/x/mod v0.3.0/go.mod h1:s0Qsj1ACt9ePp/hMypM3fl4fZqREWJwdYDEqhRiZZUA=
|
||||
golang.org/x/mod v0.29.0 h1:HV8lRxZC4l2cr3Zq1LvtOsi/ThTgWnUk/y64QSs8GwA=
|
||||
golang.org/x/mod v0.29.0/go.mod h1:NyhrlYXJ2H4eJiRy/WDBO6HMqZQ6q9nk4JzS3NuCK+w=
|
||||
golang.org/x/mod v0.30.0 h1:fDEXFVZ/fmCKProc/yAXXUijritrDzahmwwefnjoPFk=
|
||||
golang.org/x/mod v0.30.0/go.mod h1:lAsf5O2EvJeSFMiBxXDki7sCgAxEUcZHXoXMKT4GJKc=
|
||||
golang.org/x/net v0.0.0-20180724234803-3673e40ba225/go.mod h1:mL1N/T3taQHkDXs73rZJwtUhF3w3ftmwwsq0BUmARs4=
|
||||
golang.org/x/net v0.0.0-20180826012351-8a410e7b638d/go.mod h1:mL1N/T3taQHkDXs73rZJwtUhF3w3ftmwwsq0BUmARs4=
|
||||
@@ -231,8 +223,6 @@ golang.org/x/net v0.0.0-20190404232315-eb5bcb51f2a3/go.mod h1:t9HGtf8HONx5eT2rtn
|
||||
golang.org/x/net v0.0.0-20190620200207-3b0461eec859/go.mod h1:z5CRVTTTmAJ677TzLLGU+0bjPO0LkuOLi4/5GtJWs/s=
|
||||
golang.org/x/net v0.0.0-20200226121028-0de0cce0169b/go.mod h1:z5CRVTTTmAJ677TzLLGU+0bjPO0LkuOLi4/5GtJWs/s=
|
||||
golang.org/x/net v0.0.0-20201021035429-f5854403a974/go.mod h1:sp8m0HH+o8qH0wwXwYZr8TS3Oi6o0r6Gce1SSxlDquU=
|
||||
golang.org/x/net v0.46.0 h1:giFlY12I07fugqwPuWJi68oOnpfqFnJIJzaIIm2JVV4=
|
||||
golang.org/x/net v0.46.0/go.mod h1:Q9BGdFy1y4nkUwiLvT5qtyhAnEHgnQ/zd8PfU6nc210=
|
||||
golang.org/x/net v0.47.0 h1:Mx+4dIFzqraBXUugkia1OOvlD6LemFo1ALMHjrXDOhY=
|
||||
golang.org/x/net v0.47.0/go.mod h1:/jNxtkgq5yWUGYkaZGqo27cfGZ1c5Nen03aYrrKpVRU=
|
||||
golang.org/x/oauth2 v0.0.0-20180821212333-d2e6202438be/go.mod h1:N/0e6XlmueqKjAGxoOufVs8QHGRruUQn6yWY3a++T0U=
|
||||
@@ -241,8 +231,7 @@ golang.org/x/sync v0.0.0-20181108010431-42b317875d0f/go.mod h1:RxMgew5VJxzue5/jJ
|
||||
golang.org/x/sync v0.0.0-20190423024810-112230192c58/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM=
|
||||
golang.org/x/sync v0.0.0-20190911185100-cd5d95a43a6e/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM=
|
||||
golang.org/x/sync v0.0.0-20201020160332-67f06af15bc9/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM=
|
||||
golang.org/x/sync v0.17.0 h1:l60nONMj9l5drqw6jlhIELNv9I0A4OFgRsG9k2oT9Ug=
|
||||
golang.org/x/sync v0.17.0/go.mod h1:9KTHXmSnoGruLpwFjVSX0lNNA75CykiMECbovNTZqGI=
|
||||
golang.org/x/sync v0.18.0 h1:kr88TuHDroi+UVf+0hZnirlk8o8T+4MrK6mr60WkH/I=
|
||||
golang.org/x/sync v0.18.0/go.mod h1:9KTHXmSnoGruLpwFjVSX0lNNA75CykiMECbovNTZqGI=
|
||||
golang.org/x/sys v0.0.0-20180830151530-49385e6e1522/go.mod h1:STP8DvDyc/dI5b8T5hshtkjS+E42TnysNCUPdjciGhY=
|
||||
golang.org/x/sys v0.0.0-20190215142949-d0b11bdaac8a/go.mod h1:STP8DvDyc/dI5b8T5hshtkjS+E42TnysNCUPdjciGhY=
|
||||
@@ -251,14 +240,10 @@ golang.org/x/sys v0.0.0-20200930185726-fdedc70b468f/go.mod h1:h1NjWce9XRLGQEsW7w
|
||||
golang.org/x/sys v0.0.0-20211007075335-d3039528d8ac/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=
|
||||
golang.org/x/sys v0.0.0-20220310020820-b874c991c1a5/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=
|
||||
golang.org/x/sys v0.6.0/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=
|
||||
golang.org/x/sys v0.37.0 h1:fdNQudmxPjkdUTPnLn5mdQv7Zwvbvpaxqs831goi9kQ=
|
||||
golang.org/x/sys v0.37.0/go.mod h1:OgkHotnGiDImocRcuBABYBEXf8A9a87e/uXjp9XT3ks=
|
||||
golang.org/x/sys v0.38.0 h1:3yZWxaJjBmCWXqhN1qh02AkOnCQ1poK6oF+a7xWL6Gc=
|
||||
golang.org/x/sys v0.38.0/go.mod h1:OgkHotnGiDImocRcuBABYBEXf8A9a87e/uXjp9XT3ks=
|
||||
golang.org/x/text v0.3.0/go.mod h1:NqM8EUOU14njkJ3fqMW+pc6Ldnwhi/IjpwHt7yyuwOQ=
|
||||
golang.org/x/text v0.3.3/go.mod h1:5Zoc/QRtKVWzQhOtBMvqHzDpF6irO9z98xDceosuGiQ=
|
||||
golang.org/x/text v0.30.0 h1:yznKA/E9zq54KzlzBEAWn1NXSQ8DIp/NYMy88xJjl4k=
|
||||
golang.org/x/text v0.30.0/go.mod h1:yDdHFIX9t+tORqspjENWgzaCVXgk0yYnYuSZ8UzzBVM=
|
||||
golang.org/x/text v0.31.0 h1:aC8ghyu4JhP8VojJ2lEHBnochRno1sgL6nEi9WGFGMM=
|
||||
golang.org/x/text v0.31.0/go.mod h1:tKRAlv61yKIjGGHX/4tP1LTbc13YSec1pxVEWXzfoeM=
|
||||
golang.org/x/tools v0.0.0-20180917221912-90fa682c2a6e/go.mod h1:n7NCudcB/nEzxVGmLbDWY5pfWTLqBcC2KZ6jyYvM4mQ=
|
||||
@@ -270,8 +255,7 @@ golang.org/x/tools v0.0.0-20191119224855-298f0cb1881e/go.mod h1:b+2E5dAYhXwXZwtn
|
||||
golang.org/x/tools v0.0.0-20200130002326-2f3ba24bd6e7/go.mod h1:TB2adYChydJhpapKDTa4BR/hXlZSLoq2Wpct/0txZ28=
|
||||
golang.org/x/tools v0.0.0-20200619180055-7c47624df98f/go.mod h1:EkVYQZoAsY45+roYkvgYkIh4xh/qjgUK9TdY2XT94GE=
|
||||
golang.org/x/tools v0.0.0-20210106214847-113979e3529a/go.mod h1:emZCQorbCU4vsT4fOWvOPXz4eW1wZW4PmDk9uLelYpA=
|
||||
golang.org/x/tools v0.38.0 h1:Hx2Xv8hISq8Lm16jvBZ2VQf+RLmbd7wVUsALibYI/IQ=
|
||||
golang.org/x/tools v0.38.0/go.mod h1:yEsQ/d/YK8cjh0L6rZlY8tgtlKiBNTL14pGDJPJpYQs=
|
||||
golang.org/x/tools v0.39.0 h1:ik4ho21kwuQln40uelmciQPp9SipgNDdrafrYA4TmQQ=
|
||||
golang.org/x/tools v0.39.0/go.mod h1:JnefbkDPyD8UU2kI5fuf8ZX4/yUeh9W877ZeBONxUqQ=
|
||||
golang.org/x/tools/go/expect v0.1.1-deprecated h1:jpBZDwmgPhXsKZC6WhL20P4b/wmnpsEAGHaNy0n/rJM=
|
||||
golang.org/x/tools/go/expect v0.1.1-deprecated/go.mod h1:eihoPOH+FgIqa3FpoTwguz/bVUSGBlGQU67vpBeOrBY=
|
||||
|
||||
28
main.go
28
main.go
@@ -62,6 +62,34 @@ func main() {
|
||||
os.Exit(0)
|
||||
}
|
||||
|
||||
// Handle 'serve' subcommand: start ephemeral relay with RAM-based storage
|
||||
if config.ServeRequested() {
|
||||
const serveDataDir = "/dev/shm/orlyserve"
|
||||
log.I.F("serve mode: configuring ephemeral relay at %s", serveDataDir)
|
||||
|
||||
// Delete existing directory completely
|
||||
if err = os.RemoveAll(serveDataDir); err != nil && !os.IsNotExist(err) {
|
||||
log.E.F("failed to remove existing serve directory: %v", err)
|
||||
os.Exit(1)
|
||||
}
|
||||
|
||||
// Create fresh directory
|
||||
if err = os.MkdirAll(serveDataDir, 0755); chk.E(err) {
|
||||
log.E.F("failed to create serve directory: %v", err)
|
||||
os.Exit(1)
|
||||
}
|
||||
|
||||
// Override configuration for serve mode
|
||||
cfg.DataDir = serveDataDir
|
||||
cfg.Listen = "0.0.0.0"
|
||||
cfg.Port = 10547
|
||||
cfg.ACLMode = "none"
|
||||
cfg.ServeMode = true // Grant full owner access to all users
|
||||
|
||||
log.I.F("serve mode: listening on %s:%d with ACL mode '%s' (full owner access)",
|
||||
cfg.Listen, cfg.Port, cfg.ACLMode)
|
||||
}
|
||||
|
||||
// Ensure profiling is stopped on interrupts (SIGINT/SIGTERM) as well as on normal exit
|
||||
var profileStopOnce sync.Once
|
||||
profileStop := func() {}
|
||||
|
||||
@@ -123,14 +123,13 @@ func (f *Follows) Configure(cfg ...any) (err error) {
|
||||
}
|
||||
// log.I.F("admin follow list:\n%s", ev.Serialize())
|
||||
for _, v := range ev.Tags.GetAll([]byte("p")) {
|
||||
// log.I.F("adding follow: %s", v.Value())
|
||||
var a []byte
|
||||
if b, e := hex.DecodeString(string(v.Value())); chk.E(e) {
|
||||
// log.I.F("adding follow: %s", v.ValueHex())
|
||||
// ValueHex() automatically handles both binary and hex storage formats
|
||||
if b, e := hex.DecodeString(string(v.ValueHex())); chk.E(e) {
|
||||
continue
|
||||
} else {
|
||||
a = b
|
||||
f.follows = append(f.follows, b)
|
||||
}
|
||||
f.follows = append(f.follows, a)
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -923,8 +922,15 @@ func (f *Follows) extractFollowedPubkeys(event *event.E) {
|
||||
|
||||
// Extract all 'p' tags (followed pubkeys) from the kind 3 event
|
||||
for _, tag := range event.Tags.GetAll([]byte("p")) {
|
||||
if len(tag.Value()) == 32 { // Valid pubkey length
|
||||
f.AddFollow(tag.Value())
|
||||
// First try binary format (optimized storage: 33 bytes = 32 hash + null)
|
||||
if pubkey := tag.ValueBinary(); pubkey != nil {
|
||||
f.AddFollow(pubkey)
|
||||
continue
|
||||
}
|
||||
// Fall back to hex decoding for non-binary values
|
||||
// Use ValueHex() which handles both binary and hex storage formats
|
||||
if pubkey, err := hex.DecodeString(string(tag.ValueHex())); err == nil && len(pubkey) == 32 {
|
||||
f.AddFollow(pubkey)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -52,6 +52,11 @@ func (n *None) Configure(cfg ...any) (err error) {
|
||||
}
|
||||
|
||||
func (n *None) GetAccessLevel(pub []byte, address string) (level string) {
|
||||
// In serve mode, grant full owner access to everyone
|
||||
if n.cfg != nil && n.cfg.ServeMode {
|
||||
return "owner"
|
||||
}
|
||||
|
||||
// Check owners first
|
||||
for _, v := range n.owners {
|
||||
if utils.FastEqual(v, pub) {
|
||||
|
||||
@@ -523,11 +523,11 @@ func TestServerErrorHandling(t *testing.T) {
|
||||
statusCode: http.StatusNotFound,
|
||||
},
|
||||
{
|
||||
name: "Missing auth header",
|
||||
name: "Anonymous upload allowed",
|
||||
method: "PUT",
|
||||
path: "/upload",
|
||||
body: []byte("test"),
|
||||
statusCode: http.StatusUnauthorized,
|
||||
statusCode: http.StatusOK, // RequireAuth=false and ACL=none allows anonymous uploads
|
||||
},
|
||||
{
|
||||
name: "Invalid JSON in mirror",
|
||||
|
||||
458
pkg/database/binary_tag_filter_test.go
Normal file
458
pkg/database/binary_tag_filter_test.go
Normal file
@@ -0,0 +1,458 @@
|
||||
package database
|
||||
|
||||
import (
|
||||
"context"
|
||||
"os"
|
||||
"testing"
|
||||
|
||||
"git.mleku.dev/mleku/nostr/encoders/event"
|
||||
"git.mleku.dev/mleku/nostr/encoders/filter"
|
||||
"git.mleku.dev/mleku/nostr/encoders/hex"
|
||||
"git.mleku.dev/mleku/nostr/encoders/kind"
|
||||
"git.mleku.dev/mleku/nostr/encoders/tag"
|
||||
"git.mleku.dev/mleku/nostr/encoders/timestamp"
|
||||
"git.mleku.dev/mleku/nostr/interfaces/signer/p8k"
|
||||
"lol.mleku.dev/chk"
|
||||
)
|
||||
|
||||
// TestBinaryTagFilterRegression tests that queries with #e and #p tags work correctly
|
||||
// even when the event's tags are stored in binary format but filter values come as hex strings.
|
||||
//
|
||||
// This is a regression test for the bug where:
|
||||
// - Events with e/p tags are stored with binary-encoded values (32 bytes + null terminator)
|
||||
// - Filters from clients use hex strings (64 characters)
|
||||
// - The mismatch caused queries with #e or #p filter tags to fail
|
||||
//
|
||||
// See: https://github.com/mleku/orly/issues/XXX
|
||||
func TestBinaryTagFilterRegression(t *testing.T) {
|
||||
// Create a temporary directory for the database
|
||||
tempDir, err := os.MkdirTemp("", "test-db-binary-tag-*")
|
||||
if err != nil {
|
||||
t.Fatalf("Failed to create temporary directory: %v", err)
|
||||
}
|
||||
defer os.RemoveAll(tempDir)
|
||||
|
||||
// Create a context and cancel function for the database
|
||||
ctx, cancel := context.WithCancel(context.Background())
|
||||
defer cancel()
|
||||
|
||||
// Initialize the database
|
||||
db, err := New(ctx, cancel, tempDir, "info")
|
||||
if err != nil {
|
||||
t.Fatalf("Failed to create database: %v", err)
|
||||
}
|
||||
defer db.Close()
|
||||
|
||||
// Create signers for the test
|
||||
authorSign := p8k.MustNew()
|
||||
if err := authorSign.Generate(); chk.E(err) {
|
||||
t.Fatal(err)
|
||||
}
|
||||
|
||||
referencedPubkeySign := p8k.MustNew()
|
||||
if err := referencedPubkeySign.Generate(); chk.E(err) {
|
||||
t.Fatal(err)
|
||||
}
|
||||
|
||||
// Create a referenced event (to generate a valid event ID for e-tag)
|
||||
referencedEvent := event.New()
|
||||
referencedEvent.Kind = kind.TextNote.K
|
||||
referencedEvent.Pubkey = referencedPubkeySign.Pub()
|
||||
referencedEvent.CreatedAt = timestamp.Now().V - 7200 // 2 hours ago
|
||||
referencedEvent.Content = []byte("Referenced event")
|
||||
referencedEvent.Tags = tag.NewS()
|
||||
referencedEvent.Sign(referencedPubkeySign)
|
||||
|
||||
// Save the referenced event
|
||||
if _, err := db.SaveEvent(ctx, referencedEvent); err != nil {
|
||||
t.Fatalf("Failed to save referenced event: %v", err)
|
||||
}
|
||||
|
||||
// Get hex representations of the IDs we'll use in tags
|
||||
referencedEventIdHex := hex.Enc(referencedEvent.ID)
|
||||
referencedPubkeyHex := hex.Enc(referencedPubkeySign.Pub())
|
||||
|
||||
// Create a test event similar to the problematic case:
|
||||
// - Kind 30520 (addressable)
|
||||
// - Has d, p, e, u, t tags
|
||||
testEvent := event.New()
|
||||
testEvent.Kind = 30520 // Addressable event kind
|
||||
testEvent.Pubkey = authorSign.Pub()
|
||||
testEvent.CreatedAt = timestamp.Now().V
|
||||
testEvent.Content = []byte("Test content with binary tags")
|
||||
testEvent.Tags = tag.NewS(
|
||||
tag.NewFromAny("d", "test-d-tag-value"),
|
||||
tag.NewFromAny("p", string(referencedPubkeyHex)), // p-tag with hex pubkey
|
||||
tag.NewFromAny("e", string(referencedEventIdHex)), // e-tag with hex event ID
|
||||
tag.NewFromAny("u", "test.app"),
|
||||
tag.NewFromAny("t", "test-topic"),
|
||||
)
|
||||
testEvent.Sign(authorSign)
|
||||
|
||||
// Save the test event
|
||||
if _, err := db.SaveEvent(ctx, testEvent); err != nil {
|
||||
t.Fatalf("Failed to save test event: %v", err)
|
||||
}
|
||||
|
||||
authorPubkeyHex := hex.Enc(authorSign.Pub())
|
||||
testEventIdHex := hex.Enc(testEvent.ID)
|
||||
|
||||
// Test case 1: Query WITHOUT e/p tags (should work - baseline)
|
||||
t.Run("QueryWithoutEPTags", func(t *testing.T) {
|
||||
f := &filter.F{
|
||||
Kinds: kind.NewS(kind.New(30520)),
|
||||
Authors: tag.NewFromBytesSlice(authorSign.Pub()),
|
||||
Tags: tag.NewS(
|
||||
tag.NewFromAny("#d", "test-d-tag-value"),
|
||||
tag.NewFromAny("#u", "test.app"),
|
||||
),
|
||||
}
|
||||
|
||||
results, err := db.QueryForIds(ctx, f)
|
||||
if err != nil {
|
||||
t.Fatalf("Query without e/p tags failed: %v", err)
|
||||
}
|
||||
|
||||
if len(results) == 0 {
|
||||
t.Fatal("Expected to find event with d/u tags filter, got 0 results")
|
||||
}
|
||||
|
||||
// Verify we got the correct event
|
||||
found := false
|
||||
for _, r := range results {
|
||||
if hex.Enc(r.Id) == testEventIdHex {
|
||||
found = true
|
||||
break
|
||||
}
|
||||
}
|
||||
if !found {
|
||||
t.Errorf("Expected event ID %s not found in results", testEventIdHex)
|
||||
}
|
||||
})
|
||||
|
||||
// Test case 2: Query WITH #p tag (this was the failing case)
|
||||
t.Run("QueryWithPTag", func(t *testing.T) {
|
||||
f := &filter.F{
|
||||
Kinds: kind.NewS(kind.New(30520)),
|
||||
Authors: tag.NewFromBytesSlice(authorSign.Pub()),
|
||||
Tags: tag.NewS(
|
||||
tag.NewFromAny("#d", "test-d-tag-value"),
|
||||
tag.NewFromAny("#p", string(referencedPubkeyHex)),
|
||||
tag.NewFromAny("#u", "test.app"),
|
||||
),
|
||||
}
|
||||
|
||||
results, err := db.QueryForIds(ctx, f)
|
||||
if err != nil {
|
||||
t.Fatalf("Query with #p tag failed: %v", err)
|
||||
}
|
||||
|
||||
if len(results) == 0 {
|
||||
t.Fatalf("REGRESSION: Expected to find event with #p tag filter, got 0 results. "+
|
||||
"This suggests the binary tag encoding fix is not working. "+
|
||||
"Author: %s, #p: %s", authorPubkeyHex, referencedPubkeyHex)
|
||||
}
|
||||
|
||||
// Verify we got the correct event
|
||||
found := false
|
||||
for _, r := range results {
|
||||
if hex.Enc(r.Id) == testEventIdHex {
|
||||
found = true
|
||||
break
|
||||
}
|
||||
}
|
||||
if !found {
|
||||
t.Errorf("Expected event ID %s not found in results", testEventIdHex)
|
||||
}
|
||||
})
|
||||
|
||||
// Test case 3: Query WITH #e tag (this was also the failing case)
|
||||
t.Run("QueryWithETag", func(t *testing.T) {
|
||||
f := &filter.F{
|
||||
Kinds: kind.NewS(kind.New(30520)),
|
||||
Authors: tag.NewFromBytesSlice(authorSign.Pub()),
|
||||
Tags: tag.NewS(
|
||||
tag.NewFromAny("#d", "test-d-tag-value"),
|
||||
tag.NewFromAny("#e", string(referencedEventIdHex)),
|
||||
tag.NewFromAny("#u", "test.app"),
|
||||
),
|
||||
}
|
||||
|
||||
results, err := db.QueryForIds(ctx, f)
|
||||
if err != nil {
|
||||
t.Fatalf("Query with #e tag failed: %v", err)
|
||||
}
|
||||
|
||||
if len(results) == 0 {
|
||||
t.Fatalf("REGRESSION: Expected to find event with #e tag filter, got 0 results. "+
|
||||
"This suggests the binary tag encoding fix is not working. "+
|
||||
"Author: %s, #e: %s", authorPubkeyHex, referencedEventIdHex)
|
||||
}
|
||||
|
||||
// Verify we got the correct event
|
||||
found := false
|
||||
for _, r := range results {
|
||||
if hex.Enc(r.Id) == testEventIdHex {
|
||||
found = true
|
||||
break
|
||||
}
|
||||
}
|
||||
if !found {
|
||||
t.Errorf("Expected event ID %s not found in results", testEventIdHex)
|
||||
}
|
||||
})
|
||||
|
||||
// Test case 4: Query WITH BOTH #e AND #p tags (the most complete failing case)
|
||||
t.Run("QueryWithBothEAndPTags", func(t *testing.T) {
|
||||
f := &filter.F{
|
||||
Kinds: kind.NewS(kind.New(30520)),
|
||||
Authors: tag.NewFromBytesSlice(authorSign.Pub()),
|
||||
Tags: tag.NewS(
|
||||
tag.NewFromAny("#d", "test-d-tag-value"),
|
||||
tag.NewFromAny("#e", string(referencedEventIdHex)),
|
||||
tag.NewFromAny("#p", string(referencedPubkeyHex)),
|
||||
tag.NewFromAny("#u", "test.app"),
|
||||
),
|
||||
}
|
||||
|
||||
results, err := db.QueryForIds(ctx, f)
|
||||
if err != nil {
|
||||
t.Fatalf("Query with both #e and #p tags failed: %v", err)
|
||||
}
|
||||
|
||||
if len(results) == 0 {
|
||||
t.Fatalf("REGRESSION: Expected to find event with #e and #p tag filters, got 0 results. "+
|
||||
"This is the exact regression case from the bug report. "+
|
||||
"Author: %s, #e: %s, #p: %s", authorPubkeyHex, referencedEventIdHex, referencedPubkeyHex)
|
||||
}
|
||||
|
||||
// Verify we got the correct event
|
||||
found := false
|
||||
for _, r := range results {
|
||||
if hex.Enc(r.Id) == testEventIdHex {
|
||||
found = true
|
||||
break
|
||||
}
|
||||
}
|
||||
if !found {
|
||||
t.Errorf("Expected event ID %s not found in results", testEventIdHex)
|
||||
}
|
||||
})
|
||||
|
||||
// Test case 5: Query with kinds + #p tag (no authors)
|
||||
// Note: Queries with only kinds+tags may use different index paths
|
||||
t.Run("QueryWithKindAndPTag", func(t *testing.T) {
|
||||
f := &filter.F{
|
||||
Kinds: kind.NewS(kind.New(30520)),
|
||||
Tags: tag.NewS(
|
||||
tag.NewFromAny("#p", string(referencedPubkeyHex)),
|
||||
),
|
||||
}
|
||||
|
||||
results, err := db.QueryForIds(ctx, f)
|
||||
if err != nil {
|
||||
t.Fatalf("Query with kind+#p tag failed: %v", err)
|
||||
}
|
||||
|
||||
// This query should find results using the TagKindEnc index
|
||||
t.Logf("Query with kind+#p tag returned %d results", len(results))
|
||||
})
|
||||
|
||||
// Test case 6: Query with kinds + #e tag (no authors)
|
||||
t.Run("QueryWithKindAndETag", func(t *testing.T) {
|
||||
f := &filter.F{
|
||||
Kinds: kind.NewS(kind.New(30520)),
|
||||
Tags: tag.NewS(
|
||||
tag.NewFromAny("#e", string(referencedEventIdHex)),
|
||||
),
|
||||
}
|
||||
|
||||
results, err := db.QueryForIds(ctx, f)
|
||||
if err != nil {
|
||||
t.Fatalf("Query with kind+#e tag failed: %v", err)
|
||||
}
|
||||
|
||||
// This query should find results using the TagKindEnc index
|
||||
t.Logf("Query with kind+#e tag returned %d results", len(results))
|
||||
})
|
||||
}
|
||||
|
||||
// TestFilterNormalization tests the filter normalization utilities
|
||||
func TestFilterNormalization(t *testing.T) {
|
||||
// Test hex pubkey value (64 chars)
|
||||
hexPubkey := []byte("8b1180c2e03cbf83ab048068a7f7d6959ff0331761aba867aaecdc793045c1bc")
|
||||
|
||||
// Test IsBinaryOptimizedTag
|
||||
if !IsBinaryOptimizedTag('e') {
|
||||
t.Error("Expected 'e' to be a binary-optimized tag")
|
||||
}
|
||||
if !IsBinaryOptimizedTag('p') {
|
||||
t.Error("Expected 'p' to be a binary-optimized tag")
|
||||
}
|
||||
if IsBinaryOptimizedTag('d') {
|
||||
t.Error("Expected 'd' NOT to be a binary-optimized tag")
|
||||
}
|
||||
if IsBinaryOptimizedTag('t') {
|
||||
t.Error("Expected 't' NOT to be a binary-optimized tag")
|
||||
}
|
||||
|
||||
// Test IsValidHexValue
|
||||
if !IsValidHexValue(hexPubkey) {
|
||||
t.Error("Expected valid hex pubkey to pass IsValidHexValue")
|
||||
}
|
||||
if IsValidHexValue([]byte("not-hex")) {
|
||||
t.Error("Expected invalid hex to fail IsValidHexValue")
|
||||
}
|
||||
if IsValidHexValue([]byte("abc123")) { // Too short
|
||||
t.Error("Expected short hex to fail IsValidHexValue")
|
||||
}
|
||||
|
||||
// Test HexToBinary conversion
|
||||
binary := HexToBinary(hexPubkey)
|
||||
if binary == nil {
|
||||
t.Fatal("HexToBinary returned nil for valid hex")
|
||||
}
|
||||
if len(binary) != BinaryEncodedLen {
|
||||
t.Errorf("Expected binary length %d, got %d", BinaryEncodedLen, len(binary))
|
||||
}
|
||||
if binary[HashLen] != 0 {
|
||||
t.Error("Expected null terminator at position 32")
|
||||
}
|
||||
|
||||
// Test IsBinaryEncoded
|
||||
if !IsBinaryEncoded(binary) {
|
||||
t.Error("Expected converted binary to pass IsBinaryEncoded")
|
||||
}
|
||||
if IsBinaryEncoded(hexPubkey) {
|
||||
t.Error("Expected hex to fail IsBinaryEncoded")
|
||||
}
|
||||
|
||||
// Test BinaryToHex (round-trip)
|
||||
hexBack := BinaryToHex(binary)
|
||||
if hexBack == nil {
|
||||
t.Fatal("BinaryToHex returned nil")
|
||||
}
|
||||
if string(hexBack) != string(hexPubkey) {
|
||||
t.Errorf("Round-trip failed: expected %s, got %s", hexPubkey, hexBack)
|
||||
}
|
||||
|
||||
// Test NormalizeTagValue for p-tag (should convert hex to binary)
|
||||
normalized := NormalizeTagValue('p', hexPubkey)
|
||||
if !IsBinaryEncoded(normalized) {
|
||||
t.Error("Expected NormalizeTagValue to convert hex to binary for p-tag")
|
||||
}
|
||||
|
||||
// Test NormalizeTagValue for d-tag (should NOT convert)
|
||||
dTagValue := []byte("some-d-tag-value")
|
||||
normalizedD := NormalizeTagValue('d', dTagValue)
|
||||
if string(normalizedD) != string(dTagValue) {
|
||||
t.Error("Expected NormalizeTagValue to leave d-tag unchanged")
|
||||
}
|
||||
|
||||
// Test TagValuesMatch with different encodings
|
||||
if !TagValuesMatch('p', binary, hexPubkey) {
|
||||
t.Error("Expected binary and hex values to match for p-tag")
|
||||
}
|
||||
if !TagValuesMatch('p', hexPubkey, binary) {
|
||||
t.Error("Expected hex and binary values to match for p-tag (reverse)")
|
||||
}
|
||||
if !TagValuesMatch('p', binary, binary) {
|
||||
t.Error("Expected identical binary values to match")
|
||||
}
|
||||
if !TagValuesMatch('p', hexPubkey, hexPubkey) {
|
||||
t.Error("Expected identical hex values to match")
|
||||
}
|
||||
|
||||
// Test non-matching values
|
||||
otherHex := []byte("aaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaa")
|
||||
if TagValuesMatch('p', hexPubkey, otherHex) {
|
||||
t.Error("Expected different hex values NOT to match")
|
||||
}
|
||||
}
|
||||
|
||||
// TestNormalizeFilterTag tests the NormalizeFilterTag function
|
||||
func TestNormalizeFilterTag(t *testing.T) {
|
||||
hexPubkey := "8b1180c2e03cbf83ab048068a7f7d6959ff0331761aba867aaecdc793045c1bc"
|
||||
|
||||
// Test with #p style tag (filter format)
|
||||
pTag := tag.NewFromAny("#p", hexPubkey)
|
||||
normalized := NormalizeFilterTag(pTag)
|
||||
|
||||
if normalized == nil {
|
||||
t.Fatal("NormalizeFilterTag returned nil")
|
||||
}
|
||||
|
||||
// Check that the normalized value is binary
|
||||
normalizedValue := normalized.T[1]
|
||||
if !IsBinaryEncoded(normalizedValue) {
|
||||
t.Errorf("Expected normalized #p tag value to be binary, got length %d", len(normalizedValue))
|
||||
}
|
||||
|
||||
// Test with e style tag (event format - single letter key)
|
||||
hexEventId := "34ccd22f852544a0b7a310b50cc76189130fd3d121d1f4dd77d759862a7b7261"
|
||||
eTag := tag.NewFromAny("e", hexEventId)
|
||||
normalizedE := NormalizeFilterTag(eTag)
|
||||
|
||||
normalizedEValue := normalizedE.T[1]
|
||||
if !IsBinaryEncoded(normalizedEValue) {
|
||||
t.Errorf("Expected normalized e tag value to be binary, got length %d", len(normalizedEValue))
|
||||
}
|
||||
|
||||
// Test with non-optimized tag (should remain unchanged)
|
||||
dTag := tag.NewFromAny("#d", "some-value")
|
||||
normalizedD := NormalizeFilterTag(dTag)
|
||||
|
||||
normalizedDValue := normalizedD.T[1]
|
||||
if string(normalizedDValue) != "some-value" {
|
||||
t.Errorf("Expected #d tag value to remain unchanged, got %s", normalizedDValue)
|
||||
}
|
||||
}
|
||||
|
||||
// TestNormalizeFilter tests the full filter normalization
|
||||
func TestNormalizeFilter(t *testing.T) {
|
||||
hexPubkey := "8b1180c2e03cbf83ab048068a7f7d6959ff0331761aba867aaecdc793045c1bc"
|
||||
hexEventId := "34ccd22f852544a0b7a310b50cc76189130fd3d121d1f4dd77d759862a7b7261"
|
||||
|
||||
f := &filter.F{
|
||||
Kinds: kind.NewS(kind.New(30520)),
|
||||
Tags: tag.NewS(
|
||||
tag.NewFromAny("#d", "test-value"),
|
||||
tag.NewFromAny("#e", hexEventId),
|
||||
tag.NewFromAny("#p", hexPubkey),
|
||||
tag.NewFromAny("#u", "test.app"),
|
||||
),
|
||||
}
|
||||
|
||||
normalized := NormalizeFilter(f)
|
||||
|
||||
// Verify non-tag fields are preserved
|
||||
if normalized.Kinds == nil || normalized.Kinds.Len() != 1 {
|
||||
t.Error("Filter Kinds should be preserved")
|
||||
}
|
||||
|
||||
// Verify tags are normalized
|
||||
if normalized.Tags == nil {
|
||||
t.Fatal("Normalized filter Tags is nil")
|
||||
}
|
||||
|
||||
// Check that #e and #p tags have binary values
|
||||
for _, tg := range *normalized.Tags {
|
||||
key := tg.Key()
|
||||
if len(key) == 2 && key[0] == '#' {
|
||||
switch key[1] {
|
||||
case 'e', 'p':
|
||||
// These should have binary values
|
||||
val := tg.T[1]
|
||||
if !IsBinaryEncoded(val) {
|
||||
t.Errorf("Expected #%c tag to have binary value after normalization", key[1])
|
||||
}
|
||||
case 'd', 'u':
|
||||
// These should NOT have binary values
|
||||
val := tg.T[1]
|
||||
if IsBinaryEncoded(val) {
|
||||
t.Errorf("Expected #%c tag NOT to have binary value", key[1])
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
253
pkg/database/filter_utils.go
Normal file
253
pkg/database/filter_utils.go
Normal file
@@ -0,0 +1,253 @@
|
||||
// Package database provides filter utilities for normalizing tag values.
|
||||
//
|
||||
// The nostr library optimizes e/p tag values by storing them in binary format
|
||||
// (32 bytes + null terminator) rather than hex strings (64 chars). However,
|
||||
// filter tags from client queries come as hex strings and don't go through
|
||||
// the same binary encoding during unmarshalling.
|
||||
//
|
||||
// This file provides utilities to normalize filter tags to match the binary
|
||||
// encoding used in stored events, ensuring consistent index lookups and
|
||||
// tag comparisons.
|
||||
package database
|
||||
|
||||
import (
|
||||
"git.mleku.dev/mleku/nostr/encoders/filter"
|
||||
"git.mleku.dev/mleku/nostr/encoders/hex"
|
||||
"git.mleku.dev/mleku/nostr/encoders/tag"
|
||||
)
|
||||
|
||||
// Tag binary encoding constants (matching the nostr library)
|
||||
const (
|
||||
// BinaryEncodedLen is the length of a binary-encoded 32-byte hash with null terminator
|
||||
BinaryEncodedLen = 33
|
||||
// HexEncodedLen is the length of a hex-encoded 32-byte hash
|
||||
HexEncodedLen = 64
|
||||
// HashLen is the raw length of a hash (pubkey/event ID)
|
||||
HashLen = 32
|
||||
)
|
||||
|
||||
// binaryOptimizedTags defines which tag keys use binary encoding optimization
|
||||
var binaryOptimizedTags = map[byte]bool{
|
||||
'e': true, // event references
|
||||
'p': true, // pubkey references
|
||||
}
|
||||
|
||||
// IsBinaryOptimizedTag returns true if the given tag key uses binary encoding
|
||||
func IsBinaryOptimizedTag(key byte) bool {
|
||||
return binaryOptimizedTags[key]
|
||||
}
|
||||
|
||||
// IsBinaryEncoded checks if a value field is stored in optimized binary format
|
||||
func IsBinaryEncoded(val []byte) bool {
|
||||
return len(val) == BinaryEncodedLen && val[HashLen] == 0
|
||||
}
|
||||
|
||||
// IsValidHexValue checks if a byte slice is a valid 64-character hex string
|
||||
func IsValidHexValue(b []byte) bool {
|
||||
if len(b) != HexEncodedLen {
|
||||
return false
|
||||
}
|
||||
return IsHexString(b)
|
||||
}
|
||||
|
||||
// HexToBinary converts a 64-character hex string to 33-byte binary format
|
||||
// Returns nil if the input is not a valid hex string
|
||||
func HexToBinary(hexVal []byte) []byte {
|
||||
if !IsValidHexValue(hexVal) {
|
||||
return nil
|
||||
}
|
||||
binVal := make([]byte, BinaryEncodedLen)
|
||||
if _, err := hex.DecBytes(binVal[:HashLen], hexVal); err != nil {
|
||||
return nil
|
||||
}
|
||||
binVal[HashLen] = 0 // null terminator
|
||||
return binVal
|
||||
}
|
||||
|
||||
// BinaryToHex converts a 33-byte binary value to 64-character hex string
|
||||
// Returns nil if the input is not in binary format
|
||||
func BinaryToHex(binVal []byte) []byte {
|
||||
if !IsBinaryEncoded(binVal) {
|
||||
return nil
|
||||
}
|
||||
return hex.EncAppend(nil, binVal[:HashLen])
|
||||
}
|
||||
|
||||
// NormalizeTagValue normalizes a tag value for the given key.
|
||||
// For e/p tags, hex values are converted to binary format.
|
||||
// Other tags are returned unchanged.
|
||||
func NormalizeTagValue(key byte, val []byte) []byte {
|
||||
if !IsBinaryOptimizedTag(key) {
|
||||
return val
|
||||
}
|
||||
// If already binary, return as-is
|
||||
if IsBinaryEncoded(val) {
|
||||
return val
|
||||
}
|
||||
// If valid hex, convert to binary
|
||||
if binVal := HexToBinary(val); binVal != nil {
|
||||
return binVal
|
||||
}
|
||||
// Otherwise return as-is
|
||||
return val
|
||||
}
|
||||
|
||||
// NormalizeTagToHex returns the hex representation of a tag value.
|
||||
// For binary-encoded values, converts to hex. For hex values, returns as-is.
|
||||
func NormalizeTagToHex(val []byte) []byte {
|
||||
if IsBinaryEncoded(val) {
|
||||
return BinaryToHex(val)
|
||||
}
|
||||
return val
|
||||
}
|
||||
|
||||
// NormalizeFilterTag creates a new tag with binary-encoded values for e/p tags.
|
||||
// The original tag is not modified.
|
||||
func NormalizeFilterTag(t *tag.T) *tag.T {
|
||||
if t == nil || t.Len() < 2 {
|
||||
return t
|
||||
}
|
||||
|
||||
keyBytes := t.Key()
|
||||
var key byte
|
||||
|
||||
// Handle both "e" and "#e" style keys
|
||||
if len(keyBytes) == 1 {
|
||||
key = keyBytes[0]
|
||||
} else if len(keyBytes) == 2 && keyBytes[0] == '#' {
|
||||
key = keyBytes[1]
|
||||
} else {
|
||||
return t // Not a single-letter tag
|
||||
}
|
||||
|
||||
if !IsBinaryOptimizedTag(key) {
|
||||
return t // Not an optimized tag type
|
||||
}
|
||||
|
||||
// Create new tag with normalized values
|
||||
normalized := tag.NewWithCap(t.Len())
|
||||
normalized.T = append(normalized.T, t.T[0]) // Keep key as-is
|
||||
|
||||
// Normalize each value
|
||||
for _, val := range t.T[1:] {
|
||||
normalizedVal := NormalizeTagValue(key, val)
|
||||
normalized.T = append(normalized.T, normalizedVal)
|
||||
}
|
||||
|
||||
return normalized
|
||||
}
|
||||
|
||||
// NormalizeFilterTags normalizes all tags in a tag.S, converting e/p hex values to binary.
|
||||
// Returns a new tag.S with normalized tags.
|
||||
func NormalizeFilterTags(tags *tag.S) *tag.S {
|
||||
if tags == nil || tags.Len() == 0 {
|
||||
return tags
|
||||
}
|
||||
|
||||
normalized := tag.NewSWithCap(tags.Len())
|
||||
for _, t := range *tags {
|
||||
normalizedTag := NormalizeFilterTag(t)
|
||||
normalized.Append(normalizedTag)
|
||||
}
|
||||
return normalized
|
||||
}
|
||||
|
||||
// NormalizeFilter normalizes a filter's tags for consistent database queries.
|
||||
// This should be called before using a filter for database lookups.
|
||||
// The original filter is not modified; a copy with normalized tags is returned.
|
||||
func NormalizeFilter(f *filter.F) *filter.F {
|
||||
if f == nil {
|
||||
return nil
|
||||
}
|
||||
|
||||
// Create a shallow copy of the filter
|
||||
normalized := &filter.F{
|
||||
Ids: f.Ids,
|
||||
Kinds: f.Kinds,
|
||||
Authors: f.Authors,
|
||||
Since: f.Since,
|
||||
Until: f.Until,
|
||||
Search: f.Search,
|
||||
Limit: f.Limit,
|
||||
}
|
||||
|
||||
// Normalize the tags
|
||||
normalized.Tags = NormalizeFilterTags(f.Tags)
|
||||
|
||||
return normalized
|
||||
}
|
||||
|
||||
// TagValuesMatch compares two tag values, handling both binary and hex encodings.
|
||||
// This is useful for post-query tag matching where event values may be binary
|
||||
// and filter values may be hex (or vice versa).
|
||||
func TagValuesMatch(key byte, eventVal, filterVal []byte) bool {
|
||||
// If both are the same, they match
|
||||
if len(eventVal) == len(filterVal) {
|
||||
for i := range eventVal {
|
||||
if eventVal[i] != filterVal[i] {
|
||||
goto different
|
||||
}
|
||||
}
|
||||
return true
|
||||
}
|
||||
different:
|
||||
|
||||
// For non-optimized tags, require exact match
|
||||
if !IsBinaryOptimizedTag(key) {
|
||||
return false
|
||||
}
|
||||
|
||||
// Normalize both to hex and compare
|
||||
eventHex := NormalizeTagToHex(eventVal)
|
||||
filterHex := NormalizeTagToHex(filterVal)
|
||||
|
||||
if len(eventHex) != len(filterHex) {
|
||||
return false
|
||||
}
|
||||
for i := range eventHex {
|
||||
if eventHex[i] != filterHex[i] {
|
||||
return false
|
||||
}
|
||||
}
|
||||
return true
|
||||
}
|
||||
|
||||
// TagValuesMatchUsingTagMethods compares an event tag's value with a filter value
|
||||
// using the tag.T methods. This leverages the nostr library's ValueHex() method
|
||||
// for proper binary/hex conversion.
|
||||
func TagValuesMatchUsingTagMethods(eventTag *tag.T, filterVal []byte) bool {
|
||||
if eventTag == nil {
|
||||
return false
|
||||
}
|
||||
|
||||
keyBytes := eventTag.Key()
|
||||
if len(keyBytes) != 1 {
|
||||
// Not a single-letter tag, use direct comparison
|
||||
return bytesEqual(eventTag.Value(), filterVal)
|
||||
}
|
||||
|
||||
key := keyBytes[0]
|
||||
if !IsBinaryOptimizedTag(key) {
|
||||
// Not an optimized tag, use direct comparison
|
||||
return bytesEqual(eventTag.Value(), filterVal)
|
||||
}
|
||||
|
||||
// For e/p tags, use ValueHex() for proper conversion
|
||||
eventHex := eventTag.ValueHex()
|
||||
filterHex := NormalizeTagToHex(filterVal)
|
||||
|
||||
return bytesEqual(eventHex, filterHex)
|
||||
}
|
||||
|
||||
// bytesEqual is a fast equality check that avoids allocation
|
||||
func bytesEqual(a, b []byte) bool {
|
||||
if len(a) != len(b) {
|
||||
return false
|
||||
}
|
||||
for i := range a {
|
||||
if a[i] != b[i] {
|
||||
return false
|
||||
}
|
||||
}
|
||||
return true
|
||||
}
|
||||
@@ -30,6 +30,16 @@ func IsHexString(data []byte) (isHex bool) {
|
||||
return true
|
||||
}
|
||||
|
||||
// NormalizeTagValueForHash normalizes a tag value for consistent hashing.
|
||||
// For 'e' and 'p' tags, the nostr library stores values in binary format (32 bytes),
|
||||
// but filters from clients come with hex strings (64 chars). This function ensures
|
||||
// that filter values are converted to binary to match the stored index format.
|
||||
//
|
||||
// This function delegates to NormalizeTagValue from filter_utils.go for consistency.
|
||||
func NormalizeTagValueForHash(key byte, valueBytes []byte) []byte {
|
||||
return NormalizeTagValue(key, valueBytes)
|
||||
}
|
||||
|
||||
// CreateIdHashFromData creates an IdHash from data that could be hex or binary
|
||||
func CreateIdHashFromData(data []byte) (i *types2.IdHash, err error) {
|
||||
i = new(types2.IdHash)
|
||||
@@ -190,14 +200,18 @@ func GetIndexesFromFilter(f *filter.F) (idxs []Range, err error) {
|
||||
keyBytes := t.Key()
|
||||
key := new(types2.Letter)
|
||||
// If the tag key starts with '#', use the second character as the key
|
||||
var keyByte byte
|
||||
if len(keyBytes) == 2 && keyBytes[0] == '#' {
|
||||
key.Set(keyBytes[1])
|
||||
keyByte = keyBytes[1]
|
||||
} else {
|
||||
key.Set(keyBytes[0])
|
||||
keyByte = keyBytes[0]
|
||||
}
|
||||
key.Set(keyByte)
|
||||
for _, valueBytes := range t.T[1:] {
|
||||
// Normalize e/p tag values from hex to binary for consistent hashing
|
||||
normalizedValue := NormalizeTagValueForHash(keyByte, valueBytes)
|
||||
valueHash := new(types2.Ident)
|
||||
valueHash.FromIdent(valueBytes)
|
||||
valueHash.FromIdent(normalizedValue)
|
||||
start, end := new(bytes.Buffer), new(bytes.Buffer)
|
||||
idxS := indexes.TagKindPubkeyEnc(
|
||||
key, valueHash, kind, p, caStart, nil,
|
||||
@@ -234,14 +248,18 @@ func GetIndexesFromFilter(f *filter.F) (idxs []Range, err error) {
|
||||
keyBytes := t.Key()
|
||||
key := new(types2.Letter)
|
||||
// If the tag key starts with '#', use the second character as the key
|
||||
var keyByte byte
|
||||
if len(keyBytes) == 2 && keyBytes[0] == '#' {
|
||||
key.Set(keyBytes[1])
|
||||
keyByte = keyBytes[1]
|
||||
} else {
|
||||
key.Set(keyBytes[0])
|
||||
keyByte = keyBytes[0]
|
||||
}
|
||||
key.Set(keyByte)
|
||||
for _, valueBytes := range t.T[1:] {
|
||||
// Normalize e/p tag values from hex to binary for consistent hashing
|
||||
normalizedValue := NormalizeTagValueForHash(keyByte, valueBytes)
|
||||
valueHash := new(types2.Ident)
|
||||
valueHash.FromIdent(valueBytes)
|
||||
valueHash.FromIdent(normalizedValue)
|
||||
start, end := new(bytes.Buffer), new(bytes.Buffer)
|
||||
idxS := indexes.TagKindEnc(
|
||||
key, valueHash, kind, caStart, nil,
|
||||
@@ -280,14 +298,18 @@ func GetIndexesFromFilter(f *filter.F) (idxs []Range, err error) {
|
||||
keyBytes := t.Key()
|
||||
key := new(types2.Letter)
|
||||
// If the tag key starts with '#', use the second character as the key
|
||||
var keyByte byte
|
||||
if len(keyBytes) == 2 && keyBytes[0] == '#' {
|
||||
key.Set(keyBytes[1])
|
||||
keyByte = keyBytes[1]
|
||||
} else {
|
||||
key.Set(keyBytes[0])
|
||||
keyByte = keyBytes[0]
|
||||
}
|
||||
key.Set(keyByte)
|
||||
for _, valueBytes := range t.T[1:] {
|
||||
// Normalize e/p tag values from hex to binary for consistent hashing
|
||||
normalizedValue := NormalizeTagValueForHash(keyByte, valueBytes)
|
||||
valueHash := new(types2.Ident)
|
||||
valueHash.FromIdent(valueBytes)
|
||||
valueHash.FromIdent(normalizedValue)
|
||||
start, end := new(bytes.Buffer), new(bytes.Buffer)
|
||||
idxS := indexes.TagPubkeyEnc(
|
||||
key, valueHash, p, caStart, nil,
|
||||
@@ -318,14 +340,18 @@ func GetIndexesFromFilter(f *filter.F) (idxs []Range, err error) {
|
||||
keyBytes := t.Key()
|
||||
key := new(types2.Letter)
|
||||
// If the tag key starts with '#', use the second character as the key
|
||||
var keyByte byte
|
||||
if len(keyBytes) == 2 && keyBytes[0] == '#' {
|
||||
key.Set(keyBytes[1])
|
||||
keyByte = keyBytes[1]
|
||||
} else {
|
||||
key.Set(keyBytes[0])
|
||||
keyByte = keyBytes[0]
|
||||
}
|
||||
key.Set(keyByte)
|
||||
for _, valueBytes := range t.T[1:] {
|
||||
// Normalize e/p tag values from hex to binary for consistent hashing
|
||||
normalizedValue := NormalizeTagValueForHash(keyByte, valueBytes)
|
||||
valueHash := new(types2.Ident)
|
||||
valueHash.FromIdent(valueBytes)
|
||||
valueHash.FromIdent(normalizedValue)
|
||||
start, end := new(bytes.Buffer), new(bytes.Buffer)
|
||||
idxS := indexes.TagEnc(key, valueHash, caStart, nil)
|
||||
if err = idxS.MarshalWrite(start); chk.E(err) {
|
||||
|
||||
@@ -29,13 +29,14 @@ func (d *D) ProcessDelete(ev *event.E, admins [][]byte) (err error) {
|
||||
if eTag.Len() < 2 {
|
||||
continue
|
||||
}
|
||||
eventId := eTag.Value()
|
||||
if len(eventId) != 64 { // hex encoded event ID
|
||||
// Use ValueHex() to handle both binary and hex storage formats
|
||||
eventIdHex := eTag.ValueHex()
|
||||
if len(eventIdHex) != 64 { // hex encoded event ID
|
||||
continue
|
||||
}
|
||||
// Decode hex event ID
|
||||
var eid []byte
|
||||
if eid, err = hexenc.DecAppend(nil, eventId); chk.E(err) {
|
||||
if eid, err = hexenc.DecAppend(nil, eventIdHex); chk.E(err) {
|
||||
continue
|
||||
}
|
||||
// Fetch the event to verify ownership
|
||||
|
||||
@@ -281,12 +281,14 @@ func (d *D) QueryEventsWithOptions(c context.Context, f *filter.F, includeDelete
|
||||
// For replaceable events, we need to check if there are any
|
||||
// e-tags that reference events with the same kind and pubkey
|
||||
for _, eTag := range eTags {
|
||||
if len(eTag.Value()) != 64 {
|
||||
// Use ValueHex() to handle both binary and hex storage formats
|
||||
eTagHex := eTag.ValueHex()
|
||||
if len(eTagHex) != 64 {
|
||||
continue
|
||||
}
|
||||
// Get the event ID from the e-tag
|
||||
evId := make([]byte, sha256.Size)
|
||||
if _, err = hex.DecBytes(evId, eTag.Value()); err != nil {
|
||||
if _, err = hex.DecBytes(evId, eTagHex); err != nil {
|
||||
continue
|
||||
}
|
||||
|
||||
@@ -363,10 +365,10 @@ func (d *D) QueryEventsWithOptions(c context.Context, f *filter.F, includeDelete
|
||||
eventTag.Key(), actualKey,
|
||||
) {
|
||||
// Check if the event's tag value matches any of the filter's values
|
||||
// Using TagValuesMatchUsingTagMethods handles binary/hex conversion
|
||||
// for e/p tags automatically
|
||||
for _, filterValue := range filterTag.T[1:] {
|
||||
if bytes.Equal(
|
||||
eventTag.Value(), filterValue,
|
||||
) {
|
||||
if TagValuesMatchUsingTagMethods(eventTag, filterValue) {
|
||||
eventHasTag = true
|
||||
break
|
||||
}
|
||||
|
||||
@@ -78,10 +78,20 @@ func (d *D) QueryPTagGraph(f *filter.F) (sers types.Uint40s, err error) {
|
||||
var pubkeySerials []*types.Uint40
|
||||
for _, pTagBytes := range pTags {
|
||||
var pubkeyBytes []byte
|
||||
// Try to decode as hex
|
||||
if pubkeyBytes, err = hex.Dec(string(pTagBytes)); chk.E(err) {
|
||||
log.D.F("QueryPTagGraph: failed to decode pubkey hex: %v", err)
|
||||
continue
|
||||
|
||||
// Handle both binary-encoded (33 bytes) and hex-encoded (64 chars) values
|
||||
// Filter tags may come as either format depending on how they were parsed
|
||||
if IsBinaryEncoded(pTagBytes) {
|
||||
// Already binary-encoded, extract the 32-byte hash
|
||||
pubkeyBytes = pTagBytes[:HashLen]
|
||||
} else {
|
||||
// Try to decode as hex using NormalizeTagToHex for consistent handling
|
||||
hexBytes := NormalizeTagToHex(pTagBytes)
|
||||
var decErr error
|
||||
if pubkeyBytes, decErr = hex.Dec(string(hexBytes)); chk.E(decErr) {
|
||||
log.D.F("QueryPTagGraph: failed to decode pubkey hex: %v", decErr)
|
||||
continue
|
||||
}
|
||||
}
|
||||
if len(pubkeyBytes) != 32 {
|
||||
log.D.F("QueryPTagGraph: invalid pubkey length: %d", len(pubkeyBytes))
|
||||
|
||||
@@ -214,10 +214,11 @@ func (d *D) SaveEvent(c context.Context, ev *event.E) (
|
||||
// Extract p-tag pubkeys using GetAll
|
||||
pTags := ev.Tags.GetAll([]byte("p"))
|
||||
for _, pTag := range pTags {
|
||||
if len(pTag.T) >= 2 {
|
||||
// Decode hex pubkey from p-tag
|
||||
if pTag.Len() >= 2 {
|
||||
// Get pubkey from p-tag, handling both binary and hex storage formats
|
||||
// ValueHex() returns hex regardless of internal storage format
|
||||
var ptagPubkey []byte
|
||||
if ptagPubkey, err = hex.Dec(string(pTag.T[tag.Value])); err == nil && len(ptagPubkey) == 32 {
|
||||
if ptagPubkey, err = hex.Dec(string(pTag.ValueHex())); err == nil && len(ptagPubkey) == 32 {
|
||||
pkHex := hex.Enc(ptagPubkey)
|
||||
// Skip if already added as author
|
||||
if _, exists := pubkeysForGraph[pkHex]; !exists {
|
||||
|
||||
@@ -78,7 +78,8 @@ func (n *N) ProcessDelete(ev *event.E, admins [][]byte) error {
|
||||
continue
|
||||
}
|
||||
|
||||
eventIDStr := string(eTag.T[1])
|
||||
// Use ValueHex() to correctly handle both binary and hex storage formats
|
||||
eventIDStr := string(eTag.ValueHex())
|
||||
eventID, err := hex.Dec(eventIDStr)
|
||||
if err != nil {
|
||||
continue
|
||||
|
||||
@@ -313,7 +313,7 @@ func New(policyJSON []byte) (p *P, err error) {
|
||||
// 2. Mentioned in a p-tag of the event
|
||||
//
|
||||
// Both ev.Pubkey and userPubkey must be binary ([]byte), not hex-encoded.
|
||||
// P-tags are assumed to contain hex-encoded pubkeys that will be decoded.
|
||||
// P-tags may be stored in either binary-optimized format (33 bytes) or hex format.
|
||||
//
|
||||
// This is the single source of truth for "parties_involved" / "privileged" checks.
|
||||
func IsPartyInvolved(ev *event.E, userPubkey []byte) bool {
|
||||
@@ -330,8 +330,8 @@ func IsPartyInvolved(ev *event.E, userPubkey []byte) bool {
|
||||
// Check if user is in p tags
|
||||
pTags := ev.Tags.GetAll([]byte("p"))
|
||||
for _, pTag := range pTags {
|
||||
// pTag.Value() returns hex-encoded string; decode to bytes for comparison
|
||||
pt, err := hex.Dec(string(pTag.Value()))
|
||||
// ValueHex() handles both binary and hex storage formats automatically
|
||||
pt, err := hex.Dec(string(pTag.ValueHex()))
|
||||
if err != nil {
|
||||
// Skip malformed tags
|
||||
continue
|
||||
|
||||
@@ -4,6 +4,7 @@ import (
|
||||
"encoding/json"
|
||||
"testing"
|
||||
|
||||
"git.mleku.dev/mleku/nostr/encoders/event"
|
||||
"git.mleku.dev/mleku/nostr/encoders/hex"
|
||||
)
|
||||
|
||||
@@ -241,3 +242,97 @@ func TestNoReadAllowNoPrivileged(t *testing.T) {
|
||||
}
|
||||
})
|
||||
}
|
||||
|
||||
// TestPrivilegedWithBinaryEncodedPTags tests that privileged access works correctly
|
||||
// when p-tags are stored in binary-optimized format (as happens after JSON unmarshaling).
|
||||
// This is the real-world scenario where events come from network JSON.
|
||||
func TestPrivilegedWithBinaryEncodedPTags(t *testing.T) {
|
||||
_, alicePubkey := generateTestKeypair(t)
|
||||
_, bobPubkey := generateTestKeypair(t)
|
||||
_, charliePubkey := generateTestKeypair(t)
|
||||
|
||||
// Create policy with privileged flag
|
||||
policyJSON := map[string]interface{}{
|
||||
"rules": map[string]interface{}{
|
||||
"4": map[string]interface{}{
|
||||
"description": "DM - privileged only",
|
||||
"privileged": true,
|
||||
},
|
||||
},
|
||||
}
|
||||
|
||||
policyBytes, err := json.Marshal(policyJSON)
|
||||
if err != nil {
|
||||
t.Fatalf("Failed to marshal policy: %v", err)
|
||||
}
|
||||
|
||||
policy, err := New(policyBytes)
|
||||
if err != nil {
|
||||
t.Fatalf("Failed to create policy: %v", err)
|
||||
}
|
||||
|
||||
// Create event JSON with p-tag (simulating real network event)
|
||||
// When this JSON is unmarshaled, the p-tag value will be converted to binary format
|
||||
eventJSON := `{
|
||||
"id": "0000000000000000000000000000000000000000000000000000000000000001",
|
||||
"pubkey": "` + hex.Enc(alicePubkey) + `",
|
||||
"created_at": 1234567890,
|
||||
"kind": 4,
|
||||
"tags": [["p", "` + hex.Enc(bobPubkey) + `"]],
|
||||
"content": "Secret message",
|
||||
"sig": "00000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000"
|
||||
}`
|
||||
|
||||
var ev event.E
|
||||
if err := json.Unmarshal([]byte(eventJSON), &ev); err != nil {
|
||||
t.Fatalf("Failed to unmarshal event: %v", err)
|
||||
}
|
||||
|
||||
// Verify the p-tag is stored in binary format
|
||||
pTags := ev.Tags.GetAll([]byte("p"))
|
||||
if len(pTags) == 0 {
|
||||
t.Fatal("Event should have p-tag")
|
||||
}
|
||||
pTag := pTags[0]
|
||||
binValue := pTag.ValueBinary()
|
||||
t.Logf("P-tag Value() length: %d", len(pTag.Value()))
|
||||
t.Logf("P-tag ValueBinary(): %v (len=%d)", binValue != nil, len(binValue))
|
||||
if binValue == nil {
|
||||
t.Log("Warning: P-tag is NOT in binary format (test may not exercise the binary code path)")
|
||||
} else {
|
||||
t.Log("P-tag IS in binary format - testing binary-encoded path")
|
||||
}
|
||||
|
||||
// Test: Bob (in p-tag) should be able to read even with binary-encoded tag
|
||||
t.Run("bob_binary_ptag_can_read", func(t *testing.T) {
|
||||
allowed, err := policy.CheckPolicy("read", &ev, bobPubkey, "127.0.0.1")
|
||||
if err != nil {
|
||||
t.Fatalf("Unexpected error: %v", err)
|
||||
}
|
||||
if !allowed {
|
||||
t.Error("BUG! Recipient (in binary-encoded p-tag) should be able to read privileged event")
|
||||
}
|
||||
})
|
||||
|
||||
// Test: Alice (author) should be able to read
|
||||
t.Run("alice_author_can_read", func(t *testing.T) {
|
||||
allowed, err := policy.CheckPolicy("read", &ev, alicePubkey, "127.0.0.1")
|
||||
if err != nil {
|
||||
t.Fatalf("Unexpected error: %v", err)
|
||||
}
|
||||
if !allowed {
|
||||
t.Error("Author should be able to read their own privileged event")
|
||||
}
|
||||
})
|
||||
|
||||
// Test: Charlie (third party) should NOT be able to read
|
||||
t.Run("charlie_denied", func(t *testing.T) {
|
||||
allowed, err := policy.CheckPolicy("read", &ev, charliePubkey, "127.0.0.1")
|
||||
if err != nil {
|
||||
t.Fatalf("Unexpected error: %v", err)
|
||||
}
|
||||
if allowed {
|
||||
t.Error("Third party should NOT be able to read privileged event")
|
||||
}
|
||||
})
|
||||
}
|
||||
|
||||
@@ -200,7 +200,7 @@ func ParseTrustAct(ev *event.E) (ta *TrustAct, err error) {
|
||||
|
||||
ta = &TrustAct{
|
||||
Event: ev,
|
||||
TargetPubkey: string(pTag.Value()),
|
||||
TargetPubkey: string(pTag.ValueHex()), // ValueHex() handles binary/hex storage
|
||||
TrustLevel: trustLevel,
|
||||
RelayURL: string(relayTag.Value()),
|
||||
Expiry: expiry,
|
||||
|
||||
612
pkg/spider/directory.go
Normal file
612
pkg/spider/directory.go
Normal file
@@ -0,0 +1,612 @@
|
||||
package spider
|
||||
|
||||
import (
|
||||
"context"
|
||||
"sync"
|
||||
"sync/atomic"
|
||||
"time"
|
||||
|
||||
"git.mleku.dev/mleku/nostr/crypto/keys"
|
||||
"git.mleku.dev/mleku/nostr/encoders/event"
|
||||
"git.mleku.dev/mleku/nostr/encoders/filter"
|
||||
"git.mleku.dev/mleku/nostr/encoders/kind"
|
||||
"git.mleku.dev/mleku/nostr/encoders/tag"
|
||||
"git.mleku.dev/mleku/nostr/utils/normalize"
|
||||
"git.mleku.dev/mleku/nostr/ws"
|
||||
"lol.mleku.dev/chk"
|
||||
"lol.mleku.dev/errorf"
|
||||
"lol.mleku.dev/log"
|
||||
"next.orly.dev/pkg/database"
|
||||
"next.orly.dev/pkg/interfaces/publisher"
|
||||
dsync "next.orly.dev/pkg/sync"
|
||||
)
|
||||
|
||||
const (
|
||||
// DirectorySpiderDefaultInterval is how often the directory spider runs
|
||||
DirectorySpiderDefaultInterval = 24 * time.Hour
|
||||
// DirectorySpiderDefaultMaxHops is the maximum hop distance for relay discovery
|
||||
DirectorySpiderDefaultMaxHops = 3
|
||||
// DirectorySpiderRelayTimeout is the timeout for connecting to and querying a relay
|
||||
DirectorySpiderRelayTimeout = 30 * time.Second
|
||||
// DirectorySpiderQueryTimeout is the timeout for waiting for EOSE on a query
|
||||
DirectorySpiderQueryTimeout = 60 * time.Second
|
||||
// DirectorySpiderRelayDelay is the delay between processing relays (rate limiting)
|
||||
DirectorySpiderRelayDelay = 500 * time.Millisecond
|
||||
// DirectorySpiderMaxEventsPerQuery is the limit for each query
|
||||
DirectorySpiderMaxEventsPerQuery = 5000
|
||||
)
|
||||
|
||||
// DirectorySpider manages periodic relay discovery and metadata synchronization.
|
||||
// It discovers relays by crawling kind 10002 (relay list) events, expanding outward
|
||||
// in hops from seed pubkeys (whitelisted users), then fetches essential metadata
|
||||
// events (kinds 0, 3, 10000, 10002) from all discovered relays.
|
||||
type DirectorySpider struct {
|
||||
ctx context.Context
|
||||
cancel context.CancelFunc
|
||||
db *database.D
|
||||
pub publisher.I
|
||||
|
||||
// Configuration
|
||||
interval time.Duration
|
||||
maxHops int
|
||||
|
||||
// State
|
||||
running atomic.Bool
|
||||
lastRun time.Time
|
||||
|
||||
// Relay discovery state (reset each run)
|
||||
mu sync.Mutex
|
||||
discoveredRelays map[string]int // URL -> hop distance
|
||||
processedRelays map[string]bool // Already fetched metadata from
|
||||
|
||||
// Self-detection
|
||||
relayIdentityPubkey string
|
||||
selfURLs map[string]bool
|
||||
nip11Cache *dsync.NIP11Cache
|
||||
|
||||
// Callback for getting seed pubkeys (whitelisted users)
|
||||
getSeedPubkeys func() [][]byte
|
||||
|
||||
// Trigger channel for manual runs
|
||||
triggerChan chan struct{}
|
||||
}
|
||||
|
||||
// NewDirectorySpider creates a new DirectorySpider instance.
|
||||
func NewDirectorySpider(
|
||||
ctx context.Context,
|
||||
db *database.D,
|
||||
pub publisher.I,
|
||||
interval time.Duration,
|
||||
maxHops int,
|
||||
) (ds *DirectorySpider, err error) {
|
||||
if db == nil {
|
||||
err = errorf.E("database cannot be nil")
|
||||
return
|
||||
}
|
||||
|
||||
if interval <= 0 {
|
||||
interval = DirectorySpiderDefaultInterval
|
||||
}
|
||||
if maxHops <= 0 {
|
||||
maxHops = DirectorySpiderDefaultMaxHops
|
||||
}
|
||||
|
||||
ctx, cancel := context.WithCancel(ctx)
|
||||
|
||||
// Get relay identity pubkey for self-detection
|
||||
var relayPubkey string
|
||||
if skb, err := db.GetRelayIdentitySecret(); err == nil && len(skb) == 32 {
|
||||
pk, _ := keys.SecretBytesToPubKeyHex(skb)
|
||||
relayPubkey = pk
|
||||
}
|
||||
|
||||
ds = &DirectorySpider{
|
||||
ctx: ctx,
|
||||
cancel: cancel,
|
||||
db: db,
|
||||
pub: pub,
|
||||
interval: interval,
|
||||
maxHops: maxHops,
|
||||
discoveredRelays: make(map[string]int),
|
||||
processedRelays: make(map[string]bool),
|
||||
relayIdentityPubkey: relayPubkey,
|
||||
selfURLs: make(map[string]bool),
|
||||
nip11Cache: dsync.NewNIP11Cache(30 * time.Minute),
|
||||
triggerChan: make(chan struct{}, 1),
|
||||
}
|
||||
|
||||
return
|
||||
}
|
||||
|
||||
// SetSeedCallback sets the callback function for getting seed pubkeys (whitelisted users).
|
||||
func (ds *DirectorySpider) SetSeedCallback(getSeedPubkeys func() [][]byte) {
|
||||
ds.mu.Lock()
|
||||
defer ds.mu.Unlock()
|
||||
ds.getSeedPubkeys = getSeedPubkeys
|
||||
}
|
||||
|
||||
// Start begins the directory spider operation.
|
||||
func (ds *DirectorySpider) Start() (err error) {
|
||||
if ds.running.Load() {
|
||||
err = errorf.E("directory spider already running")
|
||||
return
|
||||
}
|
||||
|
||||
if ds.getSeedPubkeys == nil {
|
||||
err = errorf.E("seed callback must be set before starting")
|
||||
return
|
||||
}
|
||||
|
||||
ds.running.Store(true)
|
||||
go ds.mainLoop()
|
||||
|
||||
log.I.F("directory spider: started (interval: %v, max hops: %d)", ds.interval, ds.maxHops)
|
||||
return
|
||||
}
|
||||
|
||||
// Stop stops the directory spider operation.
|
||||
func (ds *DirectorySpider) Stop() {
|
||||
if !ds.running.Load() {
|
||||
return
|
||||
}
|
||||
|
||||
ds.running.Store(false)
|
||||
ds.cancel()
|
||||
|
||||
log.I.F("directory spider: stopped")
|
||||
}
|
||||
|
||||
// TriggerNow forces an immediate run of the directory spider.
|
||||
func (ds *DirectorySpider) TriggerNow() {
|
||||
select {
|
||||
case ds.triggerChan <- struct{}{}:
|
||||
log.I.F("directory spider: manual trigger sent")
|
||||
default:
|
||||
log.I.F("directory spider: trigger already pending")
|
||||
}
|
||||
}
|
||||
|
||||
// LastRun returns the time of the last completed run.
|
||||
func (ds *DirectorySpider) LastRun() time.Time {
|
||||
ds.mu.Lock()
|
||||
defer ds.mu.Unlock()
|
||||
return ds.lastRun
|
||||
}
|
||||
|
||||
// mainLoop is the main spider loop that runs periodically.
|
||||
func (ds *DirectorySpider) mainLoop() {
|
||||
// Run immediately on start
|
||||
ds.runOnce()
|
||||
|
||||
ticker := time.NewTicker(ds.interval)
|
||||
defer ticker.Stop()
|
||||
|
||||
log.I.F("directory spider: main loop started, running every %v", ds.interval)
|
||||
|
||||
for {
|
||||
select {
|
||||
case <-ds.ctx.Done():
|
||||
return
|
||||
case <-ds.triggerChan:
|
||||
log.I.F("directory spider: manual trigger received")
|
||||
ds.runOnce()
|
||||
case <-ticker.C:
|
||||
log.I.F("directory spider: scheduled run triggered")
|
||||
ds.runOnce()
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// runOnce performs a single directory spider run.
|
||||
func (ds *DirectorySpider) runOnce() {
|
||||
if !ds.running.Load() {
|
||||
return
|
||||
}
|
||||
|
||||
log.I.F("directory spider: starting run")
|
||||
start := time.Now()
|
||||
|
||||
// Reset state for this run
|
||||
ds.mu.Lock()
|
||||
ds.discoveredRelays = make(map[string]int)
|
||||
ds.processedRelays = make(map[string]bool)
|
||||
ds.mu.Unlock()
|
||||
|
||||
// Phase 1: Discover relays via hop expansion
|
||||
if err := ds.discoverRelays(); err != nil {
|
||||
log.E.F("directory spider: relay discovery failed: %v", err)
|
||||
return
|
||||
}
|
||||
|
||||
ds.mu.Lock()
|
||||
relayCount := len(ds.discoveredRelays)
|
||||
ds.mu.Unlock()
|
||||
|
||||
log.I.F("directory spider: discovered %d relays", relayCount)
|
||||
|
||||
// Phase 2: Fetch metadata from all discovered relays
|
||||
if err := ds.fetchMetadataFromRelays(); err != nil {
|
||||
log.E.F("directory spider: metadata fetch failed: %v", err)
|
||||
return
|
||||
}
|
||||
|
||||
ds.mu.Lock()
|
||||
ds.lastRun = time.Now()
|
||||
ds.mu.Unlock()
|
||||
|
||||
log.I.F("directory spider: completed run in %v", time.Since(start))
|
||||
}
|
||||
|
||||
// discoverRelays performs the multi-hop relay discovery.
|
||||
func (ds *DirectorySpider) discoverRelays() error {
|
||||
// Get seed pubkeys from callback
|
||||
seedPubkeys := ds.getSeedPubkeys()
|
||||
if len(seedPubkeys) == 0 {
|
||||
log.W.F("directory spider: no seed pubkeys available")
|
||||
return nil
|
||||
}
|
||||
|
||||
log.I.F("directory spider: starting relay discovery with %d seed pubkeys", len(seedPubkeys))
|
||||
|
||||
// Round 0: Get relay lists from seed pubkeys in local database
|
||||
seedRelays, err := ds.getRelaysFromLocalDB(seedPubkeys)
|
||||
if err != nil {
|
||||
return errorf.W("failed to get relays from local DB: %v", err)
|
||||
}
|
||||
|
||||
// Add seed relays at hop 0
|
||||
ds.mu.Lock()
|
||||
for _, url := range seedRelays {
|
||||
if !ds.isSelfRelay(url) {
|
||||
ds.discoveredRelays[url] = 0
|
||||
}
|
||||
}
|
||||
ds.mu.Unlock()
|
||||
|
||||
log.I.F("directory spider: found %d seed relays from local database", len(seedRelays))
|
||||
|
||||
// Rounds 1 to maxHops: Expand outward
|
||||
for hop := 1; hop <= ds.maxHops; hop++ {
|
||||
select {
|
||||
case <-ds.ctx.Done():
|
||||
return ds.ctx.Err()
|
||||
default:
|
||||
}
|
||||
|
||||
// Get relays at previous hop level that haven't been processed
|
||||
ds.mu.Lock()
|
||||
var relaysToProcess []string
|
||||
for url, hopLevel := range ds.discoveredRelays {
|
||||
if hopLevel == hop-1 && !ds.processedRelays[url] {
|
||||
relaysToProcess = append(relaysToProcess, url)
|
||||
}
|
||||
}
|
||||
ds.mu.Unlock()
|
||||
|
||||
if len(relaysToProcess) == 0 {
|
||||
log.I.F("directory spider: no relays to process at hop %d", hop)
|
||||
break
|
||||
}
|
||||
|
||||
log.I.F("directory spider: hop %d - processing %d relays", hop, len(relaysToProcess))
|
||||
|
||||
newRelaysThisHop := 0
|
||||
|
||||
// Process each relay serially
|
||||
for _, relayURL := range relaysToProcess {
|
||||
select {
|
||||
case <-ds.ctx.Done():
|
||||
return ds.ctx.Err()
|
||||
default:
|
||||
}
|
||||
|
||||
// Fetch kind 10002 events from this relay
|
||||
events, err := ds.fetchRelayListsFromRelay(relayURL)
|
||||
if err != nil {
|
||||
log.W.F("directory spider: failed to fetch from %s: %v", relayURL, err)
|
||||
// Mark as processed even on failure to avoid retrying
|
||||
ds.mu.Lock()
|
||||
ds.processedRelays[relayURL] = true
|
||||
ds.mu.Unlock()
|
||||
continue
|
||||
}
|
||||
|
||||
// Extract new relay URLs
|
||||
newRelays := ds.extractRelaysFromEvents(events)
|
||||
|
||||
ds.mu.Lock()
|
||||
ds.processedRelays[relayURL] = true
|
||||
for _, newURL := range newRelays {
|
||||
if _, exists := ds.discoveredRelays[newURL]; !exists {
|
||||
if !ds.isSelfRelay(newURL) {
|
||||
ds.discoveredRelays[newURL] = hop
|
||||
newRelaysThisHop++
|
||||
}
|
||||
}
|
||||
}
|
||||
ds.mu.Unlock()
|
||||
|
||||
// Rate limiting delay between relays
|
||||
time.Sleep(DirectorySpiderRelayDelay)
|
||||
}
|
||||
|
||||
log.I.F("directory spider: hop %d - discovered %d new relays", hop, newRelaysThisHop)
|
||||
}
|
||||
|
||||
return nil
|
||||
}
|
||||
|
||||
// getRelaysFromLocalDB queries the local database for kind 10002 events from seed pubkeys.
|
||||
func (ds *DirectorySpider) getRelaysFromLocalDB(seedPubkeys [][]byte) ([]string, error) {
|
||||
ctx, cancel := context.WithTimeout(ds.ctx, 30*time.Second)
|
||||
defer cancel()
|
||||
|
||||
// Query for kind 10002 from seed pubkeys
|
||||
f := &filter.F{
|
||||
Authors: tag.NewFromBytesSlice(seedPubkeys...),
|
||||
Kinds: kind.NewS(kind.New(kind.RelayListMetadata.K)),
|
||||
}
|
||||
|
||||
events, err := ds.db.QueryEvents(ctx, f)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
return ds.extractRelaysFromEvents(events), nil
|
||||
}
|
||||
|
||||
// fetchRelayListsFromRelay connects to a relay and fetches all kind 10002 events.
|
||||
func (ds *DirectorySpider) fetchRelayListsFromRelay(relayURL string) ([]*event.E, error) {
|
||||
ctx, cancel := context.WithTimeout(ds.ctx, DirectorySpiderRelayTimeout)
|
||||
defer cancel()
|
||||
|
||||
log.D.F("directory spider: connecting to %s", relayURL)
|
||||
|
||||
client, err := ws.RelayConnect(ctx, relayURL)
|
||||
if err != nil {
|
||||
return nil, errorf.W("failed to connect: %v", err)
|
||||
}
|
||||
defer client.Close()
|
||||
|
||||
// Query for all kind 10002 events
|
||||
limit := uint(DirectorySpiderMaxEventsPerQuery)
|
||||
f := filter.NewS(&filter.F{
|
||||
Kinds: kind.NewS(kind.New(kind.RelayListMetadata.K)),
|
||||
Limit: &limit,
|
||||
})
|
||||
|
||||
sub, err := client.Subscribe(ctx, f)
|
||||
if err != nil {
|
||||
return nil, errorf.W("failed to subscribe: %v", err)
|
||||
}
|
||||
defer sub.Unsub()
|
||||
|
||||
var events []*event.E
|
||||
queryCtx, queryCancel := context.WithTimeout(ctx, DirectorySpiderQueryTimeout)
|
||||
defer queryCancel()
|
||||
|
||||
// Collect events until EOSE or timeout
|
||||
for {
|
||||
select {
|
||||
case <-queryCtx.Done():
|
||||
log.D.F("directory spider: query timeout for %s, got %d events", relayURL, len(events))
|
||||
return events, nil
|
||||
case <-sub.EndOfStoredEvents:
|
||||
log.D.F("directory spider: EOSE from %s, got %d events", relayURL, len(events))
|
||||
return events, nil
|
||||
case ev := <-sub.Events:
|
||||
if ev == nil {
|
||||
return events, nil
|
||||
}
|
||||
events = append(events, ev)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// extractRelaysFromEvents parses kind 10002 events and extracts relay URLs from "r" tags.
|
||||
func (ds *DirectorySpider) extractRelaysFromEvents(events []*event.E) []string {
|
||||
seen := make(map[string]bool)
|
||||
var relays []string
|
||||
|
||||
for _, ev := range events {
|
||||
// Get all "r" tags
|
||||
rTags := ev.Tags.GetAll([]byte("r"))
|
||||
for _, rTag := range rTags {
|
||||
if len(rTag.T) < 2 {
|
||||
continue
|
||||
}
|
||||
urlBytes := rTag.Value()
|
||||
if len(urlBytes) == 0 {
|
||||
continue
|
||||
}
|
||||
|
||||
// Normalize the URL
|
||||
normalized := string(normalize.URL(string(urlBytes)))
|
||||
if normalized == "" {
|
||||
continue
|
||||
}
|
||||
|
||||
if !seen[normalized] {
|
||||
seen[normalized] = true
|
||||
relays = append(relays, normalized)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
return relays
|
||||
}
|
||||
|
||||
// fetchMetadataFromRelays iterates through all discovered relays and fetches metadata.
|
||||
func (ds *DirectorySpider) fetchMetadataFromRelays() error {
|
||||
ds.mu.Lock()
|
||||
// Copy relay list to avoid holding lock during network operations
|
||||
var relays []string
|
||||
for url := range ds.discoveredRelays {
|
||||
relays = append(relays, url)
|
||||
}
|
||||
ds.mu.Unlock()
|
||||
|
||||
log.I.F("directory spider: fetching metadata from %d relays", len(relays))
|
||||
|
||||
// Kinds to fetch: 0 (profile), 3 (follow list), 10000 (mute list), 10002 (relay list)
|
||||
kindsToFetch := []uint16{
|
||||
kind.ProfileMetadata.K, // 0
|
||||
kind.FollowList.K, // 3
|
||||
kind.MuteList.K, // 10000
|
||||
kind.RelayListMetadata.K, // 10002
|
||||
}
|
||||
|
||||
totalSaved := 0
|
||||
totalDuplicates := 0
|
||||
|
||||
for _, relayURL := range relays {
|
||||
select {
|
||||
case <-ds.ctx.Done():
|
||||
return ds.ctx.Err()
|
||||
default:
|
||||
}
|
||||
|
||||
ds.mu.Lock()
|
||||
alreadyProcessed := ds.processedRelays[relayURL]
|
||||
ds.mu.Unlock()
|
||||
|
||||
if alreadyProcessed {
|
||||
continue
|
||||
}
|
||||
|
||||
log.D.F("directory spider: fetching metadata from %s", relayURL)
|
||||
|
||||
for _, k := range kindsToFetch {
|
||||
select {
|
||||
case <-ds.ctx.Done():
|
||||
return ds.ctx.Err()
|
||||
default:
|
||||
}
|
||||
|
||||
events, err := ds.fetchKindFromRelay(relayURL, k)
|
||||
if err != nil {
|
||||
log.W.F("directory spider: failed to fetch kind %d from %s: %v", k, relayURL, err)
|
||||
continue
|
||||
}
|
||||
|
||||
saved, duplicates := ds.storeEvents(events)
|
||||
totalSaved += saved
|
||||
totalDuplicates += duplicates
|
||||
|
||||
log.D.F("directory spider: kind %d from %s: %d saved, %d duplicates",
|
||||
k, relayURL, saved, duplicates)
|
||||
}
|
||||
|
||||
ds.mu.Lock()
|
||||
ds.processedRelays[relayURL] = true
|
||||
ds.mu.Unlock()
|
||||
|
||||
// Rate limiting delay between relays
|
||||
time.Sleep(DirectorySpiderRelayDelay)
|
||||
}
|
||||
|
||||
log.I.F("directory spider: metadata fetch complete - %d events saved, %d duplicates",
|
||||
totalSaved, totalDuplicates)
|
||||
|
||||
return nil
|
||||
}
|
||||
|
||||
// fetchKindFromRelay connects to a relay and fetches events of a specific kind.
|
||||
func (ds *DirectorySpider) fetchKindFromRelay(relayURL string, k uint16) ([]*event.E, error) {
|
||||
ctx, cancel := context.WithTimeout(ds.ctx, DirectorySpiderRelayTimeout)
|
||||
defer cancel()
|
||||
|
||||
client, err := ws.RelayConnect(ctx, relayURL)
|
||||
if err != nil {
|
||||
return nil, errorf.W("failed to connect: %v", err)
|
||||
}
|
||||
defer client.Close()
|
||||
|
||||
// Query for events of this kind
|
||||
limit := uint(DirectorySpiderMaxEventsPerQuery)
|
||||
f := filter.NewS(&filter.F{
|
||||
Kinds: kind.NewS(kind.New(k)),
|
||||
Limit: &limit,
|
||||
})
|
||||
|
||||
sub, err := client.Subscribe(ctx, f)
|
||||
if err != nil {
|
||||
return nil, errorf.W("failed to subscribe: %v", err)
|
||||
}
|
||||
defer sub.Unsub()
|
||||
|
||||
var events []*event.E
|
||||
queryCtx, queryCancel := context.WithTimeout(ctx, DirectorySpiderQueryTimeout)
|
||||
defer queryCancel()
|
||||
|
||||
for {
|
||||
select {
|
||||
case <-queryCtx.Done():
|
||||
return events, nil
|
||||
case <-sub.EndOfStoredEvents:
|
||||
return events, nil
|
||||
case ev := <-sub.Events:
|
||||
if ev == nil {
|
||||
return events, nil
|
||||
}
|
||||
events = append(events, ev)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// storeEvents saves events to the database and publishes new ones.
|
||||
func (ds *DirectorySpider) storeEvents(events []*event.E) (saved, duplicates int) {
|
||||
for _, ev := range events {
|
||||
_, err := ds.db.SaveEvent(ds.ctx, ev)
|
||||
if err != nil {
|
||||
if chk.T(err) {
|
||||
// Most errors are duplicates, which is expected
|
||||
duplicates++
|
||||
}
|
||||
continue
|
||||
}
|
||||
saved++
|
||||
|
||||
// Publish event to active subscribers
|
||||
if ds.pub != nil {
|
||||
go ds.pub.Deliver(ev)
|
||||
}
|
||||
}
|
||||
return
|
||||
}
|
||||
|
||||
// isSelfRelay checks if a relay URL is ourselves by comparing NIP-11 pubkeys.
|
||||
func (ds *DirectorySpider) isSelfRelay(relayURL string) bool {
|
||||
// If we don't have a relay identity pubkey, can't compare
|
||||
if ds.relayIdentityPubkey == "" {
|
||||
return false
|
||||
}
|
||||
|
||||
ds.mu.Lock()
|
||||
// Fast path: check if we already know this URL is ours
|
||||
if ds.selfURLs[relayURL] {
|
||||
ds.mu.Unlock()
|
||||
return true
|
||||
}
|
||||
ds.mu.Unlock()
|
||||
|
||||
// Slow path: check via NIP-11 pubkey
|
||||
ctx, cancel := context.WithTimeout(context.Background(), 5*time.Second)
|
||||
defer cancel()
|
||||
|
||||
peerPubkey, err := ds.nip11Cache.GetPubkey(ctx, relayURL)
|
||||
if err != nil {
|
||||
// Can't determine, assume not self
|
||||
return false
|
||||
}
|
||||
|
||||
if peerPubkey == ds.relayIdentityPubkey {
|
||||
log.D.F("directory spider: discovered self-relay: %s", relayURL)
|
||||
ds.mu.Lock()
|
||||
ds.selfURLs[relayURL] = true
|
||||
ds.mu.Unlock()
|
||||
return true
|
||||
}
|
||||
|
||||
return false
|
||||
}
|
||||
166
pkg/spider/directory_test.go
Normal file
166
pkg/spider/directory_test.go
Normal file
@@ -0,0 +1,166 @@
|
||||
package spider
|
||||
|
||||
import (
|
||||
"context"
|
||||
"testing"
|
||||
"time"
|
||||
|
||||
"git.mleku.dev/mleku/nostr/encoders/event"
|
||||
"git.mleku.dev/mleku/nostr/encoders/kind"
|
||||
"git.mleku.dev/mleku/nostr/encoders/tag"
|
||||
"github.com/stretchr/testify/assert"
|
||||
"github.com/stretchr/testify/require"
|
||||
)
|
||||
|
||||
func TestExtractRelaysFromEvents(t *testing.T) {
|
||||
ds := &DirectorySpider{}
|
||||
|
||||
tests := []struct {
|
||||
name string
|
||||
events []*event.E
|
||||
expected []string
|
||||
}{
|
||||
{
|
||||
name: "empty events",
|
||||
events: []*event.E{},
|
||||
expected: []string{},
|
||||
},
|
||||
{
|
||||
name: "single event with relays",
|
||||
events: []*event.E{
|
||||
{
|
||||
Kind: kind.RelayListMetadata.K,
|
||||
Tags: &tag.S{
|
||||
tag.NewFromBytesSlice([]byte("r"), []byte("wss://relay1.example.com")),
|
||||
tag.NewFromBytesSlice([]byte("r"), []byte("wss://relay2.example.com")),
|
||||
},
|
||||
},
|
||||
},
|
||||
expected: []string{"wss://relay1.example.com", "wss://relay2.example.com"},
|
||||
},
|
||||
{
|
||||
name: "multiple events with duplicate relays",
|
||||
events: []*event.E{
|
||||
{
|
||||
Kind: kind.RelayListMetadata.K,
|
||||
Tags: &tag.S{
|
||||
tag.NewFromBytesSlice([]byte("r"), []byte("wss://relay1.example.com")),
|
||||
},
|
||||
},
|
||||
{
|
||||
Kind: kind.RelayListMetadata.K,
|
||||
Tags: &tag.S{
|
||||
tag.NewFromBytesSlice([]byte("r"), []byte("wss://relay1.example.com")),
|
||||
tag.NewFromBytesSlice([]byte("r"), []byte("wss://relay3.example.com")),
|
||||
},
|
||||
},
|
||||
},
|
||||
expected: []string{"wss://relay1.example.com", "wss://relay3.example.com"},
|
||||
},
|
||||
{
|
||||
name: "event with empty r tags",
|
||||
events: []*event.E{
|
||||
{
|
||||
Kind: kind.RelayListMetadata.K,
|
||||
Tags: &tag.S{
|
||||
tag.NewFromBytesSlice([]byte("r")), // empty value
|
||||
tag.NewFromBytesSlice([]byte("r"), []byte("wss://valid.relay.com")),
|
||||
},
|
||||
},
|
||||
},
|
||||
expected: []string{"wss://valid.relay.com"},
|
||||
},
|
||||
{
|
||||
name: "normalizes relay URLs",
|
||||
events: []*event.E{
|
||||
{
|
||||
Kind: kind.RelayListMetadata.K,
|
||||
Tags: &tag.S{
|
||||
tag.NewFromBytesSlice([]byte("r"), []byte("wss://relay.example.com")),
|
||||
tag.NewFromBytesSlice([]byte("r"), []byte("wss://relay.example.com/")), // duplicate with trailing slash
|
||||
},
|
||||
},
|
||||
},
|
||||
expected: []string{"wss://relay.example.com"},
|
||||
},
|
||||
}
|
||||
|
||||
for _, tt := range tests {
|
||||
t.Run(tt.name, func(t *testing.T) {
|
||||
result := ds.extractRelaysFromEvents(tt.events)
|
||||
|
||||
// For empty case, check length
|
||||
if len(tt.expected) == 0 {
|
||||
assert.Empty(t, result)
|
||||
return
|
||||
}
|
||||
|
||||
// Check that all expected relays are present (order may vary)
|
||||
assert.Len(t, result, len(tt.expected))
|
||||
for _, expected := range tt.expected {
|
||||
assert.Contains(t, result, expected)
|
||||
}
|
||||
})
|
||||
}
|
||||
}
|
||||
|
||||
func TestDirectorySpiderLifecycle(t *testing.T) {
|
||||
ctx, cancel := context.WithCancel(context.Background())
|
||||
defer cancel()
|
||||
|
||||
// Create spider without database (will return error)
|
||||
_, err := NewDirectorySpider(ctx, nil, nil, 0, 0)
|
||||
require.Error(t, err)
|
||||
assert.Contains(t, err.Error(), "database cannot be nil")
|
||||
}
|
||||
|
||||
func TestDirectorySpiderDefaults(t *testing.T) {
|
||||
// Test that defaults are applied correctly
|
||||
assert.Equal(t, 24*time.Hour, DirectorySpiderDefaultInterval)
|
||||
assert.Equal(t, 3, DirectorySpiderDefaultMaxHops)
|
||||
assert.Equal(t, 30*time.Second, DirectorySpiderRelayTimeout)
|
||||
assert.Equal(t, 60*time.Second, DirectorySpiderQueryTimeout)
|
||||
assert.Equal(t, 500*time.Millisecond, DirectorySpiderRelayDelay)
|
||||
assert.Equal(t, 5000, DirectorySpiderMaxEventsPerQuery)
|
||||
}
|
||||
|
||||
func TestTriggerNow(t *testing.T) {
|
||||
ctx, cancel := context.WithCancel(context.Background())
|
||||
defer cancel()
|
||||
|
||||
ds := &DirectorySpider{
|
||||
ctx: ctx,
|
||||
triggerChan: make(chan struct{}, 1),
|
||||
}
|
||||
|
||||
// First trigger should succeed
|
||||
ds.TriggerNow()
|
||||
|
||||
// Verify trigger was sent
|
||||
select {
|
||||
case <-ds.triggerChan:
|
||||
// Expected
|
||||
default:
|
||||
t.Error("trigger was not sent")
|
||||
}
|
||||
|
||||
// Second trigger while channel is empty should also succeed
|
||||
ds.TriggerNow()
|
||||
|
||||
// But if we trigger again without draining, it should not block
|
||||
ds.TriggerNow() // Should not block due to select default case
|
||||
}
|
||||
|
||||
func TestLastRun(t *testing.T) {
|
||||
ds := &DirectorySpider{}
|
||||
|
||||
// Initially should be zero
|
||||
assert.True(t, ds.LastRun().IsZero())
|
||||
|
||||
// Set a time
|
||||
now := time.Now()
|
||||
ds.lastRun = now
|
||||
|
||||
// Should return the set time
|
||||
assert.Equal(t, now, ds.LastRun())
|
||||
}
|
||||
@@ -1 +1 @@
|
||||
v0.30.1
|
||||
v0.31.0
|
||||
186
readme.adoc
186
readme.adoc
@@ -37,7 +37,7 @@ ORLY is a standard Go application that can be built using the Go toolchain.
|
||||
|
||||
=== prerequisites
|
||||
|
||||
- Go 1.25.0 or later
|
||||
- Go 1.25.3 or later
|
||||
- Git
|
||||
- For web UI: link:https://bun.sh/[Bun] JavaScript runtime
|
||||
|
||||
@@ -179,7 +179,7 @@ cd next.orly.dev
|
||||
|
||||
The script will:
|
||||
|
||||
1. **Install Go 1.25.0** if not present (in `~/.local/go`)
|
||||
1. **Install Go 1.25.3** if not present (in `~/.local/go`)
|
||||
2. **Configure environment** by creating `~/.goenv` and updating `~/.bashrc`
|
||||
3. **Build the relay** with embedded web UI using `update-embedded-web.sh`
|
||||
4. **Set capabilities** for port 443 binding (requires sudo)
|
||||
@@ -342,6 +342,165 @@ For detailed testing instructions, multi-relay testing scenarios, and advanced u
|
||||
|
||||
The benchmark suite provides comprehensive performance testing and comparison across multiple relay implementations, including throughput, latency, and memory usage metrics.
|
||||
|
||||
== command-line tools
|
||||
|
||||
ORLY includes several command-line utilities in the `cmd/` directory for testing, debugging, and administration.
|
||||
|
||||
=== relay-tester
|
||||
|
||||
Nostr protocol compliance testing tool. Validates that a relay correctly implements the Nostr protocol specification.
|
||||
|
||||
[source,bash]
|
||||
----
|
||||
# Run all protocol compliance tests
|
||||
go run ./cmd/relay-tester -url ws://localhost:3334
|
||||
|
||||
# List available tests
|
||||
go run ./cmd/relay-tester -list
|
||||
|
||||
# Run specific test
|
||||
go run ./cmd/relay-tester -url ws://localhost:3334 -test "Basic Event"
|
||||
|
||||
# Output results as JSON
|
||||
go run ./cmd/relay-tester -url ws://localhost:3334 -json
|
||||
----
|
||||
|
||||
=== benchmark
|
||||
|
||||
Comprehensive relay performance benchmarking tool. Tests event storage, queries, and subscription performance with detailed latency metrics (P90, P95, P99).
|
||||
|
||||
[source,bash]
|
||||
----
|
||||
# Run benchmarks against local database
|
||||
go run ./cmd/benchmark -data-dir /tmp/bench-db -events 10000 -workers 4
|
||||
|
||||
# Run benchmarks against a running relay
|
||||
go run ./cmd/benchmark -relay ws://localhost:3334 -events 5000
|
||||
|
||||
# Use different database backends
|
||||
go run ./cmd/benchmark -dgraph -events 10000
|
||||
go run ./cmd/benchmark -neo4j -events 10000
|
||||
----
|
||||
|
||||
The `cmd/benchmark/` directory also includes Docker Compose configurations for comparative benchmarks across multiple relay implementations (strfry, nostr-rs-relay, khatru, etc.).
|
||||
|
||||
=== stresstest
|
||||
|
||||
Load testing tool for evaluating relay performance under sustained high-traffic conditions. Generates events with random content and tags to simulate realistic workloads.
|
||||
|
||||
[source,bash]
|
||||
----
|
||||
# Run stress test with 10 concurrent workers
|
||||
go run ./cmd/stresstest -url ws://localhost:3334 -workers 10 -duration 60s
|
||||
|
||||
# Generate events with random p-tags (up to 100 per event)
|
||||
go run ./cmd/stresstest -url ws://localhost:3334 -workers 5
|
||||
----
|
||||
|
||||
=== blossomtest
|
||||
|
||||
Tests the Blossom blob storage protocol (BUD-01/BUD-02) implementation. Validates upload, download, and authentication flows.
|
||||
|
||||
[source,bash]
|
||||
----
|
||||
# Test with generated key
|
||||
go run ./cmd/blossomtest -url http://localhost:3334 -size 1024
|
||||
|
||||
# Test with specific nsec
|
||||
go run ./cmd/blossomtest -url http://localhost:3334 -nsec nsec1...
|
||||
|
||||
# Test anonymous uploads (no authentication)
|
||||
go run ./cmd/blossomtest -url http://localhost:3334 -no-auth
|
||||
----
|
||||
|
||||
=== aggregator
|
||||
|
||||
Event aggregation utility that fetches events from multiple relays using bloom filters for deduplication. Useful for syncing events across relays with memory-efficient duplicate detection.
|
||||
|
||||
[source,bash]
|
||||
----
|
||||
go run ./cmd/aggregator -relays wss://relay1.com,wss://relay2.com -output events.jsonl
|
||||
----
|
||||
|
||||
=== convert
|
||||
|
||||
Key format conversion utility. Converts between hex and bech32 (npub/nsec) formats for Nostr keys.
|
||||
|
||||
[source,bash]
|
||||
----
|
||||
# Convert npub to hex
|
||||
go run ./cmd/convert npub1abc...
|
||||
|
||||
# Convert hex to npub
|
||||
go run ./cmd/convert 0123456789abcdef...
|
||||
|
||||
# Convert secret key (nsec or hex) - outputs both nsec and derived npub
|
||||
go run ./cmd/convert --secret nsec1xyz...
|
||||
----
|
||||
|
||||
=== FIND
|
||||
|
||||
Free Internet Name Daemon - CLI tool for the distributed naming system. Manages name registration, transfers, and certificate issuance.
|
||||
|
||||
[source,bash]
|
||||
----
|
||||
# Validate a name format
|
||||
go run ./cmd/FIND verify-name example.nostr
|
||||
|
||||
# Generate a new key pair
|
||||
go run ./cmd/FIND generate-key
|
||||
|
||||
# Create a registration proposal
|
||||
go run ./cmd/FIND register myname.nostr
|
||||
|
||||
# Transfer a name to a new owner
|
||||
go run ./cmd/FIND transfer myname.nostr npub1newowner...
|
||||
----
|
||||
|
||||
=== policytest
|
||||
|
||||
Tests the policy system for event write control. Validates that policy rules correctly allow or reject events based on kind, pubkey, and other criteria.
|
||||
|
||||
[source,bash]
|
||||
----
|
||||
go run ./cmd/policytest -url ws://localhost:3334 -type event -kind 4678
|
||||
go run ./cmd/policytest -url ws://localhost:3334 -type req -kind 1
|
||||
go run ./cmd/policytest -url ws://localhost:3334 -type publish-and-query -count 5
|
||||
----
|
||||
|
||||
=== policyfiltertest
|
||||
|
||||
Tests policy-based filtering with authorized and unauthorized pubkeys. Validates access control rules for specific users.
|
||||
|
||||
[source,bash]
|
||||
----
|
||||
go run ./cmd/policyfiltertest -url ws://localhost:3334 \
|
||||
-allowed-pubkey <hex> -allowed-sec <hex> \
|
||||
-unauthorized-pubkey <hex> -unauthorized-sec <hex>
|
||||
----
|
||||
|
||||
=== subscription-test
|
||||
|
||||
Tests WebSocket subscription stability over extended periods. Monitors for dropped subscriptions and connection issues.
|
||||
|
||||
[source,bash]
|
||||
----
|
||||
# Run subscription stability test for 60 seconds
|
||||
go run ./cmd/subscription-test -url ws://localhost:3334 -duration 60 -kind 1
|
||||
|
||||
# With verbose output
|
||||
go run ./cmd/subscription-test -url ws://localhost:3334 -duration 120 -v
|
||||
----
|
||||
|
||||
=== subscription-test-simple
|
||||
|
||||
Simplified subscription stability test that verifies subscriptions remain active without dropping over the test duration.
|
||||
|
||||
[source,bash]
|
||||
----
|
||||
go run ./cmd/subscription-test-simple -url ws://localhost:3334 -duration 120
|
||||
----
|
||||
|
||||
== access control
|
||||
|
||||
=== follows ACL
|
||||
@@ -378,3 +537,26 @@ export ORLY_CLUSTER_PROPAGATE_PRIVILEGED_EVENTS=false
|
||||
|
||||
**Important:** When disabled, privileged events will not be replicated to peer relays. This provides better privacy but means these events will only be available on the originating relay. Users should be aware that accessing their privileged events may require connecting directly to the relay where they were originally published.
|
||||
|
||||
== developer notes
|
||||
|
||||
=== binary-optimized tag storage
|
||||
|
||||
The nostr library (`git.mleku.dev/mleku/nostr/encoders/tag`) uses binary optimization for `e` and `p` tags to reduce memory usage and improve comparison performance.
|
||||
|
||||
When events are unmarshaled from JSON, 64-character hex values in e/p tags are converted to 33-byte binary format (32 bytes hash + null terminator).
|
||||
|
||||
**Important:** When working with e/p tag values in code:
|
||||
|
||||
* **DO NOT** use `tag.Value()` directly - it returns raw bytes which may be binary, not hex
|
||||
* **ALWAYS** use `tag.ValueHex()` to get a hex string regardless of storage format
|
||||
* **Use** `tag.ValueBinary()` to get raw 32-byte binary (returns nil if not binary-encoded)
|
||||
|
||||
[source,go]
|
||||
----
|
||||
// CORRECT: Use ValueHex() for hex decoding
|
||||
pt, err := hex.Dec(string(pTag.ValueHex()))
|
||||
|
||||
// WRONG: Value() may return binary bytes, not hex
|
||||
pt, err := hex.Dec(string(pTag.Value())) // Will fail for binary-encoded tags!
|
||||
----
|
||||
|
||||
|
||||
@@ -83,7 +83,7 @@ docker-compose -f docker-compose-test.yml down -v
|
||||
Multi-stage build for ORLY:
|
||||
|
||||
**Stage 1: Builder**
|
||||
- Based on golang:1.21-alpine
|
||||
- Based on golang:1.25-alpine
|
||||
- Downloads dependencies
|
||||
- Builds static binary with `CGO_ENABLED=0`
|
||||
- Copies libsecp256k1.so for crypto operations
|
||||
@@ -365,7 +365,7 @@ start_period: 60s # Default is 20-30s
|
||||
|
||||
# Pre-pull images
|
||||
docker pull dgraph/standalone:latest
|
||||
docker pull golang:1.21-alpine
|
||||
docker pull golang:1.25-alpine
|
||||
```
|
||||
|
||||
**High memory usage**
|
||||
|
||||
@@ -193,7 +193,7 @@ echo "=== All deployment script tests passed! ==="
|
||||
echo ""
|
||||
echo "The deployment script appears to be working correctly."
|
||||
echo "In a real deployment, it would:"
|
||||
echo " 1. Install Go 1.23.1 to ~/.local/go"
|
||||
echo " 1. Install Go 1.25.3 to ~/.local/go"
|
||||
echo " 2. Set up Go environment in ~/.goenv"
|
||||
echo " 3. Install build dependencies via ubuntu_install_libsecp256k1.sh"
|
||||
echo " 4. Build the relay with embedded web UI"
|
||||
|
||||
@@ -33,11 +33,11 @@ if [[ ! -x "$BENCHMARK_BIN" ]]; then
|
||||
echo "Building benchmark binary (pure Go + purego)..."
|
||||
cd "$REPO_ROOT/cmd/benchmark"
|
||||
CGO_ENABLED=0 go build -o "$BENCHMARK_BIN" .
|
||||
# Download libsecp256k1.so from nostr repository (runtime optional)
|
||||
wget -q https://git.mleku.dev/mleku/nostr/raw/branch/main/crypto/p8k/libsecp256k1.so \
|
||||
-O "$(dirname "$BENCHMARK_BIN")/libsecp256k1.so" 2>/dev/null || \
|
||||
echo "Warning: Failed to download libsecp256k1.so (optional for performance)"
|
||||
chmod +x "$(dirname "$BENCHMARK_BIN")/libsecp256k1.so" 2>/dev/null || true
|
||||
# Copy libsecp256k1.so from repo root (runtime optional)
|
||||
if [[ -f "$REPO_ROOT/libsecp256k1.so" ]]; then
|
||||
cp "$REPO_ROOT/libsecp256k1.so" "$(dirname "$BENCHMARK_BIN")/"
|
||||
chmod +x "$(dirname "$BENCHMARK_BIN")/libsecp256k1.so" 2>/dev/null || true
|
||||
fi
|
||||
cd "$REPO_ROOT"
|
||||
fi
|
||||
|
||||
|
||||
@@ -197,13 +197,12 @@ build_application() {
|
||||
log_info "Building binary in current directory (pure Go + purego)..."
|
||||
CGO_ENABLED=0 go build -o "$BINARY_NAME"
|
||||
|
||||
# Download libsecp256k1.so from nostr repository (optional, for runtime performance)
|
||||
log_info "Downloading libsecp256k1.so from nostr repository..."
|
||||
if wget -q https://git.mleku.dev/mleku/nostr/raw/branch/main/crypto/p8k/libsecp256k1.so -O libsecp256k1.so; then
|
||||
# Verify libsecp256k1.so exists in repo (used by purego for runtime crypto)
|
||||
if [[ -f "./libsecp256k1.so" ]]; then
|
||||
chmod +x libsecp256k1.so
|
||||
log_success "Downloaded libsecp256k1.so successfully (runtime optional)"
|
||||
log_success "Found libsecp256k1.so in repository"
|
||||
else
|
||||
log_warning "Failed to download libsecp256k1.so - relay will still work but may have slower crypto"
|
||||
log_warning "libsecp256k1.so not found in repo - relay will still work but may have slower crypto"
|
||||
fi
|
||||
|
||||
if [[ -f "./$BINARY_NAME" ]]; then
|
||||
|
||||
@@ -3,9 +3,8 @@
|
||||
# libsecp256k1 is loaded dynamically at runtime if available
|
||||
export CGO_ENABLED=0
|
||||
|
||||
# Download libsecp256k1.so from nostr repository if not present
|
||||
if [ ! -f "libsecp256k1.so" ]; then
|
||||
wget -q https://git.mleku.dev/mleku/nostr/raw/branch/main/crypto/p8k/libsecp256k1.so -O libsecp256k1.so 2>/dev/null || true
|
||||
# Verify libsecp256k1.so exists in repo (should be at repo root)
|
||||
if [ -f "libsecp256k1.so" ]; then
|
||||
chmod +x libsecp256k1.so 2>/dev/null || true
|
||||
fi
|
||||
|
||||
|
||||
@@ -3,12 +3,10 @@
|
||||
# libsecp256k1 is loaded dynamically at runtime if available
|
||||
export CGO_ENABLED=0
|
||||
|
||||
# Download libsecp256k1.so from nostr repository if not present
|
||||
# Verify libsecp256k1.so exists in repo (should be at repo root)
|
||||
if [ ! -f "libsecp256k1.so" ]; then
|
||||
echo "Downloading libsecp256k1.so from nostr repository..."
|
||||
wget -q https://git.mleku.dev/mleku/nostr/raw/branch/main/crypto/p8k/libsecp256k1.so -O libsecp256k1.so || {
|
||||
echo "Warning: Failed to download libsecp256k1.so - tests may fail"
|
||||
}
|
||||
echo "Warning: libsecp256k1.so not found in repo - tests may use fallback crypto"
|
||||
else
|
||||
chmod +x libsecp256k1.so 2>/dev/null || true
|
||||
fi
|
||||
|
||||
|
||||
Reference in New Issue
Block a user