#!/usr/bin/env bash # FINAL-203: Multi-Node Network Test # Tests discovery, connection, content sharing, and ecash payments between 3 Archipelago nodes. # Usage: bash test-multi-node.sh [password] set -euo pipefail NODE1="${1:-192.168.1.228}" NODE2="${2:-192.168.1.198}" NODE3="${3:-192.168.1.199}" PASS="${4:-password123}" PASS_COUNT=0 FAIL_COUNT=0 SKIP_COUNT=0 green() { printf "\033[32m✓ %s\033[0m\n" "$1"; } red() { printf "\033[31m✗ %s\033[0m\n" "$1"; } yellow(){ printf "\033[33m⊘ %s\033[0m\n" "$1"; } header(){ printf "\n\033[1;36m━━━ %s ━━━\033[0m\n" "$1"; } pass() { PASS_COUNT=$((PASS_COUNT + 1)); green "$1"; } fail() { FAIL_COUNT=$((FAIL_COUNT + 1)); red "$1"; } skip() { SKIP_COUNT=$((SKIP_COUNT + 1)); yellow "$1 (skipped)"; } JARS=() for i in 1 2 3; do JARS+=("/tmp/multinode-cookies-${i}.txt") done login_node() { local idx="$1" local ip="$2" curl -s -c "${JARS[$((idx-1))]}" -H "Content-Type: application/json" \ -d "{\"jsonrpc\":\"2.0\",\"id\":1,\"method\":\"auth.login\",\"params\":{\"password\":\"$PASS\"}}" \ "http://${ip}/rpc/" > /dev/null 2>&1 } rpc_node() { local idx="$1" local ip="$2" local method="$3" local params="${4:-{}}" curl -s -m 15 -b "${JARS[$((idx-1))]}" -c "${JARS[$((idx-1))]}" \ -H "Content-Type: application/json" \ -d "{\"jsonrpc\":\"2.0\",\"id\":1,\"method\":\"$method\",\"params\":$params}" \ "http://${ip}/rpc/" 2>/dev/null } # ─── Phase 0: Verify All Nodes Online ──────────────────────────── header "Phase 0: Node Connectivity" NODES=("$NODE1" "$NODE2" "$NODE3") NODE_NAMES=("Node-1" "Node-2" "Node-3") ONLINE_COUNT=0 for i in 0 1 2; do ip="${NODES[$i]}" name="${NODE_NAMES[$i]}" health_code=$(curl -s -o /dev/null -w "%{http_code}" -m 5 "http://${ip}/health" 2>/dev/null || echo "000") if [ "$health_code" = "200" ]; then pass "$name ($ip) is online" login_node $((i+1)) "$ip" ONLINE_COUNT=$((ONLINE_COUNT + 1)) else fail "$name ($ip) is offline (HTTP $health_code)" fi done if [ "$ONLINE_COUNT" -lt 2 ]; then echo "" red "Need at least 2 online nodes to continue. Exiting." exit 1 fi # ─── Phase 1: Node Discovery via Nostr ─────────────────────────── header "Phase 1: Node Discovery" # Set all nodes to Discoverable for i in 0 1 2; do ip="${NODES[$i]}" name="${NODE_NAMES[$i]}" resp=$(rpc_node $((i+1)) "$ip" "network.set-visibility" '{"visibility":"discoverable"}') if echo "$resp" | grep -q '"result"'; then pass "$name set to Discoverable" else skip "$name visibility" fi done # Wait for Nostr events to propagate echo " Waiting 10s for Nostr event propagation..." sleep 10 # Node 1 discovers Node 2 DISCOVER_RESP=$(rpc_node 1 "$NODE1" "network.discover-peers") if echo "$DISCOVER_RESP" | grep -q '"result"'; then PEER_COUNT=$(echo "$DISCOVER_RESP" | python3 -c "import sys,json; r=json.load(sys.stdin); print(len(r.get('result',{}).get('peers',[])))" 2>/dev/null || echo "0") if [ "$PEER_COUNT" -gt "0" ]; then pass "Node-1 discovered $PEER_COUNT peer(s) via Nostr" else skip "Node-1 peer discovery (0 found — may need more time)" fi else skip "Peer discovery" fi # ─── Phase 2: Connection Requests ──────────────────────────────── header "Phase 2: Connection Requests" # Node 1 → Node 2 connection request CONN_RESP=$(rpc_node 1 "$NODE1" "network.request-connection" "{\"target_address\":\"${NODE2}\"}") if echo "$CONN_RESP" | grep -q '"result"'; then pass "Node-1 sent connection request to Node-2" else skip "Connection request Node-1 → Node-2" fi sleep 2 # Node 2 checks pending requests PENDING=$(rpc_node 2 "$NODE2" "network.list-requests") if echo "$PENDING" | grep -q '"result"'; then REQ_COUNT=$(echo "$PENDING" | python3 -c "import sys,json; r=json.load(sys.stdin); print(len(r.get('result',{}).get('requests',[])))" 2>/dev/null || echo "0") if [ "$REQ_COUNT" -gt "0" ]; then pass "Node-2 has $REQ_COUNT pending request(s)" # Accept request ACCEPT_RESP=$(rpc_node 2 "$NODE2" "network.accept-request" "{\"from\":\"${NODE1}\"}") if echo "$ACCEPT_RESP" | grep -q '"result"'; then pass "Node-2 accepted connection from Node-1" else skip "Accept connection" fi else skip "No pending requests on Node-2" fi else skip "List requests on Node-2" fi # Node 1 → Node 3 connection if [ "$ONLINE_COUNT" -ge 3 ]; then CONN_RESP2=$(rpc_node 1 "$NODE1" "network.request-connection" "{\"target_address\":\"${NODE3}\"}") if echo "$CONN_RESP2" | grep -q '"result"'; then pass "Node-1 sent connection request to Node-3" else skip "Connection request Node-1 → Node-3" fi sleep 2 ACCEPT_RESP2=$(rpc_node 3 "$NODE3" "network.accept-request" "{\"from\":\"${NODE1}\"}") if echo "$ACCEPT_RESP2" | grep -q '"result"'; then pass "Node-3 accepted connection from Node-1" else skip "Accept connection on Node-3" fi fi # Node 2 → Node 3 connection if [ "$ONLINE_COUNT" -ge 3 ]; then CONN_RESP3=$(rpc_node 2 "$NODE2" "network.request-connection" "{\"target_address\":\"${NODE3}\"}") if echo "$CONN_RESP3" | grep -q '"result"'; then pass "Node-2 sent connection request to Node-3" else skip "Connection request Node-2 → Node-3" fi sleep 2 ACCEPT_RESP3=$(rpc_node 3 "$NODE3" "network.accept-request" "{\"from\":\"${NODE2}\"}") if echo "$ACCEPT_RESP3" | grep -q '"result"'; then pass "Node-3 accepted connection from Node-2" else skip "Accept connection on Node-3 from Node-2" fi fi # ─── Phase 3: Content Sharing Between Pairs ────────────────────── header "Phase 3: Content Sharing" # Node 1 shares content ADD_CONTENT=$(rpc_node 1 "$NODE1" "content.add" '{"title":"Test File","path":"/var/lib/archipelago/content/test.txt","pricing":"free"}') if echo "$ADD_CONTENT" | grep -q '"result"'; then pass "Node-1 shared test content" else skip "Content sharing on Node-1" fi sleep 2 # Node 2 browses Node 1 content BROWSE=$(rpc_node 2 "$NODE2" "content.browse-peer" "{\"peer_address\":\"${NODE1}\"}") if echo "$BROWSE" | grep -q '"result"'; then ITEM_COUNT=$(echo "$BROWSE" | python3 -c "import sys,json; r=json.load(sys.stdin); print(len(r.get('result',{}).get('items',[])))" 2>/dev/null || echo "0") if [ "$ITEM_COUNT" -gt "0" ]; then pass "Node-2 browsed Node-1 catalog ($ITEM_COUNT items)" else skip "Node-2 browse (empty catalog)" fi else skip "Content browsing" fi # ─── Phase 4: Ecash Payments Between Pairs ─────────────────────── header "Phase 4: Ecash Payments" # Check ecash balances on all nodes for i in 0 1 2; do ip="${NODES[$i]}" name="${NODE_NAMES[$i]}" bal=$(rpc_node $((i+1)) "$ip" "wallet.ecash-balance") if echo "$bal" | grep -q '"result"'; then balance=$(echo "$bal" | python3 -c "import sys,json; r=json.load(sys.stdin); print(r.get('result',{}).get('balance',0))" 2>/dev/null || echo "0") pass "$name ecash balance: $balance sats" else skip "$name ecash balance" fi done # Node 1 sends ecash to Node 2 SEND_ECASH=$(rpc_node 1 "$NODE1" "wallet.ecash-send" '{"amount":100}') if echo "$SEND_ECASH" | grep -q '"result"'; then TOKEN=$(echo "$SEND_ECASH" | python3 -c "import sys,json; r=json.load(sys.stdin); print(r.get('result',{}).get('token',''))" 2>/dev/null || echo "") if [ -n "$TOKEN" ]; then pass "Node-1 created ecash token (100 sats)" # Node 2 receives RECV_ECASH=$(rpc_node 2 "$NODE2" "wallet.ecash-receive" "{\"token\":\"$TOKEN\"}") if echo "$RECV_ECASH" | grep -q '"result"'; then pass "Node-2 received ecash token" else skip "Node-2 ecash receive" fi else skip "Ecash token creation (empty token)" fi else skip "Ecash send" fi # ─── Phase 5: Peer-to-Peer Messaging ───────────────────────────── header "Phase 5: Peer Messaging" # Node 1 sends message to Node 2 MSG_SEND=$(rpc_node 1 "$NODE1" "chat.send" "{\"peer_address\":\"${NODE2}\",\"message\":\"Hello from Node-1\"}") if echo "$MSG_SEND" | grep -q '"result"'; then pass "Node-1 sent message to Node-2" else skip "Peer messaging" fi sleep 2 # Node 2 checks messages MSG_LIST=$(rpc_node 2 "$NODE2" "chat.list" "{\"peer_address\":\"${NODE1}\"}") if echo "$MSG_LIST" | grep -q '"result"'; then MSG_COUNT=$(echo "$MSG_LIST" | python3 -c "import sys,json; r=json.load(sys.stdin); print(len(r.get('result',{}).get('messages',[])))" 2>/dev/null || echo "0") if [ "$MSG_COUNT" -gt "0" ]; then pass "Node-2 received $MSG_COUNT message(s)" else skip "No messages received on Node-2" fi else skip "Message listing on Node-2" fi # ─── Phase 6: Node Offline/Online Graceful Handling ────────────── header "Phase 6: Offline/Online Handling" # Check peer status from Node 1 PEER_STATUS=$(rpc_node 1 "$NODE1" "network.list-peers") if echo "$PEER_STATUS" | grep -q '"result"'; then pass "Node-1 can list peers with status" CONNECTED=$(echo "$PEER_STATUS" | python3 -c " import sys,json r=json.load(sys.stdin) peers=r.get('result',{}).get('peers',[]) online=[p for p in peers if p.get('status')=='online' or p.get('reachable',False)] print(len(online)) " 2>/dev/null || echo "0") pass "Node-1 sees $CONNECTED online peer(s)" else skip "Peer status listing" fi # ─── Phase 7: Cross-Node Identity Verification ─────────────────── header "Phase 7: Identity Verification" # Get Node 1's DID DID1=$(rpc_node 1 "$NODE1" "identity.get" | python3 -c "import sys,json; r=json.load(sys.stdin); print(r.get('result',{}).get('did',''))" 2>/dev/null || echo "") if [ -n "$DID1" ]; then pass "Node-1 DID: ${DID1:0:30}..." # Sign a message on Node 1 SIG=$(rpc_node 1 "$NODE1" "identity.sign" '{"message":"cross-node-test"}') SIG_VAL=$(echo "$SIG" | python3 -c "import sys,json; r=json.load(sys.stdin); print(r.get('result',{}).get('signature',''))" 2>/dev/null || echo "") if [ -n "$SIG_VAL" ]; then pass "Node-1 signed message" # Verify on Node 2 VERIFY=$(rpc_node 2 "$NODE2" "identity.verify" "{\"did\":\"$DID1\",\"message\":\"cross-node-test\",\"signature\":\"$SIG_VAL\"}") if echo "$VERIFY" | grep -q '"result"'; then VALID=$(echo "$VERIFY" | python3 -c "import sys,json; r=json.load(sys.stdin); print(r.get('result',{}).get('valid',False))" 2>/dev/null || echo "False") if [ "$VALID" = "True" ]; then pass "Node-2 verified Node-1's signature" else skip "Signature verification returned invalid" fi else skip "Cross-node signature verification" fi else skip "Node-1 signing" fi else skip "Node-1 DID retrieval" fi # ─── Summary ───────────────────────────────────────────────────── header "RESULTS" echo "" printf "\033[32m Passed: %d\033[0m\n" "$PASS_COUNT" printf "\033[31m Failed: %d\033[0m\n" "$FAIL_COUNT" printf "\033[33m Skipped: %d\033[0m\n" "$SKIP_COUNT" echo "" TOTAL=$((PASS_COUNT + FAIL_COUNT + SKIP_COUNT)) if [ "$FAIL_COUNT" -eq 0 ]; then printf "\033[1;32m🎉 ALL %d TESTS PASSED (%d skipped)\033[0m\n" "$PASS_COUNT" "$SKIP_COUNT" exit 0 else printf "\033[1;31m⚠ %d/%d TESTS FAILED\033[0m\n" "$FAIL_COUNT" "$TOTAL" exit 1 fi