2069	            continue;
2070	        }
2071	        let empty = spt_net::net::registry::SubnetRegistry::new();
2072	        let reg = regs.get(&sub.name).unwrap_or(&empty);
2073	        let rows = node_status_rows(reg, &heard, &own_hex, now_ms(), stale_after_ms);
2074	        let settled: Vec<(NodeRow, bool)> = rows
2075	            .into_iter()
2076	            .map(|r| {
2077	                let online = match r.liveness {
2078	                    NodeLiveness::Online => true,
2079	                    NodeLiveness::Offline => false,
2080	                    NodeLiveness::Probe => probe_node(&r.node),
2081	                };
2082	                (r, online)
2083	            })
2084	            .collect();
2085	        out.push_str(&render_node_rows(&sub.name, &settled));
2086	    }
2087	    if out.is_empty() {
2088	        out.push_str(
2089	            "This node belongs to no subnet — a standalone node. Local messaging \
2090	             works without one; a subnet links your machines into one fabric.\n",
2091	        );
2092	    }
2093	    out.push_str(HINT_FOOTER);
2094	    print!("{out}");
2095	    let _ = std::io::stdout().flush();
2096	    0
2097	}
2098
2099	/// The join command's elevation verdict — pure over the probed level
2100	/// (REQ-SUBNET-4): joining enrolls the MACHINE into a subnet's trust
2101	/// fabric, a mutation an unprivileged process must not perform (an
2102	/// attacker-run joiner would enroll the box into the attacker's subnet).
2103	/// Same fail-safe posture as the seed-reveal gates: only confirmed
2104	/// elevation proceeds.
2105	// [impl->REQ-SUBNET-4]
2106	fn join_elevation_refusal(elevation: Elevation) -> Option<String> {
2107	    match elevation::gate_for(elevation) {
2108	        Gate::Show => None,

            continue;
        }
        let empty = spt_net::net::registry::SubnetRegistry::new();
        let reg = regs.get(&sub.name).unwrap_or(&empty);
        let rows = node_status_rows(reg, &heard, &own_hex, now_ms(), stale_after_ms);
        let settled: Vec<(NodeRow, bool)> = rows
            .into_iter()
            .map(|r| {
                let online = match r.liveness {
                    NodeLiveness::Online => true,
                    NodeLiveness::Offline => false,
                    NodeLiveness::Probe => probe_node(&r.node),
                };
                (r, online)
            })
            .collect();
        out.push_str(&render_node_rows(&sub.name, &settled));
    }
    if out.is_empty() {
        out.push_str(
            "This node belongs to no subnet — a standalone node. Local messaging \
             works without one; a subnet links your machines into one fabric.\n",
        );
    }
    out.push_str(HINT_FOOTER);
    print!("{out}");
    let _ = std::io::stdout().flush();
    0
}

