refactor: v0.8 Iteration B — cruft purge + ALPN bump to itsgoin/4

Zero-users ruling removes all wire-compat obligations. Net -1,443 LOC of Rust.

Protocol break:
- ALPN_V2 b"itsgoin/3" -> ALPN b"itsgoin/4" (renamed: the versioned name was
  what rotted). Old nodes are now refused at the QUIC handshake instead of
  half-interoperating with the v0.8 manifest/digest formats. Deploy gate from
  Iteration A is discharged.

Dead wire surface removed (MessageType 46 -> 40):
- DeleteRecord 0x51 (deletes ride ControlOp::DeletePost + InitialExchange)
- VisibilityUpdate 0x52 + its two dead senders (ControlOp::UpdateVisibility)
- SocialDisconnectNotice 0x71 (zero senders; checkin timeouts carry the signal)
- MeshPrefer 0xB3 + request_prefer + handler (preferred peers are gone)
- Legacy dual pull-matching half (have_post_ids) — behavior-identical
- PullSyncResponse.visibility_updates + counters (control posts cover it)
- serde(default) stripped from wire-only payloads (kept where
  skip_serializing_if makes it load-bearing; persisted-row defaults untouched)

Preferred-peer subsystem eliminated (design ruling: CDN file_holders replaced
the N+10 direct push/pull it served): slot tier, preferred_peers table (dropped),
preferred_tree semantics, rebalance Priority 0, find_relays_for preferred tiers,
7-day prune + 30-day watcher, dead FromStr impl. Slots are now Local/Wide only
(91 desktop / 12 mobile) pending Iteration C's single ~20-slot pool.

Other dead code: hostlist encoder + base64url helper, always-empty
downstream_addrs parameter chain, start_upnp_renewal_cycle no-op + 4 callers,
RELAY_TARGET_RATE_LIMIT, GetSecretSeed, stale comments swept.

Preserved deliberately: EDM scanner corpse (awaiting raw-UDP refactor),
PortScanHeartbeat, Iteration A startup migrations, session-relay opt-in gating.

Docs: design.html + tech.html synced (message counts, ALPN, purge status,
Rework asides flipped past-tense).

190 core tests pass; CLI + desktop build; A3 integration 9/9 (independently
re-run post-purge).

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01LGiPD2cF75mnvneSCjdDC5
This commit is contained in:
Scott Reimers 2026-07-30 09:26:16 -04:00
parent 17dbf076cb
commit 36e3871c4b
15 changed files with 152 additions and 1476 deletions

View file

