Browse Source

Fix negentropy convergence and update nostr dependency

- Update nostr dependency to use local replace for v1.0.18 fixes:
  channels→slices, frame size enforcement, responder ID list splitting
- Fix protoToFilter() to convert Ids, Kinds, Authors, Since, Until fields
  from proto filter (was only converting Limit)
- Refactor manager sync flow into phases (reconcile → fetch → push)
- Expand negentropy test infrastructure with orly relay support

Files modified:
- go.mod, go.sum: Add replace directive for nostr v1.0.18
- pkg/sync/negentropy/embedded.go: Fix protoToFilter conversion
- pkg/sync/negentropy/manager.go: Refactor sync phases
- tests/negentropy/: Expand test infrastructure

Co-Authored-By: Claude Opus 4.5 <noreply@anthropic.com>
main
woikos 4 months ago
parent
commit
6ff1cbbdd4
No known key found for this signature in database
  1. 2
      go.mod
  2. 2
      go.sum
  3. 32
      pkg/sync/negentropy/embedded.go
  4. 78
      pkg/sync/negentropy/manager.go
  5. 8
      tests/negentropy/Dockerfile.test-runner
  6. 143
      tests/negentropy/README.md
  7. 174
      tests/negentropy/comprehensive-test.sh
  8. 40
      tests/negentropy/docker-compose.yml

2
go.mod

@ -268,4 +268,6 @@ require (
gvisor.dev/gvisor v0.0.0-20250503011706-39ed1f5ac29c // indirect gvisor.dev/gvisor v0.0.0-20250503011706-39ed1f5ac29c // indirect
) )
replace git.mleku.dev/mleku/nostr => /home/mleku/src/git.mleku.dev/mleku/nostr
retract v1.0.3 retract v1.0.3

2
go.sum

@ -38,8 +38,6 @@ cloud.google.com/go/storage v1.6.0/go.mod h1:N7U0C8pVQ/+NIKOBQyamJIeKQKkZ+mxpohl
cloud.google.com/go/storage v1.8.0/go.mod h1:Wv1Oy7z6Yz3DshWRJFhqM/UCfaWIRTdp0RXyy7KQOVs= cloud.google.com/go/storage v1.8.0/go.mod h1:Wv1Oy7z6Yz3DshWRJFhqM/UCfaWIRTdp0RXyy7KQOVs=
cloud.google.com/go/storage v1.10.0/go.mod h1:FLPqc6j+Ki4BU591ie1oL6qBQGu2Bl/tZ9ullr3+Kg0= cloud.google.com/go/storage v1.10.0/go.mod h1:FLPqc6j+Ki4BU591ie1oL6qBQGu2Bl/tZ9ullr3+Kg0=
dmitri.shuralyov.com/gpu/mtl v0.0.0-20190408044501-666a987793e9/go.mod h1:H6x//7gZCb22OMCxBHrMx7a5I7Hp++hsVxbQ4BYO7hU= dmitri.shuralyov.com/gpu/mtl v0.0.0-20190408044501-666a987793e9/go.mod h1:H6x//7gZCb22OMCxBHrMx7a5I7Hp++hsVxbQ4BYO7hU=
git.mleku.dev/mleku/nostr v1.0.17 h1:FtJtKJWILra0/yO3hQ7+W3nF3OXpAeGiBcoSCWIY1Ds=
git.mleku.dev/mleku/nostr v1.0.17/go.mod h1:WzCvfe5iJjgoWtxIzSaNxAkpaz42ZL5cyCVQeR73CUs=
github.com/AdamSLevy/jsonrpc2/v14 v14.1.0 h1:Dy3M9aegiI7d7PF1LUdjbVigJReo+QOceYsMyFh9qoE= github.com/AdamSLevy/jsonrpc2/v14 v14.1.0 h1:Dy3M9aegiI7d7PF1LUdjbVigJReo+QOceYsMyFh9qoE=
github.com/AdamSLevy/jsonrpc2/v14 v14.1.0/go.mod h1:ZakZtbCXxCz82NJvq7MoREtiQesnDfrtF6RFUGzQfLo= github.com/AdamSLevy/jsonrpc2/v14 v14.1.0/go.mod h1:ZakZtbCXxCz82NJvq7MoREtiQesnDfrtF6RFUGzQfLo=
github.com/Azure/azure-sdk-for-go v68.0.0+incompatible h1:fcYLmCpyNYRnvJbPerq7U0hS+6+I79yEDJBqVNcqUzU= github.com/Azure/azure-sdk-for-go v68.0.0+incompatible h1:fcYLmCpyNYRnvJbPerq7U0hS+6+I79yEDJBqVNcqUzU=

