#!/usr/bin/env bash # v0.8 Iteration C integration test — TOPOLOGY. # # Run from the repo root after `cargo build -p itsgoin-cli`, AFTER # scripts/a3_integration_test.sh (C does not replace A). Drives interactive # REPLs over FIFOs, asserts by grepping logs and by sqlite3 against node DBs. # # SCENARIO 1 Two-pool uniques exchange over a chain: pool 1 shifts one bounce # deeper per hop, pool 2 lands at N4, and N4 is USED but NEVER # re-announced (no N5 anywhere). Plus the §layers privacy # invariant: only anchor entries carry an address. # SCENARIO 2 Anchor convection: the rolling window hands each caller the 2 # most recent prior callers, produces a REAL connection, rotates # entries out after 2 hand-outs, coordinates a RelayIntroduce when # both ends are live, and refuses a top-up CHEAPLY. # SCENARIO 3 Temp referral slots: allocated ABOVE the mesh cap, carrying no # knowledge, never evicting an established mesh peer, graduating # into a freed mesh slot. # STANDING 0xC0 AnchorRegister is gone; no automatic session relay anywhere. # # Exit code 0 = all checks passed. Logs: /tmp/itsgoin-ctop{1..7}.log set -u cd "$(dirname "$0")/.." BIN=target/debug/itsgoin [ -x "$BIN" ] || { echo "build first: cargo build -p itsgoin-cli"; exit 2; } PASS=0; FAIL=0 check() { # check local desc="$1"; shift if "$@" >/dev/null 2>&1; then PASS=$((PASS+1)); echo "PASS: $desc"; else FAIL=$((FAIL+1)); echo "FAIL: $desc"; fi } strip_ansi() { sed 's/\x1b\[[0-9;]*m//g' "$1" | grep -v "WARN.*netlink\|WARN.*buffer_tool"; } sq() { sqlite3 "/tmp/itsgoin-ctop$1/itsgoin.db" "$2"; } # Is `id` (hex) present in node N's uniques index at `bounce`? at_bounce() { # at_bounce [ "$(sq "$1" "SELECT count(*) FROM reachable WHERE lower(hex(reachable_id))='$2' AND bounce=$3")" != "0" ] } anywhere() { # anywhere [ "$(sq "$1" "SELECT count(*) FROM reachable WHERE lower(hex(reachable_id))='$2'")" != "0" ] } # `check` runs its command via `bash -c`, which does not inherit shell functions. export -f strip_ansi sq at_bounce anywhere NODES="1 2 3 4 5 6 7" FDS="" cleanup() { for fd in 21 22 23 24 25 26 27; do eval "exec $fd>&-" 2>/dev/null || true; done kill $(pgrep -f 'itsgoin.*itsgoin-ctop') 2>/dev/null rm -f /tmp/itsgoin-ctopcmd* } trap cleanup EXIT # Test gates: # - loopback referrals: the convection address filter is publicly-routable-only # (bugs-fixed #8), which would make every 127.0.0.1 referral unusable. # - mesh cap 2: reaches the temp-referral boundary with 4 nodes, not 21. # - no growth loop: on loopback the growth loop collapses any chain into a # full mesh in seconds, and a full mesh has no N2 at all (every peer is a # direct). Topology here is script-controlled on purpose. export ITSGOIN_TEST_ALLOW_LOOPBACK_REFERRALS=1 export ITSGOIN_TEST_ALLOW_REGISTRY_GENESIS=1 export ITSGOIN_TEST_NO_GROWTH=1 export RUST_LOG="${RUST_LOG:-itsgoin_core=debug,info,iroh=warn,swarm_discovery=warn}" echo "== setup ==" kill $(pgrep -f 'itsgoin.*itsgoin-ctop') 2>/dev/null; sleep 1 for i in $NODES; do rm -rf /tmp/itsgoin-ctop$i /tmp/itsgoin-ctopcmd$i /tmp/itsgoin-ctop$i.log mkdir -p /tmp/itsgoin-ctop$i mkfifo /tmp/itsgoin-ctopcmd$i done start_node() { # start_node [extra env assignments...] local i="$1" port="$2"; shift 2 ( exec env "$@" "$BIN" /tmp/itsgoin-ctop$i --bind 127.0.0.1:$port \ < /tmp/itsgoin-ctopcmd$i > /tmp/itsgoin-ctop$i.log 2>&1 ) & } # Nodes 1-6: chain + convection. Node 7 joins late (fresh index). for i in 1 2 3 4 5 6 7; do start_node $i $((18430 + i)) ITSGOIN_TEST_NO_GROWTH=1; done exec 21>/tmp/itsgoin-ctopcmd1 22>/tmp/itsgoin-ctopcmd2 23>/tmp/itsgoin-ctopcmd3 \ 24>/tmp/itsgoin-ctopcmd4 25>/tmp/itsgoin-ctopcmd5 26>/tmp/itsgoin-ctopcmd6 \ 27>/tmp/itsgoin-ctopcmd7 sleep 7 for i in $NODES; do eval "n${i}=\$(strip_ansi /tmp/itsgoin-ctop$i.log | grep -m1 'Node ID:' | awk '{print \$3}')" done echo "n1=$n1 n2=$n2 n3=$n3 n4=$n4 n5=$n5" [ -n "$n5" ] || { echo "FAIL: nodes did not start"; exit 1; } ############################################################################## echo echo "== scenario 1: two-pool uniques exchange, N4 terminal ==" ############################################################################## # Chain 1 - 2 - 3 - 4 - 5. Built inward so each hop's initial exchange already # carries the deeper knowledge where possible. echo "connect $n1@127.0.0.1:18431" >&22; sleep 3 echo "connect $n2@127.0.0.1:18432" >&23; sleep 3 echo "connect $n3@127.0.0.1:18433" >&24; sleep 3 echo "connect $n4@127.0.0.1:18434" >&25; sleep 4 # Drive propagation explicitly: each uniques-pull round moves knowledge one # bounce along the chain. This IS the v0.8 pull — an index exchange, no posts. for round in 1 2 3 4 5; do for fd in 21 22 23 24 25; do echo "uniques-pull" >&$fd; done sleep 3 done check "uniques pull is an index exchange (peers exchanged, no post transfer)" \ bash -c 'grep -q "uniques-pull: exchanged with [1-9]" /tmp/itsgoin-ctop3.log' # Pool 1 shifts one bounce deeper per hop, tagged to the reporter. check "node1 holds node3 at N2 (pool 1 shifted one bounce)" at_bounce 1 "$n3" 2 check "node1 holds node4 at N3" at_bounce 1 "$n4" 3 check "node1 holds node5 at N4 (terminal pool)" at_bounce 1 "$n5" 4 check "node5 holds node1 at N4 from the other end of the chain" at_bounce 5 "$n1" 4 # N4 is USED — address resolution and search read it. check "node1 can look up its N4 entry (used, not merely stored)" \ bash -c 'grep -q "N4 via" <(echo "uniques '"$n5"'" >&21; sleep 3; tail -40 /tmp/itsgoin-ctop1.log)' # §layers privacy invariant: ONLY anchor entries carry an address. check "no non-anchor index row carries an address (on any node)" \ bash -c 'for i in 1 2 3 4 5; do c=$(sqlite3 /tmp/itsgoin-ctop$i/itsgoin.db \ "SELECT count(*) FROM reachable WHERE is_anchor=0 AND addresses NOT IN ('"''"',\"[]\")"); [ "$c" = "0" ] || exit 1; done' # Per-reporter dedup: one row per (reporter, id) — an ID can never sit at two # depths from the same reporter. check "one index row per (reporter, id) — no duplicate depths" \ bash -c 'for i in 1 2 3 4 5; do d=$(sqlite3 /tmp/itsgoin-ctop$i/itsgoin.db \ "SELECT count(*) FROM (SELECT reporter_node_id, reachable_id FROM reachable GROUP BY reporter_node_id, reachable_id HAVING count(*) > 1)"); [ "$d" = "0" ] || exit 1; done' # Nothing is stored past bounce 4. There is no N5 table and no N5 row. check "stored horizon never exceeds 4 bounces" \ bash -c 'for i in 1 2 3 4 5; do m=$(sqlite3 /tmp/itsgoin-ctop$i/itsgoin.db "SELECT ifnull(max(bounce),0) FROM reachable"); [ "$m" -le 4 ] || exit 1; done' # THE KEY ASSERTION: a fresh node joining node1 must NOT learn node5. node1 # knows node5 only at N4, and the terminal pool is never re-announced. echo "connect $n1@127.0.0.1:18431" >&27; sleep 4 for round in 1 2 3; do echo "uniques-pull" >&27; echo "uniques-pull" >&21; sleep 3; done check "N4 is never re-announced: node7 never learns node5" \ bash -c '! /usr/bin/env bash -c "[ \"\$(sqlite3 /tmp/itsgoin-ctop7/itsgoin.db \ \"SELECT count(*) FROM reachable WHERE lower(hex(reachable_id))=\\\"'"$n5"'\\\"\")\" != \"0\" ]"' check "node7 DID learn node2 (the exchange itself works)" anywhere 7 "$n2" ############################################################################## echo echo "== scenario 2: anchor convection ==" ############################################################################## # Node 6 plays the anchor: directly dialable, and it is the one every caller # asks. Callers are node1 and node5 — the two ENDS of the chain, so they have # no path to each other and a referral that lands is unambiguous. echo "connect $n6@127.0.0.1:18436" >&21; sleep 3 echo "convect $n6" >&21; sleep 5 # node1 admitted; window was empty echo "connect $n6@127.0.0.1:18436" >&25; sleep 3 echo "convect $n6" >&25; sleep 10 # node5 is referred to node1 check "anchor serves referrals out of the rolling window" \ bash -c 'grep -q "Convection: serving referrals" /tmp/itsgoin-ctop6.log' check "the anchor admitted callers to the window without any registration msg" \ bash -c 'grep -q "Convection: admitted caller to rolling window" /tmp/itsgoin-ctop6.log' check "the anchor referred the PRIOR caller to the next one" \ bash -c 'grep "Convection: referring peer" /tmp/itsgoin-ctop6.log | grep -q "'"$n1"'"' check "a convection referral produced a real peer connection" \ bash -c 'grep -q "Convection: connected to referred peer\|Convection: connected via introduction" \ /tmp/itsgoin-ctop5.log' check "node5 is now actually mesh-connected to node1 (chain ends joined)" \ bash -c 'echo "connections" >&25; sleep 3; strip_ansi /tmp/itsgoin-ctop5.log | tail -20 | grep -q "'"${n1:0:12}"'"' # Both ends live on the anchor => coordinate an introduction, not a cold address. check "anchor coordinates RelayIntroduce when both ends are live" \ bash -c 'grep -q "Convection: coordinated introduction" /tmp/itsgoin-ctop6.log' # Rotation: an entry is handed to exactly 2 callers, then leaves the window. echo "convect $n6" >&24; sleep 6 echo "convect $n6" >&23; sleep 6 echo "convect $n6" >&27; sleep 6 check "each window entry is handed out at most twice, then rotates out" \ bash -c 'n=$(grep "Convection: referring peer" /tmp/itsgoin-ctop6.log | grep -c "referred=\"\?'"$n1"'"); \ [ "$n" -le 2 ]' check "later callers are served the more RECENT callers" \ bash -c 'grep "Convection: referring peer" /tmp/itsgoin-ctop6.log | tail -3 | grep -q "'"$n5"'\|'"$n4"'"' # CHEAP REFUSAL: node3 is an interior chain node (2 mesh peers) so its request # is TOP-UP class. It asks node5, whose own convection window is empty — there # is nothing fresh to circulate, so the refusal must be one message, not a # burned timeout. echo "connect $n5@127.0.0.1:18435" >&23; sleep 4 echo "convect $n5" >&23; sleep 6 check "top-up against an empty window is refused" \ bash -c 'grep -q "convection: refused" /tmp/itsgoin-ctop3.log' check "the refusal is CHEAP (single message, well under a timeout)" \ bash -c 'ms=$(grep -o "convection: refused in [0-9]*ms" /tmp/itsgoin-ctop3.log | tail -1 | grep -o "[0-9]*" | head -1); [ -n "$ms" ] && [ "$ms" -lt 3000 ]' check "entry class is served even where top-up was refused" \ bash -c 'grep "Convection: serving referrals" /tmp/itsgoin-ctop6.log | grep -q entry' check "the refusing anchor lands in the caller's penalty box (refusal feedback)" \ bash -c 'echo "convection" >&23; sleep 3; strip_ansi /tmp/itsgoin-ctop3.log | tail -20 | grep -oE "anchor_bias [0-9.]+" | tail -1 | awk "{ exit !(\$2 < 1.0) }"' ############################################################################## echo echo "== scenario 3: temp referral slots (mesh cap 2) ==" ############################################################################## # Restart nodes 1-4 with a mesh cap of 2 so the temp-referral band is reachable. for fd in 21 22 23 24 25 26 27; do echo "quit" >&$fd 2>/dev/null; done sleep 3 kill $(pgrep -f 'itsgoin.*itsgoin-ctop') 2>/dev/null; sleep 2 for fd in 21 22 23 24 25 26 27; do eval "exec $fd>&-" 2>/dev/null || true; done for i in 1 2 3 4; do rm -rf /tmp/itsgoin-ctop$i /tmp/itsgoin-ctopcmd$i /tmp/itsgoin-ctop$i.log mkdir -p /tmp/itsgoin-ctop$i; mkfifo /tmp/itsgoin-ctopcmd$i start_node $i $((18440 + i)) ITSGOIN_TEST_MESH_SLOTS=2 \ ITSGOIN_TEST_ALLOW_LOOPBACK_REFERRALS=1 ITSGOIN_TEST_NO_GROWTH=1 done exec 21>/tmp/itsgoin-ctopcmd1 22>/tmp/itsgoin-ctopcmd2 23>/tmp/itsgoin-ctopcmd3 \ 24>/tmp/itsgoin-ctopcmd4 sleep 7 for i in 1 2 3 4; do eval "m${i}=\$(strip_ansi /tmp/itsgoin-ctop$i.log | grep -m1 'Node ID:' | awk '{print \$3}')" done # Fill node1's 2 mesh slots, then send a third peer at it. echo "connect $m1@127.0.0.1:18441" >&22; sleep 4 echo "connect $m1@127.0.0.1:18441" >&23; sleep 4 echo "slots" >&21; sleep 3 check "mesh pool fills to the cap" \ bash -c 'strip_ansi /tmp/itsgoin-ctop1.log | grep -q "mesh 2/2"' echo "connect $m1@127.0.0.1:18441" >&24; sleep 5 echo "slots" >&21; sleep 3 check "the 4th peer lands in a temp referral slot ABOVE the cap" \ bash -c 'strip_ansi /tmp/itsgoin-ctop1.log | tail -30 | grep -q "mesh 2/2 temp-referral 1/"' check "an established mesh peer was never evicted to make room" \ bash -c 'strip_ansi /tmp/itsgoin-ctop1.log | tail -30 | grep -q "mesh 2/2"' check "the temp slot carries NO knowledge (never a reporter in our index)" \ bash -c '[ "$(sqlite3 /tmp/itsgoin-ctop1/itsgoin.db \ "SELECT count(*) FROM reachable WHERE lower(hex(reporter_node_id))=\"'"$m4"'\"")" = "0" ]' check "and a temp peer is never written to mesh_peers" \ bash -c '[ "$(sqlite3 /tmp/itsgoin-ctop1/itsgoin.db \ "SELECT count(*) FROM mesh_peers WHERE lower(hex(node_id))=\"'"$m4"'\"")" = "0" ]' # Free a mesh slot: the temp peer graduates into it. The wait covers the mesh # keepalive interval (30s) — an abruptly-gone QUIC peer is noticed when the # next keepalive write fails, not instantly. echo "quit" >&22; sleep 45 echo "slots" >&21; sleep 5 check "the temp referral slot GRADUATES into the freed mesh slot" \ bash -c 'strip_ansi /tmp/itsgoin-ctop1.log | tail -20 | grep -q "temp-referral 0/"' check "the graduated peer now exchanges knowledge (written to mesh_peers)" \ bash -c '[ "$(sqlite3 /tmp/itsgoin-ctop1/itsgoin.db \ "SELECT count(*) FROM mesh_peers WHERE lower(hex(node_id))=\"'"$m4"'\"")" != "0" ]' ############################################################################## echo echo "== standing assertions ==" ############################################################################## check "0xC0 AnchorRegister is gone from the tree" \ bash -c '! grep -rn "AnchorRegister" crates/ --include=*.rs' check "no automatic session relay anywhere in the convection path" \ bash -c '! grep -riE "session relay" /tmp/itsgoin-ctop*.log' check "no registration cycle writes known_anchors (bootstrap cache only)" \ bash -c '! grep -rn "start_anchor_register_cycle" crates/ --include=*.rs' echo echo "== results: $PASS passed, $FAIL failed ==" [ "$FAIL" -eq 0 ]