@ -31,6 +31,11 @@ use crate::types::{
/// with "UnknownIssuer" because they pin the wrong cert identity.
const DEFAULT_ANCHOR: &str = "ab2b7258ef0b75b2c6ee8bf6595232055f6199d584d3c0fc10b15a1ed549aa13@itsgoin.net:4433";
/// Cooldown between relay-introduction attempts toward the same target (5 min)
const RELAY_COOLDOWN_MS: i64 = 300_000;
/// Timeout for a single relay-introduction round trip
const RELAY_INTRO_TIMEOUT_SECS: u64 = 15;
/// A distsoc node: ties together identity, storage, and networking
pub struct Node {
pub data_dir: PathBuf,
@ -342,7 +347,7 @@ impl Node {
// Load or generate identity key (network secret — QUIC endpoint only,
// never used as content author under the v0.6.1+ clean model).
let key_path = data_dir.join("identity.key");
let (mut secret_key, mut secret_seed) = if key_path.exists() {
let (mut secret_key, secret_seed) = if key_path.exists() {
let key_bytes = std::fs::read(&key_path)?;
let bytes: [u8; 32] = key_bytes
.try_into()
@ -415,7 +420,6 @@ impl Node {
std::fs::write(&key_path, new_seed)?;
info!("v0.6.1 migration: rotated network key to decouple from default posting key");
secret_key = new_key;
secret_seed = new_seed;
}
}
}
@ -429,9 +433,9 @@ impl Node {
let last_rebalance_ms = Arc::new(AtomicU64::new(0));
let last_anchor_register_ms = Arc::new(AtomicU64::new(0));
// Start network (v2: single ALPN, connection manager)
// Start network (single ALPN, connection manager)
let network = Arc::new(
Network::new(secret_key, Arc::clone(&storage), bind_addr, secret_seed, Arc::clone(&blob_store), profile, Arc::clone(&activity_log)).await?,
Network::new(secret_key, Arc::clone(&storage), bind_addr, Arc::clone(&blob_store), profile, Arc::clone(&activity_log)).await?,
);
let node_id = network.node_id_bytes();
@ -492,7 +496,7 @@ impl Node {
// NETWORK id may still carry persona fields from the unified-id
// era. Strip them in place so no code path can ever re-broadcast
// persona data bound to the device network id. Topology fields
// (anchors/recent_peers/preferred_peers) are preserved.
// (anchors/recent_peers) are preserved.
//
// (b) The manifest signature digest dropped author_addresses, so
// rows signed by pre-v0.8 builds no longer verify. Re-sign our
@ -1302,7 +1306,7 @@ impl Node {
continue;
}
match kind {
PeerSlotKind::Preferred | PeerSlotKind::Local => social.push(nid),
PeerSlotKind::Local => social.push(nid),
PeerSlotKind::Wide => wide.push(nid),
}
}
@ -1982,7 +1986,6 @@ impl Node {
let peer_addresses = storage.build_peer_addresses_for(node_id)?;
let now = std::time::SystemTime::now().duration_since(std::time::UNIX_EPOCH)
.unwrap_or_default().as_millis() as u64;
let preferred_tree = storage.build_preferred_tree_for(node_id).unwrap_or_default();
storage.upsert_social_route(&SocialRouteEntry {
node_id: *node_id,
addresses,
@ -1992,7 +1995,6 @@ impl Node {
last_connected_ms: 0,
last_seen_ms: now,
reach_method: ReachMethod::Direct,
preferred_tree,
})?;
Ok(())
@ -2161,7 +2163,6 @@ impl Node {
updated_at: timestamp_ms,
anchors: vec![],
recent_peers: vec![],
preferred_peers: vec![],
public_visible: true,
avatar_cid,
})
@ -2183,10 +2184,9 @@ impl Node {
let recent_peers = self.current_recent_peers().await;
let profile = {
let storage = self.storage.get().await;
let preferred_peers = storage.list_preferred_peers().unwrap_or_default();
// v0.8: the network-id-keyed profile row is TOPOLOGY ONLY
// (anchors, recent_peers, preferred_peers). Persona fields
// (anchors, recent_peers). Persona fields
// (display_name, bio, avatar_cid, public_visible) live
// exclusively on posting-id-keyed rows written by profile
// posts — never copy them onto the network row, or a legacy
@ -2198,7 +2198,6 @@ impl Node {
updated_at: now,
anchors,
recent_peers,
preferred_peers,
public_visible: true,
avatar_cid: None,
};
@ -3438,7 +3437,6 @@ impl Node {
updated_at: now,
anchors: vec![],
recent_peers: vec![],
preferred_peers: vec![],
public_visible: visible,
avatar_cid: None,
},
@ -3630,8 +3628,7 @@ impl Node {
let now = control_post.timestamp_ms;
// Clean up blob storage local-side. Blobs in remote holders become
// orphans and get evicted naturally via LRU — BlobDeleteNotice is
// gone in v0.6.2.
// orphans and get evicted naturally via LRU.
let blob_cids = {
let storage = self.storage.get().await;
let cids = storage.delete_blobs_for_post(post_id)?;
@ -3805,7 +3802,7 @@ impl Node {
storage.store_post_with_visibility(&new_post_id, &new_post, &new_vis)?;
}
// delete_post already pushes the DeleteRecord.
// delete_post propagates the deletion as a signed control post.
// Replacement post propagates via the CDN to remaining recipients.
self.delete_post(post_id).await?;
@ -4021,7 +4018,7 @@ impl Node {
{
let on_cooldown = {
let storage = self.storage.get().await;
storage.is_relay_cooldown(&peer_id, 300_000).unwrap_or(false)
storage.is_relay_cooldown(&peer_id, RELAY_COOLDOWN_MS).unwrap_or(false)
};
if !on_cooldown {
@ -4041,7 +4038,7 @@ impl Node {
);
let intro_result = tokio::time::timeout(
std::time::Duration::from_secs(15),
std::time::Duration::from_secs(RELAY_INTRO_TIMEOUT_SECS),
self.network.send_relay_introduce_standalone(relay_peer, &peer_id, *ttl),
).await;
@ -4278,7 +4275,7 @@ impl Node {
/// Start pull cycle: Protocol v4 tiered pull — 60s ticks, full pull on first tick,
/// then only pull for stale authors (last_sync_ms > 4 hours old).
pub fn start_pull_cycle(self: &Arc<Self>, _interval_secs: u64) -> tokio::task::JoinHandle<()> {
pub fn start_pull_cycle(self: &Arc<Self>) -> tokio::task::JoinHandle<()> {
let node = Arc::clone(self);
tokio::spawn(async move {
let mut interval =
@ -4404,7 +4401,7 @@ impl Node {
})
}
/// Start recovery loop: triggered when mesh drops below 2 connections.
/// Start recovery loop: triggered when the mesh empties completely.
/// Immediately reconnects to anchors and requests referrals.
pub fn start_recovery_loop(&self) -> tokio::task::JoinHandle<()> {
let network = Arc::clone(&self.network);
@ -5029,18 +5026,6 @@ impl Node {
})
}
/// No-op since v0.7.2: the `portmapper::Client` held inside the active
/// `PortMapping` auto-renews internally in its own background service.
/// Retained for API compatibility with callers in CLI and Tauri until
/// they're cleaned up. Returns `None`.
///
/// TODO(v0.7.x): wire a watcher on `mapping.watch_external()` to clear
/// anchor mode if the external address stays `None` for more than ~5min
/// (parity with the old "3 renewal failures" behavior).
pub fn start_upnp_renewal_cycle(&self) -> Option<tokio::task::JoinHandle<()>> {
None
}
// --- HTTP Post Delivery ---
/// Start the HTTP server for serving public posts to browsers.
@ -5056,9 +5041,6 @@ impl Node {
}
let storage = Arc::clone(&self.storage);
let blob_store = Arc::clone(&self.blob_store);
let downstream_addrs = Arc::new(tokio::sync::Mutex::new(
std::collections::HashMap::<[u8; 32], Vec<std::net::SocketAddr>>::new(),
));
// Advertise HTTP capability to peers
let http_addr = self.network.http_addr();
@ -5076,7 +5058,7 @@ impl Node {
info!("Starting HTTP server on TCP port {}", port);
Some(tokio::spawn(async move {
if let Err(e) = crate::http::run_http_server(port, storage, blob_store, downstream_addrs).await {
if let Err(e) = crate::http::run_http_server(port, storage, blob_store).await {
warn!("HTTP server stopped: {}", e);
}
}))
@ -6963,7 +6945,8 @@ pub fn compute_blob_priority_standalone(
impl Node {
/// Start the active replication cycle: periodically ask peers to hold our
/// under-replicated recent content. Only Available/Persistent devices initiate.
/// under-replicated recent content. All devices initiate — phones need
/// their content replicated before they go to sleep.
pub fn start_replication_cycle(self: &Arc<Self>, interval_secs: u64) -> tokio::task::JoinHandle<()> {
let node = Arc::clone(self);
tokio::spawn(async move {