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
|
|
@ -6,7 +6,7 @@ use tracing::info;
|
|||
|
||||
use itsgoin_core::identity::IdentityManager;
|
||||
use itsgoin_core::node::Node;
|
||||
use itsgoin_core::types::{NodeId, PeerSlotKind, Post, PostId, PostVisibility, VisibilityIntent};
|
||||
use itsgoin_core::types::{NodeId, Post, PostId, PostVisibility, VisibilityIntent};
|
||||
|
||||
/// The active Node, swappable on identity switch.
|
||||
type AppNode = Arc<tokio::sync::RwLock<Arc<Node>>>;
|
||||
|
|
@ -1363,7 +1363,7 @@ async fn list_peers(state: State<'_, AppNode>) -> Result<Vec<PeerDto>, String> {
|
|||
.into_iter()
|
||||
.map(|(nid, _, _)| nid)
|
||||
.collect();
|
||||
let (social_ids, n2_ids, n3_ids) = {
|
||||
let (social_ids, n2_ids, n3_ids, n4_ids) = {
|
||||
let storage = node.storage.get().await;
|
||||
let social: std::collections::HashSet<_> = storage
|
||||
.list_social_routes()
|
||||
|
|
@ -1372,7 +1372,7 @@ async fn list_peers(state: State<'_, AppNode>) -> Result<Vec<PeerDto>, String> {
|
|||
.map(|r| r.node_id)
|
||||
.collect();
|
||||
let n2: std::collections::HashSet<_> = storage
|
||||
.build_n2_share()
|
||||
.list_reach_ids_at(2)
|
||||
.unwrap_or_default()
|
||||
.into_iter()
|
||||
.collect();
|
||||
|
|
@ -1381,7 +1381,12 @@ async fn list_peers(state: State<'_, AppNode>) -> Result<Vec<PeerDto>, String> {
|
|||
.unwrap_or_default()
|
||||
.into_iter()
|
||||
.collect();
|
||||
(social, n2, n3)
|
||||
let n4: std::collections::HashSet<_> = storage
|
||||
.list_distinct_n4()
|
||||
.unwrap_or_default()
|
||||
.into_iter()
|
||||
.collect();
|
||||
(social, n2, n3, n4)
|
||||
};
|
||||
|
||||
let mut dtos = Vec::with_capacity(records.len());
|
||||
|
|
@ -1406,6 +1411,8 @@ async fn list_peers(state: State<'_, AppNode>) -> Result<Vec<PeerDto>, String> {
|
|||
"n2"
|
||||
} else if n3_ids.contains(&rec.node_id) {
|
||||
"n3"
|
||||
} else if n4_ids.contains(&rec.node_id) {
|
||||
"n4"
|
||||
} else {
|
||||
"known"
|
||||
};
|
||||
|
|
@ -2066,12 +2073,12 @@ async fn list_connections(state: State<'_, AppNode>) -> Result<Vec<ConnectionDto
|
|||
let node = get_node(&state).await;
|
||||
let conns = node.list_connections().await;
|
||||
let mut dtos = Vec::with_capacity(conns.len());
|
||||
for (nid, slot_kind, connected_at) in conns {
|
||||
for (nid, slot, connected_at) in conns {
|
||||
let display_name = node.get_display_name(&nid).await.unwrap_or(None);
|
||||
dtos.push(ConnectionDto {
|
||||
node_id: hex::encode(nid),
|
||||
display_name,
|
||||
slot_kind: format!("{:?}", slot_kind),
|
||||
slot_kind: slot.to_string(),
|
||||
connected_at,
|
||||
});
|
||||
}
|
||||
|
|
@ -2493,11 +2500,13 @@ async fn sync_from_peer(state: State<'_, AppNode>, node_id_hex: String) -> Resul
|
|||
#[derive(Serialize)]
|
||||
#[serde(rename_all = "camelCase")]
|
||||
struct NetworkSummaryDto {
|
||||
local_count: usize,
|
||||
wide_count: usize,
|
||||
mesh_count: usize,
|
||||
temp_count: usize,
|
||||
mesh_cap: usize,
|
||||
total_connections: usize,
|
||||
n2_distinct: usize,
|
||||
n3_distinct: usize,
|
||||
n4_distinct: usize,
|
||||
has_public_v6: bool,
|
||||
has_public_v4: bool,
|
||||
has_upnp: bool,
|
||||
|
|
@ -2507,26 +2516,24 @@ struct NetworkSummaryDto {
|
|||
async fn get_network_summary(state: State<'_, AppNode>) -> Result<NetworkSummaryDto, String> {
|
||||
let node = get_node(&state).await;
|
||||
let conns = node.list_connections().await;
|
||||
let mut local = 0usize;
|
||||
let mut wide = 0usize;
|
||||
for (_nid, slot_kind, _at) in &conns {
|
||||
match slot_kind {
|
||||
PeerSlotKind::Local => local += 1,
|
||||
PeerSlotKind::Wide => wide += 1,
|
||||
}
|
||||
}
|
||||
let (n2, n3) = {
|
||||
let mesh = conns.iter().filter(|(_, slot, _)| slot.is_mesh()).count();
|
||||
let temp = conns.len() - mesh;
|
||||
let (n2, n3, n4) = {
|
||||
let storage = node.storage.get().await;
|
||||
let n2 = storage.count_distinct_n2().unwrap_or(0);
|
||||
let n3 = storage.count_distinct_n3().unwrap_or(0);
|
||||
(n2, n3)
|
||||
(
|
||||
storage.count_distinct_n2().unwrap_or(0),
|
||||
storage.count_distinct_n3().unwrap_or(0),
|
||||
storage.count_distinct_n4().unwrap_or(0),
|
||||
)
|
||||
};
|
||||
Ok(NetworkSummaryDto {
|
||||
local_count: local,
|
||||
wide_count: wide,
|
||||
mesh_count: mesh,
|
||||
temp_count: temp,
|
||||
mesh_cap: node.device_profile().mesh_slots(),
|
||||
total_connections: conns.len(),
|
||||
n2_distinct: n2,
|
||||
n3_distinct: n3,
|
||||
n4_distinct: n4,
|
||||
has_public_v6: node.network.has_public_v6(),
|
||||
has_public_v4: node.network.is_anchor(),
|
||||
has_upnp: node.network.has_upnp(),
|
||||
|
|
@ -2748,15 +2755,14 @@ struct ActivityLogDto {
|
|||
events: Vec<ActivityEventDto>,
|
||||
rebalance_last_ms: u64,
|
||||
rebalance_interval_secs: u64,
|
||||
anchor_register_last_ms: u64,
|
||||
anchor_register_interval_secs: u64,
|
||||
convection_last_ms: u64,
|
||||
}
|
||||
|
||||
#[tauri::command]
|
||||
async fn get_activity_log(state: State<'_, AppNode>) -> Result<ActivityLogDto, String> {
|
||||
let node = get_node(&state).await;
|
||||
let events = node.get_activity_log(200);
|
||||
let (rebalance_last, anchor_last) = node.timer_state();
|
||||
let (rebalance_last, convection_last) = node.timer_state();
|
||||
let dto_events: Vec<ActivityEventDto> = events.into_iter().map(|e| {
|
||||
ActivityEventDto {
|
||||
timestamp_ms: e.timestamp_ms,
|
||||
|
|
@ -2770,8 +2776,7 @@ async fn get_activity_log(state: State<'_, AppNode>) -> Result<ActivityLogDto, S
|
|||
events: dto_events,
|
||||
rebalance_last_ms: rebalance_last,
|
||||
rebalance_interval_secs: 600,
|
||||
anchor_register_last_ms: anchor_last,
|
||||
anchor_register_interval_secs: 600,
|
||||
convection_last_ms: convection_last,
|
||||
})
|
||||
}
|
||||
|
||||
|
|
@ -2787,31 +2792,47 @@ async fn trigger_rebalance(state: State<'_, AppNode>) -> Result<String, String>
|
|||
async fn request_referrals(state: State<'_, AppNode>) -> Result<String, String> {
|
||||
let node = get_node(&state).await;
|
||||
let node_id = node.node_id;
|
||||
// Try known_anchors table first (populated by anchor register cycle),
|
||||
// fall back to anchor peers from the peers table (is_anchor = true)
|
||||
let mesh = node.network.conn_handle().mesh_count().await;
|
||||
// Manual trigger uses the same class rule as the automatic path: below 2
|
||||
// mesh peers this is an ENTRY request and is always served.
|
||||
let class = itsgoin_core::protocol::ConvectionClass::for_mesh_count(mesh);
|
||||
|
||||
// Pool-mined anchors first (the uniques pools ARE the anchor directory),
|
||||
// then the known_anchors bootstrap cache, then anchor-flagged peers.
|
||||
let anchors: Vec<(NodeId, Vec<std::net::SocketAddr>)> = {
|
||||
let storage = node.storage.get().await;
|
||||
let known = storage.list_known_anchors().unwrap_or_default();
|
||||
if !known.is_empty() {
|
||||
known
|
||||
} else {
|
||||
storage.list_anchor_peers().unwrap_or_default()
|
||||
.into_iter()
|
||||
.map(|r| (r.node_id, r.addresses))
|
||||
.collect()
|
||||
let mut out: Vec<(NodeId, Vec<std::net::SocketAddr>)> = Vec::new();
|
||||
let mut seen: std::collections::HashSet<NodeId> = std::collections::HashSet::new();
|
||||
for (nid, addrs) in storage.list_pool_anchors(16).unwrap_or_default() {
|
||||
let socks: Vec<std::net::SocketAddr> = addrs.iter().filter_map(|a| a.parse().ok()).collect();
|
||||
if !socks.is_empty() && seen.insert(nid) {
|
||||
out.push((nid, socks));
|
||||
}
|
||||
}
|
||||
for (nid, addrs) in storage.list_known_anchors().unwrap_or_default() {
|
||||
if seen.insert(nid) {
|
||||
out.push((nid, addrs));
|
||||
}
|
||||
}
|
||||
for r in storage.list_anchor_peers().unwrap_or_default() {
|
||||
if seen.insert(r.node_id) {
|
||||
out.push((r.node_id, r.addresses));
|
||||
}
|
||||
}
|
||||
out
|
||||
};
|
||||
if anchors.is_empty() {
|
||||
return Ok("No known anchors".to_string());
|
||||
}
|
||||
let mut total = 0usize;
|
||||
let mut reachable = 0usize;
|
||||
|
||||
let mut connected = 0usize;
|
||||
let mut asked = 0usize;
|
||||
let mut refused = 0usize;
|
||||
for (anchor_nid, anchor_addrs) in &anchors {
|
||||
if *anchor_nid == node_id {
|
||||
continue;
|
||||
}
|
||||
// Connect to anchor if not already connected
|
||||
if !node.network.is_peer_connected(anchor_nid).await {
|
||||
if !node.network.is_peer_connected_or_session(anchor_nid).await {
|
||||
let endpoint_id = match itsgoin_core::EndpointId::from_bytes(anchor_nid) {
|
||||
Ok(eid) => eid,
|
||||
Err(_) => continue,
|
||||
|
|
@ -2820,40 +2841,26 @@ async fn request_referrals(state: State<'_, AppNode>) -> Result<String, String>
|
|||
for sa in anchor_addrs {
|
||||
addr = addr.with_ip_addr(*sa);
|
||||
}
|
||||
if let Err(_) = node.network.connect_to_peer(*anchor_nid, addr).await {
|
||||
if node.network.connect_to_anchor(*anchor_nid, addr).await.is_err() {
|
||||
continue;
|
||||
}
|
||||
}
|
||||
match node.network.request_anchor_referrals(anchor_nid).await {
|
||||
Ok(referrals) => {
|
||||
reachable += 1;
|
||||
for referral in &referrals {
|
||||
if referral.node_id == node_id {
|
||||
continue;
|
||||
}
|
||||
if let Some(addr_str) = referral.addresses.first() {
|
||||
let connect_str = format!(
|
||||
"{}@{}",
|
||||
hex::encode(referral.node_id),
|
||||
addr_str,
|
||||
);
|
||||
if let Ok((rid, raddr)) = itsgoin_core::parse_connect_string(&connect_str) {
|
||||
match node.network.connect_to_peer(rid, raddr).await {
|
||||
Ok(()) => {}
|
||||
Err(_) => {
|
||||
// Direct connect failed (NAT) — try hole punch via anchor
|
||||
let _ = node.network.connect_via_introduction(rid, *anchor_nid).await;
|
||||
}
|
||||
}
|
||||
total += 1;
|
||||
}
|
||||
}
|
||||
match node.network.request_convection(anchor_nid, class).await {
|
||||
Ok(response) => {
|
||||
asked += 1;
|
||||
if response.refused {
|
||||
refused += 1;
|
||||
} else {
|
||||
connected += node.network.act_on_convection(anchor_nid, &response).await;
|
||||
}
|
||||
}
|
||||
Err(_) => {}
|
||||
}
|
||||
}
|
||||
Ok(format!("Got {} referrals from {} anchors", total, reachable))
|
||||
Ok(format!(
|
||||
"Convection: {} new peers from {} anchors ({} refused)",
|
||||
connected, asked, refused
|
||||
))
|
||||
}
|
||||
|
||||
#[tauri::command]
|
||||
|
|
@ -3197,13 +3204,13 @@ async fn switch_identity(
|
|||
|
||||
// Start background tasks on the new node
|
||||
new_node.start_accept_loop();
|
||||
new_node.start_pull_cycle();
|
||||
new_node.start_sync_cycle();
|
||||
new_node.start_diff_cycle(120);
|
||||
new_node.start_rebalance_cycle(600);
|
||||
new_node.start_growth_loop();
|
||||
new_node.start_recovery_loop();
|
||||
new_node.start_social_checkin_cycle(3600);
|
||||
new_node.start_anchor_register_cycle(600);
|
||||
new_node.start_convection_loop();
|
||||
new_node.start_upnp_tcp_renewal_cycle();
|
||||
new_node.start_http_server();
|
||||
new_node.start_bootstrap_connectivity_check();
|
||||
|
|
@ -3321,13 +3328,13 @@ async fn clear_duplicate_flag(state: State<'_, AppNode>) -> Result<(), String> {
|
|||
node.network.duplicate_detected.store(false, std::sync::atomic::Ordering::Relaxed);
|
||||
// Start the sync tasks that were skipped during bootstrap
|
||||
node.start_accept_loop();
|
||||
node.start_pull_cycle();
|
||||
node.start_sync_cycle();
|
||||
node.start_diff_cycle(120);
|
||||
node.start_rebalance_cycle(600);
|
||||
node.start_growth_loop();
|
||||
node.start_recovery_loop();
|
||||
node.start_social_checkin_cycle(3600);
|
||||
node.start_anchor_register_cycle(600);
|
||||
node.start_convection_loop();
|
||||
node.start_upnp_tcp_renewal_cycle();
|
||||
node.start_http_server();
|
||||
node.start_bootstrap_connectivity_check();
|
||||
|
|
@ -3569,13 +3576,13 @@ pub fn run() {
|
|||
|
||||
// Start all background networking tasks
|
||||
boot_node.start_accept_loop();
|
||||
boot_node.start_pull_cycle();
|
||||
boot_node.start_sync_cycle();
|
||||
boot_node.start_diff_cycle(120);
|
||||
boot_node.start_rebalance_cycle(600);
|
||||
boot_node.start_growth_loop();
|
||||
boot_node.start_recovery_loop();
|
||||
boot_node.start_social_checkin_cycle(3600);
|
||||
boot_node.start_anchor_register_cycle(600);
|
||||
boot_node.start_convection_loop();
|
||||
boot_node.start_upnp_tcp_renewal_cycle();
|
||||
boot_node.start_http_server();
|
||||
boot_node.start_bootstrap_connectivity_check();
|
||||
|
|
|
|||
Loading…
Add table
Add a link
Reference in a new issue