32
pkg/sync/negentropy/embedded.go

@ -8,6 +8,9 @@ import (
"lol.mleku.dev/log" "lol.mleku.dev/log"
"git.mleku.dev/mleku/nostr/encoders/filter" "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/encoders/timestamp"
negentropylib "git.mleku.dev/mleku/nostr/negentropy" negentropylib "git.mleku.dev/mleku/nostr/negentropy"
"next.orly.dev/pkg/database" "next.orly.dev/pkg/database"
negentropyiface "next.orly.dev/pkg/interfaces/negentropy" negentropyiface "next.orly.dev/pkg/interfaces/negentropy"
@ -252,6 +255,35 @@ func protoToFilter(pf *commonv1.Filter) *filter.F {
f := &filter.F{} f := &filter.F{}
// Convert IDs (binary 32-byte event IDs)
if len(pf.Ids) > 0 {
f.Ids = &tag.T{T: pf.Ids}
}
// Convert Kinds (uint32 → kind.K with uint16)
if len(pf.Kinds) > 0 {
ks := kind.NewWithCap(len(pf.Kinds))
for _, k := range pf.Kinds {
ks.K = append(ks.K, kind.New(k))
}
f.Kinds = ks
}
// Convert Authors (binary 32-byte pubkeys)
if len(pf.Authors) > 0 {
f.Authors = &tag.T{T: pf.Authors}
}
// Convert Since
if pf.Since != nil {
f.Since = timestamp.New(*pf.Since)
}
// Convert Until
if pf.Until != nil {
f.Until = timestamp.New(*pf.Until)
}
// Convert Limit // Convert Limit
if pf.Limit != nil { if pf.Limit != nil {
limit := uint(*pf.Limit) limit := uint(*pf.Limit)

78
pkg/sync/negentropy/manager.go

@ -260,16 +260,12 @@ func (m *Manager) performNegentropy(ctx context.Context, peerURL string) (int64,
var eventsSynced int64 var eventsSynced int64
var needIDs []string var needIDs []string
var haveIDs []string var haveIDs []string
var waitingForEvents bool
var expectedEvents int
var receivedEvents int64
// Exchange messages until complete // Phase 1: Reconciliation - exchange NEG-MSG until complete
for i := 0; i < 20; i++ { // Max 20 rounds for i := 0; i < 20; i++ { // Max 20 reconciliation rounds
// Read response
_, msgBytes, err := conn.ReadMessage() _, msgBytes, err := conn.ReadMessage()
if err != nil { if err != nil {
return eventsSynced, fmt.Errorf("failed to read message: %w", err) return eventsSynced, fmt.Errorf("failed to read message during reconciliation: %w", err)
} }
var msg []json.RawMessage var msg []json.RawMessage
@ -310,28 +306,19 @@ func (m *Manager) performNegentropy(ctx context.Context, peerURL string) (int64,
needIDs = append(needIDs, neg.CollectHaveNots()...) needIDs = append(needIDs, neg.CollectHaveNots()...)
haveIDs = append(haveIDs, neg.CollectHaves()...) haveIDs = append(haveIDs, neg.CollectHaves()...)
if complete { // Always send the response to the server, even when complete.
log.I.F("negentropy: reconciliation complete, waiting for %d events from peer", len(needIDs)) // The server needs this to finalize its own reconciliation and send events.
// Don't send NEG-CLOSE yet - wait for EVENT messages if len(response) > 0 {
// The server will send events we need after this
waitingForEvents = true
expectedEvents = len(needIDs)
if expectedEvents == 0 {
// No events to receive, close now
negClose := []any{"NEG-CLOSE", subID}
conn.WriteJSON(negClose)
goto done
}
// Set a read deadline to avoid waiting forever
conn.SetReadDeadline(time.Now().Add(30 * time.Second))
continue
}
// Send NEG-MSG response
negMsgResp := []any{"NEG-MSG", subID, hex.EncodeToString(response)} negMsgResp := []any{"NEG-MSG", subID, hex.EncodeToString(response)}
if err := conn.WriteJSON(negMsgResp); err != nil { if err := conn.WriteJSON(negMsgResp); err != nil {
return eventsSynced, fmt.Errorf("failed to send NEG-MSG: %w", err) return eventsSynced, fmt.Errorf("failed to send NEG-MSG: %w", err)
} }
}
if complete {
log.I.F("negentropy: reconciliation complete, need %d events, have %d to push", len(needIDs), len(haveIDs))
goto fetchAndPush
}
case "NEG-ERR": case "NEG-ERR":
var errMsg string var errMsg string
@ -339,34 +326,35 @@ func (m *Manager) performNegentropy(ctx context.Context, peerURL string) (int64,
json.Unmarshal(msg[2], &errMsg) json.Unmarshal(msg[2], &errMsg)
} }
return eventsSynced, fmt.Errorf("peer returned error: %s", errMsg) return eventsSynced, fmt.Errorf("peer returned error: %s", errMsg)
case "EVENT":
// Peer is sending us an event
if len(msg) >= 3 {
if err := m.storeEventFromJSON(ctx, msg[2]); err != nil {
log.W.F("negentropy: failed to store event from peer: %v", err)
} else {
eventsSynced++
receivedEvents++
} }
} }
// If we've received all expected events (or more), we can close
if waitingForEvents && receivedEvents >= int64(expectedEvents) { fetchAndPush:
// Send NEG-CLOSE to end the negentropy session
{
negClose := []any{"NEG-CLOSE", subID} negClose := []any{"NEG-CLOSE", subID}
conn.WriteJSON(negClose) conn.WriteJSON(negClose)
goto done
}
}
} }
// Clear any read deadline from the negotiation phase
conn.SetReadDeadline(time.Time{})
done:
log.I.F("negentropy: need %d events, have %d events to send", len(needIDs), len(haveIDs)) log.I.F("negentropy: need %d events, have %d events to send", len(needIDs), len(haveIDs))
// Phase 2: Fetch events we need from the peer via REQ
// The negentropy library only populates haves/haveNots on the initiator (client) side.
// The server (responder) does not know which events to push. The client must
// actively fetch needed events using standard NIP-01 REQ with ID prefixes.
if len(needIDs) > 0 { if len(needIDs) > 0 {
log.I.F("negentropy: first few need IDs: %v", needIDs[:min(len(needIDs), 3)]) fetched, err := m.fetchEventsFromPeer(ctx, conn, subID, needIDs)
if err != nil {
log.W.F("negentropy: failed to fetch events: %v", err)
} else {
log.I.F("negentropy: fetched %d events from peer", fetched)
eventsSynced += int64(fetched)
}
} }
// PUSH events we have to the peer (haveIDs) // Phase 3: Push events we have to the peer
// This is more reliable than trying to PULL events using ID prefixes
if len(haveIDs) > 0 { if len(haveIDs) > 0 {
pushed, err := m.pushEventsToPeer(ctx, conn, haveIDs) pushed, err := m.pushEventsToPeer(ctx, conn, haveIDs)
if err != nil { if err != nil {
@ -377,10 +365,6 @@ done:
} }
} }
// NOTE: Events we need (needIDs) will be pushed to us by the peer's sync process
// The peer runs the same negentropy sync and will identify these events as "haves"
// to push to us. We don't need to explicitly fetch them.
return eventsSynced, nil return eventsSynced, nil
} }

