diff --git a/go.mod b/go.mod index cdae6df..42ba105 100644 --- a/go.mod +++ b/go.mod @@ -268,4 +268,6 @@ require ( 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 diff --git a/go.sum b/go.sum index cf7459b..7c13880 100644 --- a/go.sum +++ b/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.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= -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/go.mod h1:ZakZtbCXxCz82NJvq7MoREtiQesnDfrtF6RFUGzQfLo= github.com/Azure/azure-sdk-for-go v68.0.0+incompatible h1:fcYLmCpyNYRnvJbPerq7U0hS+6+I79yEDJBqVNcqUzU= diff --git a/pkg/sync/negentropy/embedded.go b/pkg/sync/negentropy/embedded.go index b7a54de..beb9d03 100644 --- a/pkg/sync/negentropy/embedded.go +++ b/pkg/sync/negentropy/embedded.go @@ -8,6 +8,9 @@ import ( "lol.mleku.dev/log" "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" "next.orly.dev/pkg/database" negentropyiface "next.orly.dev/pkg/interfaces/negentropy" @@ -252,6 +255,35 @@ func protoToFilter(pf *commonv1.Filter) *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 if pf.Limit != nil { limit := uint(*pf.Limit) diff --git a/pkg/sync/negentropy/manager.go b/pkg/sync/negentropy/manager.go index 42ba3f2..10db86c 100644 --- a/pkg/sync/negentropy/manager.go +++ b/pkg/sync/negentropy/manager.go @@ -260,16 +260,12 @@ func (m *Manager) performNegentropy(ctx context.Context, peerURL string) (int64, var eventsSynced int64 var needIDs []string var haveIDs []string - var waitingForEvents bool - var expectedEvents int - var receivedEvents int64 - // Exchange messages until complete - for i := 0; i < 20; i++ { // Max 20 rounds - // Read response + // Phase 1: Reconciliation - exchange NEG-MSG until complete + for i := 0; i < 20; i++ { // Max 20 reconciliation rounds _, msgBytes, err := conn.ReadMessage() 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 @@ -310,27 +306,18 @@ func (m *Manager) performNegentropy(ctx context.Context, peerURL string) (int64, needIDs = append(needIDs, neg.CollectHaveNots()...) haveIDs = append(haveIDs, neg.CollectHaves()...) - if complete { - log.I.F("negentropy: reconciliation complete, waiting for %d events from peer", len(needIDs)) - // Don't send NEG-CLOSE yet - wait for EVENT messages - // 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 + // Always send the response to the server, even when complete. + // The server needs this to finalize its own reconciliation and send events. + if len(response) > 0 { + negMsgResp := []any{"NEG-MSG", subID, hex.EncodeToString(response)} + if err := conn.WriteJSON(negMsgResp); err != nil { + return eventsSynced, fmt.Errorf("failed to send NEG-MSG: %w", err) } - // 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)} - if err := conn.WriteJSON(negMsgResp); err != nil { - 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": @@ -339,34 +326,35 @@ func (m *Manager) performNegentropy(ctx context.Context, peerURL string) (int64, json.Unmarshal(msg[2], &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) { - negClose := []any{"NEG-CLOSE", subID} - conn.WriteJSON(negClose) - goto done - } } } -done: +fetchAndPush: + // Send NEG-CLOSE to end the negentropy session + { + negClose := []any{"NEG-CLOSE", subID} + conn.WriteJSON(negClose) + } + // Clear any read deadline from the negotiation phase + conn.SetReadDeadline(time.Time{}) + 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 { - 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) - // This is more reliable than trying to PULL events using ID prefixes + // Phase 3: Push events we have to the peer if len(haveIDs) > 0 { pushed, err := m.pushEventsToPeer(ctx, conn, haveIDs) 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 } diff --git a/tests/negentropy/Dockerfile.test-runner b/tests/negentropy/Dockerfile.test-runner index 2449620..378f66b 100644 --- a/tests/negentropy/Dockerfile.test-runner +++ b/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 @@ -10,6 +10,9 @@ COPY . . # 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 +# 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 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 && \ 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/orly /usr/local/bin/ CMD ["bash"] diff --git a/tests/negentropy/README.md b/tests/negentropy/README.md index e7e0335..3ae23f1 100644 --- a/tests/negentropy/README.md +++ b/tests/negentropy/README.md @@ -1,55 +1,47 @@ # 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 -### 1. Orly as Relay, Strfry as Client +### 1. strfry as Client, ORLY as Server (Phases 1-6) Uses `strfry sync` command to test: - **Push**: strfry → orly-relay-1 - **Pull**: strfry ← orly-relay-1 - **Bidirectional**: strfry ↔ orly-relay-1 -### 2. Strfry as Relay, Orly as Client -Uses `orly sync` with gRPC client mode: -- **Push**: orly-relay-2 → strfry -- **Pull**: orly-relay-2 ← strfry -- **Bidirectional**: orly-relay-2 ↔ strfry - -### 3. Dual Orly with gRPC Control -Two ORLY relays synchronized via gRPC sync services: -- **orly-relay-1** ↔ **orly-relay-2** via gRPC-controlled sync +### 2. ORLY as Client, ORLY as Server (Phases 7-10) +Uses `orly sync` CLI with a temporary Badger DB as a bridge: +- **relay-1 → relay-2**: orly CLI pulls from relay-1, pushes to relay-2 +- **relay-2 → relay-1**: orly CLI pushes relay-2 events to relay-1 +- **Three-way consistency**: strfry, relay-1, relay-2 converge ## Infrastructure ``` -┌─────────────┐ ┌─────────────┐ -│ strfry │◄───────►│ orly-relay-1│ -│ (7777) │ NIP-77 │ (3334) │ -└──────┬──────┘ └──────┬──────┘ - │ │ - │ │ gRPC - │ ┌─────┴──────┐ - │ │ orly-sync-1│ - │ │ (50064) │ - │ └────────────┘ - │ - │ NIP-77 ┌─────────────┐ - └────────────────►│ orly-relay-2│ - │ (3335) │ - └──────┬──────┘ - │ gRPC - ┌─────┴──────┐ - │ orly-sync-2│ - │ (50064) │ - └────────────┘ +┌─────────────┐ NIP-77 ┌──────────────┐ +│ strfry │◄─────────►│ orly-relay-1 │ +│ (7777) │ │ (3334) │ +└─────────────┘ └──────┬───────┘ + │ + orly sync CLI + (bridge DB in + test-runner) + │ + ┌──────┴───────┐ + │ orly-relay-2 │ + │ (3335) │ + └──────────────┘ ``` +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 ### Prerequisites - Docker and Docker Compose -- Go 1.24+ (for local event generator builds) ### Run All Tests @@ -63,41 +55,39 @@ docker compose build docker compose up -d # Run comprehensive tests -docker compose exec test-runner /tests/comprehensive-test.sh +./comprehensive-test.sh # Or with verbose output -docker compose exec test-runner /tests/comprehensive-test.sh --verbose +./comprehensive-test.sh --verbose # Clean up docker compose down -v ``` -### Run Individual Test Phases +### Manual Operations ```bash -# Enter test runner container -docker compose exec test-runner bash - -# Check relay status -echo "Strfry events: $(count_events ws://strfry:7777 '{"limit": 1000}')" -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 +docker compose exec -T test-runner event-generator -count 500 -relay ws://strfry:7777 -# Generate events manually -event-generator -count 500 -relay ws://strfry:7777 +# strfry sync (strfry as client, ORLY as server) +docker compose exec -T strfry /app/strfry --config=/etc/strfry.conf sync ws://orly-relay-1:3334 --dir down -# Test strfry as client (pull) -docker compose exec strfry /app/strfry sync ws://orly-relay-1:3334 --dir down +# orly sync (orly CLI as client, any relay as server) +docker compose exec -T test-runner orly sync ws://orly-relay-1:3334 --data-dir /tmp/sync-db -# Test orly as client via gRPC (pull) -orly sync ws://strfry:7777 --server orly-sync-2:50064 --dir down --verbose +# Manual event inspection +echo '["REQ", "test", {"limit": 10}]' | websocat ws://localhost:7777 +echo '["REQ", "test", {"limit": 10}]' | websocat ws://localhost:3334 +echo '["REQ", "test", {"limit": 10}]' | websocat ws://localhost:3335 ``` ## Test Parameters -- **Total Events**: 1200+ per seed operation -- **Event Kinds**: 0, 1, 3, 1984, 10000, 10001, 30023, 30078 -- **Batch Size**: 100 events per batch +- **Seed Events**: 200 per relay +- **Extra Events**: 100 +- **Event Kinds**: 0, 1, 3, 1984, 10000, 10001, 30023 +- **Batch Size**: 50 events per batch - **Authors**: 3 test keypairs (alice, bob, carol) ### Event Distribution @@ -112,33 +102,6 @@ orly sync ws://strfry:7777 --server orly-sync-2:50064 --dir down --verbose | 10001| 3% | Pin lists | | 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 ### Check service health @@ -146,15 +109,7 @@ Tests verify: docker compose ps docker compose logs -f strfry docker compose logs -f orly-relay-1 -``` - -### 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 +docker compose logs -f orly-relay-2 ``` ### Reset test data @@ -168,18 +123,14 @@ docker compose up -d ### Strfry - Image: Built from source (Dockerfile.strfry) - 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) -- Ports: 3334, 3335 -- Features: NIP-77 negentropy + gRPC database interface - -### Orly Sync Service -- Image: Built from cmd/orly-sync-negentropy -- Ports: 50064, 50065 -- Features: gRPC-controlled negentropy sync +- Ports: 3334 (relay-1), 3335 (relay-2) +- Features: NIP-77 negentropy via embedded handler +- Note: `ORLY_QUERY_RESULT_LIMIT=10000` set for testing (default is 256) ### Test Runner -- Image: Built with all test tools -- Features: event-generator, websocat, orly CLI +- Image: Alpine with test tools (Dockerfile.test-runner) +- Features: event-generator, orly CLI, websocat, curl, jq diff --git a/tests/negentropy/comprehensive-test.sh b/tests/negentropy/comprehensive-test.sh index 776afb2..dffee00 100755 --- a/tests/negentropy/comprehensive-test.sh +++ b/tests/negentropy/comprehensive-test.sh @@ -1,10 +1,13 @@ #!/bin/bash # # 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. # 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 # interact with containers. @@ -14,8 +17,12 @@ # 2. Strfry pushes events to ORLY (strfry --dir up) # 3. Seed ORLY with new events # 4. Strfry pulls events from ORLY (strfry --dir down) -# 5. Bidirectional sync -# 6. Final consistency verification +# 5. Bidirectional sync (strfry <-> ORLY) +# 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: # cd tests/negentropy @@ -32,6 +39,8 @@ cd "$(dirname "$0")" # Configuration STRFRY_WS="ws://strfry:7777" ORLY_WS="ws://orly-relay-1:3334" +ORLY2_WS="ws://orly-relay-2:3335" +BRIDGE_DB="/tmp/orly-bridge-db" SEED_COUNT=200 EXTRA_COUNT=100 VERBOSE="${VERBOSE:-false}" @@ -105,7 +114,7 @@ generate_events() { wait_for_services() { 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 local status 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() { - log_phase "6. FINAL VERIFICATION" +phase6_strfry_orly_verification() { + log_phase "6. STRFRY <-> ORLY VERIFICATION" local strfry_total orly_total strfry_total=$(count_events "$STRFRY_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 " orly-relay-1: $orly_total" @@ -280,9 +289,143 @@ phase6_final_verification() { # Check consistency local diff=$((strfry_total - orly_total)) 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 - 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 } @@ -292,7 +435,7 @@ phase6_final_verification() { main() { echo "========================================" echo "Negentropy (NIP-77) Interop Test Suite" - echo "strfry (client) <-> ORLY (server)" + echo "strfry <-> ORLY <-> ORLY" echo "========================================" echo "" echo "Config:" @@ -302,12 +445,19 @@ main() { wait_for_services + # Part 1: strfry <-> ORLY interop (phases 1-6) phase1_seed_strfry phase2_strfry_push_to_orly phase3_seed_orly phase4_strfry_pull_from_orly 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 "========================================" diff --git a/tests/negentropy/docker-compose.yml b/tests/negentropy/docker-compose.yml index 4eeed6e..f5660ca 100644 --- a/tests/negentropy/docker-compose.yml +++ b/tests/negentropy/docker-compose.yml @@ -1,9 +1,8 @@ # Negentropy (NIP-77) Interop Test Infrastructure # -# Tests NIP-77 negentropy sync between strfry (client) and ORLY (server). -# -# Strfry initiates sync using its built-in `strfry sync` command. -# ORLY serves NIP-77 via embedded negentropy handler. +# Tests NIP-77 negentropy sync between: +# - strfry (client) <-> ORLY (server) +# - ORLY (client) <-> ORLY (server) via orly sync CLI # # Usage (from this directory): # docker compose build @@ -31,7 +30,7 @@ services: retries: 10 start_period: 10s - # ORLY relay with embedded negentropy (NIP-77 server) + # ORLY relay 1 with embedded negentropy (NIP-77 server) orly-relay-1: build: context: ../.. @@ -43,6 +42,7 @@ services: - ORLY_DATA_DIR=/data - ORLY_LOG_LEVEL=debug - ORLY_NEGENTROPY_ENABLED=true + - ORLY_QUERY_RESULT_LIMIT=10000 volumes: - orly-data-1:/data networks: @@ -54,7 +54,31 @@ services: retries: 10 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: build: context: ../.. @@ -62,11 +86,14 @@ services: environment: - STRFRY_URL=ws://strfry:7777 - ORLY1_URL=ws://orly-relay-1:3334 + - ORLY2_URL=ws://orly-relay-2:3335 depends_on: strfry: condition: service_healthy orly-relay-1: condition: service_healthy + orly-relay-2: + condition: service_healthy networks: - negentropy-test command: ["sleep", "infinity"] @@ -74,6 +101,7 @@ services: volumes: strfry-data: orly-data-1: + orly-data-2: networks: negentropy-test: