File created successfully at: C:\Users\decid\Documents\projects\spt-core\MESH-D2-PLAN.md (file state is current in your context — no need to Read it back)

# Mesh-D2 — seed-proof at connect + ConnEntry membership set

> JIT plan for `SUBNET-MESH-PLAN.md` §Build phases · phase 2. **Wires the D1 codec onto live connections.** Run the mutual channel-bound seed-proof on every dial + accept in `nethost` *before* the connection is usable; cache the proven membership on `ConnEntry`; fail → drop. Verify/enable QUIC keep-alive so conns stay warm. **Old `is_trusted` gates keep running in parallel** (belt + suspenders — no regression; the gate swap is D5). **Enriches REQ-MESH-1 (impl)**; activates REQ-MESH-1 **int**.

## The one design decision (and why it's forced, not chosen)

D1 imagined one `subnet_id` per connection. The codebase says otherwise:

- `dial(addr)` (`broker.rs:593/605`) takes only an address — **connections are not subnet-scoped**. iroh dedups to one QUIC conn per peer-pair on the shared `SPT_NET_ALPN`, so a single conn carries traffic for **every subnet the two nodes share**.
- The five inbound gates D5 will swap are all **per-`(subnet, origin)`, evaluated per item** (`registryhost.rs:81/228/291`, `notifsync.rs:60/106`, `sync.rs:102/107`) — each registry row / notif / sync item carries its own `subnet`, and the gate asks `is_trusted(item.subnet, origin)`.

Therefore the cached verdict D5 consumes must answer *"has this peer proven subnet S?"* for **any** S an item names → **a set of proven subnets per `ConnEntry`, not a bool.** Single-subnet-per-conn would require threading a subnet through `dial`/accept (more plumbing) and still couldn't answer the multi-subnet gate. The set is both more correct and less invasive.

**Decision:** at connect, the two nodes prove the **full set of subnets they share**; `ConnEntry` caches `proven_subnets: HashSet<String>` (current-epoch). D5's gate becomes `conn.proven_subnets.contains(item.subnet)`.

## The connect-time exchange (one dedicated control stream, before the conn is usable)

The seed-proof rides the **first bidi stream**, opened by the dialer and consumed *before* `register_conn` wires the general `accept_bi` app pump — so it is never confused with an app stream.

```
dial:    connect → open_bi(ctrl) → run_seedproof(Dialer)   → ok? register_conn(conn, proven) : drop
accept:  connecting.await → conn → accept_bi(ctrl)         → ok? register_conn(conn, proven) : drop
         (pairing-ALPN branch is untouched — pre-trust, separate)
```

`run_seedproof` (new `spt-daemon` module `seedproofx.rs`, using `spt_net::net::mesh::seedproof` primitives — orchestration lives in the daemon; the crypto stays in `spt-net`):

1. **Dialer → `Hello { nonce_d, subnets: [names] }`** — its local subnet names + a fresh 32-byte OS-CSPRNG nonce.
2. **Acceptor → `Hello { nonce_a, subnets: shared }`** — replies with **only the intersection** of the dialer's list and its own (minimizes name disclosure to an unauthenticated dialer; see Security notes). `shared` is sorted (deterministic order both sides agree on).
3. Empty `shared` ⇒ both **drop** (a legit member always shares ≥1 subnet; no shared subnet = nothing to mesh).
4. For each `S` in `shared`: each side derives `MK_S = MembershipKey::derive(seed_S, S, epoch_S)` **from its own local subnet record** (each uses *its own* `epoch_S` — an epoch skew makes the tags diverge → exact-epoch reject, which is correct), builds the shared `SeedProofTranscript { S, epoch_S, dialer_pub, acceptor_pub, nonce_d, nonce_a }`, sends `Proof { tag_S(own_role) }`, and `verify`s the peer's `Proof` for the opposite role.
5. `proven = { S | peer's tag verified }`. **Any agreed-shared subnet that fails to prove ⇒ drop the whole connection** (an impostor naming a subnet it can't prove, or an epoch mismatch — neither is a usable member conn). On full success `proven == shared`; cache it.

Nonces are exchanged **once per connection** and reused across the per-subnet proofs (the transcript already binds `subnet_id` + `seed_epoch`, so per-subnet tags differ; the nonces only need per-connection freshness). Pubkeys come from the QUIC handshake (`conn.remote_id()` + local identity) — the channel binding.

### Wire: extend the D1 frame set (additive)

`SeedProofFrame` (D1: `Challenge{nonce}` / `Proof{tag}`) gains:

```rust
Hello { nonce: [u8; NONCE_LEN], subnets: Vec<String> },   // tag byte 2; len-prefixed count + len-prefixed names
```

`Challenge` is effectively subsumed by `Hello` (nonce + subnet list in one frame) — keep `Challenge` defined (D1 tests pin it) but the D2 driver uses `Hello`/`Proof`. `decode` stays total (unknown tag / bad length / bad UTF-8 ⇒ `None`). Length-prefix the name vector (u32 count, then per name u32 len + bytes) — same framing discipline as `pairing::wire`.

Subnet **names** go on the wire (not seeds, not epochs). Names are human labels, not secrets; the seed never leaves the node (ADR-0017 invariant). See Security notes.

## ConnEntry + register_conn changes (`nethost.rs`)

```rust
struct ConnEntry {
    conn: Connection,
    remote_id_hex: String,
    proven_subnets: HashSet<String>,   // NEW — current-epoch membership proven at connect
}
```

- `register_conn(shared, conn)` → `register_conn(shared, conn, proven_subnets)` — stores the set; everything else (presence append, closed-watcher, app `accept_bi` loop) unchanged. The app `accept_bi` loop now starts on the conn **after** the ctrl stream is consumed, so stream #1 (ctrl) is never registered as an app stream.
- New accessor `conn_proven_subnets(conn_id) -> HashSet<String>` (the seam D5 reads; unused by any gate at D2 — belt + suspenders).
- `dial` and the accept-loop trusted branch each run `run_seedproof` and drop on failure (don't register). Drop = simply return without `register_conn` (dialer) / let the conn fall out of scope (acceptor) → QUIC closes it.

### Where the subnet material comes from

`NetHost` has no `SubnetStore` access today. For D2, `run_seedproof` reads `SubnetStore::load()` at connect time (always-current across a future rotation; connects are rare, the load is cheap). No new long-lived handle, no snapshot-staleness. (D7's confidential re-seed push may want a live handle; out of scope here.) Pass the loaded `Vec<(name, seed_bytes, epoch)>` into the exchange. `nethost` already sits in `spt-daemon` and may depend on `spt-store`.

## Keep-alive (open item #1 — verify, then set)

Connections must stay warm so re-proof is **restart/partition/rotation-only**, never a per-message tax. `endpoint.rs` binds via `Endpoint::builder(presets::N0)` with **no explicit transport config**, so the conn lives under iroh/quinn defaults.

**Execution step (verify-then-set, don't guess):**
1. Determine iroh 0.98.2 `presets::N0` defaults for `max_idle_timeout` and `keep_alive_interval` (quinn's `keep_alive_interval` default is `None` = no keep-alive → conns idle out). Confirm the builder exposes a transport-config seam.
2. Set an explicit transport config with **`keep_alive_interval` < `max_idle_timeout`** (e.g. keep-alive ≈ idle/3) so an otherwise-idle member conn is held open by PING frames. Pick conservative values (idle on the order of tens of seconds; keep-alive a few seconds under it) — "multi-week" means *survives indefinite idleness while both ends are up*, not a single conn object literally pinned for weeks; a real restart/partition drops it and the next connect re-proves.
3. Apply on the production scope; the change is transport-only and orthogonal to `BindScope` (loopback hermetic tests inherit it harmlessly).

Tag the transport-config change `// [impl->REQ-MESH-1]` (keep-warm is part of the REQ-MESH-1 "kept warm via QUIC keep-alive" clause).

## Security notes (the invariants this phase must not break)

- **Channel-bound + mutual** — inherited from the D1 transcript (both handshake pubkeys + both nonces in the MAC; both sides prove). D2 must feed the *real* handshake pubkeys (`conn.remote_id()`, local identity) and *fresh* per-connect nonces — not fixed test arrays.
- **Drop-closed, not gate-open** — a failed/empty proof drops the connection; it does **not** fall through to an unauthenticated usable conn. Until D5 the `is_trusted` gates also still run, so even a (hypothetical) proven non-member conn carries no authority.
- **No-regression** — a legit directly-paired peer shares the subnet seed at the same epoch (epoch only bumps at D7's rotation; pre-D7 it's stable), so it always proves → never dropped. Note the one edge: a pre-existing epoch skew would now drop a conn `is_trusted` would have allowed; impossible before D7, acceptable to surface then.
- **Name disclosure** — the acceptor replies with the **intersection only**, so an unauthenticated dialer learns only which of the names *it already supplied* are shared (a weak name-enumeration oracle, not a seed/membership oracle). Acceptable under the same-user threat model (ADR-0017); the seed-proof, not name secrecy, is the boundary.

## Tests

**unit (`spt-net`, `mesh::seedproof`)** — extend D1's codec tests for the new frame, `// [unit->REQ-MESH-1]`:
- `Hello` round-trips (nonce + multi-name list incl. empty list and unicode name); malformed (bad count, short name, non-UTF-8, unknown tag) ⇒ `None`.

**int (`spt-daemon`, `nethost` tests)** — the connect-level integration, `// [int->REQ-MESH-1]`:
1. **both members → conn + proof** — two in-process `NetHost`s sharing a subnet (same seed+epoch) dial; assert the conn registers on both sides AND `conn_proven_subnets` contains the shared subnet on both.
2. **multi-subnet set** — two hosts sharing {`home`,`work`} but not `solo`; assert `proven_subnets == {home, work}` on both (the set, not a bool; `solo` absent).
3. **non-prover dropped** — a host with the wrong seed (or no shared subnet) dials a member; assert the connection does **not** register (dropped) on the acceptor and the dialer sees a dropped/closed conn. (Models "a non-prover is dropped.")
4. **keep-alive** *(light)* — assert the transport config carries a non-`None` `keep_alive_interval` strictly less than `max_idle_timeout` (config-level assertion; a real multi-week idle isn't testable in CI). If iroh hides these post-build, assert at the config-construction seam instead.

Hermetic throughout (`BindScope::Loopback`, `RelayPolicy::Disabled`, `LocalDiscovery::Off`) — same shape as the existing `two_hosts_dial_over_loopback`.

## Done when

- Seed-proof runs on every trusted-ALPN dial + accept before the conn is usable; `ConnEntry.proven_subnets` populated; failure/empty drops the conn.
- `cargo test -p spt-net mesh::seedproof` + `cargo test -p spt-daemon nethost` green; the four int cases pass.
- `cargo build -p spt-daemon` + `-p spt-net` (and `spt-net --no-default-features`) clean; mesh module stays `net`-gated.
- `keep_alive_interval` < `max_idle_timeout` confirmed set on the production endpoint.
- The existing `two_hosts_dial_over_loopback` still passes (now with a shared subnet in the hermetic config so the proof succeeds) — **no-regression** proven.
- `traceable-reqs check` clean with **REQ-MESH-1 `required_stages = ["impl","unit","int"]`** (D2 activates `int`; enriches `impl`). Evidence tags on the connect wiring, the `ConnEntry` set, the transport config, and the int tests.
- The `is_trusted` gates are **untouched** (D5 owns the swap) — grep confirms the five call sites unchanged.
- Commit: `feat(mesh): seed-proof at connect + ConnEntry membership set (REQ-MESH-1) — Mesh-D2`.

## Carry-forward (not D2)

- **Pre-D5 open item #3** — confirm the self-update path is node-local (ADR-0016), not riding member conns, before the cutover roll. Not D2.
- **Open item #2** (REQ-SUBNET-5 probe loop serial? → D8).
- D3 consumes nothing from D2's wire beyond the established conns; the roster (`Frame::Seed` growth) is independent.


Plan written. Now execute — start with the lowest-risk piece: extend the D1 codec with the `Hello` frame + unit test.


Edit

/// The two wire frames of the mutual exchange. Each side opens with its
/// [`Challenge`](SeedProofFrame::Challenge) (its nonce), then — once both nonces
/// and the two handshake pubkeys fix the transcript — sends its
/// [`Proof`](SeedProofFrame::Proof). Defined here (Mesh-D1); *driven* onto a
/// connection at Mesh-D2.
///
/// Encoding: a 1-byte tag (`0` = Challenge, `1` = Proof) followed by the
/// fixed-width payload. Decoding is total and rejects any malformed buffer with
/// `None` (additive/robust, like the pairing wire frames).
#[derive(Debug, Clone, PartialEq, Eq)]
pub enum SeedProofFrame {
    /// A party's per-connection nonce, sent first.
    Challenge { nonce: [u8; NONCE_LEN] },
    /// A party's seed-proof tag over the agreed transcript.
    Proof { tag: [u8; PROOF_TAG_LEN] },
}

const FRAME_CHALLENGE: u8 = 0;
const FRAME_PROOF: u8 = 1;

impl SeedProofFrame {
    /// Serialize to the wire form.
    pub fn encode(&self) -> Vec<u8> {
        match self {
            SeedProofFrame::Challenge { nonce } => {
                let mut out = Vec::with_capacity(1 + NONCE_LEN);
                out.push(FRAME_CHALLENGE);
                out.extend_from_slice(nonce);
                out
            }
            SeedProofFrame::Proof { tag } => {
                let mut out = Vec::with_capacity(1 + PROOF_TAG_LEN);
                out.push(FRAME_PROOF);
                out.extend_from_slice(tag);
                out
            }
        }
    }

    /// Parse from the wire form. `None` on an unknown tag byte or a
    /// wrong-length payload — never panics, never partial-reads.
    pub fn decode(bytes: &[u8]) -> Option<SeedProofFrame> {
        let (&tag, rest) = bytes.split_first()?;
        match tag {
            FRAME_CHALLENGE if rest.len() == NONCE_LEN => {
                let mut nonce = [0u8; NONCE_LEN];
                nonce.copy_from_slice(rest);
                Some(SeedProofFrame::Challenge { nonce })
            }
            FRAME_PROOF if rest.len() == PROOF_TAG_LEN => {
                let mut t = [0u8; PROOF_TAG_LEN];
                t.copy_from_slice(rest);
                Some(SeedProofFrame::Proof { tag: t })
            }
            _ => None,
        }
    }
}

/// The wire frames of the mutual exchange.
///
/// Mesh-D2 drives the exchange with [`Hello`](SeedProofFrame::Hello) (a party's
/// nonce **plus** the subnet names it offers — the acceptor replies with the
/// intersection only) followed by one [`Proof`](SeedProofFrame::Proof) per shared
/// subnet. [`Challenge`](SeedProofFrame::Challenge) is the D1 nonce-only opener,
/// retained for the codec's stability tests; `Hello` subsumes it on the wire.
///
/// Encoding: a 1-byte tag followed by the payload. `Challenge` (`0`) and `Proof`
/// (`1`) are fixed-width; `Hello` (`2`) is `nonce ‖ u32 count ‖ {u32 len ‖ utf8}*`
/// — every variable field length-prefixed (the `pairing::wire` discipline).
/// Decoding is total: any malformed buffer (unknown tag, wrong length, bad
/// count, non-UTF-8 name, trailing/short bytes) decodes to `None`.
#[derive(Debug, Clone, PartialEq, Eq)]
pub enum SeedProofFrame {
    /// A party's per-connection nonce, sent first. (D1 opener; `Hello` subsumes it.)
    Challenge { nonce: [u8; NONCE_LEN] },
    /// A party's seed-proof tag over the agreed transcript.
    Proof { tag: [u8; PROOF_TAG_LEN] },
    /// A party's per-connection nonce together with the subnet names it offers.
    /// The dialer sends its full local list; the acceptor replies with only the
    /// intersection (minimizing name disclosure). The proven set is established
    /// by the per-subnet [`Proof`](SeedProofFrame::Proof)s that follow.
    Hello {
        /// This party's fresh per-connection nonce.
        nonce: [u8; NONCE_LEN],
        /// Offered subnet names (dialer: local set; acceptor: intersection).
        subnets: Vec<String>,
    },
}

const FRAME_CHALLENGE: u8 = 0;
const FRAME_PROOF: u8 = 1;
const FRAME_HELLO: u8 = 2;

impl SeedProofFrame {
    /// Serialize to the wire form.
    pub fn encode(&self) -> Vec<u8> {
        match self {
            SeedProofFrame::Challenge { nonce } => {
                let mut out = Vec::with_capacity(1 + NONCE_LEN);
                out.push(FRAME_CHALLENGE);
                out.extend_from_slice(nonce);
                out
            }
            SeedProofFrame::Proof { tag } => {
                let mut out = Vec::with_capacity(1 + PROOF_TAG_LEN);
                out.push(FRAME_PROOF);
                out.extend_from_slice(tag);
                out
            }
            SeedProofFrame::Hello { nonce, subnets } => {
                let mut out = Vec::with_capacity(1 + NONCE_LEN + 4 + subnets.len() * 16);
                out.push(FRAME_HELLO);
                out.extend_from_slice(nonce);
                out.extend_from_slice(&(subnets.len() as u32).to_be_bytes());
                for name in subnets {
                    out.extend_from_slice(&(name.len() as u32).to_be_bytes());
                    out.extend_from_slice(name.as_bytes());
                }
                out
            }
        }
    }

    /// Parse from the wire form. `None` on an unknown tag byte, a wrong-length
    /// payload, a name-count/length that overruns the buffer, or a non-UTF-8
    /// name — never panics, never partial-reads.
    pub fn decode(bytes: &[u8]) -> Option<SeedProofFrame> {
        let (&tag, rest) = bytes.split_first()?;
        match tag {
            FRAME_CHALLENGE if rest.len() == NONCE_LEN => {
                let mut nonce = [0u8; NONCE_LEN];
                nonce.copy_from_slice(rest);
                Some(SeedProofFrame::Challenge { nonce })
            }
            FRAME_PROOF if rest.len() == PROOF_TAG_LEN => {
                let mut t = [0u8; PROOF_TAG_LEN];
                t.copy_from_slice(rest);
                Some(SeedProofFrame::Proof { tag: t })
            }
            FRAME_HELLO => Self::decode_hello(rest),
            _ => None,
        }
    }

    /// Decode the `Hello` payload (everything after the tag byte). Total: any
    /// truncation or non-UTF-8 name yields `None`.
    fn decode_hello(rest: &[u8]) -> Option<SeedProofFrame> {
        if rest.len() < NONCE_LEN + 4 {
            return None;
        }
        let mut nonce = [0u8; NONCE_LEN];
        nonce.copy_from_slice(&rest[..NONCE_LEN]);
        let mut pos = NONCE_LEN;
        let count = u32::from_be_bytes(rest[pos..pos + 4].try_into().ok()?) as usize;
        pos += 4;
        let mut subnets = Vec::with_capacity(count.min(64));
        for _ in 0..count {
            if pos + 4 > rest.len() {
                return None;
            }
            let len = u32::from_be_bytes(rest[pos..pos + 4].try_into().ok()?) as usize;
            pos += 4;
            if pos + len > rest.len() {
                return None;
            }
            let name = std::str::from_utf8(&rest[pos..pos + len]).ok()?;
            subnets.push(name.to_string());
            pos += len;
        }
        // No trailing bytes beyond the declared names.
        if pos != rest.len() {
            return None;
        }
        Some(SeedProofFrame::Hello { nonce, subnets })
    }
}


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

/// The two wire frames of the mutual exchange. Each side opens with its
/// [`Challenge`](SeedProofFrame::Challenge) (its nonce), then — once both nonces
/// and the two handshake pubkeys fix the transcript — sends its
/// [`Proof`](SeedProofFrame::Proof). Defined here (Mesh-D1); *driven* onto a
/// connection at Mesh-D2.
///
/// Encoding: a 1-byte tag (`0` = Challenge, `1` = Proof) followed by the
/// fixed-width payload. Decoding is total and rejects any malformed buffer with
/// `None` (additive/robust, like the pairing wire frames).
#[derive(Debug, Clone, PartialEq, Eq)]
pub enum SeedProofFrame {
    /// A party's per-connection nonce, sent first.
    Challenge { nonce: [u8; NONCE_LEN] },
    /// A party's seed-proof tag over the agreed transcript.
    Proof { tag: [u8; PROOF_TAG_LEN] },
}

const FRAME_CHALLENGE: u8 = 0;
const FRAME_PROOF: u8 = 1;

impl SeedProofFrame {
    /// Serialize to the wire form.
    pub fn encode(&self) -> Vec<u8> {
        match self {
            SeedProofFrame::Challenge { nonce } => {
                let mut out = Vec::with_capacity(1 + NONCE_LEN);
                out.push(FRAME_CHALLENGE);
                out.extend_from_slice(nonce);
                out
            }
            SeedProofFrame::Proof { tag } => {
                let mut out = Vec::with_capacity(1 + PROOF_TAG_LEN);
                out.push(FRAME_PROOF);
                out.extend_from_slice(tag);
                out
            }
        }
    }

    /// Parse from the wire form. `None` on an unknown tag byte or a
    /// wrong-length payload — never panics, never partial-reads.
    pub fn decode(bytes: &[u8]) -> Option<SeedProofFrame> {
        let (&tag, rest) = bytes.split_first()?;
        match tag {
            FRAME_CHALLENGE if rest.len() == NONCE_LEN => {
                let mut nonce = [0u8; NONCE_LEN];
                nonce.copy_from_slice(rest);
                Some(SeedProofFrame::Challenge { nonce })
            }
            FRAME_PROOF if rest.len() == PROOF_TAG_LEN => {
                let mut t = [0u8; PROOF_TAG_LEN];
                t.copy_from_slice(rest);
                Some(SeedProofFrame::Proof { tag: t })
            }
            _ => None,
        }
    }
}

/// The wire frames of the mutual exchange.
///
/// Mesh-D2 drives the exchange with [`Hello`](SeedProofFrame::Hello) (a party's
/// nonce **plus** the subnet names it offers — the acceptor replies with the
/// intersection only) followed by one [`Proof`](SeedProofFrame::Proof) per shared
/// subnet. [`Challenge`](SeedProofFrame::Challenge) is the D1 nonce-only opener,
/// retained for the codec's stability tests; `Hello` subsumes it on the wire.
///
/// Encoding: a 1-byte tag followed by the payload. `Challenge` (`0`) and `Proof`
/// (`1`) are fixed-width; `Hello` (`2`) is `nonce ‖ u32 count ‖ {u32 len ‖ utf8}*`
/// — every variable field length-prefixed (the `pairing::wire` discipline).
/// Decoding is total: any malformed buffer (unknown tag, wrong length, bad
/// count, non-UTF-8 name, trailing/short bytes) decodes to `None`.
#[derive(Debug, Clone, PartialEq, Eq)]
pub enum SeedProofFrame {
    /// A party's per-connection nonce, sent first. (D1 opener; `Hello` subsumes it.)
    Challenge { nonce: [u8; NONCE_LEN] },
    /// A party's seed-proof tag over the agreed transcript.
    Proof { tag: [u8; PROOF_TAG_LEN] },
    /// A party's per-connection nonce together with the subnet names it offers.
    /// The dialer sends its full local list; the acceptor replies with only the
    /// intersection (minimizing name disclosure). The proven set is established
    /// by the per-subnet [`Proof`](SeedProofFrame::Proof)s that follow.
    Hello {
        /// This party's fresh per-connection nonce.
        nonce: [u8; NONCE_LEN],
        /// Offered subnet names (dialer: local set; acceptor: intersection).
        subnets: Vec<String>,
    },
}

const FRAME_CHALLENGE: u8 = 0;
const FRAME_PROOF: u8 = 1;
const FRAME_HELLO: u8 = 2;

impl SeedProofFrame {
    /// Serialize to the wire form.
    pub fn encode(&self) -> Vec<u8> {
        match self {
            SeedProofFrame::Challenge { nonce } => {
                let mut out = Vec::with_capacity(1 + NONCE_LEN);
                out.push(FRAME_CHALLENGE);
                out.extend_from_slice(nonce);
                out
            }
            SeedProofFrame::Proof { tag } => {
                let mut out = Vec::with_capacity(1 + PROOF_TAG_LEN);
                out.push(FRAME_PROOF);
                out.extend_from_slice(tag);
                out
            }
            SeedProofFrame::Hello { nonce, subnets } => {
                let mut out = Vec::with_capacity(1 + NONCE_LEN + 4 + subnets.len() * 16);
                out.push(FRAME_HELLO);
                out.extend_from_slice(nonce);
                out.extend_from_slice(&(subnets.len() as u32).to_be_bytes());
                for name in subnets {
                    out.extend_from_slice(&(name.len() as u32).to_be_bytes());
                    out.extend_from_slice(name.as_bytes());
                }
                out
            }
        }
    }

    /// Parse from the wire form. `None` on an unknown tag byte, a wrong-length
    /// payload, a name-count/length that overruns the buffer, or a non-UTF-8
    /// name — never panics, never partial-reads.
    pub fn decode(bytes: &[u8]) -> Option<SeedProofFrame> {
        let (&tag, rest) = bytes.split_first()?;
        match tag {
            FRAME_CHALLENGE if rest.len() == NONCE_LEN => {
                let mut nonce = [0u8; NONCE_LEN];
                nonce.copy_from_slice(rest);
                Some(SeedProofFrame::Challenge { nonce })
            }
            FRAME_PROOF if rest.len() == PROOF_TAG_LEN => {
                let mut t = [0u8; PROOF_TAG_LEN];
                t.copy_from_slice(rest);
                Some(SeedProofFrame::Proof { tag: t })
            }
            FRAME_HELLO => Self::decode_hello(rest),
            _ => None,
        }
    }

    /// Decode the `Hello` payload (everything after the tag byte). Total: any
    /// truncation or non-UTF-8 name yields `None`.
    fn decode_hello(rest: &[u8]) -> Option<SeedProofFrame> {
        if rest.len() < NONCE_LEN + 4 {
            return None;
        }
        let mut nonce = [0u8; NONCE_LEN];
        nonce.copy_from_slice(&rest[..NONCE_LEN]);
        let mut pos = NONCE_LEN;
        let count = u32::from_be_bytes(rest[pos..pos + 4].try_into().ok()?) as usize;
        pos += 4;
        let mut subnets = Vec::with_capacity(count.min(64));
        for _ in 0..count {
            if pos + 4 > rest.len() {
                return None;
            }
            let len = u32::from_be_bytes(rest[pos..pos + 4].try_into().ok()?) as usize;
            pos += 4;
            if pos + len > rest.len() {
                return None;
            }
            let name = std::str::from_utf8(&rest[pos..pos + len]).ok()?;
            subnets.push(name.to_string());
            pos += len;
        }
        // No trailing bytes beyond the declared names.
        if pos != rest.len() {
            return None;
        }
        Some(SeedProofFrame::Hello { nonce, subnets })
    }
}

-/// The two wire frames of the mutual exchange. Each side opens with its

-/// [`Challenge`](SeedProofFrame::Challenge) (its nonce), then — once both nonces

-/// and the two handshake pubkeys fix the transcript — sends its

-/// [`Proof`](SeedProofFrame::Proof). Defined here (Mesh-D1); *driven* onto a

+/// The wire frames of the mutual exchange.

-/// Encoding: a 1-byte tag (`0` = Challenge, `1` = Proof) followed by the

-/// fixed-width payload. Decoding is total and rejects any malformed buffer with

-/// `None` (additive/robust, like the pairing wire frames).

+/// Mesh-D2 drives the exchange with [`Hello`](SeedProofFrame::Hello) (a party's

+/// nonce **plus** the subnet names it offers — the acceptor replies with the

+/// intersection only) followed by one [`Proof`](SeedProofFrame::Proof) per shared

+/// subnet. [`Challenge`](SeedProofFrame::Challenge) is the D1 nonce-only opener,

+/// retained for the codec's stability tests; `Hello` subsumes it on the wire.

+/// Encoding: a 1-byte tag followed by the payload. `Challenge` (`0`) and `Proof`

+/// (`1`) are fixed-width; `Hello` (`2`) is `nonce ‖ u32 count ‖ {u32 len ‖ utf8}*`

+/// — every variable field length-prefixed (the `pairing::wire` discipline).

+/// Decoding is total: any malformed buffer (unknown tag, wrong length, bad

+/// count, non-UTF-8 name, trailing/short bytes) decodes to `None`.

-    /// A party's per-connection nonce, sent first.

+    /// A party's per-connection nonce, sent first. (D1 opener; `Hello` subsumes it.)

+    /// A party's per-connection nonce together with the subnet names it offers.

+    /// The dialer sends its full local list; the acceptor replies with only the

+    /// intersection (minimizing name disclosure). The proven set is established

+    /// by the per-subnet [`Proof`](SeedProofFrame::Proof)s that follow.

+        /// This party's fresh per-connection nonce.

+        /// Offered subnet names (dialer: local set; acceptor: intersection).

+            SeedProofFrame::Hello { nonce, subnets } => {

+                let mut out = Vec::with_capacity(1 + NONCE_LEN + 4 + subnets.len() * 16);

+                out.extend_from_slice(nonce);

+                out.extend_from_slice(&(subnets.len() as u32).to_be_bytes());

+                    out.extend_from_slice(&(name.len() as u32).to_be_bytes());

+                    out.extend_from_slice(name.as_bytes());

-    /// Parse from the wire form. `None` on an unknown tag byte or a

-    /// wrong-length payload — never panics, never partial-reads.

+    /// Parse from the wire form. `None` on an unknown tag byte, a wrong-length

+    /// payload, a name-count/length that overruns the buffer, or a non-UTF-8

+    /// name — never panics, never partial-reads.

+            FRAME_HELLO => Self::decode_hello(rest),

+    /// Decode the `Hello` payload (everything after the tag byte). Total: any

+    /// truncation or non-UTF-8 name yields `None`.

+    fn decode_hello(rest: &[u8]) -> Option<SeedProofFrame> {

+        if rest.len() < NONCE_LEN + 4 {

+        let mut nonce = [0u8; NONCE_LEN];

+        nonce.copy_from_slice(&rest[..NONCE_LEN]);

+        let count = u32::from_be_bytes(rest[pos..pos + 4].try_into().ok()?) as usize;

+        let mut subnets = Vec::with_capacity(count.min(64));

+            let len = u32::from_be_bytes(rest[pos..pos + 4].try_into().ok()?) as usize;

+            if pos + len > rest.len() {

+            let name = std::str::from_utf8(&rest[pos..pos + len]).ok()?;

+            subnets.push(name.to_string());

+        // No trailing bytes beyond the declared names.

+        Some(SeedProofFrame::Hello { nonce, subnets })

// A pubkey-length cross-check so a future PublicKey size change trips the test


Now extend the codec test for the `Hello` frame:


        assert_eq!(SeedProofFrame::decode(&[]), None, "empty");
        assert_eq!(SeedProofFrame::decode(&[9u8; 33]), None, "unknown tag");
        assert_eq!(
            SeedProofFrame::decode(&[FRAME_CHALLENGE, 1, 2, 3]),
            None,
            "short challenge"
        );
        assert_eq!(
            SeedProofFrame::decode(&[FRAME_PROOF; 8]),
            None,
            "short proof"
        );
    }
}

        assert_eq!(SeedProofFrame::decode(&[]), None, "empty");
        assert_eq!(SeedProofFrame::decode(&[9u8; 33]), None, "unknown tag");
        assert_eq!(
            SeedProofFrame::decode(&[FRAME_CHALLENGE, 1, 2, 3]),
            None,
            "short challenge"
        );
        assert_eq!(
            SeedProofFrame::decode(&[FRAME_PROOF; 8]),
            None,
            "short proof"
        );
    }

    // [unit->REQ-MESH-1] the Mesh-D2 Hello frame (nonce + offered subnet names)
    // round-trips for the empty list, a multi-name list, and a unicode name; and
    // every malformed shape (truncated count, short name, name overrun, non-UTF-8,
    // trailing bytes) decodes to None — the connect-time negotiation can't be
    // tricked into a partial read.
    #[test]
    fn hello_frame_round_trips_and_rejects_malformed() {
        let nonce = [4u8; NONCE_LEN];
        for subnets in [
            vec![],
            vec!["home".to_string()],
            vec!["home".to_string(), "work".to_string(), "".to_string()],
            vec!["réseau-maison".to_string()],
        ] {
            let f = SeedProofFrame::Hello {
                nonce,
                subnets: subnets.clone(),
            };
            assert_eq!(
                SeedProofFrame::decode(&f.encode()),
                Some(f),
                "round-trip {subnets:?}"
            );
        }

        // Truncated: tag + short-of-(nonce+count).
        assert_eq!(
            SeedProofFrame::decode(&[FRAME_HELLO, 0, 0, 0]),
            None,
            "short hello header"
        );
        // Count claims 1 name but no length follows.
        let mut overrun = vec![FRAME_HELLO];
        overrun.extend_from_slice(&nonce);
        overrun.extend_from_slice(&1u32.to_be_bytes());
        assert_eq!(SeedProofFrame::decode(&overrun), None, "missing name len");
        // Name length overruns the buffer.
        let mut nameover = overrun.clone();
        nameover.extend_from_slice(&99u32.to_be_bytes());
        nameover.extend_from_slice(b"hi");
        assert_eq!(SeedProofFrame::decode(&nameover), None, "name overrun");
        // Non-UTF-8 name body.
        let mut badutf = vec![FRAME_HELLO];
        badutf.extend_from_slice(&nonce);
        badutf.extend_from_slice(&1u32.to_be_bytes());
        badutf.extend_from_slice(&2u32.to_be_bytes());
        badutf.extend_from_slice(&[0xff, 0xfe]);
        assert_eq!(SeedProofFrame::decode(&badutf), None, "non-utf8 name");
        // Trailing bytes past the declared (empty) name list.
        let mut trailing = vec![FRAME_HELLO];
        trailing.extend_from_slice(&nonce);
        trailing.extend_from_slice(&0u32.to_be_bytes());
        trailing.push(0x00);
        assert_eq!(SeedProofFrame::decode(&trailing), None, "trailing bytes");
    }
}


        assert_eq!(SeedProofFrame::decode(&[]), None, "empty");
        assert_eq!(SeedProofFrame::decode(&[9u8; 33]), None, "unknown tag");
        assert_eq!(
            SeedProofFrame::decode(&[FRAME_CHALLENGE, 1, 2, 3]),
            None,
            "short challenge"
        );
        assert_eq!(
            SeedProofFrame::decode(&[FRAME_PROOF; 8]),
            None,
            "short proof"
        );
    }
}

        assert_eq!(SeedProofFrame::decode(&[]), None, "empty");
        assert_eq!(SeedProofFrame::decode(&[9u8; 33]), None, "unknown tag");
        assert_eq!(
            SeedProofFrame::decode(&[FRAME_CHALLENGE, 1, 2, 3]),
            None,
            "short challenge"
        );
        assert_eq!(
            SeedProofFrame::decode(&[FRAME_PROOF; 8]),
            None,
            "short proof"
        );
    }

    // [unit->REQ-MESH-1] the Mesh-D2 Hello frame (nonce + offered subnet names)
    // round-trips for the empty list, a multi-name list, and a unicode name; and
    // every malformed shape (truncated count, short name, name overrun, non-UTF-8,
    // trailing bytes) decodes to None — the connect-time negotiation can't be
    // tricked into a partial read.
    #[test]
    fn hello_frame_round_trips_and_rejects_malformed() {
        let nonce = [4u8; NONCE_LEN];
        for subnets in [
            vec![],
            vec!["home".to_string()],
            vec!["home".to_string(), "work".to_string(), "".to_string()],
            vec!["réseau-maison".to_string()],
        ] {
            let f = SeedProofFrame::Hello {
                nonce,
                subnets: subnets.clone(),
            };
            assert_eq!(
                SeedProofFrame::decode(&f.encode()),
                Some(f),
                "round-trip {subnets:?}"
            );
        }

        // Truncated: tag + short-of-(nonce+count).
        assert_eq!(
            SeedProofFrame::decode(&[FRAME_HELLO, 0, 0, 0]),
            None,
            "short hello header"
        );
        // Count claims 1 name but no length follows.
        let mut overrun = vec![FRAME_HELLO];
        overrun.extend_from_slice(&nonce);
        overrun.extend_from_slice(&1u32.to_be_bytes());
        assert_eq!(SeedProofFrame::decode(&overrun), None, "missing name len");
        // Name length overruns the buffer.
        let mut nameover = overrun.clone();
        nameover.extend_from_slice(&99u32.to_be_bytes());
        nameover.extend_from_slice(b"hi");
        assert_eq!(SeedProofFrame::decode(&nameover), None, "name overrun");
        // Non-UTF-8 name body.
        let mut badutf = vec![FRAME_HELLO];
        badutf.extend_from_slice(&nonce);
        badutf.extend_from_slice(&1u32.to_be_bytes());
        badutf.extend_from_slice(&2u32.to_be_bytes());
        badutf.extend_from_slice(&[0xff, 0xfe]);
        assert_eq!(SeedProofFrame::decode(&badutf), None, "non-utf8 name");
        // Trailing bytes past the declared (empty) name list.
        let mut trailing = vec![FRAME_HELLO];
        trailing.extend_from_slice(&nonce);
        trailing.extend_from_slice(&0u32.to_be_bytes());
        trailing.push(0x00);
        assert_eq!(SeedProofFrame::decode(&trailing), None, "trailing bytes");
    }
}

+    // [unit->REQ-MESH-1] the Mesh-D2 Hello frame (nonce + offered subnet names)

+    // round-trips for the empty list, a multi-name list, and a unicode name; and

+    // every malformed shape (truncated count, short name, name overrun, non-UTF-8,

+    // trailing bytes) decodes to None — the connect-time negotiation can't be

+    fn hello_frame_round_trips_and_rejects_malformed() {

+            vec!["home".to_string(), "work".to_string(), "".to_string()],

+            vec!["réseau-maison".to_string()],

+            let f = SeedProofFrame::Hello {

+                subnets: subnets.clone(),

+                SeedProofFrame::decode(&f.encode()),

+                "round-trip {subnets:?}"

+        // Truncated: tag + short-of-(nonce+count).

+            SeedProofFrame::decode(&[FRAME_HELLO, 0, 0, 0]),

+        // Count claims 1 name but no length follows.

+        let mut overrun = vec![FRAME_HELLO];

+        overrun.extend_from_slice(&nonce);

+        overrun.extend_from_slice(&1u32.to_be_bytes());

+        assert_eq!(SeedProofFrame::decode(&overrun), None, "missing name len");

+        // Name length overruns the buffer.

+        let mut nameover = overrun.clone();

+        nameover.extend_from_slice(&99u32.to_be_bytes());

+        nameover.extend_from_slice(b"hi");

+        assert_eq!(SeedProofFrame::decode(&nameover), None, "name overrun");

+        let mut badutf = vec![FRAME_HELLO];

+        badutf.extend_from_slice(&nonce);

+        badutf.extend_from_slice(&1u32.to_be_bytes());

+        badutf.extend_from_slice(&2u32.to_be_bytes());

+        badutf.extend_from_slice(&[0xff, 0xfe]);

+        assert_eq!(SeedProofFrame::decode(&badutf), None, "non-utf8 name");

+        // Trailing bytes past the declared (empty) name list.

+        let mut trailing = vec![FRAME_HELLO];

+        trailing.extend_from_slice(&nonce);

+        trailing.extend_from_slice(&0u32.to_be_bytes());

+        assert_eq!(SeedProofFrame::decode(&trailing), None, "trailing bytes");


Bash

cargo test -p spt-net mesh::seedproof 2>&1 | tail -25

Run seedproof codec tests


   Compiling spt-net v0.2.0 (C:\Users\decid\Documents\projects\spt-core\crates\spt-net)
    Finished `test` profile [unoptimized + debuginfo] target(s) in 19.24s
     Running unittests src\lib.rs (target\debug\deps\spt_net-b028b09ce8c0cc73.exe)

running 9 tests
test net::mesh::seedproof::tests::frame_codec_round_trips_and_rejects_garbage ... ok
test net::mesh::seedproof::tests::wrong_subnet_fails ... ok
test net::mesh::seedproof::tests::mk_derivation_is_deterministic_and_input_sensitive ... ok
test net::mesh::seedproof::tests::hello_frame_round_trips_and_rejects_malformed ... ok
test net::mesh::seedproof::tests::forged_proof_fails ... ok
test net::mesh::seedproof::tests::valid_proof_verifies_and_roles_differ ... ok
test net::mesh::seedproof::tests::mutual_requires_both_sides ... ok
test net::mesh::seedproof::tests::wrong_epoch_fails ... ok
test net::mesh::seedproof::tests::cross_connection_replay_fails ... ok

test result: ok. 9 passed; 0 failed; 0 ignored; 0 measured; 109 filtered out; finished in 0.00s


Codec green (9 tests). Now the keep-alive investigation — find iroh's transport-config seam and `presets::N0` defaults.


grep -n "^name = \"iroh\"\|^version" target/../Cargo.lock 2>/dev/null | grep -A1 '"iroh"' | head; echo "---iroh ver---"; grep -A2 'name = "iroh"$' Cargo.lock | head

Find iroh version in lockfile


1747:name = "iroh"
1748:version = "0.98.2"
---iroh ver---
name = "iroh"
version = "0.98.2"
source = "registry+https://github.com/rust-lang/crates.io-index"


CARGO_HOME=${CARGO_HOME:-$HOME/.cargo}; IROH=$(find "$CARGO_HOME"/registry/src -maxdepth 1 -type d -name 'iroh-0.98.2' 2>/dev/null | head -1); echo "IROH=$IROH"; if [ -n "$IROH" ]; then grep -rn "transport_config\|keep_alive_interval\|max_idle_timeout\|pub fn builder" "$IROH/src/endpoint.rs" | head -40; fi

Search iroh endpoint source for transport config API


IROH=


for base in "$HOME/.cargo" "$USERPROFILE/.cargo" "/c/Users/decid/.cargo"; do echo "== $base =="; ls "$base/registry/src" 2>/dev/null; done

Locate cargo registry src dirs


== /c/Users/decid/.cargo ==
index.crates.io-1949cf8c6b5b557f
== C:\Users\decid/.cargo ==
index.crates.io-1949cf8c6b5b557f
== /c/Users/decid/.cargo ==
index.crates.io-1949cf8c6b5b557f


SRC="/c/Users/decid/.cargo/registry/src/index.crates.io-1949cf8c6b5b557f"; ls "$SRC" | grep -i '^iroh'; echo "---"; IROH="$SRC/iroh-0.98.2"; grep -rn "pub fn transport_config\|keep_alive_interval\|fn max_idle_timeout\|pub fn builder\|TransportConfig" "$IROH/src/endpoint.rs" 2>/dev/null | head -40

Find iroh transport config API in registry src


iroh-0.98.2
iroh-base-0.98.0
iroh-dns-0.98.0
iroh-metrics-0.38.3
iroh-metrics-derive-0.4.1
iroh-relay-0.98.0
---
91:        QuicTransportConfig, QuicTransportConfigBuilder, ReadDatagram, ReadError, ReadExactError,
102:use crate::socket::transports::TransportConfig;
114:    transport_config: QuicTransportConfig,
125:    transports: Vec<TransportConfig>,
134:impl From<RelayMode> for Option<TransportConfig> {
138:            RelayMode::Default => Some(TransportConfig::Relay {
142:            RelayMode::Staging => Some(TransportConfig::Relay {
146:            RelayMode::Custom(relay_map) => Some(TransportConfig::Relay {
175:            TransportConfig::default_ipv4(),
177:            TransportConfig::default_ipv6(),
183:            transport_config: QuicTransportConfig::default(),
442:                self.transports.push(TransportConfig::Ip {
462:                self.transports.push(TransportConfig::Ip {
481:            .retain(|t| !matches!(t, TransportConfig::Ip { .. }));
488:            .retain(|t| !matches!(t, TransportConfig::Relay { .. }));
540:                    .find(|t| matches!(t, TransportConfig::Relay { .. }))
549:                    .retain(|t| !matches!(t, TransportConfig::Relay { .. }));
634:    /// Sets a custom [`QuicTransportConfig`] for this endpoint.
645:    pub fn transport_config(mut self, transport_config: QuicTransportConfig) -> Self {
761:        self.transports.push(TransportConfig::Custom(factory));
881:    pub fn builder(preset: impl Preset) -> Builder {
1011:    /// 2. The [`QuicTransportConfig`] for the connection can be modified via the provided
1690:    transport_config: Option<QuicTransportConfig>,
1698:    /// as [`Endpoint::connect`], e.g. a default [`QuicTransportConfig`].
1704:    pub fn with_transport_config(mut self, transport_config: QuicTransportConfig) -> Self {


`builder.transport_config(QuicTransportConfig)` exists (line 645). Let me find what `QuicTransportConfig` is (re-export?) and its keep-alive/idle defaults.


SRC="/c/Users/decid/.cargo/registry/src/index.crates.io-1949cf8c6b5b557f"; IROH="$SRC/iroh-0.98.2"; echo "== QuicTransportConfig def/import =="; grep -rn "QuicTransportConfig" "$IROH/src/"*.rs "$IROH/src/endpoint"*.rs 2>/dev/null | grep -i "type\|use\|pub use\|= " | head; echo; echo "== search whole iroh src =="; grep -rn "QuicTransportConfig =\|pub use.*TransportConfig\|use quinn" "$IROH/src" 2>/dev/null | head -20

Resolve QuicTransportConfig type origin


== QuicTransportConfig def/import ==
/c/Users/decid/.cargo/registry/src/index.crates.io-1949cf8c6b5b557f/iroh-0.98.2/src/address_lookup.rs:702:        endpoint::{ConnectOptions, IdleTimeout, QuicTransportConfig, presets},
/c/Users/decid/.cargo/registry/src/index.crates.io-1949cf8c6b5b557f/iroh-0.98.2/src/address_lookup.rs:1107:        let cfg = QuicTransportConfig::builder()
/c/Users/decid/.cargo/registry/src/index.crates.io-1949cf8c6b5b557f/iroh-0.98.2/src/endpoint.rs:91:        QuicTransportConfig, QuicTransportConfigBuilder, ReadDatagram, ReadError, ReadExactError,
/c/Users/decid/.cargo/registry/src/index.crates.io-1949cf8c6b5b557f/iroh-0.98.2/src/endpoint.rs:114:    transport_config: QuicTransportConfig,
/c/Users/decid/.cargo/registry/src/index.crates.io-1949cf8c6b5b557f/iroh-0.98.2/src/endpoint.rs:183:            transport_config: QuicTransportConfig::default(),
/c/Users/decid/.cargo/registry/src/index.crates.io-1949cf8c6b5b557f/iroh-0.98.2/src/endpoint.rs:634:    /// Sets a custom [`QuicTransportConfig`] for this endpoint.
/c/Users/decid/.cargo/registry/src/index.crates.io-1949cf8c6b5b557f/iroh-0.98.2/src/endpoint.rs:645:    pub fn transport_config(mut self, transport_config: QuicTransportConfig) -> Self {
/c/Users/decid/.cargo/registry/src/index.crates.io-1949cf8c6b5b557f/iroh-0.98.2/src/endpoint.rs:1011:    /// 2. The [`QuicTransportConfig`] for the connection can be modified via the provided
/c/Users/decid/.cargo/registry/src/index.crates.io-1949cf8c6b5b557f/iroh-0.98.2/src/endpoint.rs:1690:    transport_config: Option<QuicTransportConfig>,
/c/Users/decid/.cargo/registry/src/index.crates.io-1949cf8c6b5b557f/iroh-0.98.2/src/endpoint.rs:1698:    /// as [`Endpoint::connect`], e.g. a default [`QuicTransportConfig`].

== search whole iroh src ==


SRC="/c/Users/decid/.cargo/registry/src/index.crates.io-1949cf8c6b5b557f"; IROH="$SRC/iroh-0.98.2"; echo "== example usage =="; sed -n '1100,1125p' "$IROH/src/address_lookup.rs"; echo "== QuicTransportConfigBuilder methods =="; grep -rn "QuicTransportConfigBuilder\|pub fn keep_alive_interval\|pub fn max_idle_timeout\|pub fn builder\|IdleTimeout" "$IROH/src/socket/"*.rs "$IROH/src/socket/"**/*.rs 2>/dev/null | head -30

Find transport config builder methods and example


== example usage ==
        let (ep2, _guard2) = new_endpoint_add(&mut rng, |ep| {
            let address_lookup1 = address_lookup_shared.create_lying_address_lookup(ep.id());
            ep.address_lookup().unwrap().add(address_lookup1)
        })
        .await;

        // 10x faster test via a 3s idle timeout instead of the 30s default
        let cfg = QuicTransportConfig::builder()
            .keep_alive_interval(Duration::from_secs(1))
            .max_idle_timeout(Some(IdleTimeout::try_from(Duration::from_secs(3)).unwrap()))
            .build();
        let opts = ConnectOptions::new().with_transport_config(cfg);

        let res = ep2
            .connect_with_opts(ep1.id(), TEST_ALPN, opts)
            .await? // -> Connecting works
            .await; // -> Connection is expected to fail
        assert!(res.is_err());
        Ok(())
    }

    /// This test first adds a wrong address manually (e.g. from an outdated&endpoint_id ticket).
    /// Connect should still succeed because the address lookup service service will be invoked (after a delay).
    #[tokio::test]
    #[traced_test]
    async fn address_lookup_with_wrong_existing_addr() -> Result {
== QuicTransportConfigBuilder methods ==


Confirmed: iroh default `max_idle_timeout` = 30s, `keep_alive_interval` = **None** (idle conns die at 30s). Open item #1 is real — must set keep-alive. API: `QuicTransportConfig::builder().keep_alive_interval(..).max_idle_timeout(..).build()`, types under `iroh::endpoint`. Let me wire it into `bind_scoped` with a testable seam.


Edit

use iroh::address_lookup::MdnsAddressLookup;
use iroh::endpoint::{presets, Incoming, PortmapperConfig};
use iroh::{Endpoint, EndpointId, RelayMap, RelayMode, SecretKey};
use spt_proto::identity::{Identity, PublicKey};

use std::time::Duration;

use iroh::address_lookup::MdnsAddressLookup;
use iroh::endpoint::{presets, IdleTimeout, Incoming, PortmapperConfig, QuicTransportConfig};
use iroh::{Endpoint, EndpointId, RelayMap, RelayMode, SecretKey};
use spt_proto::identity::{Identity, PublicKey};


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

use iroh::address_lookup::MdnsAddressLookup;
use iroh::endpoint::{presets, Incoming, PortmapperConfig};
use iroh::{Endpoint, EndpointId, RelayMap, RelayMode, SecretKey};
use spt_proto::identity::{Identity, PublicKey};

use std::time::Duration;

use iroh::address_lookup::MdnsAddressLookup;
use iroh::endpoint::{presets, IdleTimeout, Incoming, PortmapperConfig, QuicTransportConfig};
use iroh::{Endpoint, EndpointId, RelayMap, RelayMode, SecretKey};
use spt_proto::identity::{Identity, PublicKey};

//! that holds a live endpoint across brain restarts — [`NetEndpoint::iroh`] is the

//! escape hatch the daemon uses to take ownership.

-use iroh::endpoint::{presets, Incoming, PortmapperConfig};

+use iroh::endpoint::{presets, IdleTimeout, Incoming, PortmapperConfig, QuicTransportConfig};


/// The ALPN every spt-core node-to-node QUIC connection negotiates. Versioned so a
/// future wire break is a new protocol id, not a silent incompatibility.
pub const SPT_NET_ALPN: &[u8] = b"spt-core/net/0";

/// The ALPN every spt-core node-to-node QUIC connection negotiates. Versioned so a
/// future wire break is a new protocol id, not a silent incompatibility.
pub const SPT_NET_ALPN: &[u8] = b"spt-core/net/0";

/// QUIC keep-alive PING interval — the mesh's "stay warm" knob (REQ-MESH-1).
/// A member connection that proves seed-knowledge once at connect must then stay
/// open through indefinite idleness, so re-proof is a restart/partition/rotation
/// event, never a per-message tax. iroh's default leaves keep-alive **off** and
/// idles a quiet connection out at 30s; without this a meshed-but-quiet member
/// would silently drop and re-handshake constantly. Must be strictly less than
/// [`MESH_MAX_IDLE`] so a PING always lands before the idle timer fires.
const MESH_KEEP_ALIVE: Duration = Duration::from_secs(15);

/// QUIC max idle timeout for spt connections. A live PING every
/// [`MESH_KEEP_ALIVE`] resets this timer, so a connection with both ends up
/// stays open indefinitely; a real partition or peer death tears it down within
/// this bound (then the next dial re-proves membership). Bounded, not infinite:
/// a dead peer must eventually leave the broker conn table (the D4c presence seam).
const MESH_MAX_IDLE: Duration = Duration::from_secs(60);

/// The spt QUIC transport config: keep-alive on, bounded idle timeout (see
/// [`MESH_KEEP_ALIVE`] / [`MESH_MAX_IDLE`]). Factored out so the keep-warm
/// invariant (`keep_alive < max_idle`) is assertable without reading back iroh's
/// opaque config type.
// [impl->REQ-MESH-1]
fn spt_transport_config() -> QuicTransportConfig {
    QuicTransportConfig::builder()
        .keep_alive_interval(MESH_KEEP_ALIVE)
        .max_idle_timeout(Some(
            IdleTimeout::try_from(MESH_MAX_IDLE).expect("60s is a valid QUIC idle timeout"),
        ))
        .build()
}


/// The ALPN every spt-core node-to-node QUIC connection negotiates. Versioned so a
/// future wire break is a new protocol id, not a silent incompatibility.
pub const SPT_NET_ALPN: &[u8] = b"spt-core/net/0";

/// QUIC keep-alive PING interval — the mesh's "stay warm" knob (REQ-MESH-1).
/// A member connection that proves seed-knowledge once at connect must then stay
/// open through indefinite idleness, so re-proof is a restart/partition/rotation
/// event, never a per-message tax. iroh's default leaves keep-alive **off** and
/// idles a quiet connection out at 30s; without this a meshed-but-quiet member
/// would silently drop and re-handshake constantly. Must be strictly less than
/// [`MESH_MAX_IDLE`] so a PING always lands before the idle timer fires.
const MESH_KEEP_ALIVE: Duration = Duration::from_secs(15);

/// QUIC max idle timeout for spt connections. A live PING every
/// [`MESH_KEEP_ALIVE`] resets this timer, so a connection with both ends up
/// stays open indefinitely; a real partition or peer death tears it down within
/// this bound (then the next dial re-proves membership). Bounded, not infinite:
/// a dead peer must eventually leave the broker conn table (the D4c presence seam).
const MESH_MAX_IDLE: Duration = Duration::from_secs(60);

/// The spt QUIC transport config: keep-alive on, bounded idle timeout (see
/// [`MESH_KEEP_ALIVE`] / [`MESH_MAX_IDLE`]). Factored out so the keep-warm
/// invariant (`keep_alive < max_idle`) is assertable without reading back iroh's
/// opaque config type.
// [impl->REQ-MESH-1]
fn spt_transport_config() -> QuicTransportConfig {
    QuicTransportConfig::builder()
        .keep_alive_interval(MESH_KEEP_ALIVE)
        .max_idle_timeout(Some(
            IdleTimeout::try_from(MESH_MAX_IDLE).expect("60s is a valid QUIC idle timeout"),
        ))
        .build()
}

+/// QUIC keep-alive PING interval — the mesh's "stay warm" knob (REQ-MESH-1).

+/// A member connection that proves seed-knowledge once at connect must then stay

+/// open through indefinite idleness, so re-proof is a restart/partition/rotation

+/// event, never a per-message tax. iroh's default leaves keep-alive **off** and

+/// idles a quiet connection out at 30s; without this a meshed-but-quiet member

+/// would silently drop and re-handshake constantly. Must be strictly less than

+/// [`MESH_MAX_IDLE`] so a PING always lands before the idle timer fires.

+const MESH_KEEP_ALIVE: Duration = Duration::from_secs(15);

+/// QUIC max idle timeout for spt connections. A live PING every

+/// [`MESH_KEEP_ALIVE`] resets this timer, so a connection with both ends up

+/// stays open indefinitely; a real partition or peer death tears it down within

+/// this bound (then the next dial re-proves membership). Bounded, not infinite:

+/// a dead peer must eventually leave the broker conn table (the D4c presence seam).

+const MESH_MAX_IDLE: Duration = Duration::from_secs(60);

+/// The spt QUIC transport config: keep-alive on, bounded idle timeout (see

+/// [`MESH_KEEP_ALIVE`] / [`MESH_MAX_IDLE`]). Factored out so the keep-warm

+/// invariant (`keep_alive < max_idle`) is assertable without reading back iroh's

+fn spt_transport_config() -> QuicTransportConfig {

+        .keep_alive_interval(MESH_KEEP_ALIVE)

+            IdleTimeout::try_from(MESH_MAX_IDLE).expect("60s is a valid QUIC idle timeout"),

/// Plain-language disclosure of the soft runtime dependency on n0-operated relay

/// infrastructure (ADR-0002 v1 relay stance). Surfaced to the user by the CLI/setup

/// path; the dependency is operational + metadata-only (traffic is end-to-end


Now apply the transport config in `bind_scoped` and add the keep-warm invariant test:


        let secret = SecretKey::from_bytes(&identity.seed());
        let builder = Endpoint::builder(presets::N0)
            .secret_key(secret)
            .alpns(alpns);

        let secret = SecretKey::from_bytes(&identity.seed());
        let builder = Endpoint::builder(presets::N0)
            .secret_key(secret)
            .alpns(alpns)
            // Keep meshed member connections warm (REQ-MESH-1): seed-proof runs
            // once at connect, so a quiet connection must not idle out and force
            // a re-handshake. Transport-only, orthogonal to BindScope.
            .transport_config(spt_transport_config());


        let secret = SecretKey::from_bytes(&identity.seed());
        let builder = Endpoint::builder(presets::N0)
            .secret_key(secret)
            .alpns(alpns);

        let secret = SecretKey::from_bytes(&identity.seed());
        let builder = Endpoint::builder(presets::N0)
            .secret_key(secret)
            .alpns(alpns)
            // Keep meshed member connections warm (REQ-MESH-1): seed-proof runs
            // once at connect, so a quiet connection must not idle out and force
            // a re-handshake. Transport-only, orthogonal to BindScope.
            .transport_config(spt_transport_config());

+            // Keep meshed member connections warm (REQ-MESH-1): seed-proof runs

+            // once at connect, so a quiet connection must not idle out and force

+            // a re-handshake. Transport-only, orthogonal to BindScope.

+            .transport_config(spt_transport_config());

// Loopback scope replaces the builder's pre-configured unspecified binds

// (0.0.0.0 / [::]) with loopback-only sockets, AND disables iroh's

// portmapper (UPnP/PCP/NAT-PMP — on by default), which would otherwise


    // [unit->REQ-NET-1] the endpoint binds to the node's own ed25519 identity: the
    // iroh EndpointId equals the node's public key (same seed → same keypair).
    #[tokio::test]
    async fn endpoint_id_binds_to_identity() {

    // [unit->REQ-MESH-1] the keep-warm invariant: keep-alive must be strictly
    // less than the idle timeout, or a PING never lands before the idle timer
    // fires and a quiet member connection drops (forcing needless re-proof).
    // Asserted at the constant seam (iroh's QuicTransportConfig has no getters);
    // the production path feeds these same constants into spt_transport_config.
    #[test]
    fn keep_alive_is_under_idle_timeout() {
        assert!(
            MESH_KEEP_ALIVE < MESH_MAX_IDLE,
            "keep-alive {MESH_KEEP_ALIVE:?} must be < idle {MESH_MAX_IDLE:?}"
        );
        // Constructs without panicking (the IdleTimeout conversion is valid).
        let _ = spt_transport_config();
    }

    // [unit->REQ-NET-1] the endpoint binds to the node's own ed25519 identity: the
    // iroh EndpointId equals the node's public key (same seed → same keypair).
    #[tokio::test]
    async fn endpoint_id_binds_to_identity() {


    // [unit->REQ-MESH-1] the keep-warm invariant: keep-alive must be strictly
    // less than the idle timeout, or a PING never lands before the idle timer
    // fires and a quiet member connection drops (forcing needless re-proof).
    // Asserted at the constant seam (iroh's QuicTransportConfig has no getters);
    // the production path feeds these same constants into spt_transport_config.
    #[test]
    fn keep_alive_is_under_idle_timeout() {
        assert!(
            MESH_KEEP_ALIVE < MESH_MAX_IDLE,
            "keep-alive {MESH_KEEP_ALIVE:?} must be < idle {MESH_MAX_IDLE:?}"
        );
        // Constructs without panicking (the IdleTimeout conversion is valid).
        let _ = spt_transport_config();
    }

    // [unit->REQ-NET-1] the endpoint binds to the node's own ed25519 identity: the
    // iroh EndpointId equals the node's public key (same seed → same keypair).
    #[tokio::test]
    async fn endpoint_id_binds_to_identity() {

assert_eq!(resp, b"echo:ping", "bidi echo over QUIC");

+    // [unit->REQ-MESH-1] the keep-warm invariant: keep-alive must be strictly

+    // less than the idle timeout, or a PING never lands before the idle timer

+    // fires and a quiet member connection drops (forcing needless re-proof).

+    // Asserted at the constant seam (iroh's QuicTransportConfig has no getters);

+    // the production path feeds these same constants into spt_transport_config.

+    fn keep_alive_is_under_idle_timeout() {

+            MESH_KEEP_ALIVE < MESH_MAX_IDLE,

+            "keep-alive {MESH_KEEP_ALIVE:?} must be < idle {MESH_MAX_IDLE:?}"

+        // Constructs without panicking (the IdleTimeout conversion is valid).

+        let _ = spt_transport_config();


Bash

cargo test -p spt-net net::endpoint::tests::keep_alive_is_under_idle_timeout 2>&1 | tail -15

Build and test keep-alive config


   Compiling spt-net v0.2.0 (C:\Users\decid\Documents\projects\spt-core\crates\spt-net)
    Finished `test` profile [unoptimized + debuginfo] target(s) in 4.98s
     Running unittests src\lib.rs (target\debug\deps\spt_net-b028b09ce8c0cc73.exe)

running 1 test
test net::endpoint::tests::keep_alive_is_under_idle_timeout ... ok

test result: ok. 1 passed; 0 failed; 0 ignored; 0 measured; 118 filtered out; finished in 0.00s


endpoint.rs done. Now the core: the orchestration module. Let me verify the `RecvStream::read_exact` API exists in iroh's re-export before writing.


SRC="/c/Users/decid/.cargo/registry/src/index.crates.io-1949cf8c6b5b557f"; echo "== iroh RecvStream methods =="; grep -rn "pub async fn read_exact\|pub async fn read(\|pub fn read_exact\|ReadExactError" "$SRC/iroh-0.98.2/src/endpoint.rs" | head; echo "== does iroh re-export quinn RecvStream? =="; grep -rn "pub use.*RecvStream\|pub use.*SendStream\|RecvStream\b" "$SRC/iroh-0.98.2/src/endpoint.rs" | grep -i "use\|read_exact" | head

Check RecvStream read_exact availability


== iroh RecvStream methods ==
91:        QuicTransportConfig, QuicTransportConfigBuilder, ReadDatagram, ReadError, ReadExactError,
== does iroh re-export quinn RecvStream? ==


SRC="/c/Users/decid/.cargo/registry/src/index.crates.io-1949cf8c6b5b557f"; grep -rn "RecvStream\|read_exact\|pub async fn write_all\|pub fn finish" "$SRC/iroh-0.98.2/src/endpoint/rtt.rs" "$SRC/iroh-0.98.2/src/endpoint/stream.rs" 2>/dev/null | head; echo "== find where RecvStream is defined/aliased =="; grep -rln "pub.*RecvStream" "$SRC/iroh-0.98.2/src/" | head; grep -rn "type RecvStream\|struct RecvStream\|pub use.*RecvStream" "$SRC/iroh-0.98.2/src/"**/*.rs 2>/dev/null | head

Locate RecvStream definition


== find where RecvStream is defined/aliased ==


SRC="/c/Users/decid/.cargo/registry/src/index.crates.io-1949cf8c6b5b557f"; grep -rn "RecvStream" "$SRC/iroh-0.98.2/src/lib.rs" "$SRC/iroh-0.98.2/src/"*.rs 2>/dev/null | grep -i "use\|pub" | head; echo "== iroh-quinn? =="; ls "$SRC" | grep -i quinn

Find RecvStream re-export and quinn crate


/c/Users/decid/.cargo/registry/src/index.crates.io-1949cf8c6b5b557f/iroh-0.98.2/src/endpoint.rs:92:        ReadToEndError, RecvStream, ResetError, RttEstimator, SendDatagram, SendDatagramError,
/c/Users/decid/.cargo/registry/src/index.crates.io-1949cf8c6b5b557f/iroh-0.98.2/src/endpoint.rs:95:        UdpStats, UnorderedRecvStream, UnsupportedVersion, ValidationTokenConfig, VarInt,
== iroh-quinn? ==


SRC="/c/Users/decid/.cargo/registry/src/index.crates.io-1949cf8c6b5b557f"; sed -n '85,100p' "$SRC/iroh-0.98.2/src/endpoint.rs"; echo "== quinn dep =="; grep -n "quinn" "$SRC/iroh-0.98.2/Cargo.toml" | head

Find quinn re-export crate


    quic::{
        AcceptBi, AcceptUni, AckFrequencyConfig, ApplicationClose, Chunk, ClosedStream,
        ConnectionClose, ConnectionError, ConnectionStats, Controller, ControllerFactory,
        ControllerMetrics, CryptoError, DecryptedInitial, Dir, ExportKeyingMaterialError,
        FrameStats, FrameType, HandshakeTokenKey, HeaderKey, IdleTimeout, IncomingAlpns, Keys,
        MtuDiscoveryConfig, OpenBi, OpenUni, PacketKey, PathId, PathStats, QuicConnectError,
        QuicTransportConfig, QuicTransportConfigBuilder, ReadDatagram, ReadError, ReadExactError,
        ReadToEndError, RecvStream, ResetError, RttEstimator, SendDatagram, SendDatagramError,
        SendStream, ServerConfig, ServerConfigBuilder, Side, StoppedError, StreamId, TimeSource,
        TokenLog, TokenReuseError, TransportError, TransportErrorCode, TransportParameters,
        UdpStats, UnorderedRecvStream, UnsupportedVersion, ValidationTokenConfig, VarInt,
        VarIntBoundsExceeded, WriteError, Written,
    },
};
pub use crate::portmapper::PortmapperConfig;
#[cfg(not(wasm_browser))]
== quinn dep ==


`ReadExactError` exported ⇒ `RecvStream::read_exact` exists. Good. Let me confirm the mesh re-export path and spt-daemon's deps.


cat crates/spt-net/src/net/mesh.rs 2>/dev/null; echo "=== net.rs mesh decl ==="; grep -n "mesh" crates/spt-net/src/net.rs; echo "=== spt-daemon deps (store/net) ==="; grep -n "spt-store\|spt-net\|spt-proto" crates/spt-daemon/Cargo.toml

Check mesh module re-exports and daemon deps


//! Subnet mesh membership primitives (ADR-0017, `SUBNET-MESH-PLAN.md`).
//!
//! A subnet member is a node that can prove *current-epoch seed knowledge* on a
//! connection — symmetric, channel-bound, and crucially **without either side
//! having pre-pinned the other** (the mesh property that the pairwise
//! `is_trusted` gate lacks). [`seedproof`] is the crypto that proof rests on:
//! the membership key `MK = HKDF(seed, …)` and the mutual challenge-response
//! transcript. Pure primitives here (Mesh-D1) — the connect-time handshake that
//! drives them onto a live connection is Mesh-D2; the gate swap that consults
//! the result is Mesh-D5.

pub mod seedproof;
=== net.rs mesh decl ===
10:pub mod mesh;
=== spt-daemon deps (store/net) ===
12:spt-proto = { path = "../spt-proto" }
13:spt-store = { path = "../spt-store" }
19:# they survive a brain restart. spt-net provides the *mechanism* (NetEndpoint);
22:spt-net = { path = "../spt-net" }
49:# workspace's existing libc usage (spt, spt-msg, spt-store); the Windows half
60:spt-store = { path = "../spt-store" }
62:spt-proto = { path = "../spt-proto" }
63:spt-net = { path = "../spt-net" }


mesh path = `spt_net::net::mesh::seedproof`; daemon has all deps. Now — the subnet material source. Loading `SubnetStore::load()` inside the proof would break the existing hermetic `two_hosts_dial_over_loopback` test (no subnets → empty intersection → drop). Cleaner + testable: inject the credential source via `NetConfig`. Let me find NetConfig construction sites.


grep -rn "NetConfig {" --include=*.rs crates | grep -v "pub struct"; echo "=== struct def line ==="; grep -rn "pub struct NetConfig" --include=*.rs crates

Find NetConfig construction sites


crates/spt/src/wansend.rs:568:        let host = NetHost::start(NetConfig {
crates/spt-daemon/src/daemon.rs:157:                    let net_cfg = NetConfig {
crates/spt-daemon/src/nethost.rs:753:    fn hermetic(identity: &Identity) -> NetConfig {
crates/spt-daemon/src/nethost.rs:754:        NetConfig {
crates/spt-daemon/tests/access.rs:51:fn hermetic() -> NetConfig {
crates/spt-daemon/tests/access.rs:52:    NetConfig {
crates/spt-daemon/tests/attach.rs:56:fn hermetic() -> NetConfig {
crates/spt-daemon/tests/attach.rs:57:    NetConfig {
crates/spt-daemon/tests/dispatch.rs:64:fn hermetic() -> NetConfig {
crates/spt-daemon/tests/dispatch.rs:65:    NetConfig {
crates/spt-daemon/tests/mesh.rs:80:fn hermetic() -> NetConfig {
crates/spt-daemon/tests/mesh.rs:81:    NetConfig {
crates/spt-daemon/tests/netbroker.rs:38:fn hermetic(identity: Identity) -> NetConfig {
crates/spt-daemon/tests/netbroker.rs:39:    NetConfig {
crates/spt-daemon/tests/netstream.rs:35:fn hermetic() -> NetConfig {
crates/spt-daemon/tests/netstream.rs:36:    NetConfig {
crates/spt-daemon/tests/notifsync.rs:40:fn hermetic() -> NetConfig {
crates/spt-daemon/tests/notifsync.rs:41:    NetConfig {
crates/spt-daemon/tests/pairjoin.rs:59:    let host = NetHost::start(NetConfig {
crates/spt-daemon/tests/peerloop.rs:57:fn hermetic() -> NetConfig {
crates/spt-daemon/tests/peerloop.rs:58:    NetConfig {
crates/spt-daemon/tests/presence.rs:34:fn hermetic() -> NetConfig {
crates/spt-daemon/tests/presence.rs:35:    NetConfig {
crates/spt-daemon/tests/propagate.rs:49:fn hermetic() -> NetConfig {
crates/spt-daemon/tests/propagate.rs:50:    NetConfig {
crates/spt-daemon/tests/replicate.rs:41:fn hermetic() -> NetConfig {
crates/spt-daemon/tests/replicate.rs:42:    NetConfig {
crates/spt-daemon/tests/sync.rs:71:fn hermetic() -> NetConfig {
crates/spt-daemon/tests/sync.rs:72:    NetConfig {
crates/spt-daemon/tests/twohost.rs:224:    let host = NetHost::start(NetConfig {
crates/spt-daemon/tests/wanmsg.rs:60:fn hermetic() -> NetConfig {
crates/spt-daemon/tests/wanmsg.rs:61:    NetConfig {
crates/spt-daemon/tests/xfer.rs:39:fn hermetic() -> NetConfig {
crates/spt-daemon/tests/xfer.rs:40:    NetConfig {
=== struct def line ===
crates/spt-daemon/src/nethost.rs:56:pub struct NetConfig {


This is bigger than the plan's "two_hosts test" line implied: ~15 integration-test `NetConfig` literals would all drop connections once seed-proof is mandatory. The clean seam: make the membership source **optional** on `NetConfig` — `None` = skip the proof entirely (no control stream), so unrelated tests keep their behavior; production + the D2 mesh test wire `Some`. Both peers must agree (both `Some` or both `None`), which always holds in production. Let me write the orchestration module first.


Write

//! Connect-time seed-proof exchange (Mesh-D2, REQ-MESH-1) — the daemon-side
//! orchestration that drives the `spt-net` [`seedproof`] codec onto a live QUIC
//! connection.
//!
//! [`seedproof`](spt_net::net::mesh::seedproof) is the pure crypto (membership
//! key + mutual channel-bound transcript). This module runs the **mutual
//! exchange** over a dedicated control stream at every dial + accept, *before*
//! the connection is registered as usable: each side proves current-epoch
//! knowledge of the seed for **every subnet the two nodes share**, and the
//! verified set is cached on the broker `ConnEntry`. A failed or empty proof
//! drops the connection.
//!
//! ## Why a set, not a single subnet
//!
//! Connections are not subnet-scoped (`dial` takes only an address; iroh dedups
//! to one connection per peer-pair on the shared ALPN), and the inbound gates
//! the mesh swaps at D5 are per-`(subnet, origin)`, evaluated per item. So one
//! connection carries traffic for every shared subnet, and the cached verdict
//! must answer "did this peer prove subnet S?" for any S — a [`HashSet`], not a
//! bool.
//!
//! ## The exchange (one bidi control stream)
//!
//! Dialer opens it (`open_bi`); acceptor accepts it (`accept_bi`) *before*
//! `register_conn` wires the general app-stream acceptor, so it is never
//! confused with an app stream. Then a four-leg ping-pong:
//!
//! 1. dialer → `Hello { nonce_d, subnets = its local names }`
//! 2. acceptor → `Hello { nonce_a, subnets = intersection }` (reveals only shared names)
//! 3. dialer → one `Proof` per shared subnet; acceptor reads + verifies each
//! 4. acceptor → one `Proof` per shared subnet; dialer reads + verifies each
//!
//! Each side derives `MK_S` from **its own** local seed + epoch for S; an epoch
//! skew or a wrong seed makes the tags diverge → that subnet fails → the whole
//! connection drops (exact-epoch; the N-1 re-seed grace is Mesh-D7). Nonces are
//! per-connection-fresh and reused across the per-subnet proofs (the transcript
//! already binds `subnet_id` + `seed_epoch`).
// [impl->REQ-MESH-1]

use std::collections::{HashMap, HashSet};
use std::sync::Arc;

use spt_net::net::endpoint::{Connection, RecvStream, SendStream};
use spt_net::net::mesh::seedproof::{
    MembershipKey, ProofRole, SeedProofFrame, SeedProofTranscript, NONCE_LEN,
};
use spt_proto::identity::{Identity, PublicKey};

/// One subnet's membership credential as the proof needs it: the human name
/// (goes on the wire), the raw seed bytes, and the current epoch (both local,
/// never on the wire). Built from `spt_store::subnet::SubnetStore`.
#[derive(Clone)]
pub struct SubnetCred {
    /// Subnet name — the only field that crosses the wire (a label, not a secret).
    pub name: String,
    /// The subnet seed bytes (HKDF input material). Never transmitted.
    pub seed: Vec<u8>,
    /// The local current `seed_epoch` for this subnet. Never transmitted.
    pub epoch: u64,
}

/// A live source of this node's subnet credentials, re-read per connection so a
/// rotation (Mesh-D7) is picked up without a restart. Production wires this to
/// `SubnetStore::load()`; `None` on [`NetConfig`](crate::nethost::NetConfig)
/// disables seed-proof entirely (both peers must agree — a mixed pair would
/// deadlock on the missing control stream).
pub type MembershipSource = Arc<dyn Fn() -> Vec<SubnetCred> + Send + Sync>;

/// Cap on the seed-proof control-stream framing — a single small handshake; a
/// length claiming more is a malformed/hostile peer, dropped.
const MAX_FRAME: usize = 64 * 1024;

/// Generate a fresh 32-byte per-connection nonce from the OS CSPRNG. Reuses
/// `spt-proto`'s Ed25519 seed generator (OsRng) as the randomness source — the
/// same no-extra-`rand`-dependency trick the stores use.
fn fresh_nonce() -> [u8; NONCE_LEN] {
    Identity::generate().seed()
}

/// Run the mutual seed-proof on `conn` and return the set of subnets BOTH sides
/// proved, or `None` if the connection must be dropped (no shared subnet, a
/// failed proof, or a transport/protocol error). `local_id` is this node's
/// pubkey; `role` is which side of the connection we are.
// [impl->REQ-MESH-1]
pub async fn prove_membership(
    conn: &Connection,
    local_id: PublicKey,
    role: ProofRole,
    creds: &[SubnetCred],
) -> Option<HashSet<String>> {
    let remote = PublicKey::from_bytes(conn.remote_id().as_bytes()).ok()?;
    // Channel binding: the transcript pins both handshake-proven pubkeys in a
    // role-fixed order (dialer first), so a tag is valid on exactly this conn.
    let (dialer_pub, acceptor_pub) = match role {
        ProofRole::Dialer => (local_id, remote),
        ProofRole::Acceptor => (remote, local_id),
    };
    let by_name: HashMap<&str, &SubnetCred> =
        creds.iter().map(|c| (c.name.as_str(), c)).collect();

    match role {
        ProofRole::Dialer => {
            let (mut send, mut recv) = conn.open_bi().await.ok()?;
            prove_as_dialer(
                &mut send,
                &mut recv,
                creds,
                &by_name,
                dialer_pub,
                acceptor_pub,
            )
            .await
        }
        ProofRole::Acceptor => {
            let (mut send, mut recv) = conn.accept_bi().await.ok()?;
            prove_as_acceptor(
                &mut send,
                &mut recv,
                creds,
                &by_name,
                dialer_pub,
                acceptor_pub,
            )
            .await
        }
    }
}

async fn prove_as_dialer(
    send: &mut SendStream,
    recv: &mut RecvStream,
    creds: &[SubnetCred],
    by_name: &HashMap<&str, &SubnetCred>,
    dialer_pub: PublicKey,
    acceptor_pub: PublicKey,
) -> Option<HashSet<String>> {
    let nonce_d = fresh_nonce();
    let my_names: Vec<String> = creds.iter().map(|c| c.name.clone()).collect();
    write_frame(send, &SeedProofFrame::Hello { nonce: nonce_d, subnets: my_names }.encode())
        .await?;

    // Acceptor replies with the intersection (in its order) + its nonce.
    let (nonce_a, shared) = match read_frame(recv, MAX_FRAME).await.and_then(|b| SeedProofFrame::decode(&b)) {
        Some(SeedProofFrame::Hello { nonce, subnets }) => (nonce, subnets),
        _ => return None,
    };
    // Defensive: only honor subnets we actually offered.
    if shared.is_empty() || shared.iter().any(|s| !by_name.contains_key(s.as_str())) {
        let _ = send.finish();
        return None;
    }

    let transcripts = build_transcripts(&shared, by_name, dialer_pub, acceptor_pub, nonce_d, nonce_a)?;

    // Send all our (dialer-role) proofs first; they buffer while the acceptor
    // reads them, so this ping-pong never deadlocks on the single stream.
    for (s, t, mk) in &transcripts {
        let tag = t.tag(mk, ProofRole::Dialer);
        write_frame(send, &SeedProofFrame::Proof { tag }.encode()).await?;
        let _ = s; // name unused here; bound for symmetry/readability
    }
    // Then read + verify the acceptor's proofs.
    let mut proven = HashSet::new();
    for (name, t, mk) in &transcripts {
        let tag = match read_frame(recv, MAX_FRAME).await.and_then(|b| SeedProofFrame::decode(&b)) {
            Some(SeedProofFrame::Proof { tag }) => tag,
            _ => return None,
        };
        if !t.verify(mk, ProofRole::Acceptor, &tag) {
            return None; // any agreed subnet failing ⇒ drop the whole connection
        }
        proven.insert(name.clone());
    }
    let _ = send.finish();
    Some(proven)
}

async fn prove_as_acceptor(
    send: &mut SendStream,
    recv: &mut RecvStream,
    creds: &[SubnetCred],
    by_name: &HashMap<&str, &SubnetCred>,
    dialer_pub: PublicKey,
    acceptor_pub: PublicKey,
) -> Option<HashSet<String>> {
    let (nonce_d, dialer_names) = match read_frame(recv, MAX_FRAME).await.and_then(|b| SeedProofFrame::decode(&b)) {
        Some(SeedProofFrame::Hello { nonce, subnets }) => (nonce, subnets),
        _ => return None,
    };
    // Intersection in a deterministic (sorted) order both sides walk identically.
    let mut shared: Vec<String> = dialer_names
        .into_iter()
        .filter(|s| by_name.contains_key(s.as_str()))
        .collect();
    shared.sort();
    shared.dedup();

    let nonce_a = fresh_nonce();
    write_frame(
        send,
        &SeedProofFrame::Hello { nonce: nonce_a, subnets: shared.clone() }.encode(),
    )
    .await?;
    if shared.is_empty() {
        let _ = send.finish();
        return None; // no shared subnet — nothing to mesh, drop
    }

    let transcripts = build_transcripts(&shared, by_name, dialer_pub, acceptor_pub, nonce_d, nonce_a)?;

    // Read + verify the dialer's proofs first, then send ours (mirror of the
    // dialer's send-then-read, so the ping-pong stays deadlock-free).
    let mut proven = HashSet::new();
    for (name, t, mk) in &transcripts {
        let tag = match read_frame(recv, MAX_FRAME).await.and_then(|b| SeedProofFrame::decode(&b)) {
            Some(SeedProofFrame::Proof { tag }) => tag,
            _ => return None,
        };
        if !t.verify(mk, ProofRole::Dialer, &tag) {
            return None;
        }
        proven.insert(name.clone());
    }
    for (_name, t, mk) in &transcripts {
        let tag = t.tag(mk, ProofRole::Acceptor);
        write_frame(send, &SeedProofFrame::Proof { tag }.encode()).await?;
    }
    let _ = send.finish();
    Some(proven)
}

/// Build `(name, transcript, MK)` for each shared subnet, deriving each `MK`
/// from the local credential's seed + epoch. `None` if a shared name has no
/// local credential (cannot happen after the membership filter, but kept total).
fn build_transcripts(
    shared: &[String],
    by_name: &HashMap<&str, &SubnetCred>,
    dialer_pub: PublicKey,
    acceptor_pub: PublicKey,
    nonce_d: [u8; NONCE_LEN],
    nonce_a: [u8; NONCE_LEN],
) -> Option<Vec<(String, SeedProofTranscript, MembershipKey)>> {
    let mut out = Vec::with_capacity(shared.len());
    for name in shared {
        let cred = by_name.get(name.as_str())?;
        let mk = MembershipKey::derive(&cred.seed, name, cred.epoch);
        let t = SeedProofTranscript {
            subnet_id: name.clone(),
            seed_epoch: cred.epoch,
            dialer_pub,
            acceptor_pub,
            dialer_nonce: nonce_d,
            acceptor_nonce: nonce_a,
        };
        out.push((name.clone(), t, mk));
    }
    Some(out)
}

/// Write a length-delimited frame (`u32` BE length prefix + body).
async fn write_frame(send: &mut SendStream, body: &[u8]) -> Option<()> {
    send.write_all(&(body.len() as u32).to_be_bytes()).await.ok()?;
    send.write_all(body).await.ok()?;
    Some(())
}

/// Read one length-delimited frame, rejecting a length of 0 or > `max`.
async fn read_frame(recv: &mut RecvStream, max: usize) -> Option<Vec<u8>> {
    let mut lenb = [0u8; 4];
    recv.read_exact(&mut lenb).await.ok()?;
    let n = u32::from_be_bytes(lenb) as usize;
    if n == 0 || n > max {
        return None;
    }
    let mut buf = vec![0u8; n];
    recv.read_exact(&mut buf).await.ok()?;
    Some(buf)
}


File created successfully at: C:\Users\decid\Documents\projects\spt-core\crates\spt-daemon\src\seedproofx.rs (file state is current in your context — no need to Read it back)

//! Connect-time seed-proof exchange (Mesh-D2, REQ-MESH-1) — the daemon-side
//! orchestration that drives the `spt-net` [`seedproof`] codec onto a live QUIC
//! connection.
//!
//! [`seedproof`](spt_net::net::mesh::seedproof) is the pure crypto (membership
//! key + mutual channel-bound transcript). This module runs the **mutual
//! exchange** over a dedicated control stream at every dial + accept, *before*
//! the connection is registered as usable: each side proves current-epoch
//! knowledge of the seed for **every subnet the two nodes share**, and the
//! verified set is cached on the broker `ConnEntry`. A failed or empty proof
//! drops the connection.
//!
//! ## Why a set, not a single subnet
//!
//! Connections are not subnet-scoped (`dial` takes only an address; iroh dedups
//! to one connection per peer-pair on the shared ALPN), and the inbound gates
//! the mesh swaps at D5 are per-`(subnet, origin)`, evaluated per item. So one
//! connection carries traffic for every shared subnet, and the cached verdict
//! must answer "did this peer prove subnet S?" for any S — a [`HashSet`], not a
//! bool.
//!
//! ## The exchange (one bidi control stream)
//!
//! Dialer opens it (`open_bi`); acceptor accepts it (`accept_bi`) *before*
//! `register_conn` wires the general app-stream acceptor, so it is never
//! confused with an app stream. Then a four-leg ping-pong:
//!
//! 1. dialer → `Hello { nonce_d, subnets = its local names }`
//! 2. acceptor → `Hello { nonce_a, subnets = intersection }` (reveals only shared names)
//! 3. dialer → one `Proof` per shared subnet; acceptor reads + verifies each
//! 4. acceptor → one `Proof` per shared subnet; dialer reads + verifies each
//!
//! Each side derives `MK_S` from **its own** local seed + epoch for S; an epoch
//! skew or a wrong seed makes the tags diverge → that subnet fails → the whole
//! connection drops (exact-epoch; the N-1 re-seed grace is Mesh-D7). Nonces are
//! per-connection-fresh and reused across the per-subnet proofs (the transcript
//! already binds `subnet_id` + `seed_epoch`).
// [impl->REQ-MESH-1]

use std::collections::{HashMap, HashSet};
use std::sync::Arc;

use spt_net::net::endpoint::{Connection, RecvStream, SendStream};
use spt_net::net::mesh::seedproof::{
    MembershipKey, ProofRole, SeedProofFrame, SeedProofTranscript, NONCE_LEN,
};
use spt_proto::identity::{Identity, PublicKey};

/// One subnet's membership credential as the proof needs it: the human name
/// (goes on the wire), the raw seed bytes, and the current epoch (both local,
/// never on the wire). Built from `spt_store::subnet::SubnetStore`.
#[derive(Clone)]
pub struct SubnetCred {
    /// Subnet name — the only field that crosses the wire (a label, not a secret).
    pub name: String,
    /// The subnet seed bytes (HKDF input material). Never transmitted.
    pub seed: Vec<u8>,
    /// The local current `seed_epoch` for this subnet. Never transmitted.
    pub epoch: u64,
}

/// A live source of this node's subnet credentials, re-read per connection so a
/// rotation (Mesh-D7) is picked up without a restart. Production wires this to
/// `SubnetStore::load()`; `None` on [`NetConfig`](crate::nethost::NetConfig)
/// disables seed-proof entirely (both peers must agree — a mixed pair would
/// deadlock on the missing control stream).
pub type MembershipSource = Arc<dyn Fn() -> Vec<SubnetCred> + Send + Sync>;

/// Cap on the seed-proof control-stream framing — a single small handshake; a
/// length claiming more is a malformed/hostile peer, dropped.
const MAX_FRAME: usize = 64 * 1024;

/// Generate a fresh 32-byte per-connection nonce from the OS CSPRNG. Reuses
/// `spt-proto`'s Ed25519 seed generator (OsRng) as the randomness source — the
/// same no-extra-`rand`-dependency trick the stores use.
fn fresh_nonce() -> [u8; NONCE_LEN] {
    Identity::generate().seed()
}

/// Run the mutual seed-proof on `conn` and return the set of subnets BOTH sides
/// proved, or `None` if the connection must be dropped (no shared subnet, a
/// failed proof, or a transport/protocol error). `local_id` is this node's
/// pubkey; `role` is which side of the connection we are.
// [impl->REQ-MESH-1]
pub async fn prove_membership(
    conn: &Connection,
    local_id: PublicKey,
    role: ProofRole,
    creds: &[SubnetCred],
) -> Option<HashSet<String>> {
    let remote = PublicKey::from_bytes(conn.remote_id().as_bytes()).ok()?;
    // Channel binding: the transcript pins both handshake-proven pubkeys in a
    // role-fixed order (dialer first), so a tag is valid on exactly this conn.
    let (dialer_pub, acceptor_pub) = match role {
        ProofRole::Dialer => (local_id, remote),
        ProofRole::Acceptor => (remote, local_id),
    };
    let by_name: HashMap<&str, &SubnetCred> =
        creds.iter().map(|c| (c.name.as_str(), c)).collect();

    match role {
        ProofRole::Dialer => {
            let (mut send, mut recv) = conn.open_bi().await.ok()?;
            prove_as_dialer(
                &mut send,
                &mut recv,
                creds,
                &by_name,
                dialer_pub,
                acceptor_pub,
            )
            .await
        }
        ProofRole::Acceptor => {
            let (mut send, mut recv) = conn.accept_bi().await.ok()?;
            prove_as_acceptor(
                &mut send,
                &mut recv,
                creds,
                &by_name,
                dialer_pub,
                acceptor_pub,
            )
            .await
        }
    }
}

async fn prove_as_dialer(
    send: &mut SendStream,
    recv: &mut RecvStream,
    creds: &[SubnetCred],
    by_name: &HashMap<&str, &SubnetCred>,
    dialer_pub: PublicKey,
    acceptor_pub: PublicKey,
) -> Option<HashSet<String>> {
    let nonce_d = fresh_nonce();
    let my_names: Vec<String> = creds.iter().map(|c| c.name.clone()).collect();
    write_frame(send, &SeedProofFrame::Hello { nonce: nonce_d, subnets: my_names }.encode())
        .await?;

    // Acceptor replies with the intersection (in its order) + its nonce.
    let (nonce_a, shared) = match read_frame(recv, MAX_FRAME).await.and_then(|b| SeedProofFrame::decode(&b)) {
        Some(SeedProofFrame::Hello { nonce, subnets }) => (nonce, subnets),
        _ => return None,
    };
    // Defensive: only honor subnets we actually offered.
    if shared.is_empty() || shared.iter().any(|s| !by_name.contains_key(s.as_str())) {
        let _ = send.finish();
        return None;
    }

    let transcripts = build_transcripts(&shared, by_name, dialer_pub, acceptor_pub, nonce_d, nonce_a)?;

    // Send all our (dialer-role) proofs first; they buffer while the acceptor
    // reads them, so this ping-pong never deadlocks on the single stream.
    for (s, t, mk) in &transcripts {
        let tag = t.tag(mk, ProofRole::Dialer);
        write_frame(send, &SeedProofFrame::Proof { tag }.encode()).await?;
        let _ = s; // name unused here; bound for symmetry/readability
    }
    // Then read + verify the acceptor's proofs.
    let mut proven = HashSet::new();
    for (name, t, mk) in &transcripts {
        let tag = match read_frame(recv, MAX_FRAME).await.and_then(|b| SeedProofFrame::decode(&b)) {
            Some(SeedProofFrame::Proof { tag }) => tag,
            _ => return None,
        };
        if !t.verify(mk, ProofRole::Acceptor, &tag) {
            return None; // any agreed subnet failing ⇒ drop the whole connection
        }
        proven.insert(name.clone());
    }
    let _ = send.finish();
    Some(proven)
}

async fn prove_as_acceptor(
    send: &mut SendStream,
    recv: &mut RecvStream,
    creds: &[SubnetCred],
    by_name: &HashMap<&str, &SubnetCred>,
    dialer_pub: PublicKey,
    acceptor_pub: PublicKey,
) -> Option<HashSet<String>> {
    let (nonce_d, dialer_names) = match read_frame(recv, MAX_FRAME).await.and_then(|b| SeedProofFrame::decode(&b)) {
        Some(SeedProofFrame::Hello { nonce, subnets }) => (nonce, subnets),
        _ => return None,
    };
    // Intersection in a deterministic (sorted) order both sides walk identically.
    let mut shared: Vec<String> = dialer_names
        .into_iter()
        .filter(|s| by_name.contains_key(s.as_str()))
        .collect();
    shared.sort();
    shared.dedup();

    let nonce_a = fresh_nonce();
    write_frame(
        send,
        &SeedProofFrame::Hello { nonce: nonce_a, subnets: shared.clone() }.encode(),
    )
    .await?;
    if shared.is_empty() {
        let _ = send.finish();
        return None; // no shared subnet — nothing to mesh, drop
    }

    let transcripts = build_transcripts(&shared, by_name, dialer_pub, acceptor_pub, nonce_d, nonce_a)?;

    // Read + verify the dialer's proofs first, then send ours (mirror of the
    // dialer's send-then-read, so the ping-pong stays deadlock-free).
    let mut proven = HashSet::new();
    for (name, t, mk) in &transcripts {
        let tag = match read_frame(recv, MAX_FRAME).await.and_then(|b| SeedProofFrame::decode(&b)) {
            Some(SeedProofFrame::Proof { tag }) => tag,
            _ => return None,
        };
        if !t.verify(mk, ProofRole::Dialer, &tag) {
            return None;
        }
        proven.insert(name.clone());
    }
    for (_name, t, mk) in &transcripts {
        let tag = t.tag(mk, ProofRole::Acceptor);
        write_frame(send, &SeedProofFrame::Proof { tag }.encode()).await?;
    }
    let _ = send.finish();
    Some(proven)
}

/// Build `(name, transcript, MK)` for each shared subnet, deriving each `MK`
/// from the local credential's seed + epoch. `None` if a shared name has no
/// local credential (cannot happen after the membership filter, but kept total).
fn build_transcripts(
    shared: &[String],
    by_name: &HashMap<&str, &SubnetCred>,
    dialer_pub: PublicKey,
    acceptor_pub: PublicKey,
    nonce_d: [u8; NONCE_LEN],
    nonce_a: [u8; NONCE_LEN],
) -> Option<Vec<(String, SeedProofTranscript, MembershipKey)>> {
    let mut out = Vec::with_capacity(shared.len());
    for name in shared {
        let cred = by_name.get(name.as_str())?;
        let mk = MembershipKey::derive(&cred.seed, name, cred.epoch);
        let t = SeedProofTranscript {
            subnet_id: name.clone(),
            seed_epoch: cred.epoch,
            dialer_pub,
            acceptor_pub,
            dialer_nonce: nonce_d,
            acceptor_nonce: nonce_a,
        };
        out.push((name.clone(), t, mk));
    }
    Some(out)
}

/// Write a length-delimited frame (`u32` BE length prefix + body).
async fn write_frame(send: &mut SendStream, body: &[u8]) -> Option<()> {
    send.write_all(&(body.len() as u32).to_be_bytes()).await.ok()?;
    send.write_all(body).await.ok()?;
    Some(())
}

/// Read one length-delimited frame, rejecting a length of 0 or > `max`.
async fn read_frame(recv: &mut RecvStream, max: usize) -> Option<Vec<u8>> {
    let mut lenb = [0u8; 4];
    recv.read_exact(&mut lenb).await.ok()?;
    let n = u32::from_be_bytes(lenb) as usize;
    if n == 0 || n > max {
        return None;
    }
    let mut buf = vec![0u8; n];
    recv.read_exact(&mut buf).await.ok()?;
    Some(buf)
}


Now wire it into nethost. First check the daemon lib module list, then edit nethost.


Bash

grep -n "pub mod\|^mod " crates/spt-daemon/src/lib.rs | head -50

List daemon lib modules


106:pub mod access;
107:pub mod adapter_update;
108:pub mod applyhost;
109:pub mod attach;
110:pub mod brain;
111:pub mod broker;
112:pub mod codec;
113:pub mod config;
114:pub mod consent;
115:pub mod daemon;
116:pub mod deelevate;
117:pub mod digest;
118:pub mod digesthub;
119:pub mod dispatch;
120:pub mod effect;
121:pub mod endpoint;
122:pub mod frame;
123:pub mod grants;
124:pub mod lifecycle;
125:pub mod linkhost;
126:pub mod machineid;
127:pub mod msg;
128:pub mod nethost;
129:pub mod notif;
130:pub mod notifsync;
131:pub mod pairhost;
132:pub mod peerloop;
133:pub mod presence;
134:pub mod propagate;
135:pub mod psyrelay;
136:pub mod reconcile;
137:pub mod registryhost;
138:pub mod relay;
139:pub mod relcache;
140:pub mod release;
141:pub mod resthost;
142:pub mod resting;
143:pub mod seedmap;
144:pub mod serveprobe;
145:pub mod shellchan;
146:pub mod shelldisc;
147:pub mod shellhost;
148:pub mod shellwake;
149:pub mod sync;
150:pub mod transport;
151:pub mod update;
152:pub mod wan;
153:pub mod xfer;
156:mod test_home;


Edit

pub mod seedmap;
pub mod seedproofx;
pub mod serveprobe;


<tool_use_error>File has not been read yet. Read it first before writing to it.</tool_use_error>

Error: File has not been read yet. Read it first before writing to it.


143	pub mod seedmap;
144	pub mod serveprobe;
145	pub mod shellchan;

pub mod seedmap;
pub mod serveprobe;
pub mod shellchan;


pub mod seedmap;
pub mod seedproofx;
pub mod serveprobe;


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

pub mod seedmap;
pub mod seedproofx;
pub mod serveprobe;

pub mod resthost;
pub mod resting;
pub mod seedmap;
+pub mod seedproofx;
pub mod serveprobe;
pub mod shellchan;
pub mod shelldisc;


Now edit nethost.rs — imports, NetConfig field, ConnEntry field, register_conn signature, dial + accept-loop proof, accessor, and NetHost storing the membership source:


use std::collections::{HashMap, VecDeque};
use std::io;
use std::sync::atomic::{AtomicU64, Ordering};
use std::sync::{Arc, Mutex};

use spt_net::net::endpoint::{
    BindScope, Connection, EndpointAddr, LocalDiscovery, NetEndpoint, RecvStream, RelayPolicy,
    SendStream, SPT_NET_ALPN,
};
use spt_proto::identity::{Identity, PublicKey};

use crate::broker::SharedSend;
use crate::codec::write_frame;

use std::collections::{HashMap, HashSet, VecDeque};
use std::io;
use std::sync::atomic::{AtomicU64, Ordering};
use std::sync::{Arc, Mutex};

use spt_net::net::endpoint::{
    BindScope, Connection, EndpointAddr, LocalDiscovery, NetEndpoint, RecvStream, RelayPolicy,
    SendStream, SPT_NET_ALPN,
};
use spt_net::net::mesh::seedproof::ProofRole;
use spt_proto::identity::{Identity, PublicKey};

use crate::broker::SharedSend;
use crate::codec::write_frame;
use crate::seedproofx::{prove_membership, MembershipSource};


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

use std::collections::{HashMap, VecDeque};
use std::io;
use std::sync::atomic::{AtomicU64, Ordering};
use std::sync::{Arc, Mutex};

use spt_net::net::endpoint::{
    BindScope, Connection, EndpointAddr, LocalDiscovery, NetEndpoint, RecvStream, RelayPolicy,
    SendStream, SPT_NET_ALPN,
};
use spt_proto::identity::{Identity, PublicKey};

use crate::broker::SharedSend;
use crate::codec::write_frame;

use std::collections::{HashMap, HashSet, VecDeque};
use std::io;
use std::sync::atomic::{AtomicU64, Ordering};
use std::sync::{Arc, Mutex};

use spt_net::net::endpoint::{
    BindScope, Connection, EndpointAddr, LocalDiscovery, NetEndpoint, RecvStream, RelayPolicy,
    SendStream, SPT_NET_ALPN,
};
use spt_net::net::mesh::seedproof::ProofRole;
use spt_proto::identity::{Identity, PublicKey};

use crate::broker::SharedSend;
use crate::codec::write_frame;
use crate::seedproofx::{prove_membership, MembershipSource};

+use std::collections::{HashMap, HashSet, VecDeque};

NetPresenceEvent, NetStreamInfo, PRESENCE_CONNECTED, PRESENCE_DISCONNECTED,


    /// Socket bind scope: [`BindScope::All`] in production;
    /// [`BindScope::Loopback`] in hermetic tests (no real interface touched,
    /// no per-test-binary Windows firewall prompt).
    pub scope: BindScope,
}

    /// Socket bind scope: [`BindScope::All`] in production;
    /// [`BindScope::Loopback`] in hermetic tests (no real interface touched,
    /// no per-test-binary Windows firewall prompt).
    pub scope: BindScope,
    /// Live source of this node's subnet credentials for the connect-time
    /// seed-proof (Mesh-D2, REQ-MESH-1). `Some` enables the mutual proof at
    /// every dial + accept (fail → drop); `None` disables it entirely — both
    /// peers must agree, or the side expecting the control stream would hang.
    /// Production wires `Some(SubnetStore::load …)`; mechanics-only tests that
    /// don't exercise membership pass `None`.
    pub membership: Option<MembershipSource>,
}


    /// Socket bind scope: [`BindScope::All`] in production;
    /// [`BindScope::Loopback`] in hermetic tests (no real interface touched,
    /// no per-test-binary Windows firewall prompt).
    pub scope: BindScope,
}

    /// Socket bind scope: [`BindScope::All`] in production;
    /// [`BindScope::Loopback`] in hermetic tests (no real interface touched,
    /// no per-test-binary Windows firewall prompt).
    pub scope: BindScope,
    /// Live source of this node's subnet credentials for the connect-time
    /// seed-proof (Mesh-D2, REQ-MESH-1). `Some` enables the mutual proof at
    /// every dial + accept (fail → drop); `None` disables it entirely — both
    /// peers must agree, or the side expecting the control stream would hang.
    /// Production wires `Some(SubnetStore::load …)`; mechanics-only tests that
    /// don't exercise membership pass `None`.
    pub membership: Option<MembershipSource>,
}

+    /// Live source of this node's subnet credentials for the connect-time

+    /// seed-proof (Mesh-D2, REQ-MESH-1). `Some` enables the mutual proof at

+    /// every dial + accept (fail → drop); `None` disables it entirely — both

+    /// peers must agree, or the side expecting the control stream would hang.

+    /// Production wires `Some(SubnetStore::load …)`; mechanics-only tests that

+    /// don't exercise membership pass `None`.

+    pub membership: Option<MembershipSource>,

/// Default bounded stream-read ring depth (chunks) — same sizing rationale as


/// One live QUIC connection the broker holds.
struct ConnEntry {
    /// The connection itself — kept alive by ownership here; dropping it closes
    /// the QUIC connection. Cloned (cheap handle) for stream opens.
    conn: Connection,
    /// The remote node's stable name (hex pubkey), proven by the QUIC handshake.
    remote_id_hex: String,
}

/// One live QUIC connection the broker holds.
struct ConnEntry {
    /// The connection itself — kept alive by ownership here; dropping it closes
    /// the QUIC connection. Cloned (cheap handle) for stream opens.
    conn: Connection,
    /// The remote node's stable name (hex pubkey), proven by the QUIC handshake.
    remote_id_hex: String,
    /// The subnets the peer proved current-epoch seed-knowledge of at connect
    /// (Mesh-D2, REQ-MESH-1). Empty when seed-proof is disabled (`membership:
    /// None`). The D5 gate swap reads this in place of `is_trusted(subnet, …)`;
    /// at D2 it is cached but not yet consumed (the `is_trusted` gates still run
    /// in parallel — belt + suspenders, no regression).
    proven_subnets: HashSet<String>,
}