8
tests/negentropy/Dockerfile.test-runner

@ -1,4 +1,4 @@
# Test Runner Dockerfile with event generator and test tools # Test Runner Dockerfile with event generator, orly CLI, and test tools
FROM golang:1.24-alpine AS builder FROM golang:1.24-alpine AS builder
@ -10,6 +10,9 @@ COPY . .
# Build event generator as a static binary (use vendored deps for local replace directives) # Build event generator as a static binary (use vendored deps for local replace directives)
RUN GOTOOLCHAIN=auto CGO_ENABLED=0 go build -mod=vendor -ldflags='-extldflags=-static' -o event-generator ./tests/negentropy/event-generator RUN GOTOOLCHAIN=auto CGO_ENABLED=0 go build -mod=vendor -ldflags='-extldflags=-static' -o event-generator ./tests/negentropy/event-generator
# Build unified orly CLI for sync testing (includes "orly sync" subcommand)
RUN GOTOOLCHAIN=auto CGO_ENABLED=0 go build -mod=vendor -o orly ./cmd/orly
# Runtime image # Runtime image
FROM alpine:3.21 FROM alpine:3.21
@ -19,7 +22,8 @@ RUN apk add --no-cache ca-certificates curl jq bash
RUN curl -L -o /usr/local/bin/websocat https://github.com/vi/websocat/releases/download/v1.13.0/websocat.x86_64-unknown-linux-musl && \ RUN curl -L -o /usr/local/bin/websocat https://github.com/vi/websocat/releases/download/v1.13.0/websocat.x86_64-unknown-linux-musl && \
chmod +x /usr/local/bin/websocat chmod +x /usr/local/bin/websocat
# Copy event-generator binary # Copy event-generator and orly binaries
COPY --from=builder /build/event-generator /usr/local/bin/ COPY --from=builder /build/event-generator /usr/local/bin/
COPY --from=builder /build/orly /usr/local/bin/
CMD ["bash"] CMD ["bash"]