/// The join command's elevation verdict — pure over the probed level
/// (REQ-SUBNET-4): joining enrolls the MACHINE into a subnet's trust
/// fabric, a mutation an unprivileged process must not perform (an
/// attacker-run joiner would enroll the box into the attacker's subnet).
/// Same fail-safe posture as the seed-reveal gates: only confirmed
/// elevation proceeds.
// [impl->REQ-SUBNET-4]
fn join_elevation_refusal(elevation: Elevation) -> Option<String> {
    match elevation::gate_for(elevation) {
        Gate::Show => None,


Implementing: membership (trust + self) seeds both views; registry rows enrich.


Edit

/// Build the status rows from the subnet store + registry snapshots. Pure over
/// its inputs so the view is unit-tested hermetically. Counts are
/// gossip-derived and stale-tolerant: a missing snapshot renders as this node
/// alone with no advertised endpoints (counts catch up as gossip lands).
// [impl->REQ-SUBNET-1]
fn subnet_status_rows(
    store: &SubnetStore,
    regs: &std::collections::BTreeMap<String, spt_net::net::registry::SubnetRegistry>,
    only: Option<&str>,
) -> Vec<SubnetStatusRow> {
    store
        .subnets
        .iter()
        .filter(|s| only.is_none_or(|n| n == s.name))
        .map(|s| {
            let (mut nodes, mut endpoints) = (std::collections::BTreeSet::new(), 0usize);
            if let Some(reg) = regs.get(&s.name) {
                for id in reg.endpoint_ids() {
                    endpoints += 1;
                    for inst in reg.instances(id) {
                        nodes.insert(inst.node.clone());
                    }
                }
            }
            // This node is a member even before its first row gossips in.
            SubnetStatusRow { name: s.name.clone(), nodes: nodes.len().max(1), endpoints }
        })
        .collect()
}

/// Build the status rows from the subnet store + registry snapshots + the
/// trust store. Pure over its inputs so the view is unit-tested hermetically.
///
/// Node membership is TRUST-derived (self + pinned peers — the durable
/// pairing truth), with registry-row nodes unioned in as enrichment: a
/// member node with zero advertised endpoints (fresh box, empty owlery) is
/// still a member, and an offline peer whose rows the 4.10 eviction decayed
/// must not vanish from its own subnet's view. Endpoint counts stay
/// gossip-derived and stale-tolerant (they catch up as gossip lands).
// [impl->REQ-SUBNET-1]
fn subnet_status_rows(
    store: &SubnetStore,
    regs: &std::collections::BTreeMap<String, spt_net::net::registry::SubnetRegistry>,
    trust: &spt_store::trust::TrustStore,
    own_hex: Option<&str>,
    only: Option<&str>,
) -> Vec<SubnetStatusRow> {
    store
        .subnets
        .iter()
        .filter(|s| only.is_none_or(|n| n == s.name))
        .map(|s| {
            let (mut nodes, mut endpoints) = (std::collections::BTreeSet::new(), 0usize);
            if let Some(own) = own_hex {
                nodes.insert(own.to_string());
            }
            for peer in trust.peers_in(&s.name) {
                nodes.insert(peer.pubkey_hex.clone());
            }
            if let Some(reg) = regs.get(&s.name) {
                for id in reg.endpoint_ids() {
                    endpoints += 1;
                    for inst in reg.instances(id) {
                        nodes.insert(inst.node.clone());
                    }
                }
            }
            // This node is a member even when its identity could not load.
            SubnetStatusRow { name: s.name.clone(), nodes: nodes.len().max(1), endpoints }
        })
        .collect()
}


The file C:\Users\decid\Documents\projects\spt-core\crates\spt\src\cli.rs has been updated successfully. (file state is current in your context — no need to Read it back)

/// Build the status rows from the subnet store + registry snapshots. Pure over
/// its inputs so the view is unit-tested hermetically. Counts are
/// gossip-derived and stale-tolerant: a missing snapshot renders as this node
/// alone with no advertised endpoints (counts catch up as gossip lands).
// [impl->REQ-SUBNET-1]
fn subnet_status_rows(
    store: &SubnetStore,
    regs: &std::collections::BTreeMap<String, spt_net::net::registry::SubnetRegistry>,
    only: Option<&str>,
) -> Vec<SubnetStatusRow> {
    store
        .subnets
        .iter()
        .filter(|s| only.is_none_or(|n| n == s.name))
        .map(|s| {
            let (mut nodes, mut endpoints) = (std::collections::BTreeSet::new(), 0usize);
            if let Some(reg) = regs.get(&s.name) {
                for id in reg.endpoint_ids() {
                    endpoints += 1;
                    for inst in reg.instances(id) {
                        nodes.insert(inst.node.clone());
                    }
                }
            }
            // This node is a member even before its first row gossips in.
            SubnetStatusRow { name: s.name.clone(), nodes: nodes.len().max(1), endpoints }
        })
        .collect()
}

/// Build the status rows from the subnet store + registry snapshots + the
/// trust store. Pure over its inputs so the view is unit-tested hermetically.
///
/// Node membership is TRUST-derived (self + pinned peers — the durable
/// pairing truth), with registry-row nodes unioned in as enrichment: a
/// member node with zero advertised endpoints (fresh box, empty owlery) is
/// still a member, and an offline peer whose rows the 4.10 eviction decayed
/// must not vanish from its own subnet's view. Endpoint counts stay
/// gossip-derived and stale-tolerant (they catch up as gossip lands).
// [impl->REQ-SUBNET-1]
fn subnet_status_rows(
    store: &SubnetStore,
    regs: &std::collections::BTreeMap<String, spt_net::net::registry::SubnetRegistry>,
    trust: &spt_store::trust::TrustStore,
    own_hex: Option<&str>,
    only: Option<&str>,
) -> Vec<SubnetStatusRow> {
    store
        .subnets
        .iter()
        .filter(|s| only.is_none_or(|n| n == s.name))
        .map(|s| {
            let (mut nodes, mut endpoints) = (std::collections::BTreeSet::new(), 0usize);
            if let Some(own) = own_hex {
                nodes.insert(own.to_string());
            }
            for peer in trust.peers_in(&s.name) {
                nodes.insert(peer.pubkey_hex.clone());
            }
            if let Some(reg) = regs.get(&s.name) {
                for id in reg.endpoint_ids() {
                    endpoints += 1;
                    for inst in reg.instances(id) {
                        nodes.insert(inst.node.clone());
                    }
                }
            }
            // This node is a member even when its identity could not load.
            SubnetStatusRow { name: s.name.clone(), nodes: nodes.len().max(1), endpoints }
        })
        .collect()
}

-/// Build the status rows from the subnet store + registry snapshots. Pure over

-/// its inputs so the view is unit-tested hermetically. Counts are

-/// gossip-derived and stale-tolerant: a missing snapshot renders as this node

-/// alone with no advertised endpoints (counts catch up as gossip lands).

+/// Build the status rows from the subnet store + registry snapshots + the

+/// trust store. Pure over its inputs so the view is unit-tested hermetically.

+/// Node membership is TRUST-derived (self + pinned peers — the durable

+/// pairing truth), with registry-row nodes unioned in as enrichment: a

+/// member node with zero advertised endpoints (fresh box, empty owlery) is

+/// still a member, and an offline peer whose rows the 4.10 eviction decayed

+/// must not vanish from its own subnet's view. Endpoint counts stay

+/// gossip-derived and stale-tolerant (they catch up as gossip lands).

+    trust: &spt_store::trust::TrustStore,

+            if let Some(own) = own_hex {

+                nodes.insert(own.to_string());

+            for peer in trust.peers_in(&s.name) {

+                nodes.insert(peer.pubkey_hex.clone());

-            // This node is a member even before its first row gossips in.

+            // This node is a member even when its identity could not load.


re-joined BIGNET. of note, the join had zero tolerance for a late code (strict 30s window). i tried twice and the code had shifted by the time i pressed enter, and both times was denied. i thought it…


Proceed with bug root cause analysis and fixes


/// Build one subnet's per-node rows with the gossip-recency verdict. Pure
/// over its inputs (the probe is the caller's impure follow-up): own node →
/// online (we are running); all rows Offline → offline; heard within
/// `stale_after_ms` → online; otherwise → probe.
// [impl->REQ-SUBNET-1]
fn node_status_rows(
    reg: &spt_net::net::registry::SubnetRegistry,
    heard: &std::collections::BTreeMap<String, u64>,
    own_hex: &str,
    now_ms: u64,
    stale_after_ms: u64,
) -> Vec<NodeRow> {
    use spt_net::net::registry::Status;
    // node → (label, active, total, any_routable)
    let mut by_node: std::collections::BTreeMap<String, (Option<String>, usize, usize, bool)> =
        std::collections::BTreeMap::new();
    for id in reg.endpoint_ids() {
        for inst in reg.instances(id) {
            let e = by_node.entry(inst.node.clone()).or_default();
            if e.0.is_none() {
                e.0 = inst.node_label.clone();
            }
            e.1 += usize::from(inst.status == Status::Active);
            e.2 += 1;
            e.3 |= inst.status.routable();
        }
    }
    by_node
        .into_iter()
        .map(|(node, (label, active, total, any_routable))| {
            let liveness = if node == own_hex {
                NodeLiveness::Online
            } else if !any_routable {
                NodeLiveness::Offline
            } else if heard
                .get(&node)
                .is_some_and(|&at| now_ms.saturating_sub(at) <= stale_after_ms)
            {
                NodeLiveness::Online
            } else {
                NodeLiveness::Probe
            };
            NodeRow { node, label, active, total, liveness }
        })
        .collect()
}

/// Build one subnet's per-node rows with the gossip-recency verdict. Pure
/// over its inputs (the probe is the caller's impure follow-up).
///
/// `members` seeds the row set — `(node_hex, label)` for self + every
/// trust-pinned peer — so a member with zero registry rows still renders
/// (`[0/0]`): a fresh box has no endpoints yet, and an offline peer's rows
/// decay under the 4.10 eviction; neither may vanish from the view.
/// Liveness: own node → online (we are running); rows all-Offline →
/// offline; heard within `stale_after_ms` → online; otherwise (including
/// a rowless member — absence is not evidence) → probe.
// [impl->REQ-SUBNET-1]
fn node_status_rows(
    reg: &spt_net::net::registry::SubnetRegistry,
    members: &[(String, Option<String>)],
    heard: &std::collections::BTreeMap<String, u64>,
    own_hex: &str,
    now_ms: u64,
    stale_after_ms: u64,
) -> Vec<NodeRow> {
    use spt_net::net::registry::Status;
    // node → (label, active, total, any_routable)
    let mut by_node: std::collections::BTreeMap<String, (Option<String>, usize, usize, bool)> =
        std::collections::BTreeMap::new();
    for (node, label) in members {
        let e = by_node.entry(node.clone()).or_default();
        if e.0.is_none() {
            e.0 = label.clone();
        }
    }
    for id in reg.endpoint_ids() {
        for inst in reg.instances(id) {
            let e = by_node.entry(inst.node.clone()).or_default();
            if e.0.is_none() {
                e.0 = inst.node_label.clone();
            }
            e.1 += usize::from(inst.status == Status::Active);
            e.2 += 1;
            e.3 |= inst.status.routable();
        }
    }
    by_node
        .into_iter()
        .map(|(node, (label, active, total, any_routable))| {
            let liveness = if node == own_hex {
                NodeLiveness::Online
            } else if total > 0 && !any_routable {
                NodeLiveness::Offline
            } else if heard
                .get(&node)
                .is_some_and(|&at| now_ms.saturating_sub(at) <= stale_after_ms)
            {
                NodeLiveness::Online
            } else {
                NodeLiveness::Probe
            };
            NodeRow { node, label, active, total, liveness }
        })
        .collect()
}


/// Build one subnet's per-node rows with the gossip-recency verdict. Pure
/// over its inputs (the probe is the caller's impure follow-up): own node →
/// online (we are running); all rows Offline → offline; heard within
/// `stale_after_ms` → online; otherwise → probe.
// [impl->REQ-SUBNET-1]
fn node_status_rows(
    reg: &spt_net::net::registry::SubnetRegistry,
    heard: &std::collections::BTreeMap<String, u64>,
    own_hex: &str,
    now_ms: u64,
    stale_after_ms: u64,
) -> Vec<NodeRow> {
    use spt_net::net::registry::Status;
    // node → (label, active, total, any_routable)
    let mut by_node: std::collections::BTreeMap<String, (Option<String>, usize, usize, bool)> =
        std::collections::BTreeMap::new();
    for id in reg.endpoint_ids() {
        for inst in reg.instances(id) {
            let e = by_node.entry(inst.node.clone()).or_default();
            if e.0.is_none() {
                e.0 = inst.node_label.clone();
            }
            e.1 += usize::from(inst.status == Status::Active);
            e.2 += 1;
            e.3 |= inst.status.routable();
        }
    }
    by_node
        .into_iter()
        .map(|(node, (label, active, total, any_routable))| {
            let liveness = if node == own_hex {
                NodeLiveness::Online
            } else if !any_routable {
                NodeLiveness::Offline
            } else if heard
                .get(&node)
                .is_some_and(|&at| now_ms.saturating_sub(at) <= stale_after_ms)
            {
                NodeLiveness::Online
            } else {
                NodeLiveness::Probe
            };
            NodeRow { node, label, active, total, liveness }
        })
        .collect()
}

/// Build one subnet's per-node rows with the gossip-recency verdict. Pure
/// over its inputs (the probe is the caller's impure follow-up).
///
/// `members` seeds the row set — `(node_hex, label)` for self + every
/// trust-pinned peer — so a member with zero registry rows still renders
/// (`[0/0]`): a fresh box has no endpoints yet, and an offline peer's rows
/// decay under the 4.10 eviction; neither may vanish from the view.
/// Liveness: own node → online (we are running); rows all-Offline →
/// offline; heard within `stale_after_ms` → online; otherwise (including
/// a rowless member — absence is not evidence) → probe.
// [impl->REQ-SUBNET-1]
fn node_status_rows(
    reg: &spt_net::net::registry::SubnetRegistry,
    members: &[(String, Option<String>)],
    heard: &std::collections::BTreeMap<String, u64>,
    own_hex: &str,
    now_ms: u64,
    stale_after_ms: u64,
) -> Vec<NodeRow> {
    use spt_net::net::registry::Status;
    // node → (label, active, total, any_routable)
    let mut by_node: std::collections::BTreeMap<String, (Option<String>, usize, usize, bool)> =
        std::collections::BTreeMap::new();
    for (node, label) in members {
        let e = by_node.entry(node.clone()).or_default();
        if e.0.is_none() {
            e.0 = label.clone();
        }
    }
    for id in reg.endpoint_ids() {
        for inst in reg.instances(id) {
            let e = by_node.entry(inst.node.clone()).or_default();
            if e.0.is_none() {
                e.0 = inst.node_label.clone();
            }
            e.1 += usize::from(inst.status == Status::Active);
            e.2 += 1;
            e.3 |= inst.status.routable();
        }
    }
    by_node
        .into_iter()
        .map(|(node, (label, active, total, any_routable))| {
            let liveness = if node == own_hex {
                NodeLiveness::Online
            } else if total > 0 && !any_routable {
                NodeLiveness::Offline
            } else if heard
                .get(&node)
                .is_some_and(|&at| now_ms.saturating_sub(at) <= stale_after_ms)
            {
                NodeLiveness::Online
            } else {
                NodeLiveness::Probe
            };
            NodeRow { node, label, active, total, liveness }
        })
        .collect()
}

-/// over its inputs (the probe is the caller's impure follow-up): own node →

-/// online (we are running); all rows Offline → offline; heard within

-/// `stale_after_ms` → online; otherwise → probe.

+/// over its inputs (the probe is the caller's impure follow-up).

+/// `members` seeds the row set — `(node_hex, label)` for self + every

+/// trust-pinned peer — so a member with zero registry rows still renders

+/// (`[0/0]`): a fresh box has no endpoints yet, and an offline peer's rows

+/// decay under the 4.10 eviction; neither may vanish from the view.

+/// Liveness: own node → online (we are running); rows all-Offline →

+/// offline; heard within `stale_after_ms` → online; otherwise (including

+/// a rowless member — absence is not evidence) → probe.

+    members: &[(String, Option<String>)],

+        let e = by_node.entry(node.clone()).or_default();

+            } else if total > 0 && !any_routable {


Now the callers:


    let snap_dir = perch::identity_dir().join("registry");
    let regs = crate::wansend::load_snapshots(&snap_dir);
    if !nodes {
        let rows = subnet_status_rows(&store, &regs, name.as_deref());
        print!("{}", render_subnet_status(&rows));
        let _ = std::io::stdout().flush();
        return 0;
    }

    // --nodes: per-node rows under the hybrid liveness (decision 6).
    let own_hex = spt_store::nodeid::load_or_create()
        .map(|i| i.public_key().to_hex())
        .unwrap_or_default();

    let snap_dir = perch::identity_dir().join("registry");
    let regs = crate::wansend::load_snapshots(&snap_dir);
    let trust = spt_store::trust::TrustStore::load();
    let own_hex = spt_store::nodeid::load_or_create()
        .map(|i| i.public_key().to_hex())
        .unwrap_or_default();
    if !nodes {
        let rows = subnet_status_rows(
            &store,
            &regs,
            &trust,
            Some(&own_hex).filter(|h| !h.is_empty()).map(|h| h.as_str()),
            name.as_deref(),
        );
        print!("{}", render_subnet_status(&rows));
        let _ = std::io::stdout().flush();
        return 0;
    }

    // --nodes: per-node rows under the hybrid liveness (decision 6).


    let snap_dir = perch::identity_dir().join("registry");
    let regs = crate::wansend::load_snapshots(&snap_dir);
    if !nodes {
        let rows = subnet_status_rows(&store, &regs, name.as_deref());
        print!("{}", render_subnet_status(&rows));
        let _ = std::io::stdout().flush();
        return 0;
    }

    // --nodes: per-node rows under the hybrid liveness (decision 6).
    let own_hex = spt_store::nodeid::load_or_create()
        .map(|i| i.public_key().to_hex())
        .unwrap_or_default();

    let snap_dir = perch::identity_dir().join("registry");
    let regs = crate::wansend::load_snapshots(&snap_dir);
    let trust = spt_store::trust::TrustStore::load();
    let own_hex = spt_store::nodeid::load_or_create()
        .map(|i| i.public_key().to_hex())
        .unwrap_or_default();
    if !nodes {
        let rows = subnet_status_rows(
            &store,
            &regs,
            &trust,
            Some(&own_hex).filter(|h| !h.is_empty()).map(|h| h.as_str()),
            name.as_deref(),
        );
        print!("{}", render_subnet_status(&rows));
        let _ = std::io::stdout().flush();
        return 0;
    }

    // --nodes: per-node rows under the hybrid liveness (decision 6).

+    let trust = spt_store::trust::TrustStore::load();

+    let own_hex = spt_store::nodeid::load_or_create()

+        .map(|i| i.public_key().to_hex())

-        let rows = subnet_status_rows(&store, &regs, name.as_deref());

+            Some(&own_hex).filter(|h| !h.is_empty()).map(|h| h.as_str()),

-    let own_hex = spt_store::nodeid::load_or_create()

-        .map(|i| i.public_key().to_hex())

let heard: std::collections::BTreeMap<String, u64> =


        let empty = spt_net::net::registry::SubnetRegistry::new();
        let reg = regs.get(&sub.name).unwrap_or(&empty);
        let rows = node_status_rows(reg, &heard, &own_hex, now_ms(), stale_after_ms);

        let empty = spt_net::net::registry::SubnetRegistry::new();
        let reg = regs.get(&sub.name).unwrap_or(&empty);
        // Membership seed: self + trust-pinned peers (rowless members still
        // render — fresh boxes and eviction-decayed offline peers alike).
        let mut members: Vec<(String, Option<String>)> = Vec::new();
        if !own_hex.is_empty() {
            members.push((own_hex.clone(), spt_store::hostlabel::os_hostname()));
        }
        for peer in trust.peers_in(&sub.name) {
            members.push((peer.pubkey_hex.clone(), peer.label.clone()));
        }
        let rows = node_status_rows(reg, &members, &heard, &own_hex, now_ms(), stale_after_ms);


        let empty = spt_net::net::registry::SubnetRegistry::new();
        let reg = regs.get(&sub.name).unwrap_or(&empty);
        let rows = node_status_rows(reg, &heard, &own_hex, now_ms(), stale_after_ms);

        let empty = spt_net::net::registry::SubnetRegistry::new();
        let reg = regs.get(&sub.name).unwrap_or(&empty);
        // Membership seed: self + trust-pinned peers (rowless members still
        // render — fresh boxes and eviction-decayed offline peers alike).
        let mut members: Vec<(String, Option<String>)> = Vec::new();
        if !own_hex.is_empty() {
            members.push((own_hex.clone(), spt_store::hostlabel::os_hostname()));
        }
        for peer in trust.peers_in(&sub.name) {
            members.push((peer.pubkey_hex.clone(), peer.label.clone()));
        }
        let rows = node_status_rows(reg, &members, &heard, &own_hex, now_ms(), stale_after_ms);

-        let rows = node_status_rows(reg, &heard, &own_hex, now_ms(), stale_after_ms);

+        // Membership seed: self + trust-pinned peers (rowless members still

+        // render — fresh boxes and eviction-decayed offline peers alike).

+        let mut members: Vec<(String, Option<String>)> = Vec::new();

+            members.push((own_hex.clone(), spt_store::hostlabel::os_hostname()));

+        for peer in trust.peers_in(&sub.name) {

+            members.push((peer.pubkey_hex.clone(), peer.label.clone()));

+        let rows = node_status_rows(reg, &members, &heard, &own_hex, now_ms(), stale_after_ms);

let settled: Vec<(NodeRow, bool)> = rows


1811:fn subnet_status_rows(
1916:fn node_status_rows(
2077:        let rows = subnet_status_rows(
2115:        let rows = node_status_rows(reg, &members, &heard, &own_hex, now_ms(), stale_after_ms);
4823:        let rows = subnet_status_rows(&store, &regs, None);
4843:        let no_gossip = subnet_status_rows(&store, &std::collections::BTreeMap::new(), None);
4853:    fn node_status_rows_settle_cheap_signals_probe_only_stale() {
4874:        let rows = node_status_rows(&reg, &heard, "self00aa", now, 33_000);


4795	    #[test]
4796	    fn subnet_status_renders_rows_and_hints_never_secrets() {
4797	        use spt_net::net::registry::{Instance, Status, SubnetRegistry};
4798
4799	        // Zero subnets: explanatory standalone text + hints.
4800	        let empty = render_subnet_status(&[]);
4801	        assert!(empty.contains("standalone node"));
4802	        assert!(empty.contains("spt subnet create"));
4803	        assert!(empty.contains("show-code"));
4804
4805	        // A member subnet with gossip: counts come from the snapshot.
4806	        let store = store_with(&["home"]);
4807	        let mut reg = SubnetRegistry::new();
4808	        for (id, node) in [("lea", "aa"), ("sergey", "bb"), ("lea2", "aa")] {
4809	            reg.merge_instance(
4810	                id,
4811	                Instance {
4812	                    node: node.into(),
4813	                    status: Status::Active,
4814	                    epoch: 1,
4815	                    resources: None,
4816	                    last_active_ms: None,
4817	                    shell_adapters: Vec::new(),
4818	                    node_label: None,
4819	                },
4820	            );
4821	        }
4822	        let regs = std::collections::BTreeMap::from([("home".to_string(), reg)]);
4823	        let rows = subnet_status_rows(&store, &regs, None);
4824	        assert_eq!(rows.len(), 1);
4825	        assert_eq!((rows[0].nodes, rows[0].endpoints), (2, 3));
4826	        let view = render_subnet_status(&rows);
4827	        assert!(view.contains("home"));
4828	        assert!(view.contains("ENDPOINTS"));
4829	        assert!(view.contains("hint:"), "hint footer always printed");
4830	        let seed_hex = {
4831	            use std::fmt::Write as _;
4832	            store.find("home").unwrap().seed_bytes().unwrap().iter().fold(
4833	                String::new(),
4834	                |mut s, b| {
4835	                    let _ = write!(s, "{b:02x}");
4836	                    s
4837	                },
4838	            )
4839	        };
4840	        assert!(!view.contains(&seed_hex), "the status view never carries seed material");
4841
4842	        // A missing snapshot degrades to this-node-alone, not a panic/zero.
4843	        let no_gossip = subnet_status_rows(&store, &std::collections::BTreeMap::new(), None);
4844	        assert_eq!((no_gossip[0].nodes, no_gossip[0].endpoints), (1, 0));
4845	    }
4846
4847	    // [unit->REQ-SUBNET-1] [unit->REQ-SUBNET-3] the --nodes hybrid liveness
4848	    // verdict is settled by the cheap signals first: own node online, an
4849	    // all-Offline node offline, a recently-heard node online without a
4850	    // probe, and ONLY the silent-but-routable remainder asks for a dial;
4851	    // rows carry the advertised label + [active/total] endpoint counts.
4852	    #[test]
4853	    fn node_status_rows_settle_cheap_signals_probe_only_stale() {
4854	        use spt_net::net::registry::{Instance, Status, SubnetRegistry};
4855	        let inst = |node: &str, status: Status, label: Option<&str>| Instance {
4856	            node: node.into(),
4857	            status,
4858	            epoch: 1,
4859	            resources: None,
4860	            last_active_ms: None,
4861	            shell_adapters: Vec::new(),
4862	            node_label: label.map(str::to_string),
4863	        };
4864	        let mut reg = SubnetRegistry::new();
4865	        reg.merge_instance("a1", inst("self00aa", Status::Active, Some("ME")));
4866	        reg.merge_instance("a2", inst("self00aa", Status::Dormant, Some("ME")));
4867	        reg.merge_instance("b1", inst("heard0bb", Status::Active, Some("FRESH")));
4868	        reg.merge_instance("c1", inst("dead00cc", Status::Offline, None));
4869	        reg.merge_instance("d1", inst("stale0dd", Status::Active, Some("QUIET")));
4870
4871	        let now = 1_000_000u64;
4872	        let heard =
4873	            std::collections::BTreeMap::from([("heard0bb".to_string(), now - 10_000)]);
4874	        let rows = node_status_rows(&reg, &heard, "self00aa", now, 33_000);
4875	        let by: std::collections::BTreeMap<&str, &NodeRow> =
4876	            rows.iter().map(|r| (r.node.as_str(), r)).collect();
4877
4878	        assert_eq!(by["self00aa"].liveness, NodeLiveness::Online, "own node never probes");
4879	        assert_eq!((by["self00aa"].active, by["self00aa"].total), (1, 2));
4880	        assert_eq!(by["heard0bb"].liveness, NodeLiveness::Online, "recently heard ⇒ online");
4881	        assert_eq!(by["dead00cc"].liveness, NodeLiveness::Offline, "all-Offline ⇒ offline");
4882	        assert_eq!(by["stale0dd"].liveness, NodeLiveness::Probe, "silent + routable ⇒ probe");
4883	        assert_eq!(by["stale0dd"].label.as_deref(), Some("QUIET"));
4884
4885	        // Render: labels as `LABEL (keypfx…)`, bare prefix when unlabeled,
4886	        // online/offline + [active/total]; never the full pubkey alone.
4887	        let settled: Vec<(NodeRow, bool)> = rows
4888	            .into_iter()
4889	            .map(|r| {
4890	                let online = r.liveness == NodeLiveness::Online;
4891	                (r, online)
4892	            })
4893	            .collect();
4894	        let view = render_node_rows("home", &settled);
4895	        assert!(view.contains("SUBNET home"));
4896	        assert!(view.contains("ME (self00aa…)"), "labeled node renders label + prefix: {view}");
4897	        assert!(view.contains("dead00cc…"), "unlabeled node renders the bare prefix");
4898	        assert!(view.contains("[1/2]"), "active/total counts render: {view}");
4899	        assert!(view.contains("online"));
4900	        assert!(view.contains("offline"));
4901
4902	        // Zero rows: an explanatory line, not an empty table.
4903	        let none = render_node_rows("home", &[]);
4904	        assert!(none.contains("no gossip yet"));
4905	    }
4906
4907	    // [unit->REQ-DAEMON-3] [unit->REQ-SUBNET-2] the coming-online banner's
4908	    // subnets-joined section: header + underline, one `NAME: state` row per
4909	    // held subnet ((none) when empty), and it always concludes with the

    #[test]
    fn subnet_status_renders_rows_and_hints_never_secrets() {
        use spt_net::net::registry::{Instance, Status, SubnetRegistry};

        // Zero subnets: explanatory standalone text + hints.
        let empty = render_subnet_status(&[]);
        assert!(empty.contains("standalone node"));
        assert!(empty.contains("spt subnet create"));
        assert!(empty.contains("show-code"));

        // A member subnet with gossip: counts come from the snapshot.
        let store = store_with(&["home"]);
        let mut reg = SubnetRegistry::new();
        for (id, node) in [("lea", "aa"), ("sergey", "bb"), ("lea2", "aa")] {
            reg.merge_instance(
                id,
                Instance {
                    node: node.into(),
                    status: Status::Active,
                    epoch: 1,
                    resources: None,
                    last_active_ms: None,
                    shell_adapters: Vec::new(),
                    node_label: None,
                },
            );
        }
        let regs = std::collections::BTreeMap::from([("home".to_string(), reg)]);
        let rows = subnet_status_rows(&store, &regs, None);
        assert_eq!(rows.len(), 1);
        assert_eq!((rows[0].nodes, rows[0].endpoints), (2, 3));
        let view = render_subnet_status(&rows);
        assert!(view.contains("home"));
        assert!(view.contains("ENDPOINTS"));
        assert!(view.contains("hint:"), "hint footer always printed");
        let seed_hex = {
            use std::fmt::Write as _;
            store.find("home").unwrap().seed_bytes().unwrap().iter().fold(
                String::new(),
                |mut s, b| {
                    let _ = write!(s, "{b:02x}");
                    s
                },
            )
        };
        assert!(!view.contains(&seed_hex), "the status view never carries seed material");

        // A missing snapshot degrades to this-node-alone, not a panic/zero.
        let no_gossip = subnet_status_rows(&store, &std::collections::BTreeMap::new(), None);
        assert_eq!((no_gossip[0].nodes, no_gossip[0].endpoints), (1, 0));
    }

    // [unit->REQ-SUBNET-1] [unit->REQ-SUBNET-3] the --nodes hybrid liveness
    // verdict is settled by the cheap signals first: own node online, an
    // all-Offline node offline, a recently-heard node online without a
    // probe, and ONLY the silent-but-routable remainder asks for a dial;
    // rows carry the advertised label + [active/total] endpoint counts.
    #[test]
    fn node_status_rows_settle_cheap_signals_probe_only_stale() {
        use spt_net::net::registry::{Instance, Status, SubnetRegistry};
        let inst = |node: &str, status: Status, label: Option<&str>| Instance {
            node: node.into(),
            status,
            epoch: 1,
            resources: None,
            last_active_ms: None,
            shell_adapters: Vec::new(),
            node_label: label.map(str::to_string),
        };
        let mut reg = SubnetRegistry::new();
        reg.merge_instance("a1", inst("self00aa", Status::Active, Some("ME")));
        reg.merge_instance("a2", inst("self00aa", Status::Dormant, Some("ME")));
        reg.merge_instance("b1", inst("heard0bb", Status::Active, Some("FRESH")));
        reg.merge_instance("c1", inst("dead00cc", Status::Offline, None));
        reg.merge_instance("d1", inst("stale0dd", Status::Active, Some("QUIET")));

        let now = 1_000_000u64;
        let heard =
            std::collections::BTreeMap::from([("heard0bb".to_string(), now - 10_000)]);
        let rows = node_status_rows(&reg, &heard, "self00aa", now, 33_000);
        let by: std::collections::BTreeMap<&str, &NodeRow> =
            rows.iter().map(|r| (r.node.as_str(), r)).collect();

        assert_eq!(by["self00aa"].liveness, NodeLiveness::Online, "own node never probes");
        assert_eq!((by["self00aa"].active, by["self00aa"].total), (1, 2));
        assert_eq!(by["heard0bb"].liveness, NodeLiveness::Online, "recently heard ⇒ online");
        assert_eq!(by["dead00cc"].liveness, NodeLiveness::Offline, "all-Offline ⇒ offline");
        assert_eq!(by["stale0dd"].liveness, NodeLiveness::Probe, "silent + routable ⇒ probe");
        assert_eq!(by["stale0dd"].label.as_deref(), Some("QUIET"));

        // Render: labels as `LABEL (keypfx…)`, bare prefix when unlabeled,
        // online/offline + [active/total]; never the full pubkey alone.
        let settled: Vec<(NodeRow, bool)> = rows
            .into_iter()
            .map(|r| {
                let online = r.liveness == NodeLiveness::Online;
                (r, online)
            })
            .collect();
        let view = render_node_rows("home", &settled);
        assert!(view.contains("SUBNET home"));
        assert!(view.contains("ME (self00aa…)"), "labeled node renders label + prefix: {view}");
        assert!(view.contains("dead00cc…"), "unlabeled node renders the bare prefix");
        assert!(view.contains("[1/2]"), "active/total counts render: {view}");
        assert!(view.contains("online"));
        assert!(view.contains("offline"));

        // Zero rows: an explanatory line, not an empty table.
        let none = render_node_rows("home", &[]);
        assert!(none.contains("no gossip yet"));
    }

    // [unit->REQ-DAEMON-3] [unit->REQ-SUBNET-2] the coming-online banner's
    // subnets-joined section: header + underline, one `NAME: state` row per
    // held subnet ((none) when empty), and it always concludes with the


        let regs = std::collections::BTreeMap::from([("home".to_string(), reg)]);
        let rows = subnet_status_rows(&store, &regs, None);
        assert_eq!(rows.len(), 1);
        assert_eq!((rows[0].nodes, rows[0].endpoints), (2, 3));
        let view = render_subnet_status(&rows);
        assert!(view.contains("home"));
        assert!(view.contains("ENDPOINTS"));
        assert!(view.contains("hint:"), "hint footer always printed");

        let regs = std::collections::BTreeMap::from([("home".to_string(), reg)]);
        let trust = spt_store::trust::TrustStore::default();
        let rows = subnet_status_rows(&store, &regs, &trust, Some("aa"), None);
        assert_eq!(rows.len(), 1);
        assert_eq!((rows[0].nodes, rows[0].endpoints), (2, 3));
        let view = render_subnet_status(&rows);
        assert!(view.contains("home"));
        assert!(view.contains("ENDPOINTS"));
        assert!(view.contains("hint:"), "hint footer always printed");

        // MEMBERSHIP is trust-derived, rows enrich (the re-join field bug,
        // 2026-06-06): a member node with ZERO advertised endpoints — fresh
        // box, or an offline peer whose rows the 4.10 eviction decayed —
        // still counts. Self (cc, rowless) + a pinned rowless peer (dd) +
        // the two row-nodes = 4.
        let mut pinned = spt_store::trust::TrustStore::default();
        pinned.record("home", "dd", None, None);
        let rows = subnet_status_rows(&store, &regs, &pinned, Some("cc"), None);
        assert_eq!(
            (rows[0].nodes, rows[0].endpoints),
            (4, 3),
            "rowless self + rowless pinned peer still count as nodes"
        );


        let regs = std::collections::BTreeMap::from([("home".to_string(), reg)]);
        let rows = subnet_status_rows(&store, &regs, None);
        assert_eq!(rows.len(), 1);
        assert_eq!((rows[0].nodes, rows[0].endpoints), (2, 3));
        let view = render_subnet_status(&rows);
        assert!(view.contains("home"));
        assert!(view.contains("ENDPOINTS"));
        assert!(view.contains("hint:"), "hint footer always printed");

        let regs = std::collections::BTreeMap::from([("home".to_string(), reg)]);
        let trust = spt_store::trust::TrustStore::default();
        let rows = subnet_status_rows(&store, &regs, &trust, Some("aa"), None);
        assert_eq!(rows.len(), 1);
        assert_eq!((rows[0].nodes, rows[0].endpoints), (2, 3));
        let view = render_subnet_status(&rows);
        assert!(view.contains("home"));
        assert!(view.contains("ENDPOINTS"));
        assert!(view.contains("hint:"), "hint footer always printed");

        // MEMBERSHIP is trust-derived, rows enrich (the re-join field bug,
        // 2026-06-06): a member node with ZERO advertised endpoints — fresh
        // box, or an offline peer whose rows the 4.10 eviction decayed —
        // still counts. Self (cc, rowless) + a pinned rowless peer (dd) +
        // the two row-nodes = 4.
        let mut pinned = spt_store::trust::TrustStore::default();
        pinned.record("home", "dd", None, None);
        let rows = subnet_status_rows(&store, &regs, &pinned, Some("cc"), None);
        assert_eq!(
            (rows[0].nodes, rows[0].endpoints),
            (4, 3),
            "rowless self + rowless pinned peer still count as nodes"
        );

-        let rows = subnet_status_rows(&store, &regs, None);

+        let trust = spt_store::trust::TrustStore::default();

+        let rows = subnet_status_rows(&store, &regs, &trust, Some("aa"), None);

+        // MEMBERSHIP is trust-derived, rows enrich (the re-join field bug,

+        // 2026-06-06): a member node with ZERO advertised endpoints — fresh

+        // box, or an offline peer whose rows the 4.10 eviction decayed —

+        // still counts. Self (cc, rowless) + a pinned rowless peer (dd) +

+        let mut pinned = spt_store::trust::TrustStore::default();

+        pinned.record("home", "dd", None, None);

+        let rows = subnet_status_rows(&store, &regs, &pinned, Some("cc"), None);

+            (rows[0].nodes, rows[0].endpoints),

+            "rowless self + rowless pinned peer still count as nodes"


        // A missing snapshot degrades to this-node-alone, not a panic/zero.
        let no_gossip = subnet_status_rows(&store, &std::collections::BTreeMap::new(), None);
        assert_eq!((no_gossip[0].nodes, no_gossip[0].endpoints), (1, 0));
    }

        // A missing snapshot degrades to this-node-alone, not a panic/zero —
        // even when the identity itself could not load (own_hex None).
        let no_gossip = subnet_status_rows(
            &store,
            &std::collections::BTreeMap::new(),
            &trust,
            None,
            None,
        );
        assert_eq!((no_gossip[0].nodes, no_gossip[0].endpoints), (1, 0));
    }


        // A missing snapshot degrades to this-node-alone, not a panic/zero.
        let no_gossip = subnet_status_rows(&store, &std::collections::BTreeMap::new(), None);
        assert_eq!((no_gossip[0].nodes, no_gossip[0].endpoints), (1, 0));
    }

        // A missing snapshot degrades to this-node-alone, not a panic/zero —
        // even when the identity itself could not load (own_hex None).
        let no_gossip = subnet_status_rows(
            &store,
            &std::collections::BTreeMap::new(),
            &trust,
            None,
            None,
        );
        assert_eq!((no_gossip[0].nodes, no_gossip[0].endpoints), (1, 0));
    }

assert!(!view.contains(&seed_hex), "the status view never carries seed material");

-        // A missing snapshot degrades to this-node-alone, not a panic/zero.

-        let no_gossip = subnet_status_rows(&store, &std::collections::BTreeMap::new(), None);

+        // A missing snapshot degrades to this-node-alone, not a panic/zero —

+        // even when the identity itself could not load (own_hex None).

+        let no_gossip = subnet_status_rows(

+            &std::collections::BTreeMap::new(),


        let now = 1_000_000u64;
        let heard =
            std::collections::BTreeMap::from([("heard0bb".to_string(), now - 10_000)]);
        let rows = node_status_rows(&reg, &heard, "self00aa", now, 33_000);
        let by: std::collections::BTreeMap<&str, &NodeRow> =
            rows.iter().map(|r| (r.node.as_str(), r)).collect();

        assert_eq!(by["self00aa"].liveness, NodeLiveness::Online, "own node never probes");
        assert_eq!((by["self00aa"].active, by["self00aa"].total), (1, 2));
        assert_eq!(by["heard0bb"].liveness, NodeLiveness::Online, "recently heard ⇒ online");
        assert_eq!(by["dead00cc"].liveness, NodeLiveness::Offline, "all-Offline ⇒ offline");
        assert_eq!(by["stale0dd"].liveness, NodeLiveness::Probe, "silent + routable ⇒ probe");
        assert_eq!(by["stale0dd"].label.as_deref(), Some("QUIET"));

        let now = 1_000_000u64;
        let heard =
            std::collections::BTreeMap::from([("heard0bb".to_string(), now - 10_000)]);
        // Membership seed: self (also row-backed) + a ROWLESS pinned peer
        // (the re-join field bug, 2026-06-06 — a fresh/evicted member must
        // still render, [0/0], probed rather than presumed).
        let members = vec![
            ("self00aa".to_string(), Some("ME".to_string())),
            ("rowless0".to_string(), Some("FRESHBOX".to_string())),
        ];
        let rows = node_status_rows(&reg, &members, &heard, "self00aa", now, 33_000);
        let by: std::collections::BTreeMap<&str, &NodeRow> =
            rows.iter().map(|r| (r.node.as_str(), r)).collect();

        assert_eq!(by["self00aa"].liveness, NodeLiveness::Online, "own node never probes");
        assert_eq!((by["self00aa"].active, by["self00aa"].total), (1, 2));
        assert_eq!(by["heard0bb"].liveness, NodeLiveness::Online, "recently heard ⇒ online");
        assert_eq!(by["dead00cc"].liveness, NodeLiveness::Offline, "all-Offline ⇒ offline");
        assert_eq!(by["stale0dd"].liveness, NodeLiveness::Probe, "silent + routable ⇒ probe");
        assert_eq!(by["stale0dd"].label.as_deref(), Some("QUIET"));
        assert_eq!(
            (by["rowless0"].active, by["rowless0"].total),
            (0, 0),
            "rowless member renders [0/0]"
        );
        assert_eq!(
            by["rowless0"].liveness,
            NodeLiveness::Probe,
            "rowless member probes — absence is not evidence of offline"
        );
        assert_eq!(by["rowless0"].label.as_deref(), Some("FRESHBOX"), "trust label renders");


        let now = 1_000_000u64;
        let heard =
            std::collections::BTreeMap::from([("heard0bb".to_string(), now - 10_000)]);
        let rows = node_status_rows(&reg, &heard, "self00aa", now, 33_000);
        let by: std::collections::BTreeMap<&str, &NodeRow> =
            rows.iter().map(|r| (r.node.as_str(), r)).collect();

        assert_eq!(by["self00aa"].liveness, NodeLiveness::Online, "own node never probes");
        assert_eq!((by["self00aa"].active, by["self00aa"].total), (1, 2));
        assert_eq!(by["heard0bb"].liveness, NodeLiveness::Online, "recently heard ⇒ online");
        assert_eq!(by["dead00cc"].liveness, NodeLiveness::Offline, "all-Offline ⇒ offline");
        assert_eq!(by["stale0dd"].liveness, NodeLiveness::Probe, "silent + routable ⇒ probe");
        assert_eq!(by["stale0dd"].label.as_deref(), Some("QUIET"));

        let now = 1_000_000u64;
        let heard =
            std::collections::BTreeMap::from([("heard0bb".to_string(), now - 10_000)]);
        // Membership seed: self (also row-backed) + a ROWLESS pinned peer
        // (the re-join field bug, 2026-06-06 — a fresh/evicted member must
        // still render, [0/0], probed rather than presumed).
        let members = vec![
            ("self00aa".to_string(), Some("ME".to_string())),
            ("rowless0".to_string(), Some("FRESHBOX".to_string())),
        ];
        let rows = node_status_rows(&reg, &members, &heard, "self00aa", now, 33_000);
        let by: std::collections::BTreeMap<&str, &NodeRow> =
            rows.iter().map(|r| (r.node.as_str(), r)).collect();

        assert_eq!(by["self00aa"].liveness, NodeLiveness::Online, "own node never probes");
        assert_eq!((by["self00aa"].active, by["self00aa"].total), (1, 2));
        assert_eq!(by["heard0bb"].liveness, NodeLiveness::Online, "recently heard ⇒ online");
        assert_eq!(by["dead00cc"].liveness, NodeLiveness::Offline, "all-Offline ⇒ offline");
        assert_eq!(by["stale0dd"].liveness, NodeLiveness::Probe, "silent + routable ⇒ probe");
        assert_eq!(by["stale0dd"].label.as_deref(), Some("QUIET"));
        assert_eq!(
            (by["rowless0"].active, by["rowless0"].total),
            (0, 0),
            "rowless member renders [0/0]"
        );
        assert_eq!(
            by["rowless0"].liveness,
            NodeLiveness::Probe,
            "rowless member probes — absence is not evidence of offline"
        );
        assert_eq!(by["rowless0"].label.as_deref(), Some("FRESHBOX"), "trust label renders");

-        let rows = node_status_rows(&reg, &heard, "self00aa", now, 33_000);

+        // Membership seed: self (also row-backed) + a ROWLESS pinned peer

+        // (the re-join field bug, 2026-06-06 — a fresh/evicted member must

+        // still render, [0/0], probed rather than presumed).

+            ("self00aa".to_string(), Some("ME".to_string())),

+            ("rowless0".to_string(), Some("FRESHBOX".to_string())),

+        let rows = node_status_rows(&reg, &members, &heard, "self00aa", now, 33_000);

+            (by["rowless0"].active, by["rowless0"].total),

+            "rowless member renders [0/0]"

+            "rowless member probes — absence is not evidence of offline"

+        assert_eq!(by["rowless0"].label.as_deref(), Some("FRESHBOX"), "trust label renders");

// Render: labels as `LABEL (keypfx…)`, bare prefix when unlabeled,

// online/offline + [active/total]; never the full pubkey alone.


Now the join code window. Find the responder's step acceptance:


Bash

ls C:/Users/decid/Documents/projects/spt-core/crates/spt-net/src/net/pairing/; grep -rn "window\|±1\|step" C:/Users/decid/Documents/projects/spt-core/crates/spt-net/src/net/pairing/totp.rs | head -15

Pairing module + totp window refs


meet.rs
ratelimit.rs
rendezvous.rs
spake.rs
totp.rs
transcript.rs
wire.rs
11://! The pairing transcript MUST bind the **raw time-step counter** `T` (= unix/period)
12://! so a stale-step pairing is rejectable (ADR-0005 #12). Every surveyed TOTP crate
15://! HMAC/SHA primitives keeps the step counter first-class, avoids a heavyweight
19://! ## ±1 window (ADR-0005 #11 interaction)
22://! steps — but the step is **bound into the transcript**, not folded into the
23://! password (a changed step would change the password *and* the transcript, so a
24://! windowed retry can't work password-side). The dialer announces its step in the
25://! clear; both sides bind that exact value and reject `|Δstep| > 1`
26://! ([`accepts_step`]). The ±1 widening of the valid-password space is the reason the
99:    /// The 6-digit code at a given absolute time-step `T`. Pure RFC 4226 HOTP with the
100:    /// step as the counter (RFC 6238 §4): HMAC-SHA-1 over the big-endian step, dynamic
101:    /// truncation, modulo `10^digits`. The [step](time_step) is the value the
103:    pub fn code_at_step(&self, step: u64, digits: u32) -> u32 {
105:        mac.update(&step.to_be_bytes());
108:        // 4-byte window; mask the high bit; reduce to the requested digit count.


grep -rn "wrong.code\|WrongCode\|run_initiator\|re-prompt\|reprompt" C:/Users/decid/Documents/projects/spt-core/crates/spt/src/cli.rs C:/Users/decid/Documents/projects/spt-core/crates/spt-daemon/src/pairhost.rs 2>/dev/null | head -15

Find join initiator wrong-code loop


C:/Users/decid/Documents/projects/spt-core/crates/spt/src/cli.rs:2178:/// wrong codes re-prompt (codes rotate; bounded attempts), rate limits
C:/Users/decid/Documents/projects/spt-core/crates/spt/src/cli.rs:2257:            // Wrong code: re-prompt against a FRESH window (codes rotate
C:/Users/decid/Documents/projects/spt-core/crates/spt/src/cli.rs:2259:            Some("wrong-code") if interactive && attempt < MAX_ATTEMPTS => {
C:/Users/decid/Documents/projects/spt-core/crates/spt/src/cli.rs:2265:                        eprintln!("JOIN_FAIL:{name}: wrong code");
C:/Users/decid/Documents/projects/spt-core/crates/spt/src/cli.rs:2270:            Some("wrong-code") => {
C:/Users/decid/Documents/projects/spt-core/crates/spt/src/cli.rs:2272:                    "JOIN_FAIL:{name}: wrong code — read the CURRENT code off the member \
C:/Users/decid/Documents/projects/spt-core/crates/spt-daemon/src/pairhost.rs:35:use spt_net::net::pairing::wire::{run_initiator, run_responder, PairWireError};
C:/Users/decid/Documents/projects/spt-core/crates/spt-daemon/src/pairhost.rs:82:        // A failed ceremony is normal traffic (wrong code, impostor probe,
C:/Users/decid/Documents/projects/spt-core/crates/spt-daemon/src/pairhost.rs:190:/// (M7 plan decision 10: wrong code re-prompts, rate limits surface
C:/Users/decid/Documents/projects/spt-core/crates/spt-daemon/src/pairhost.rs:197:    /// The ceremony ran and the confirmation failed — in practice: wrong code.
C:/Users/decid/Documents/projects/spt-core/crates/spt-daemon/src/pairhost.rs:198:    WrongCode,
C:/Users/decid/Documents/projects/spt-core/crates/spt-daemon/src/pairhost.rs:214:            JoinFail::WrongCode => write!(f, "pairing confirmation failed (wrong code?)"),
C:/Users/decid/Documents/projects/spt-core/crates/spt-daemon/src/pairhost.rs:230:/// One attempt — the CLI owns the re-prompt loop.
C:/Users/decid/Documents/projects/spt-core/crates/spt-daemon/src/pairhost.rs:266:        run_initiator(
C:/Users/decid/Documents/projects/spt-core/crates/spt-daemon/src/pairhost.rs:287:        PairWireError::Pairing(_) => JoinFail::WrongCode,


218	            JoinFail::ClockSkew => write!(f, "TOTP step rejected — check this machine's clock"),
219	            JoinFail::Other(e) => write!(f, "{e}"),
220	        }
221	    }
222	}
223
224	/// The joiner leg (M7 plan decision 10): find a seed-holder via the LAN+relay
225	/// rendezvous (or dial `responder_addr` directly — the static-`ip:port` rig
226	/// seam, mirroring [`meet_at`](spt_net::net::pairing::meet::meet_at)'s
227	/// explicit-address form), run the unchanged initiator ceremony against its
228	/// real endpoint, and persist the results (peer trust-pinned; the
229	/// transferred seed makes this node a full member / future seed-holder).
230	/// One attempt — the CLI owns the re-prompt loop.
231	// [impl->REQ-SUBNET-2]
232	pub async fn join_ceremony(
233	    endpoint: &NetEndpoint,
234	    subnet_name: &str,
235	    code: &str,
236	    responder_addr: Option<spt_net::net::endpoint::EndpointAddr>,
237	) -> Result<String, JoinFail> {
238	    let conn = match responder_addr {
239	        Some(addr) => endpoint
240	            .connect_with_alpn(addr, spt_net::net::pairing::wire::SPT_PAIR_ALPN)
241	            .await
242	            .map_err(|e| JoinFail::NoSeedHolder(e.to_string()))?,
243	        None => {
244	            // Sweep the rendezvous repeatedly until the search deadline: one
245	            // sweep races the ±1 window's tokens (bounded probes), and a
246	            // sweep that straddles a rotation simply misses — the next one
247	            // derives fresh tokens. ~2.5 windows of trying before the
248	            // NoSeedHolder verdict (M7 acceptance fix).
249	            const SEARCH_DEADLINE: Duration = Duration::from_secs(75);
250	            let start = std::time::Instant::now();
251	            loop {
252	                match dial_via_rendezvous(endpoint, subnet_name, time_step(now_secs())).await {
253	                    Ok(conn) => break conn,
254	                    Err(e) if start.elapsed() >= SEARCH_DEADLINE => {
255	                        return Err(JoinFail::NoSeedHolder(e.to_string()));
256	                    }
257	                    Err(_) => tokio::time::sleep(Duration::from_millis(500)).await,
258	                }
259	            }
260	        }
261	    };
262	    let mut trust = TrustStore::load();
263	    let mut subnets = SubnetStore::load();
264	    let outcome = tokio::time::timeout(
265	        CEREMONY_TIMEOUT,
266	        run_initiator(
267	            &conn,
268	            endpoint.node_id(),
269	            subnet_name,
270	            code,
271	            &mut trust,
272	            &mut subnets,
273	            now_secs(),
274	        ),
275	    )
276	    .await
277	    .map_err(|_| JoinFail::Other(format!("ceremony exceeded {CEREMONY_TIMEOUT_SECS}s")))?
278	    .map_err(classify_wire_error)?;
279	    trust.save().map_err(|e| JoinFail::Other(format!("trust save: {e}")))?;
280	    subnets.save().map_err(|e| JoinFail::Other(format!("seed save: {e}")))?;
281	    Ok(format!("{outcome:?}"))
282	}
283
284	/// Map the wire error onto the CLI-facing failure classes.
285	fn classify_wire_error(e: PairWireError) -> JoinFail {
286	    match e {
287	        PairWireError::Pairing(_) => JoinFail::WrongCode,
288	        PairWireError::Refused(RateLimited::Busy) => JoinFail::RateLimited(None),
289	        PairWireError::Refused(RateLimited::Backoff { retry_after_secs }) => {
290	            JoinFail::RateLimited(Some(retry_after_secs))
291	        }
292	        PairWireError::UnknownSubnet(n) => JoinFail::UnknownSubnet(n),
293	        PairWireError::StepRejected => JoinFail::ClockSkew,
294	        PairWireError::Io(e) | PairWireError::Protocol(e) => JoinFail::Other(e),
295	    }
296	}
297
298	/// Unix seconds (the ceremony clock — TOTP steps + limiter time).
299	fn now_secs() -> u64 {
300	    std::time::SystemTime::now()
301	        .duration_since(std::time::UNIX_EPOCH)
302	        .map(|d| d.as_secs())

            JoinFail::ClockSkew => write!(f, "TOTP step rejected — check this machine's clock"),
            JoinFail::Other(e) => write!(f, "{e}"),
        }
    }
}

/// The joiner leg (M7 plan decision 10): find a seed-holder via the LAN+relay
/// rendezvous (or dial `responder_addr` directly — the static-`ip:port` rig
/// seam, mirroring [`meet_at`](spt_net::net::pairing::meet::meet_at)'s
/// explicit-address form), run the unchanged initiator ceremony against its
/// real endpoint, and persist the results (peer trust-pinned; the
/// transferred seed makes this node a full member / future seed-holder).
/// One attempt — the CLI owns the re-prompt loop.
// [impl->REQ-SUBNET-2]
pub async fn join_ceremony(
    endpoint: &NetEndpoint,
    subnet_name: &str,
    code: &str,
    responder_addr: Option<spt_net::net::endpoint::EndpointAddr>,
) -> Result<String, JoinFail> {
    let conn = match responder_addr {
        Some(addr) => endpoint
            .connect_with_alpn(addr, spt_net::net::pairing::wire::SPT_PAIR_ALPN)
            .await
            .map_err(|e| JoinFail::NoSeedHolder(e.to_string()))?,
        None => {
            // Sweep the rendezvous repeatedly until the search deadline: one
            // sweep races the ±1 window's tokens (bounded probes), and a
            // sweep that straddles a rotation simply misses — the next one
            // derives fresh tokens. ~2.5 windows of trying before the
            // NoSeedHolder verdict (M7 acceptance fix).
            const SEARCH_DEADLINE: Duration = Duration::from_secs(75);
            let start = std::time::Instant::now();
            loop {
                match dial_via_rendezvous(endpoint, subnet_name, time_step(now_secs())).await {
                    Ok(conn) => break conn,
                    Err(e) if start.elapsed() >= SEARCH_DEADLINE => {
                        return Err(JoinFail::NoSeedHolder(e.to_string()));
                    }
                    Err(_) => tokio::time::sleep(Duration::from_millis(500)).await,
                }
            }
        }
    };
    let mut trust = TrustStore::load();
    let mut subnets = SubnetStore::load();
    let outcome = tokio::time::timeout(
        CEREMONY_TIMEOUT,
        run_initiator(
            &conn,
            endpoint.node_id(),
            subnet_name,
            code,
            &mut trust,
            &mut subnets,
            now_secs(),
        ),
    )
    .await
    .map_err(|_| JoinFail::Other(format!("ceremony exceeded {CEREMONY_TIMEOUT_SECS}s")))?
    .map_err(classify_wire_error)?;
    trust.save().map_err(|e| JoinFail::Other(format!("trust save: {e}")))?;
    subnets.save().map_err(|e| JoinFail::Other(format!("seed save: {e}")))?;
    Ok(format!("{outcome:?}"))
}

/// Map the wire error onto the CLI-facing failure classes.
fn classify_wire_error(e: PairWireError) -> JoinFail {
    match e {
        PairWireError::Pairing(_) => JoinFail::WrongCode,
        PairWireError::Refused(RateLimited::Busy) => JoinFail::RateLimited(None),
        PairWireError::Refused(RateLimited::Backoff { retry_after_secs }) => {
            JoinFail::RateLimited(Some(retry_after_secs))
        }
        PairWireError::UnknownSubnet(n) => JoinFail::UnknownSubnet(n),
        PairWireError::StepRejected => JoinFail::ClockSkew,
        PairWireError::Io(e) | PairWireError::Protocol(e) => JoinFail::Other(e),
    }
}

/// Unix seconds (the ceremony clock — TOTP steps + limiter time).
fn now_secs() -> u64 {
    std::time::SystemTime::now()
        .duration_since(std::time::UNIX_EPOCH)
        .map(|d| d.as_secs())


grep -n "pub async fn run_initiator" -A 30 C:/Users/decid/Documents/projects/spt-core/crates/spt-net/src/net/pairing/wire.rs | head -45

run_initiator signature + step binding


307:pub async fn run_initiator(
308-    conn: &Connection,
309-    local_pub: PublicKey,
310-    subnet_name: &str,
311-    code: &str,
312-    trust: &mut TrustStore,
313-    subnets: &mut SubnetStore,
314-    now: u64,
315-) -> Result<TrustOutcome, PairWireError> {
316-    let (mut send, mut recv) = conn
317-        .open_bi()
318-        .await
319-        .map_err(|e| PairWireError::Io(format!("open_bi: {e}")))?;
320-    let responder_pub = remote_pub(conn)?;
321-    let step = time_step(now);
322-
323-    // F1: Hello — opens the stream (the dialer must write first for accept_bi to fire).
324-    write_frame(&mut send, &enc_hello(subnet_name, step)).await?;
325-
326-    // F2: Announce(epoch) — or an Abort if the responder refused admission / step / name.
327-    let epoch = match read_frame(&mut recv).await? {
328-        Frame::Announce { epoch } => epoch,
329-        Frame::Abort { reason, retry_after } => return Err(map_abort(reason, retry_after, subnet_name)),
330-        other => return Err(unexpected("Announce", other)),
331-    };
332-
333-    // Now the joiner can bind the full transcript and run SPAKE2 side A.
334-    let transcript = PairingTranscript {
335-        subnet_name: subnet_name.to_string(),
336-        epoch,
337-        step,


sed -n '307,420p' C:/Users/decid/Documents/projects/spt-core/crates/spt-net/src/net/pairing/wire.rs | grep -n "now\b" ; grep -rn "run_initiator(" C:/Users/decid/Documents/projects/spt-core/crates --include=*.rs | grep -v "pub async fn"

now usage inside run_initiator + all callers


8:    now: u64,
15:    let step = time_step(now);
C:/Users/decid/Documents/projects/spt-core/crates/spt-daemon/src/pairhost.rs:266:        run_initiator(
C:/Users/decid/Documents/projects/spt-core/crates/spt-net/src/net/pairing/wire.rs:729:        let init_outcome = run_initiator(
C:/Users/decid/Documents/projects/spt-core/crates/spt-net/src/net/pairing/wire.rs:788:        let init_result = run_initiator(
C:/Users/decid/Documents/projects/spt-core/crates/spt-net/src/net/pairing/wire.rs:845:        let init_result = run_initiator(
C:/Users/decid/Documents/projects/spt-core/crates/spt-net/src/net/pairing/wire.rs:898:        let init_result = run_initiator(
C:/Users/decid/Documents/projects/spt-core/crates/spt-net/src/net/pairing/wire.rs:950:        let init_result = run_initiator(
C:/Users/decid/Documents/projects/spt-core/crates/spt-net/src/net/pairing/wire.rs:1007:        run_initiator(
C:/Users/decid/Documents/projects/spt-core/crates/spt-net/src/net/pairing/wire.rs:1062:        let init_outcome = run_initiator(


280	            let outcome = trust.record(p.subnet_name, &p.initiator_pub.to_hex(), None, None);
281	            write_frame(send, &enc_done(true)).await?;
282	            // F7: transfer the subnet seed so the new node becomes a full member /
283	            // future seed-holder ("every trusted node holds the seed", CONTEXT.md). Sent
284	            // ONLY on confirm-success, over the iroh-authenticated TLS stream to the
285	            // now-trust-pinned peer. `seed` is Some here (a None seed aborted upstream).
286	            let seed = p.seed.expect("seed present on the confirm-success path");
287	            write_frame(send, &enc_seed(&seed, p.epoch)).await?;
288	            let _ = send.finish();
289	            Ok(outcome)
290	        }
291	        Err(e) => {
292	            write_frame(send, &enc_done(false)).await?;
293	            let _ = send.finish();
294	            Err(PairWireError::Pairing(e))
295	        }
296	    }
297	}
298
299	/// Run the **initiator** (joining node) side of the ceremony over a dialed connection.
300	///
301	/// `local_pub` is this node's own pubkey; `subnet_name` + `code` are the two user-entered
302	/// inputs (the subnet label and the current 6-digit TOTP code); `now` is the local clock.
303	/// On success the responder is pinned in `trust` AND the transferred subnet seed is written
304	/// into `subnets` (the joiner becomes a full member / future seed-holder — D2f, REQ-PAIR-5);
305	/// the returned [`TrustOutcome`] is the TOFU decision.
306	// [impl->REQ-PAIR-1] [impl->REQ-PAIR-5]
307	pub async fn run_initiator(
308	    conn: &Connection,
309	    local_pub: PublicKey,
310	    subnet_name: &str,
311	    code: &str,
312	    trust: &mut TrustStore,
313	    subnets: &mut SubnetStore,
314	    now: u64,
315	) -> Result<TrustOutcome, PairWireError> {
316	    let (mut send, mut recv) = conn
317	        .open_bi()
318	        .await
319	        .map_err(|e| PairWireError::Io(format!("open_bi: {e}")))?;
320	    let responder_pub = remote_pub(conn)?;
321	    let step = time_step(now);
322
323	    // F1: Hello — opens the stream (the dialer must write first for accept_bi to fire).
324	    write_frame(&mut send, &enc_hello(subnet_name, step)).await?;
325
326	    // F2: Announce(epoch) — or an Abort if the responder refused admission / step / name.
327	    let epoch = match read_frame(&mut recv).await? {
328	        Frame::Announce { epoch } => epoch,
329	        Frame::Abort { reason, retry_after } => return Err(map_abort(reason, retry_after, subnet_name)),

            let outcome = trust.record(p.subnet_name, &p.initiator_pub.to_hex(), None, None);
            write_frame(send, &enc_done(true)).await?;
            // F7: transfer the subnet seed so the new node becomes a full member /
            // future seed-holder ("every trusted node holds the seed", CONTEXT.md). Sent
            // ONLY on confirm-success, over the iroh-authenticated TLS stream to the
            // now-trust-pinned peer. `seed` is Some here (a None seed aborted upstream).
            let seed = p.seed.expect("seed present on the confirm-success path");
            write_frame(send, &enc_seed(&seed, p.epoch)).await?;
            let _ = send.finish();
            Ok(outcome)
        }
        Err(e) => {
            write_frame(send, &enc_done(false)).await?;
            let _ = send.finish();
            Err(PairWireError::Pairing(e))
        }
    }
}

/// Run the **initiator** (joining node) side of the ceremony over a dialed connection.
///
/// `local_pub` is this node's own pubkey; `subnet_name` + `code` are the two user-entered
/// inputs (the subnet label and the current 6-digit TOTP code); `now` is the local clock.
/// On success the responder is pinned in `trust` AND the transferred subnet seed is written
/// into `subnets` (the joiner becomes a full member / future seed-holder — D2f, REQ-PAIR-5);
/// the returned [`TrustOutcome`] is the TOFU decision.
// [impl->REQ-PAIR-1] [impl->REQ-PAIR-5]
pub async fn run_initiator(
    conn: &Connection,
    local_pub: PublicKey,
    subnet_name: &str,
    code: &str,
    trust: &mut TrustStore,
    subnets: &mut SubnetStore,
    now: u64,
) -> Result<TrustOutcome, PairWireError> {
    let (mut send, mut recv) = conn
        .open_bi()
        .await
        .map_err(|e| PairWireError::Io(format!("open_bi: {e}")))?;
    let responder_pub = remote_pub(conn)?;
    let step = time_step(now);

    // F1: Hello — opens the stream (the dialer must write first for accept_bi to fire).
    write_frame(&mut send, &enc_hello(subnet_name, step)).await?;

    // F2: Announce(epoch) — or an Abort if the responder refused admission / step / name.
    let epoch = match read_frame(&mut recv).await? {
        Frame::Announce { epoch } => epoch,
        Frame::Abort { reason, retry_after } => return Err(map_abort(reason, retry_after, subnet_name)),


Edit

/// Run the **initiator** (joining node) side of the ceremony over a dialed connection.
///
/// `local_pub` is this node's own pubkey; `subnet_name` + `code` are the two user-entered
/// inputs (the subnet label and the current 6-digit TOTP code); `now` is the local clock.
/// On success the responder is pinned in `trust` AND the transferred subnet seed is written
/// into `subnets` (the joiner becomes a full member / future seed-holder — D2f, REQ-PAIR-5);
/// the returned [`TrustOutcome`] is the TOFU decision.
// [impl->REQ-PAIR-1] [impl->REQ-PAIR-5]
pub async fn run_initiator(
    conn: &Connection,
    local_pub: PublicKey,
    subnet_name: &str,
    code: &str,
    trust: &mut TrustStore,
    subnets: &mut SubnetStore,
    now: u64,
) -> Result<TrustOutcome, PairWireError> {
    let (mut send, mut recv) = conn
        .open_bi()
        .await
        .map_err(|e| PairWireError::Io(format!("open_bi: {e}")))?;
    let responder_pub = remote_pub(conn)?;
    let step = time_step(now);

/// Run the **initiator** (joining node) side of the ceremony over a dialed connection.
///
/// `local_pub` is this node's own pubkey; `subnet_name` + `code` are the two user-entered
/// inputs (the subnet label and the current 6-digit TOTP code); `now` is the local clock.
/// On success the responder is pinned in `trust` AND the transferred subnet seed is written
/// into `subnets` (the joiner becomes a full member / future seed-holder — D2f, REQ-PAIR-5);
/// the returned [`TrustOutcome`] is the TOFU decision.
///
/// `step_back` shifts the ANNOUNCED time-step below the local clock's (0 =
/// the normal attempt). The joiner cannot validate the typed code locally
/// (it holds no seed yet), so a code read just before a rotation and entered
/// just after binds to the wrong step and fails as "wrong code" — the ±1
/// responder window ([`accepts_step`](super::totp)) tolerates an announced
/// `step-1`, letting the caller retry the SAME typed code against the
/// previous step before re-prompting the user (the late-code retry, field
/// report 2026-06-06).
// [impl->REQ-PAIR-1] [impl->REQ-PAIR-5]
#[allow(clippy::too_many_arguments)]
pub async fn run_initiator(
    conn: &Connection,
    local_pub: PublicKey,
    subnet_name: &str,
    code: &str,
    trust: &mut TrustStore,
    subnets: &mut SubnetStore,
    now: u64,
    step_back: u64,
) -> Result<TrustOutcome, PairWireError> {
    let (mut send, mut recv) = conn
        .open_bi()
        .await
        .map_err(|e| PairWireError::Io(format!("open_bi: {e}")))?;
    let responder_pub = remote_pub(conn)?;
    let step = time_step(now).saturating_sub(step_back);


The file C:\Users\decid\Documents\projects\spt-core\crates\spt-net\src\net\pairing\wire.rs has been updated successfully. (file state is current in your context — no need to Read it back)

/// Run the **initiator** (joining node) side of the ceremony over a dialed connection.
///
/// `local_pub` is this node's own pubkey; `subnet_name` + `code` are the two user-entered
/// inputs (the subnet label and the current 6-digit TOTP code); `now` is the local clock.
/// On success the responder is pinned in `trust` AND the transferred subnet seed is written
/// into `subnets` (the joiner becomes a full member / future seed-holder — D2f, REQ-PAIR-5);
/// the returned [`TrustOutcome`] is the TOFU decision.
// [impl->REQ-PAIR-1] [impl->REQ-PAIR-5]
pub async fn run_initiator(
    conn: &Connection,
    local_pub: PublicKey,
    subnet_name: &str,
    code: &str,
    trust: &mut TrustStore,
    subnets: &mut SubnetStore,
    now: u64,
) -> Result<TrustOutcome, PairWireError> {
    let (mut send, mut recv) = conn
        .open_bi()
        .await
        .map_err(|e| PairWireError::Io(format!("open_bi: {e}")))?;
    let responder_pub = remote_pub(conn)?;
    let step = time_step(now);

/// Run the **initiator** (joining node) side of the ceremony over a dialed connection.
///
/// `local_pub` is this node's own pubkey; `subnet_name` + `code` are the two user-entered
/// inputs (the subnet label and the current 6-digit TOTP code); `now` is the local clock.
/// On success the responder is pinned in `trust` AND the transferred subnet seed is written
/// into `subnets` (the joiner becomes a full member / future seed-holder — D2f, REQ-PAIR-5);
/// the returned [`TrustOutcome`] is the TOFU decision.
///
/// `step_back` shifts the ANNOUNCED time-step below the local clock's (0 =
/// the normal attempt). The joiner cannot validate the typed code locally
/// (it holds no seed yet), so a code read just before a rotation and entered
/// just after binds to the wrong step and fails as "wrong code" — the ±1
/// responder window ([`accepts_step`](super::totp)) tolerates an announced
/// `step-1`, letting the caller retry the SAME typed code against the
/// previous step before re-prompting the user (the late-code retry, field
/// report 2026-06-06).
// [impl->REQ-PAIR-1] [impl->REQ-PAIR-5]
#[allow(clippy::too_many_arguments)]
pub async fn run_initiator(
    conn: &Connection,
    local_pub: PublicKey,
    subnet_name: &str,
    code: &str,
    trust: &mut TrustStore,
    subnets: &mut SubnetStore,
    now: u64,
    step_back: u64,
) -> Result<TrustOutcome, PairWireError> {
    let (mut send, mut recv) = conn
        .open_bi()
        .await
        .map_err(|e| PairWireError::Io(format!("open_bi: {e}")))?;
    let responder_pub = remote_pub(conn)?;
    let step = time_step(now).saturating_sub(step_back);

+/// `step_back` shifts the ANNOUNCED time-step below the local clock's (0 =

+/// the normal attempt). The joiner cannot validate the typed code locally

+/// (it holds no seed yet), so a code read just before a rotation and entered

+/// just after binds to the wrong step and fails as "wrong code" — the ±1

+/// responder window ([`accepts_step`](super::totp)) tolerates an announced

+/// `step-1`, letting the caller retry the SAME typed code against the

+/// previous step before re-prompting the user (the late-code retry, field

+    let step = time_step(now).saturating_sub(step_back);

// F1: Hello — opens the stream (the dialer must write first for accept_bi to fire).

write_frame(&mut send, &enc_hello(subnet_name, step)).await?;


Bash

cd C:/Users/decid/Documents/projects/spt-core && sed -n '740,760p' crates/spt-net/src/net/pairing/wire.rs

Inspect a test callsite shape


        let init_outcome = run_initiator(
            &conn,
            init_pub,
            "home",
            &code,
            &mut init_trust,
            &mut init_subnets,
            NOW,
        )
        .await
        .expect("initiator pairs");
        conn.close(0u32.into(), b"done");

        let (resp_trust, resp_outcome) = resp_task.await.expect("responder task");

        assert_eq!(init_outcome, TrustOutcome::Pinned, "initiator pins responder");
        assert_eq!(resp_outcome, TrustOutcome::Pinned, "responder pins initiator");
        // Each side trusts the OTHER's real pubkey within the subnet.
        assert!(init_trust.is_trusted("home", &resp_pub.to_hex()));
        assert!(resp_trust.is_trusted("home", &init_pub.to_hex()));
        assert!(!init_trust.is_trusted("home", &init_pub.to_hex()), "self not pinned");


cd C:/Users/decid/Documents/projects/spt-core && python - <<'EOF'
import io, re
p = 'crates/spt-net/src/net/pairing/wire.rs'
s = io.open(p, encoding='utf-8').read()
# Append `, 0` step_back to every test callsite arg list ending in NOW/now-expr before `)` of run_initiator(
# Callsites: run_initiator(\n ... \n NOW,\n ) — last arg line before `)` then `.await`.
pat = re.compile(r'(run_initiator\(\s*(?:[^;]*?))\n(\s*)\)\s*\n(\s*)\.await')
def fix(m):
    body, ind, ind2 = m.group(1), m.group(2), m.group(3)
    if 'pub async fn' in body:
        return m.group(0)
    return f"{body}\n{ind}    0, // step_back: the normal attempt\n{ind})\n{ind2}.await"
s2, n = pat.subn(fix, s)
io.open(p, 'w', encoding='utf-8', newline='').write(s2)
print('patched', n)
EOF

Append step_back=0 to wire.rs test callsites


patched 7


1	//! D2-wire: the pairing ALPN ceremony driver (ADR-0005, REQ-PAIR-1).
2	//!
3	//! This is the transport glue that runs the TOTP-SPAKE2 ceremony (the crypto core of
4	//! [`spake`](super::spake) + [`transcript`](super::transcript)) over a real iroh bidi
5	//! stream on a dedicated pre-trust **pairing ALPN** ([`SPT_PAIR_ALPN`]), gated by the
6	//! subnet-global [rate limiter](super::ratelimit), and writes the confirmed peer into
7	//! each side's [`TrustStore`]. It turns "two machines pair via one TOTP code" from a
8	//! same-process unit test into an over-the-wire exchange.
9	//!
10	//! ## Why a responder `Announce{epoch}` precedes the SPAKE messages
11	//!
12	//! [`Initiator::start`](super::spake::Initiator::start) bakes the **full transcript**
13	//! into `msg_a`, and the transcript binds the seed-rotation [`epoch`] — which is
14	//! *responder-authoritative* (only seed-holders track it; the joiner has just the
15	//! 6-digit code + subnet name, per [`transcript`](super::transcript) docs). So the
16	//! joiner cannot construct `msg_a` until the responder tells it the current epoch. The
17	//! wire therefore opens with the joiner's [`Hello`](Frame::Hello) (subnet name + its
18	//! clock-derived TOTP step), the responder's [`Announce`](Frame::Announce) (the epoch,
19	//! *after* it has admitted the ceremony and accepted the ±1 step), and only then the
20	//! four SPAKE messages. The TOTP **step** is dialer-announced and clock-derived
21	//! (`totp.rs`), so it needs no announce — only the seed epoch does.
22	//!
23	//! ## Authenticated identities
24	//!
25	//! Both node pubkeys bound into the transcript come from the **iroh-authenticated**
26	//! [`Connection::remote_id`] (the QUIC handshake already proves the peer holds the
27	//! Ed25519 key that *is* its node id) plus the local node's own pubkey — never the
28	//! bytes a peer claims in a frame. A MITM substituting its key is rejected by the
29	//! transcript binding (`spake.rs` `substituted_pubkey_fails`).
30	//!
31	//! ## Scope (component-level; WAN is D9)
32	//!
33	//! Like [`endpoint`](super::super::endpoint) tests, this dials by `EndpointAddr` over
34	//! loopback with relays disabled. The relay **rendezvous** (`H(subnet-name ‖ step)`,
35	//! `CONTEXT.md`) that lets two nodes that don't know each other's id *meet* is deferred
36	//! to D9; here the joiner already holds the responder's dialable address. Seed transfer
37	//! to the joiner (so it becomes a future seed-holder) is deferred to D2f — this driver
38	//! delivers the ceremony + mutual trust write only.
39	//
40	// [impl->REQ-PAIR-1]
41
42	use iroh::endpoint::{Connection, RecvStream, SendStream};
43	use spt_proto::identity::PublicKey;
44	use spt_store::trust::{TrustOutcome, TrustStore};
45
46	use spt_store::subnet::{SubnetError, SubnetStore, TOTP_SEED_LEN};
47
48	use super::ratelimit::{PairingRateLimiter, RateLimited};
49	use super::spake::{Initiator, PairError, Responder};
50	use super::totp::{accepts_step, time_step, TotpSeed, DIGITS};
51	use super::transcript::{PairingTranscript, TAG_LEN};
52
53	/// The pre-trust pairing ALPN. Distinct from the trusted
54	/// [`SPT_NET_ALPN`](super::super::endpoint::SPT_NET_ALPN) so the relay can permit
55	/// pre-trust contact (a seed-holder must be reachable by an untrusted new node) without
56	/// exposing the trusted message surface (ADR-0005; `pairing.rs`). Versioned for a clean
57	/// future wire break.
58	pub const SPT_PAIR_ALPN: &[u8] = b"spt-core/pair/0";
59
60	/// Hard cap on a single wire frame. The ceremony's largest field is a SPAKE2 message
61	/// (~tens of bytes) or a subnet name; 4 KiB is generous slack while bounding a hostile
62	/// length prefix so a pre-trust peer cannot force a huge allocation.
63	const MAX_FRAME: usize = 4096;
64
65	// Frame tag bytes.
66	const T_ABORT: u8 = 0;
67	const T_HELLO: u8 = 1;
68	const T_ANNOUNCE: u8 = 2;
69	const T_SPAKE1: u8 = 3;
70	const T_SPAKE2: u8 = 4;
71	const T_CONFIRM: u8 = 5;
72	const T_DONE: u8 = 6;
73	const T_SEED: u8 = 7;
74
75	// Abort reason bytes.
76	const A_BUSY: u8 = 1;
77	const A_BACKOFF: u8 = 2;
78	const A_BAD_STEP: u8 = 3;
79	const A_UNKNOWN_SUBNET: u8 = 4;
80	const A_BAD_MSG: u8 = 5;
81	const A_CONFIRM_FAIL: u8 = 6;
82
83	/// A failure of the pairing wire ceremony. Carries no secret material.
84	#[derive(Debug)]
85	pub enum PairWireError {
86	    /// Stream read/write/framing failure (peer vanished, malformed length, EOF).
87	    Io(String),
88	    /// A frame was the wrong type or malformed for the current ceremony step.
89	    Protocol(String),
90	    /// The responder refused to admit the ceremony (rate-limit): another is in flight
91	    /// ([`Busy`](RateLimited::Busy)) or the backoff window is open
92	    /// ([`Backoff`](RateLimited::Backoff)).
93	    Refused(RateLimited),
94	    /// The dialer's announced TOTP step was outside the responder's ±1 window.
95	    StepRejected,
96	    /// The responder holds no subnet by that name.
97	    UnknownSubnet(String),
98	    /// The SPAKE2 / key-confirmation step failed: wrong code, MITM, wrong subnet, stale
99	    /// step — the one signal a bad pairing gives ([`spake`](super::spake)).
100	    Pairing(PairError),
101	}
102
103	impl std::fmt::Display for PairWireError {
104	    fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
105	        match self {
106	            PairWireError::Io(e) => write!(f, "pairing wire io: {e}"),
107	            PairWireError::Protocol(e) => write!(f, "pairing wire protocol error: {e}"),
108	            PairWireError::Refused(r) => write!(f, "pairing refused: {r}"),
109	            PairWireError::StepRejected => {
110	                write!(f, "pairing rejected: TOTP step outside the ±1 window")
111	            }
112	            PairWireError::UnknownSubnet(n) => write!(f, "pairing rejected: unknown subnet {n}"),
113	            PairWireError::Pairing(e) => write!(f, "{e}"),
114	        }
115	    }
116	}
117
118	impl std::error::Error for PairWireError {}
119
120	/// Run the **responder** (seed-holder) side of the ceremony over an accepted connection.
121	///
122	/// `local_pub` is this node's own pubkey (the iroh endpoint id); `subnets` supplies the
123	/// seed + epoch for the named subnet; `rate` is the subnet-global admission gate; `trust`
124	/// receives the peer on success. `now` (unix seconds) is injected for deterministic
125	/// tests, mirroring [`ratelimit`](super::ratelimit) / [`totp`](super::totp).
126	///
127	/// On `Ok` the peer was confirmed and pinned (the returned [`TrustOutcome`]); the rate
128	/// limiter slot is released as a success. Every error path releases the slot too — as a
129	/// failure when a ceremony was admitted (charging backoff), or not at all when admission
130	/// itself was refused.
131	// [impl->REQ-PAIR-1]
132	pub async fn run_responder(
133	    conn: &Connection,
134	    local_pub: PublicKey,
135	    subnets: &SubnetStore,
136	    rate: &mut PairingRateLimiter,
137	    trust: &mut TrustStore,
138	    now: u64,
139	) -> Result<TrustOutcome, PairWireError> {
140	    let (mut send, mut recv) = conn
141	        .accept_bi()
142	        .await
143	        .map_err(|e| PairWireError::Io(format!("accept_bi: {e}")))?;
144	    let initiator_pub = remote_pub(conn)?;
145
146	    // F1: Hello — the joiner's subnet name + clock-derived TOTP step.
147	    let (subnet_name, peer_step) = match read_frame(&mut recv).await? {
148	        Frame::Hello { subnet_name, step } => (subnet_name, step),
149	        other => return Err(unexpected("Hello", other)),
150	    };
151
152	    // Subnet lookup BEFORE admission so an unknown name never consumes a ceremony slot.
153	    let record = match subnets.find(&subnet_name) {
154	        Some(r) => r,
155	        None => {
156	            abort(&mut send, A_UNKNOWN_SUBNET, 0).await?;
157	            return Err(PairWireError::UnknownSubnet(subnet_name));
158	        }
159	    };
160
161	    // Admission: one ceremony per subnet + exponential backoff (#11). On refusal the
162	    // slot is NOT taken, so there is nothing to release.
163	    if let Err(rl) = rate.begin(&subnet_name, now) {
164	        let (reason, retry) = match &rl {

... [964 lines truncated] ...


Now the late-code retry in `join_ceremony`:


Edit

    let mut trust = TrustStore::load();
    let mut subnets = SubnetStore::load();
    let outcome = tokio::time::timeout(
        CEREMONY_TIMEOUT,
        run_initiator(
            &conn,
            endpoint.node_id(),
            subnet_name,
            code,
            &mut trust,
            &mut subnets,
            now_secs(),
        ),
    )
    .await
    .map_err(|_| JoinFail::Other(format!("ceremony exceeded {CEREMONY_TIMEOUT_SECS}s")))?
    .map_err(classify_wire_error)?;
    trust.save().map_err(|e| JoinFail::Other(format!("trust save: {e}")))?;
    subnets.save().map_err(|e| JoinFail::Other(format!("seed save: {e}")))?;
    Ok(format!("{outcome:?}"))
}

    let mut trust = TrustStore::load();
    let mut subnets = SubnetStore::load();
    let ceremony = |trust: &mut TrustStore, subnets: &mut SubnetStore, step_back: u64| {
        run_initiator(
            &conn,
            endpoint.node_id(),
            subnet_name,
            code,
            trust,
            subnets,
            now_secs(),
            step_back,
        )
    };
    let attempt = tokio::time::timeout(CEREMONY_TIMEOUT, ceremony(&mut trust, &mut subnets, 0))
        .await
        .map_err(|_| JoinFail::Other(format!("ceremony exceeded {CEREMONY_TIMEOUT_SECS}s")))?;
    let outcome = match attempt {
        Ok(o) => o,
        // The LATE-CODE retry (field report 2026-06-06): the joiner holds no
        // seed, so a code read just before a rotation and entered just after
        // binds to the wrong step and reads as "wrong code" — before
        // bouncing that back at the user, retry the SAME code announced one
        // step back (the responder's ±1 window exists for exactly this
        // tolerance). One retry, after the limiter's first-failure backoff
        // (1s) has lapsed; any second failure surfaces as the original
        // wrong-code re-prompt.
        Err(PairWireError::Pairing(first)) => {
            tokio::time::sleep(Duration::from_millis(1_200)).await;
            match tokio::time::timeout(CEREMONY_TIMEOUT, ceremony(&mut trust, &mut subnets, 1))
                .await
                .map_err(|_| JoinFail::Other(format!("ceremony exceeded {CEREMONY_TIMEOUT_SECS}s")))?
            {
                Ok(o) => {
                    eprintln!("(code had just rotated — accepted against the previous step)");
                    o
                }
                // Report the FIRST failure's class: the retry was a guess on
                // the user's behalf, not a second user attempt.
                Err(_) => return Err(classify_wire_error(PairWireError::Pairing(first))),
            }
        }
        Err(e) => return Err(classify_wire_error(e)),
    };
    trust.save().map_err(|e| JoinFail::Other(format!("trust save: {e}")))?;
    subnets.save().map_err(|e| JoinFail::Other(format!("seed save: {e}")))?;
    Ok(format!("{outcome:?}"))
}


The file C:\Users\decid\Documents\projects\spt-core\crates\spt-daemon\src\pairhost.rs has been updated successfully. (file state is current in your context — no need to Read it back)

    let mut trust = TrustStore::load();
    let mut subnets = SubnetStore::load();
    let outcome = tokio::time::timeout(
        CEREMONY_TIMEOUT,
        run_initiator(
            &conn,
            endpoint.node_id(),
            subnet_name,
            code,
            &mut trust,
            &mut subnets,
            now_secs(),
        ),
    )
    .await
    .map_err(|_| JoinFail::Other(format!("ceremony exceeded {CEREMONY_TIMEOUT_SECS}s")))?
    .map_err(classify_wire_error)?;
    trust.save().map_err(|e| JoinFail::Other(format!("trust save: {e}")))?;
    subnets.save().map_err(|e| JoinFail::Other(format!("seed save: {e}")))?;
    Ok(format!("{outcome:?}"))
}

    let mut trust = TrustStore::load();
    let mut subnets = SubnetStore::load();
    let ceremony = |trust: &mut TrustStore, subnets: &mut SubnetStore, step_back: u64| {
        run_initiator(
            &conn,
            endpoint.node_id(),
            subnet_name,
            code,
            trust,
            subnets,
            now_secs(),
            step_back,
        )
    };
    let attempt = tokio::time::timeout(CEREMONY_TIMEOUT, ceremony(&mut trust, &mut subnets, 0))
        .await
        .map_err(|_| JoinFail::Other(format!("ceremony exceeded {CEREMONY_TIMEOUT_SECS}s")))?;
    let outcome = match attempt {
        Ok(o) => o,
        // The LATE-CODE retry (field report 2026-06-06): the joiner holds no
        // seed, so a code read just before a rotation and entered just after
        // binds to the wrong step and reads as "wrong code" — before
        // bouncing that back at the user, retry the SAME code announced one
        // step back (the responder's ±1 window exists for exactly this
        // tolerance). One retry, after the limiter's first-failure backoff
        // (1s) has lapsed; any second failure surfaces as the original
        // wrong-code re-prompt.
        Err(PairWireError::Pairing(first)) => {
            tokio::time::sleep(Duration::from_millis(1_200)).await;
            match tokio::time::timeout(CEREMONY_TIMEOUT, ceremony(&mut trust, &mut subnets, 1))
                .await
                .map_err(|_| JoinFail::Other(format!("ceremony exceeded {CEREMONY_TIMEOUT_SECS}s")))?
            {
                Ok(o) => {
                    eprintln!("(code had just rotated — accepted against the previous step)");
                    o
                }
                // Report the FIRST failure's class: the retry was a guess on
                // the user's behalf, not a second user attempt.
                Err(_) => return Err(classify_wire_error(PairWireError::Pairing(first))),
            }
        }
        Err(e) => return Err(classify_wire_error(e)),
    };
    trust.save().map_err(|e| JoinFail::Other(format!("trust save: {e}")))?;
    subnets.save().map_err(|e| JoinFail::Other(format!("seed save: {e}")))?;
    Ok(format!("{outcome:?}"))
}

-    let outcome = tokio::time::timeout(

+    let ceremony = |trust: &mut TrustStore, subnets: &mut SubnetStore, step_back: u64| {

-    .map_err(|_| JoinFail::Other(format!("ceremony exceeded {CEREMONY_TIMEOUT_SECS}s")))?

+    let attempt = tokio::time::timeout(CEREMONY_TIMEOUT, ceremony(&mut trust, &mut subnets, 0))

+        .map_err(|_| JoinFail::Other(format!("ceremony exceeded {CEREMONY_TIMEOUT_SECS}s")))?;

+        // The LATE-CODE retry (field report 2026-06-06): the joiner holds no

+        // seed, so a code read just before a rotation and entered just after

+        // binds to the wrong step and reads as "wrong code" — before

+        // bouncing that back at the user, retry the SAME code announced one

+        // step back (the responder's ±1 window exists for exactly this

+        // tolerance). One retry, after the limiter's first-failure backoff

+        // (1s) has lapsed; any second failure surfaces as the original

+        Err(PairWireError::Pairing(first)) => {

+            tokio::time::sleep(Duration::from_millis(1_200)).await;

+            match tokio::time::timeout(CEREMONY_TIMEOUT, ceremony(&mut trust, &mut subnets, 1))

+                    eprintln!("(code had just rotated — accepted against the previous step)");

+                // Report the FIRST failure's class: the retry was a guess on

+                // the user's behalf, not a second user attempt.

+                Err(_) => return Err(classify_wire_error(PairWireError::Pairing(first))),

+        Err(e) => return Err(classify_wire_error(e)),


55	///
56	/// Concurrency: ONE ceremony at a time per daemon, enforced at the door —
57	/// a second connection finds the limiter lock held and is closed immediately
58	/// (the joiner sees the stream end and retries; same UX as the limiter's
59	/// `Busy` abort, shed before any frame is read).
60	// [impl->REQ-SUBNET-2]
61	pub async fn respond(
62	    conn: Connection,
63	    local_pub: PublicKey,
64	    rate: Arc<tokio::sync::Mutex<PairingRateLimiter>>,
65	) {
66	    let Ok(mut rate) = rate.try_lock() else {
67	        conn.close(1u32.into(), b"pairing busy");
68	        return;
69	    };
70	    let subnets = SubnetStore::load();
71	    let mut trust = TrustStore::load();
72	    match tokio::time::timeout(
73	        CEREMONY_TIMEOUT,
74	        run_responder(&conn, local_pub, &subnets, &mut rate, &mut trust, now_secs()),
75	    )
76	    .await
77	    {
78	        Ok(Ok(outcome)) => match trust.save() {
79	            Ok(()) => eprintln!("PAIRED: joiner pinned ({outcome:?})"),
80	            Err(e) => eprintln!("PAIRING_TRUST_SAVE_FAIL: {e}"),
81	        },
82	        // A failed ceremony is normal traffic (wrong code, impostor probe,
83	        // vanished peer) — logged, never fatal; the limiter charged it.
84	        Ok(Err(e)) => eprintln!("PAIRING_ATTEMPT_FAIL: {e}"),
85	        Err(_) => eprintln!("PAIRING_TIMEOUT: ceremony exceeded {CEREMONY_TIMEOUT_SECS}s"),
86	    }
87	    // Release the ceremony slot before the close-grace wait — the next
88	    // joiner must not be shed at the door for the lame-duck duration of
89	    // this connection's teardown.
90	    drop(rate);
91	    // Let the JOINER close first (the `meet::serve_once` discipline, bounded):
92	    // `SendStream::finish()` only queues the final Done/Seed (or abort)
93	    // frames — dropping the connection here sends an immediate QUIC CLOSE
94	    // that discards unacked stream data, and the joiner loses the race and
95	    // reads "connection lost" instead of its seed (the exact failure the
96	    // first twohost run of the product-surface rung hit). The joiner drops
97	    // its side once it has read the final frame; only then (or after the
98	    // grace bound, for a vanished peer) does this side release the conn.
99	    let _ = tokio::time::timeout(CLOSE_GRACE, conn.closed()).await;
100	}
101
102	/// The meet-rotation loop: for every member subnet, keep a rendezvous meet
103	/// listener bound under the (name, current-step)-derived identity, answering
104	/// joiner probes with `real_addr` — the responder's actual pairing-endpoint
105	/// address. Re-binds at each TOTP step boundary (window rotation = re-bind,
106	/// `pairing::meet` docs); re-reads the subnet store per window so a freshly
107	/// created/joined subnet starts meeting within one step.
108	///
109	/// Bind failures are soft (logged, that subnet skipped this window): the
110	/// daemon must keep running net-less-for-pairing rather than die. A
111	/// [`BindScope::Port`] scope is mapped to [`BindScope::All`] — the fixed
112	/// port belongs to the main endpoint; a second bind on it would always fail.
113	// [impl->REQ-SUBNET-2]
114	pub fn spawn_meet_rotation(

///
/// Concurrency: ONE ceremony at a time per daemon, enforced at the door —
/// a second connection finds the limiter lock held and is closed immediately
/// (the joiner sees the stream end and retries; same UX as the limiter's
/// `Busy` abort, shed before any frame is read).
// [impl->REQ-SUBNET-2]
pub async fn respond(
    conn: Connection,
    local_pub: PublicKey,
    rate: Arc<tokio::sync::Mutex<PairingRateLimiter>>,
) {
    let Ok(mut rate) = rate.try_lock() else {
        conn.close(1u32.into(), b"pairing busy");
        return;
    };
    let subnets = SubnetStore::load();
    let mut trust = TrustStore::load();
    match tokio::time::timeout(
        CEREMONY_TIMEOUT,
        run_responder(&conn, local_pub, &subnets, &mut rate, &mut trust, now_secs()),
    )
    .await
    {
        Ok(Ok(outcome)) => match trust.save() {
            Ok(()) => eprintln!("PAIRED: joiner pinned ({outcome:?})"),
            Err(e) => eprintln!("PAIRING_TRUST_SAVE_FAIL: {e}"),
        },
        // A failed ceremony is normal traffic (wrong code, impostor probe,
        // vanished peer) — logged, never fatal; the limiter charged it.
        Ok(Err(e)) => eprintln!("PAIRING_ATTEMPT_FAIL: {e}"),
        Err(_) => eprintln!("PAIRING_TIMEOUT: ceremony exceeded {CEREMONY_TIMEOUT_SECS}s"),
    }
    // Release the ceremony slot before the close-grace wait — the next
    // joiner must not be shed at the door for the lame-duck duration of
    // this connection's teardown.
    drop(rate);
    // Let the JOINER close first (the `meet::serve_once` discipline, bounded):
    // `SendStream::finish()` only queues the final Done/Seed (or abort)
    // frames — dropping the connection here sends an immediate QUIC CLOSE
    // that discards unacked stream data, and the joiner loses the race and
    // reads "connection lost" instead of its seed (the exact failure the
    // first twohost run of the product-surface rung hit). The joiner drops
    // its side once it has read the final frame; only then (or after the
    // grace bound, for a vanished peer) does this side release the conn.
    let _ = tokio::time::timeout(CLOSE_GRACE, conn.closed()).await;
}

/// The meet-rotation loop: for every member subnet, keep a rendezvous meet
/// listener bound under the (name, current-step)-derived identity, answering
/// joiner probes with `real_addr` — the responder's actual pairing-endpoint
/// address. Re-binds at each TOTP step boundary (window rotation = re-bind,
/// `pairing::meet` docs); re-reads the subnet store per window so a freshly
/// created/joined subnet starts meeting within one step.
///
/// Bind failures are soft (logged, that subnet skipped this window): the
/// daemon must keep running net-less-for-pairing rather than die. A
/// [`BindScope::Port`] scope is mapped to [`BindScope::All`] — the fixed
/// port belongs to the main endpoint; a second bind on it would always fail.
// [impl->REQ-SUBNET-2]
pub fn spawn_meet_rotation(