Topology rework. The mesh's job is width; depth now comes from knowledge.
Slots:
- Local/Wide collapse into ONE mesh pool: 20 desktop / 15 mobile
- Temp referral slots +4..+10 ABOVE the cap (10 desktop / 4 mobile), carry no
knowledge exchange, graduate into a freed mesh slot or expire (5 min TTL),
never evict an established mesh peer
- Fixes the Wide-never-fills bug: outbound paths hardcoded Local while the
growth gate counted Local only, so growth stopped at 71 and 20 slots were
inbound-only. Also closes register_connection paths that enforced no cap.
Knowledge — two-pool uniques announce (replaces N1/N2/N3 share lists):
- Pool 1 (forwardable): our N0-N2 uniques, deduped. Receiver stores shifted one
bounce deeper as N1-N3, reporter-tagged.
- Pool 2 (terminal): our N3, deduped against pool 1. Receiver stores as N4,
USED for search/resolution but NEVER re-announced. No N5 anywhere.
- Uniques merge mesh peers + social directs + CDN file authors + holder peers,
so receivers cannot tell how we know an ID
- Anchor entries carry addresses; every other entry is a bare ID. The pools
double as the anchor directory.
- Dedup at both store and announce; per-bounce caps; 2 MB payload ceiling
- Device-tiered depth: mobile holds 3 bounces and drops the terminal pool
Anchor convection (register loop retired — 0xC0 AnchorRegister deleted):
- Rolling in-memory caller window (no registration DB): a caller gets the 2
most recent viable prior callers, its address goes to the next 2, then
rotates out; still-connected callers preferred; RelayIntroduce coordinated
when both ends are live
- Request classes: entry (<2 connections) always served; top-up refused
cheaply under load, and the refusal feeds the adaptive weights
- Stochastic per-disconnect action {nothing | anchor intro | mesh intro} with
weights from local anchor density + refusal feedback. No thresholds, no
jitter timer. Recovery (mesh < 2) stays immediate and non-stochastic.
- Anchors mined from the uniques pools incl. retained pools of disconnected
peers; known_anchors demoted to a bootstrap cache
Security fixes from review: remote address poisoning (hearsay anchors could be
dialled — known_anchors/is_anchor now written only from completed handshakes),
N4 leaking back into announcements via the anchor mirror (infinite propagation),
missing payload size caps, unbounded candidate scoring, and a bulk SQLite write
held under the ConnectionManager mutex.
deploy.sh: version regex now captures pre-release suffixes (0.8.0-alpha would
have truncated to 0.8.0 and broken every artifact path), announce channel
derives from the suffix, and the anchor runs --publish-registry at genesis.
228 core tests; a3 integration 9/9; new c_topology_test.sh 33/33 (both
independently re-run). EDM corpse, session-relay gating, Iteration A/B intact.
Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01LGiPD2cF75mnvneSCjdDC5
142 lines
5.5 KiB
Bash
Executable file
142 lines
5.5 KiB
Bash
Executable file
#!/usr/bin/env bash
|
|
# v0.8 A3 integration test — registry + greetings across 3 local nodes.
|
|
#
|
|
# Spec: A3-SPEC §6 multi-node scenario. Run from the repo root after
|
|
# `cargo build -p itsgoin-cli`. Uses FIFOs to drive the interactive REPL.
|
|
#
|
|
# 1. All nodes self-materialize the same registry post ID; node1 also
|
|
# runs the --publish-registry genesis one-shot (debug gate override).
|
|
# 2. node2 registers "Alice rust p2p" → node3 `search rust` finds her.
|
|
# node2 `unregister` → node3 re-search finds nothing.
|
|
# 3. node2's bio has a greeting slot; node3 greets it; node2 sees the
|
|
# unsealed text + real sender persona; node2 replies; node3 sees the
|
|
# reply. node1 (holder) stores only ciphertext.
|
|
# 5. Newest-wins: node2 re-registers; node3 sees exactly one entry.
|
|
# 6. (Optional, ITSGOIN_TEST_TTL_SECS) comment expiry sweep.
|
|
#
|
|
# Exit code 0 = all checks passed. Logs: /tmp/itsgoin-cli{1,2,3}.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 <desc> <cmd...>
|
|
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"; }
|
|
# Same noise filter for the tail-window checks below. The netlink/mDNS watcher
|
|
# logs in unpredictable bursts, and a burst landing between a REPL command and
|
|
# its assertion pushes the answer out of a raw `tail -N` window — a false
|
|
# failure with nothing wrong in the node.
|
|
quiet_tail() { grep -av "netlink\|buffer_tool\|iroh_quinn\|swarm_discovery" "$1" | tail -"$2"; }
|
|
export -f quiet_tail
|
|
sq() { sqlite3 "/tmp/itsgoin-test$1/itsgoin.db" "$2"; }
|
|
|
|
cleanup() {
|
|
{ exec 13>&- 14>&- 15>&-; } 2>/dev/null || true
|
|
kill $(pgrep -f 'itsgoin.*itsgoin-test') 2>/dev/null
|
|
rm -f /tmp/itsgoin-cmd{1,2,3}
|
|
}
|
|
trap cleanup EXIT
|
|
|
|
echo "== setup =="
|
|
kill $(pgrep -f 'itsgoin.*itsgoin-test') 2>/dev/null; sleep 1
|
|
for i in 1 2 3; do
|
|
rm -rf /tmp/itsgoin-test$i /tmp/itsgoin-cmd$i /tmp/itsgoin-cli$i.log
|
|
mkdir -p /tmp/itsgoin-test$i
|
|
mkfifo /tmp/itsgoin-cmd$i
|
|
done
|
|
|
|
export ITSGOIN_TEST_ALLOW_REGISTRY_GENESIS=1
|
|
|
|
# Genesis one-shot on node1's data dir (debug-gate override).
|
|
"$BIN" /tmp/itsgoin-test1 --bind 127.0.0.1:18411 --publish-registry > /tmp/itsgoin-genesis.log 2>&1
|
|
check "genesis prints registry post id matching shipped constant" \
|
|
bash -c 'g=$(grep registry_post_id /tmp/itsgoin-genesis.log | awk "{print \$2}");
|
|
c=$(grep shipped_constant /tmp/itsgoin-genesis.log | awk "{print \$2}");
|
|
[ -n "$g" ] && [ "$g" = "$c" ]'
|
|
|
|
# Start the three nodes with FIFO-driven stdin.
|
|
for i in 1 2 3; do
|
|
( exec "$BIN" /tmp/itsgoin-test$i --bind 127.0.0.1:1841$i \
|
|
< /tmp/itsgoin-cmd$i > /tmp/itsgoin-cli$i.log 2>&1 ) &
|
|
done
|
|
# Keep FIFO write ends open for the whole run.
|
|
exec 13>/tmp/itsgoin-cmd1 14>/tmp/itsgoin-cmd2 15>/tmp/itsgoin-cmd3
|
|
sleep 6
|
|
|
|
n1_id=$(strip_ansi /tmp/itsgoin-cli1.log | grep -m1 "Node ID:" | awk '{print $3}')
|
|
n2_id=$(strip_ansi /tmp/itsgoin-cli2.log | grep -m1 "Node ID:" | awk '{print $3}')
|
|
echo "node1=$n1_id node2=$n2_id"
|
|
check "all nodes self-materialized the registry post" \
|
|
bash -c 'for i in 1 2 3; do sqlite3 /tmp/itsgoin-test$i/itsgoin.db \
|
|
"SELECT count(*) FROM posts WHERE hex(id) = upper(\"'"$(grep shipped_constant /tmp/itsgoin-genesis.log | awk '{print $2}')"'\")" \
|
|
| grep -q 1 || exit 1; done'
|
|
|
|
echo "== mesh: 2,3 -> 1; 3 -> 2 =="
|
|
echo "connect $n1_id@127.0.0.1:18411" >&14
|
|
echo "connect $n1_id@127.0.0.1:18411" >&15
|
|
sleep 4
|
|
echo "connect $n2_id@127.0.0.1:18412" >&15
|
|
sleep 4
|
|
|
|
echo "== step 2: register / search / unregister =="
|
|
echo "name Alice" >&14
|
|
sleep 3
|
|
echo "register Alice rust p2p" >&14
|
|
sleep 3
|
|
echo "search rust" >&15
|
|
sleep 8
|
|
check "node3 search finds Alice" grep -q "Alice" /tmp/itsgoin-cli3.log
|
|
|
|
echo "unregister" >&14
|
|
sleep 3
|
|
echo "search rust" >&15
|
|
sleep 8
|
|
check "node3 re-search finds nothing after signed delete" \
|
|
bash -c 'quiet_tail /tmp/itsgoin-cli3.log 20 | grep -q "no registry matches"'
|
|
|
|
echo "== step 3: greeting roundtrip =="
|
|
# node2's bio post id (latest Profile post) from its DB.
|
|
bio2=$(sq 2 'SELECT lower(hex(id)) FROM posts WHERE visibility_intent = '"'"'"Profile"'"'"' ORDER BY timestamp_ms DESC LIMIT 1')
|
|
check "node2 has a bio post with a greeting slot" \
|
|
bash -c '[ -n "'"$bio2"'" ] && sqlite3 /tmp/itsgoin-test2/itsgoin.db \
|
|
"SELECT fof_gating_json FROM posts WHERE lower(hex(id))=\"'"$bio2"'\"" | grep -q Greeting'
|
|
|
|
echo "name Bob" >&15
|
|
sleep 3
|
|
echo "greet $bio2 hello alice from bob" >&15
|
|
sleep 6
|
|
echo "greetings" >&14
|
|
sleep 4
|
|
check "node2 unseals the greeting text" grep -q "hello alice from bob" /tmp/itsgoin-cli2.log
|
|
|
|
echo "reply 0 hello back bob" >&14
|
|
sleep 6
|
|
echo "greetings" >&15
|
|
sleep 4
|
|
check "node3 unseals the reply via its stored fresh reply key" \
|
|
grep -q "hello back bob" /tmp/itsgoin-cli3.log
|
|
|
|
# Holder-side opacity: node1 never sees plaintext greeting bodies.
|
|
check "node1 stores only ciphertext (no greeting plaintext in db)" \
|
|
bash -c '! sqlite3 /tmp/itsgoin-test1/itsgoin.db \
|
|
"SELECT content FROM comments" | grep -q "hello alice"'
|
|
|
|
echo "== step 5: newest-wins =="
|
|
echo "register Alice rust p2p" >&14
|
|
sleep 2
|
|
echo "register AliceV2 rust mesh" >&14
|
|
sleep 4
|
|
echo "search rust" >&15
|
|
sleep 8
|
|
check "node3 sees exactly one (newest) entry for node2's persona" \
|
|
bash -c 'quiet_tail /tmp/itsgoin-cli3.log 6 | grep -c "rust" | grep -q "^1$" &&
|
|
quiet_tail /tmp/itsgoin-cli3.log 6 | grep -q AliceV2'
|
|
|
|
echo
|
|
echo "== results: $PASS passed, $FAIL failed =="
|
|
[ "$FAIL" -eq 0 ]
|