feat: v0.8 Iteration C — narrow mesh, N1-N4 uniques index, anchor convection
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
This commit is contained in:
parent
36e3871c4b
commit
e756fabb11
14 changed files with 5503 additions and 2367 deletions
|
|
@ -230,6 +230,11 @@ async fn main() -> anyhow::Result<()> {
|
|||
println!(" redundancy Show replica counts for your posts");
|
||||
println!(" worm <node_id> Worm lookup (find peer beyond 3-hop map)");
|
||||
println!(" connections Show mesh connections");
|
||||
println!(" slots Show mesh / temp-referral slot usage");
|
||||
println!(" uniques [node_id_hex] Uniques index counts, or where an ID is known");
|
||||
println!(" convection Adaptive convection weights + pool-mined anchors");
|
||||
println!(" convect <anchor_id> Ask an anchor for peers (convection)");
|
||||
println!(" uniques-pull Exchange uniques indexes with mesh peers");
|
||||
println!(" social-routes Show social routing cache");
|
||||
println!(" name <display_name> Set your display name");
|
||||
println!(" register <name> [keywords...] Register default persona in the network registry (30d, re-run to renew)");
|
||||
|
|
@ -248,13 +253,22 @@ async fn main() -> anyhow::Result<()> {
|
|||
|
||||
// Start background tasks (v2: mesh connections)
|
||||
let _accept_handle = node.start_accept_loop();
|
||||
let _pull_handle = node.start_pull_cycle(); // 60s tiered pull cycle
|
||||
let _sync_handle = node.start_sync_cycle(); // 60s: uniques-index exchange + content sync
|
||||
let _diff_handle = node.start_diff_cycle(120); // 2 min routing diff
|
||||
let _rebalance_handle = node.start_rebalance_cycle(600); // 10 min rebalance
|
||||
let _growth_handle = node.start_growth_loop(); // reactive mesh growth
|
||||
// Reactive mesh growth. `ITSGOIN_TEST_NO_GROWTH` suppresses it so an
|
||||
// integration test can hold an exact topology (on loopback the growth loop
|
||||
// otherwise collapses any chain into a full mesh within seconds, and a
|
||||
// full mesh has no N2 at all — every peer is a direct). Test gate only.
|
||||
let _growth_handle = if std::env::var("ITSGOIN_TEST_NO_GROWTH").is_ok() {
|
||||
eprintln!("[test gate] growth loop disabled (ITSGOIN_TEST_NO_GROWTH)");
|
||||
None
|
||||
} else {
|
||||
Some(node.start_growth_loop())
|
||||
};
|
||||
let _recovery_handle = node.start_recovery_loop(); // reactive anchor reconnect on mesh loss
|
||||
let _checkin_handle = node.start_social_checkin_cycle(3600); // 1 hour social checkin
|
||||
let _anchor_handle = node.start_anchor_register_cycle(600); // 10 min anchor register
|
||||
let _convection_handle = node.start_convection_loop(); // stochastic anchor convection + anchor self-probe
|
||||
let _upnp_tcp_handle = node.start_upnp_tcp_renewal_cycle(); // UPnP TCP lease renewal
|
||||
let _http_handle = node.start_http_server(); // HTTP post delivery (if publicly reachable)
|
||||
let _bootstrap_check = node.start_bootstrap_connectivity_check(); // 24h isolation check
|
||||
|
|
@ -858,7 +872,7 @@ async fn main() -> anyhow::Result<()> {
|
|||
println!("(no mesh connections)");
|
||||
} else {
|
||||
println!("Mesh connections ({}):", conns.len());
|
||||
for (nid, slot_kind, connected_at) in conns {
|
||||
for (nid, slot, connected_at) in conns {
|
||||
let name = node.get_display_name(&nid).await.unwrap_or(None);
|
||||
let id_short = &hex::encode(nid)[..12];
|
||||
let label = name.map(|n| format!("{} ({})", n, id_short))
|
||||
|
|
@ -870,7 +884,95 @@ async fn main() -> anyhow::Result<()> {
|
|||
.as_millis() as u64;
|
||||
(now.saturating_sub(connected_at)) / 1000
|
||||
};
|
||||
println!(" {} [{:?}] connected {}s ago", label, slot_kind, duration_secs);
|
||||
println!(" {} [{}] connected {}s ago", label, slot, duration_secs);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// v0.8 Iteration C observability: slot pool + uniques index.
|
||||
"slots" => {
|
||||
let conns = node.list_connections().await;
|
||||
let mesh = conns.iter().filter(|(_, s, _)| s.is_mesh()).count();
|
||||
let temp = conns.len() - mesh;
|
||||
println!("mesh {}/{} temp-referral {}/{}",
|
||||
mesh, node.device_profile().mesh_slots(),
|
||||
temp, node.device_profile().temp_referral_slots());
|
||||
for (nid, slot, _) in conns {
|
||||
println!(" {} {}", &hex::encode(nid)[..12], slot);
|
||||
}
|
||||
}
|
||||
|
||||
"uniques" => {
|
||||
let storage = node.storage.get().await;
|
||||
if parts.len() > 1 {
|
||||
match itsgoin_core::parse_node_id_hex(parts[1]) {
|
||||
Ok(target) => {
|
||||
let mut any = false;
|
||||
for bounce in 2u8..=4 {
|
||||
for reporter in storage.find_reporters_at(&target, bounce).unwrap_or_default() {
|
||||
println!(" N{} via {}", bounce, &hex::encode(reporter)[..12]);
|
||||
any = true;
|
||||
}
|
||||
}
|
||||
if !any {
|
||||
println!("(not in the uniques index)");
|
||||
}
|
||||
}
|
||||
Err(e) => println!("Bad node id: {}", e),
|
||||
}
|
||||
} else {
|
||||
println!("N2 {} N3 {} N4 {} (anchor density {:.2})",
|
||||
storage.count_distinct_n2().unwrap_or(0),
|
||||
storage.count_distinct_n3().unwrap_or(0),
|
||||
storage.count_distinct_n4().unwrap_or(0),
|
||||
storage.anchor_density().unwrap_or(0.0));
|
||||
}
|
||||
}
|
||||
|
||||
// The v0.8 "pull": exchange uniques indexes with every mesh peer.
|
||||
"uniques-pull" => {
|
||||
let n = node.uniques_pull().await;
|
||||
println!("uniques-pull: exchanged with {} mesh peers", n);
|
||||
}
|
||||
|
||||
// On-demand convection against a named anchor.
|
||||
"convect" => {
|
||||
if parts.len() < 2 {
|
||||
println!("Usage: convect <anchor_node_id_hex>");
|
||||
} else {
|
||||
match itsgoin_core::parse_node_id_hex(parts[1]) {
|
||||
Ok(anchor) => match node.convection_request(anchor).await {
|
||||
Ok((_, true, ms)) => println!("convection: refused in {}ms", ms),
|
||||
Ok((n, false, ms)) => println!("convection: {} new peers in {}ms", n, ms),
|
||||
Err(e) => println!("convection failed: {}", e),
|
||||
},
|
||||
Err(e) => println!("Bad node id: {}", e),
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// Convection diagnostics: the adaptive action weights, the
|
||||
// pool-mined anchor directory, and a manual entry-class request.
|
||||
"convection" => {
|
||||
let (density, bias, mesh) = node.network.conn_handle().convection_state().await;
|
||||
let cap = node.device_profile().mesh_slots();
|
||||
let fill = if cap == 0 { 1.0 } else { mesh as f64 / cap as f64 };
|
||||
let w = itsgoin_core::connection::convection_weights(density, bias, fill);
|
||||
let total = w.nothing + w.anchor + w.mesh;
|
||||
println!("anchor_density {:.3} anchor_bias {:.2} mesh {}/{}", density, bias, mesh, cap);
|
||||
println!("weights: nothing {:.3} anchor {:.3} mesh {:.3} (P: {:.0}% / {:.0}% / {:.0}%)",
|
||||
w.nothing, w.anchor, w.mesh,
|
||||
100.0 * w.nothing / total, 100.0 * w.anchor / total, 100.0 * w.mesh / total);
|
||||
let anchors = {
|
||||
let storage = node.storage.get().await;
|
||||
storage.list_pool_anchors(16).unwrap_or_default()
|
||||
};
|
||||
if anchors.is_empty() {
|
||||
println!("pool anchors: (none)");
|
||||
} else {
|
||||
println!("pool anchors ({}):", anchors.len());
|
||||
for (nid, addrs) in anchors {
|
||||
println!(" {} {}", &hex::encode(nid)[..12], addrs.join(","));
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
|
|||
Loading…
Add table
Add a link
Reference in a new issue