143
tests/negentropy/README.md

@ -1,55 +1,47 @@
# Comprehensive Negentropy Sync Test Suite # Comprehensive Negentropy Sync Test Suite
This test suite validates NIP-77 negentropy synchronization between ORLY and strfry relays in all possible configurations. Tests NIP-77 negentropy synchronization between ORLY and strfry relays.
## Test Scenarios ## Test Scenarios
### 1. Orly as Relay, Strfry as Client ### 1. strfry as Client, ORLY as Server (Phases 1-6)
Uses `strfry sync` command to test: Uses `strfry sync` command to test:
- **Push**: strfry → orly-relay-1 - **Push**: strfry → orly-relay-1
- **Pull**: strfry ← orly-relay-1 - **Pull**: strfry ← orly-relay-1
- **Bidirectional**: strfry ↔ orly-relay-1 - **Bidirectional**: strfry ↔ orly-relay-1
### 2. Strfry as Relay, Orly as Client ### 2. ORLY as Client, ORLY as Server (Phases 7-10)
Uses `orly sync` with gRPC client mode: Uses `orly sync` CLI with a temporary Badger DB as a bridge:
- **Push**: orly-relay-2 → strfry - **relay-1 → relay-2**: orly CLI pulls from relay-1, pushes to relay-2
- **Pull**: orly-relay-2 ← strfry - **relay-2 → relay-1**: orly CLI pushes relay-2 events to relay-1
- **Bidirectional**: orly-relay-2 ↔ strfry - **Three-way consistency**: strfry, relay-1, relay-2 converge
### 3. Dual Orly with gRPC Control
Two ORLY relays synchronized via gRPC sync services:
- **orly-relay-1****orly-relay-2** via gRPC-controlled sync
## Infrastructure ## Infrastructure
``` ```
┌─────────────┐ ┌─────────────┐ ┌─────────────┐ NIP-77 ┌──────────────┐
│ strfry │◄───────►│ orly-relay-1│ │ strfry │◄─────────►│ orly-relay-1 │
│ (7777) │ NIP-77 │ (3334) │ │ (7777) │ │ (3334) │
└──────┬──────┘ └──────┬──────┘ └─────────────┘ └──────┬───────┘
│ │
│ │ gRPC orly sync CLI
│ ┌─────┴──────┐ (bridge DB in
│ │ orly-sync-1│ test-runner)
│ │ (50064) │
│ └────────────┘
│ NIP-77 ┌─────────────┐ ┌──────┴───────┐
└────────────────►│ orly-relay-2 │ orly-relay-2
│ (3335) │ │ (3335) │
└──────┬──────┘ └──────────────┘
│ gRPC
┌─────┴──────┐
│ orly-sync-2│
│ (50064) │
└────────────┘
``` ```
The `orly sync` CLI runs inside the test-runner container. It opens a
temporary Badger database and uses NIP-77 negentropy to sync with each
relay, effectively bridging events between the two ORLY instances.
## Quick Start ## Quick Start
### Prerequisites ### Prerequisites
- Docker and Docker Compose - Docker and Docker Compose
- Go 1.24+ (for local event generator builds)
### Run All Tests ### Run All Tests
@ -63,41 +55,39 @@ docker compose build
docker compose up -d docker compose up -d
# Run comprehensive tests # Run comprehensive tests
docker compose exec test-runner /tests/comprehensive-test.sh ./comprehensive-test.sh
# Or with verbose output # Or with verbose output
docker compose exec test-runner /tests/comprehensive-test.sh --verbose ./comprehensive-test.sh --verbose
# Clean up # Clean up
docker compose down -v docker compose down -v
``` ```
### Run Individual Test Phases ### Manual Operations
```bash ```bash
# Enter test runner container # Generate events
docker compose exec test-runner bash docker compose exec -T test-runner event-generator -count 500 -relay ws://strfry:7777
# Check relay status # strfry sync (strfry as client, ORLY as server)
echo "Strfry events: $(count_events ws://strfry:7777 '{"limit": 1000}')" docker compose exec -T strfry /app/strfry --config=/etc/strfry.conf sync ws://orly-relay-1:3334 --dir down
echo "Orly-1 events: $(count_events ws://orly-relay-1:3334 '{"limit": 1000}')"
echo "Orly-2 events: $(count_events ws://orly-relay-2:3335 '{"limit": 1000}')"
# Generate events manually # orly sync (orly CLI as client, any relay as server)
event-generator -count 500 -relay ws://strfry:7777 docker compose exec -T test-runner orly sync ws://orly-relay-1:3334 --data-dir /tmp/sync-db
# Test strfry as client (pull) # Manual event inspection
docker compose exec strfry /app/strfry sync ws://orly-relay-1:3334 --dir down echo '["REQ", "test", {"limit": 10}]' | websocat ws://localhost:7777
echo '["REQ", "test", {"limit": 10}]' | websocat ws://localhost:3334
# Test orly as client via gRPC (pull) echo '["REQ", "test", {"limit": 10}]' | websocat ws://localhost:3335
orly sync ws://strfry:7777 --server orly-sync-2:50064 --dir down --verbose
``` ```
## Test Parameters ## Test Parameters
- **Total Events**: 1200+ per seed operation - **Seed Events**: 200 per relay
- **Event Kinds**: 0, 1, 3, 1984, 10000, 10001, 30023, 30078 - **Extra Events**: 100
- **Batch Size**: 100 events per batch - **Event Kinds**: 0, 1, 3, 1984, 10000, 10001, 30023
- **Batch Size**: 50 events per batch
- **Authors**: 3 test keypairs (alice, bob, carol) - **Authors**: 3 test keypairs (alice, bob, carol)
### Event Distribution ### Event Distribution
@ -112,33 +102,6 @@ orly sync ws://strfry:7777 --server orly-sync-2:50064 --dir down --verbose
| 10001| 3% | Pin lists | | 10001| 3% | Pin lists |
| 30023| 2% | Long-form articles | | 30023| 2% | Long-form articles |
## Filter Testing
The test suite validates sync with various filters:
```bash
# Kind filter
'{"kinds": [1, 3]}'
# Time range
'{"since": 1700000000, "until": 1800000000}'
# Limit
'{"limit": 100}'
# Combined
'{"kinds": [1], "since": 1700000000, "limit": 500}'
```
## Verification
Tests verify:
1. Event counts match expected values
2. Bidirectional sync achieves consistency
3. Filtered sync respects constraints
4. Different event kinds sync correctly
5. No data corruption during sync
## Troubleshooting ## Troubleshooting
### Check service health ### Check service health
@ -146,15 +109,7 @@ Tests verify:
docker compose ps docker compose ps
docker compose logs -f strfry docker compose logs -f strfry
docker compose logs -f orly-relay-1 docker compose logs -f orly-relay-1
``` docker compose logs -f orly-relay-2
### Manual event inspection
```bash
# Get events from strfry
echo '["REQ", "test", {"limit": 10}]' | websocat ws://localhost:7777
# Get events from orly
echo '["REQ", "test", {"limit": 10}]' | websocat ws://localhost:3334
``` ```
### Reset test data ### Reset test data
@ -168,18 +123,14 @@ docker compose up -d
### Strfry ### Strfry
- Image: Built from source (Dockerfile.strfry) - Image: Built from source (Dockerfile.strfry)
- Port: 7777 - Port: 7777
- Features: Full NIP-77 negentropy support - Features: Full NIP-77 negentropy support, `strfry sync` CLI
### Orly Relay ### ORLY Relay
- Image: Built from project (Dockerfile.orly) - Image: Built from project (Dockerfile.orly)
- Ports: 3334, 3335 - Ports: 3334 (relay-1), 3335 (relay-2)
- Features: NIP-77 negentropy + gRPC database interface - Features: NIP-77 negentropy via embedded handler
- Note: `ORLY_QUERY_RESULT_LIMIT=10000` set for testing (default is 256)
### Orly Sync Service
- Image: Built from cmd/orly-sync-negentropy
- Ports: 50064, 50065
- Features: gRPC-controlled negentropy sync
### Test Runner ### Test Runner
- Image: Built with all test tools - Image: Alpine with test tools (Dockerfile.test-runner)
- Features: event-generator, websocat, orly CLI - Features: event-generator, orly CLI, websocat, curl, jq

174
tests/negentropy/comprehensive-test.sh

@ -1,10 +1,13 @@
#!/bin/bash #!/bin/bash
# #
# Comprehensive Negentropy Sync Test Suite # Comprehensive Negentropy Sync Test Suite
# Tests NIP-77 negentropy sync between strfry (client) and ORLY (server). # Tests NIP-77 negentropy sync between:
# - strfry (client) <-> ORLY (server)
# - ORLY (client) <-> ORLY (server) via orly sync CLI bridge
# #
# Strfry has a built-in `sync` command that uses the negentropy protocol. # Strfry has a built-in `sync` command that uses the negentropy protocol.
# ORLY serves NIP-77 via its embedded negentropy handler. # ORLY serves NIP-77 via its embedded negentropy handler.
# The orly CLI can sync between its local Badger DB and a remote relay.
# #
# This script runs from the HOST and uses `docker compose exec` to # This script runs from the HOST and uses `docker compose exec` to
# interact with containers. # interact with containers.
@ -14,8 +17,12 @@
# 2. Strfry pushes events to ORLY (strfry --dir up) # 2. Strfry pushes events to ORLY (strfry --dir up)
# 3. Seed ORLY with new events # 3. Seed ORLY with new events
# 4. Strfry pulls events from ORLY (strfry --dir down) # 4. Strfry pulls events from ORLY (strfry --dir down)
# 5. Bidirectional sync # 5. Bidirectional sync (strfry <-> ORLY)
# 6. Final consistency verification # 6. strfry <-> ORLY consistency verification
# 7. Seed orly-relay-2 with events
# 8. orly CLI bridge: relay-1 -> relay-2
# 9. orly CLI bridge: relay-2 -> relay-1
# 10. Three-way consistency verification
# #
# Usage: # Usage:
# cd tests/negentropy # cd tests/negentropy
@ -32,6 +39,8 @@ cd "$(dirname "$0")"
# Configuration # Configuration
STRFRY_WS="ws://strfry:7777" STRFRY_WS="ws://strfry:7777"
ORLY_WS="ws://orly-relay-1:3334" ORLY_WS="ws://orly-relay-1:3334"
ORLY2_WS="ws://orly-relay-2:3335"
BRIDGE_DB="/tmp/orly-bridge-db"
SEED_COUNT=200 SEED_COUNT=200
EXTRA_COUNT=100 EXTRA_COUNT=100
VERBOSE="${VERBOSE:-false}" VERBOSE="${VERBOSE:-false}"
@ -105,7 +114,7 @@ generate_events() {
wait_for_services() { wait_for_services() {
log_info "Checking service health..." log_info "Checking service health..."
local services=("strfry" "orly-relay-1" "test-runner") local services=("strfry" "orly-relay-1" "orly-relay-2" "test-runner")
for svc in "${services[@]}"; do for svc in "${services[@]}"; do
local status local status
status=$(docker compose ps --format '{{.Health}}' "$svc" 2>/dev/null || echo "unknown") status=$(docker compose ps --format '{{.Health}}' "$svc" 2>/dev/null || echo "unknown")
@ -257,16 +266,16 @@ phase5_bidirectional() {
} }
# ============================================================ # ============================================================
# Phase 6: Final verification # Phase 6: strfry <-> ORLY consistency verification
# ============================================================ # ============================================================
phase6_final_verification() { phase6_strfry_orly_verification() {
log_phase "6. FINAL VERIFICATION" log_phase "6. STRFRY <-> ORLY VERIFICATION"
local strfry_total orly_total local strfry_total orly_total
strfry_total=$(count_events "$STRFRY_WS" '{"limit":10000}') strfry_total=$(count_events "$STRFRY_WS" '{"limit":10000}')
orly_total=$(count_events "$ORLY_WS" '{"limit":10000}') orly_total=$(count_events "$ORLY_WS" '{"limit":10000}')
log_info "Final event counts:" log_info "Event counts:"
log_info " strfry: $strfry_total" log_info " strfry: $strfry_total"
log_info " orly-relay-1: $orly_total" log_info " orly-relay-1: $orly_total"
@ -280,9 +289,143 @@ phase6_final_verification() {
# Check consistency # Check consistency
local diff=$((strfry_total - orly_total)) local diff=$((strfry_total - orly_total))
if [ "${diff#-}" -le 50 ]; then if [ "${diff#-}" -le 50 ]; then
log_pass "Relays are consistent (diff: $diff)" log_pass "strfry and ORLY are consistent (diff: $diff)"
else
log_warn "strfry and ORLY differ by $diff events"
fi
}
# ============================================================
# Phase 7: Seed orly-relay-2 with events
# ============================================================
phase7_seed_orly2() {
log_phase "7. SEED ORLY-RELAY-2 - Generate $SEED_COUNT events"
generate_events "$ORLY2_WS" "$SEED_COUNT"
local count
count=$(count_events "$ORLY2_WS" '{"limit":10000}')
log_info "orly-relay-2 has $count events"
local min_expected=$((SEED_COUNT / 2))
if [ "$count" -ge "$min_expected" ]; then
log_pass "orly-relay-2 seeded with $count events (sent $SEED_COUNT, some replaceable)"
else else
log_warn "Relays differ by $diff events" log_fail "orly-relay-2 only has $count events (expected >= $min_expected)"
fi
}
# ============================================================
# Phase 8: orly CLI bridge: relay-1 -> relay-2
# Uses orly sync CLI with a temporary Badger DB to bridge events
# from orly-relay-1 to orly-relay-2 (tests orly as NIP-77 client
# against orly as NIP-77 server).
# ============================================================
phase8_orly_bridge_r1_to_r2() {
log_phase "8. ORLY CLI BRIDGE - relay-1 -> relay-2"
local orly2_before
orly2_before=$(count_events "$ORLY2_WS" '{"limit":10000}')
log_info "orly-relay-2 has $orly2_before events before bridge sync"
# Clean bridge DB
run_test "rm -rf $BRIDGE_DB" || true
# Step 1: Pull events from relay-1 into bridge DB
log_info "Step 1: orly sync (pull from relay-1 into bridge DB)"
run_test "ORLY_LOG_LEVEL=info orly sync $ORLY_WS --data-dir $BRIDGE_DB" 2>&1 || true
sleep 3
# Step 2: Push bridge DB events to relay-2
log_info "Step 2: orly sync (push bridge DB to relay-2)"
run_test "ORLY_LOG_LEVEL=info orly sync $ORLY2_WS --data-dir $BRIDGE_DB" 2>&1 || true
sleep 5
local orly2_after
orly2_after=$(count_events "$ORLY2_WS" '{"limit":10000}')
log_info "orly-relay-2 has $orly2_after events after bridge sync (was $orly2_before)"
if [ "$orly2_after" -gt "$orly2_before" ]; then
local synced=$((orly2_after - orly2_before))
log_pass "Bridged $synced events from relay-1 to relay-2 via orly CLI"
else
log_fail "No events bridged to relay-2 (still $orly2_after)"
fi
}
# ============================================================
# Phase 9: orly CLI bridge: relay-2 -> relay-1
# The bridge DB already has relay-2 events from Phase 8 (bidirectional
# sync picked them up). Sync with relay-1 to push relay-2's events.
# ============================================================
phase9_orly_bridge_r2_to_r1() {
log_phase "9. ORLY CLI BRIDGE - relay-2 -> relay-1"
local orly1_before
orly1_before=$(count_events "$ORLY_WS" '{"limit":10000}')
log_info "orly-relay-1 has $orly1_before events before bridge sync"
# Bridge DB already has relay-2 events from Phase 8 step 2 (bidirectional).
# Sync with relay-1 to push those events.
log_info "Syncing bridge DB with relay-1 (pushes relay-2 events)"
run_test "ORLY_LOG_LEVEL=info orly sync $ORLY_WS --data-dir $BRIDGE_DB" 2>&1 || true
sleep 5
local orly1_after
orly1_after=$(count_events "$ORLY_WS" '{"limit":10000}')
log_info "orly-relay-1 has $orly1_after events after bridge sync (was $orly1_before)"
if [ "$orly1_after" -gt "$orly1_before" ]; then
local synced=$((orly1_after - orly1_before))
log_pass "Bridged $synced events from relay-2 to relay-1 via orly CLI"
else
log_fail "No events bridged to relay-1 (still $orly1_after)"
fi
# Clean up bridge DB
run_test "rm -rf $BRIDGE_DB" || true
}
# ============================================================
# Phase 10: Three-way consistency verification
# ============================================================
phase10_three_way_verification() {
log_phase "10. THREE-WAY CONSISTENCY VERIFICATION"
local strfry_total orly1_total orly2_total
strfry_total=$(count_events "$STRFRY_WS" '{"limit":10000}')
orly1_total=$(count_events "$ORLY_WS" '{"limit":10000}')
orly2_total=$(count_events "$ORLY2_WS" '{"limit":10000}')
log_info "Final event counts:"
log_info " strfry: $strfry_total"
log_info " orly-relay-1: $orly1_total"
log_info " orly-relay-2: $orly2_total"
# All three should have events
if [ "$strfry_total" -gt 0 ] && [ "$orly1_total" -gt 0 ] && [ "$orly2_total" -gt 0 ]; then
log_pass "All three relays have events"
else
log_fail "One or more relays are empty"
fi
# Check orly-relay-1 vs orly-relay-2 consistency
local diff_orly=$((orly1_total - orly2_total))
if [ "${diff_orly#-}" -le 50 ]; then
log_pass "orly-relay-1 and orly-relay-2 are consistent (diff: $diff_orly)"
else
log_fail "orly relays differ significantly (diff: $diff_orly)"
fi
# Check strfry vs orly-relay-1 consistency
local diff_strfry=$((strfry_total - orly1_total))
if [ "${diff_strfry#-}" -le 50 ]; then
log_pass "strfry and orly-relay-1 are consistent (diff: $diff_strfry)"
else
log_warn "strfry and orly-relay-1 differ by $diff_strfry events"
fi fi
} }
@ -292,7 +435,7 @@ phase6_final_verification() {
main() { main() {
echo "========================================" echo "========================================"
echo "Negentropy (NIP-77) Interop Test Suite" echo "Negentropy (NIP-77) Interop Test Suite"
echo "strfry (client) <-> ORLY (server)" echo "strfry <-> ORLY <-> ORLY"
echo "========================================" echo "========================================"
echo "" echo ""
echo "Config:" echo "Config:"
@ -302,12 +445,19 @@ main() {
wait_for_services wait_for_services
# Part 1: strfry <-> ORLY interop (phases 1-6)
phase1_seed_strfry phase1_seed_strfry
phase2_strfry_push_to_orly phase2_strfry_push_to_orly
phase3_seed_orly phase3_seed_orly
phase4_strfry_pull_from_orly phase4_strfry_pull_from_orly
phase5_bidirectional phase5_bidirectional
phase6_final_verification phase6_strfry_orly_verification
# Part 2: ORLY <-> ORLY interop via CLI bridge (phases 7-10)
phase7_seed_orly2
phase8_orly_bridge_r1_to_r2
phase9_orly_bridge_r2_to_r1
phase10_three_way_verification
echo "" echo ""
echo "========================================" echo "========================================"

40
tests/negentropy/docker-compose.yml

@ -1,9 +1,8 @@
# Negentropy (NIP-77) Interop Test Infrastructure # Negentropy (NIP-77) Interop Test Infrastructure
# #
# Tests NIP-77 negentropy sync between strfry (client) and ORLY (server). # Tests NIP-77 negentropy sync between:
# # - strfry (client) <-> ORLY (server)
# Strfry initiates sync using its built-in `strfry sync` command. # - ORLY (client) <-> ORLY (server) via orly sync CLI
# ORLY serves NIP-77 via embedded negentropy handler.
# #
# Usage (from this directory): # Usage (from this directory):
# docker compose build # docker compose build
@ -31,7 +30,7 @@ services:
retries: 10 retries: 10
start_period: 10s start_period: 10s
# ORLY relay with embedded negentropy (NIP-77 server) # ORLY relay 1 with embedded negentropy (NIP-77 server)
orly-relay-1: orly-relay-1:
build: build:
context: ../.. context: ../..
@ -43,6 +42,7 @@ services:
- ORLY_DATA_DIR=/data - ORLY_DATA_DIR=/data
- ORLY_LOG_LEVEL=debug - ORLY_LOG_LEVEL=debug
- ORLY_NEGENTROPY_ENABLED=true - ORLY_NEGENTROPY_ENABLED=true
- ORLY_QUERY_RESULT_LIMIT=10000
volumes: volumes:
- orly-data-1:/data - orly-data-1:/data
networks: networks:
@ -54,7 +54,31 @@ services:
retries: 10 retries: 10
start_period: 10s start_period: 10s
# Test runner with event-generator and websocat # ORLY relay 2 with embedded negentropy (for orly<->orly sync testing)
orly-relay-2:
build:
context: ../..
dockerfile: tests/negentropy/Dockerfile.orly
ports:
- "3335:3335"
environment:
- ORLY_PORT=3335
- ORLY_DATA_DIR=/data
- ORLY_LOG_LEVEL=debug
- ORLY_NEGENTROPY_ENABLED=true
- ORLY_QUERY_RESULT_LIMIT=10000
volumes:
- orly-data-2:/data
networks:
- negentropy-test
healthcheck:
test: ["CMD", "curl", "-sf", "http://localhost:3335"]
interval: 5s
timeout: 5s
retries: 10
start_period: 10s
# Test runner with event-generator, orly CLI, and websocat
test-runner: test-runner:
build: build:
context: ../.. context: ../..
@ -62,11 +86,14 @@ services:
environment: environment:
- STRFRY_URL=ws://strfry:7777 - STRFRY_URL=ws://strfry:7777
- ORLY1_URL=ws://orly-relay-1:3334 - ORLY1_URL=ws://orly-relay-1:3334
- ORLY2_URL=ws://orly-relay-2:3335
depends_on: depends_on:
strfry: strfry:
condition: service_healthy condition: service_healthy
orly-relay-1: orly-relay-1:
condition: service_healthy condition: service_healthy
orly-relay-2:
condition: service_healthy
networks: networks:
- negentropy-test - negentropy-test
command: ["sleep", "infinity"] command: ["sleep", "infinity"]
@ -74,6 +101,7 @@ services:
volumes: volumes:
strfry-data: strfry-data:
orly-data-1: orly-data-1:
orly-data-2:
networks: networks:
negentropy-test: negentropy-test:

Loading…
Cancel
Save