    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,
                roster,
                self_addr,
            )
            .await
        }
        ProofRole::Acceptor => {
            let (mut send, mut recv) = conn.accept_bi().await.ok()?;
            prove_as_acceptor(
                &mut send,
                &mut recv,
                &by_name,
                dialer_pub,
                acceptor_pub,
                roster,
                self_addr,
            )
            .await
        }
    }
}

    let remote = PublicKey::from_bytes(conn.remote_id().as_bytes()).ok()?;
    let remote_hex = remote.to_hex();
    // 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,
                &remote_hex,
                roster,
                self_addr,
            )
            .await
        }
        ProofRole::Acceptor => {
            let (mut send, mut recv) = conn.accept_bi().await.ok()?;
            prove_as_acceptor(
                &mut send,
                &mut recv,
                &by_name,
                dialer_pub,
                acceptor_pub,
                &remote_hex,
                roster,
                self_addr,
            )
            .await
        }
    }
}

/// One subnet's local generations, as the proof exchange needs them: the
/// channel-bound transcript + membership key for the current generation and
/// (one-deep) the prior one (Mesh-D7). The outbound proof set is one tag per
/// generation; the inbound is graded against both.
struct GenProof {
    epoch: u64,
    mk: MembershipKey,
    transcript: SeedProofTranscript,
}

struct LocalGens {
    name: String,
    current: GenProof,
    prev: Option<GenProof>,
}

impl LocalGens {
    /// The `(epoch, tag)` set this node sends for the subnet, proving each
    /// generation it holds in role `mine` (current first).
    fn outbound(&self, mine: ProofRole) -> Vec<(u64, [u8; 32])> {
        let mut out = vec![(self.current.epoch, self.current.transcript.tag(&self.current.mk, mine))];
        if let Some(p) = &self.prev {
            out.push((p.epoch, p.transcript.tag(&p.mk, mine)));
        }
        out
    }

    /// `(matched_current, matched_prev)` for the peer's offered proofs, verified
    /// in the peer's role `prover`. A tag is matched only at the generation whose
    /// epoch it carries — the transcript binds the epoch, so a tag for one
    /// generation can never satisfy another.
    fn matches(&self, peer_proofs: &[(u64, [u8; 32])], prover: ProofRole) -> (bool, bool) {
        let mc = peer_proofs.iter().any(|(e, t)| {
            *e == self.current.epoch && self.current.transcript.verify(&self.current.mk, prover, t)
        });
        let mp = self.prev.as_ref().is_some_and(|p| {
            peer_proofs
                .iter()
                .any(|(e, t)| *e == p.epoch && p.transcript.verify(&p.mk, prover, t))
        });
        (mc, mp)
    }
}

/// Build a [`GenProof`] for one generation (seed + epoch) under the connection's
/// channel binding.
fn gen_proof(
    seed: &[u8],
    name: &str,
    epoch: u64,
    dialer_pub: PublicKey,
    acceptor_pub: PublicKey,
    nonce_d: [u8; NONCE_LEN],
    nonce_a: [u8; NONCE_LEN],
) -> GenProof {
    GenProof {
        epoch,
        mk: MembershipKey::derive(seed, name, epoch),
        transcript: SeedProofTranscript {
            subnet_id: name.to_string(),
            seed_epoch: epoch,
            dialer_pub,
            acceptor_pub,
            dialer_nonce: nonce_d,
            acceptor_nonce: nonce_a,
        },
    }
}

/// Build the per-subnet generations for the shared set (current + one-deep
/// prior). `None` if a shared name has no local credential (cannot happen after
/// the membership filter, but kept total).
fn build_gens(
    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<LocalGens>> {
    let mut out = Vec::with_capacity(shared.len());
    for name in shared {
        let cred = by_name.get(name.as_str())?;
        let current = gen_proof(
            &cred.seed, name, cred.epoch, dialer_pub, acceptor_pub, nonce_d, nonce_a,
        );
        let prev = cred.prev.as_ref().map(|(seed, epoch)| {
            gen_proof(seed, name, *epoch, dialer_pub, acceptor_pub, nonce_d, nonce_a)
        });
        out.push(LocalGens {
            name: name.clone(),
            current,
            prev,
        });
    }
    Some(out)
}

/// This peer's roster status for a subnet, via the (opt-in) roster seam. `None`
/// when no seam is wired — the mechanics-only path grades on epoch alone.
fn peer_status(roster: Option<&RosterExchange>, subnet: &str, peer_hex: &str) -> Option<MemberStatus> {
    roster.map(|rx| (rx.member_status)(subnet, peer_hex))
}

+/// One subnet's local generations, as the proof exchange needs them: the

+/// channel-bound transcript + membership key for the current generation and

+/// (one-deep) the prior one (Mesh-D7). The outbound proof set is one tag per

+/// generation; the inbound is graded against both.

+    /// The `(epoch, tag)` set this node sends for the subnet, proving each

+    /// generation it holds in role `mine` (current first).

+    fn outbound(&self, mine: ProofRole) -> Vec<(u64, [u8; 32])> {

+        let mut out = vec![(self.current.epoch, self.current.transcript.tag(&self.current.mk, mine))];

+            out.push((p.epoch, p.transcript.tag(&p.mk, mine)));

+    /// `(matched_current, matched_prev)` for the peer's offered proofs, verified

+    /// in the peer's role `prover`. A tag is matched only at the generation whose

+    /// epoch it carries — the transcript binds the epoch, so a tag for one

+    /// generation can never satisfy another.

+    fn matches(&self, peer_proofs: &[(u64, [u8; 32])], prover: ProofRole) -> (bool, bool) {

+        let mc = peer_proofs.iter().any(|(e, t)| {

+            *e == self.current.epoch && self.current.transcript.verify(&self.current.mk, prover, t)

+        let mp = self.prev.as_ref().is_some_and(|p| {

+                .any(|(e, t)| *e == p.epoch && p.transcript.verify(&p.mk, prover, t))

+/// Build a [`GenProof`] for one generation (seed + epoch) under the connection's

+        mk: MembershipKey::derive(seed, name, epoch),

+        transcript: SeedProofTranscript {

+            subnet_id: name.to_string(),

+/// Build the per-subnet generations for the shared set (current + one-deep

+/// prior). `None` if a shared name has no local credential (cannot happen after

+/// the membership filter, but kept total).

+    by_name: &HashMap<&str, &SubnetCred>,

+    let mut out = Vec::with_capacity(shared.len());

+        let cred = by_name.get(name.as_str())?;

+            &cred.seed, name, cred.epoch, dialer_pub, acceptor_pub, nonce_d, nonce_a,

+        let prev = cred.prev.as_ref().map(|(seed, epoch)| {

+            gen_proof(seed, name, *epoch, dialer_pub, acceptor_pub, nonce_d, nonce_a)

+/// This peer's roster status for a subnet, via the (opt-in) roster seam. `None`

+/// when no seam is wired — the mechanics-only path grades on epoch alone.

+fn peer_status(roster: Option<&RosterExchange>, subnet: &str, peer_hex: &str) -> Option<MemberStatus> {

+    roster.map(|rx| (rx.member_status)(subnet, peer_hex))


Now replace the two prove functions and `build_transcripts`:


Edit

#[allow(clippy::too_many_arguments)]
async fn prove_as_dialer(
    send: &mut SendStream,
    recv: &mut RecvStream,
    creds: &[SubnetCred],
    by_name: &HashMap<&str, &SubnetCred>,
    dialer_pub: PublicKey,
    acceptor_pub: PublicKey,
    roster: Option<&RosterExchange>,
    self_addr: &serde_json::Value,
) -> 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 (_name, t, mk) in &transcripts {
        let tag = t.tag(mk, ProofRole::Dialer);
        write_frame(send, &SeedProofFrame::Proof { tag }.encode()).await?;
    }
    // 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());
    }
    // Mesh-D4 roster leg (REQ-MESH-2): dialer sends its slice first (it buffers
    // while the acceptor reads), then reads the acceptor's. Best-effort — never
    // fails the already-proven connection.
    exchange_roster_dialer(send, recv, roster, self_addr, &proven).await;
    let _ = send.finish();
    Some(proven)
}

#[allow(clippy::too_many_arguments)]
async fn prove_as_acceptor(
    send: &mut SendStream,
    recv: &mut RecvStream,
    by_name: &HashMap<&str, &SubnetCred>,
    dialer_pub: PublicKey,
    acceptor_pub: PublicKey,
    roster: Option<&RosterExchange>,
    self_addr: &serde_json::Value,
) -> 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?;
    }
    // Mesh-D4 roster leg (REQ-MESH-2): mirror of the dialer — read its slice
    // first, then send ours. Best-effort (see `prove_membership`).
    exchange_roster_acceptor(send, recv, roster, self_addr, &proven).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)
}

/// The graded outcome of an exchange: subnets that proved full, and subnets
/// where THIS node is the fresh side and must push its current seed.
struct Graded {
    full: HashSet<String>,
    reseed: Vec<String>,
}

/// Grade every shared subnet from the peer's proof sets + roster status. The
/// `full`/`reseed` split drives what the legs do; both are derived from the same
/// per-subnet [`grade_subnet`] truth table.
fn grade_all(
    gens: &[LocalGens],
    peer_sets: &[Vec<(u64, [u8; 32])>],
    prover: ProofRole,
    remote_hex: &str,
    roster: Option<&RosterExchange>,
) -> Graded {
    let mut full = HashSet::new();
    let mut reseed = Vec::new();
    for (g, peer_proofs) in gens.iter().zip(peer_sets) {
        let (mc, mp) = g.matches(peer_proofs, prover);
        match grade_subnet(mc, mp, peer_status(roster, &g.name, remote_hex)) {
            Grade::Full => {
                full.insert(g.name.clone());
            }
            Grade::ReseedOnly => reseed.push(g.name.clone()),
            Grade::Denied => {}
        }
    }
    Graded { full, reseed }
}

#[allow(clippy::too_many_arguments)]
async fn prove_as_dialer(
    send: &mut SendStream,
    recv: &mut RecvStream,
    creds: &[SubnetCred],
    by_name: &HashMap<&str, &SubnetCred>,
    dialer_pub: PublicKey,
    acceptor_pub: PublicKey,
    remote_hex: &str,
    roster: Option<&RosterExchange>,
    self_addr: &serde_json::Value,
) -> 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 gens = build_gens(&shared, by_name, dialer_pub, acceptor_pub, nonce_d, nonce_a)?;

    // Send all our (dialer-role) proof sets first — current + prior generation
    // per subnet (Mesh-D7), so a peer one epoch ahead or behind can still match.
    // They buffer while the acceptor reads, so the ping-pong never deadlocks.
    for g in &gens {
        write_frame(send, &SeedProofFrame::ProofSet { proofs: g.outbound(ProofRole::Dialer) }.encode())
            .await?;
    }
    // Then read the acceptor's proof sets (no early abort — grade after the full
    // read so the legs below stay symmetric with the acceptor; tombstone grading
    // is one-sided, so an aborting verify would desync the stream).
    let mut peer_sets = Vec::with_capacity(gens.len());
    for _ in &gens {
        match read_frame(recv, MAX_FRAME).await.and_then(|b| SeedProofFrame::decode(&b)) {
            Some(SeedProofFrame::ProofSet { proofs }) => peer_sets.push(proofs),
            _ => return None,
        }
    }
    let graded = grade_all(&gens, &peer_sets, ProofRole::Acceptor, remote_hex, roster);

    // The legs run over the SHARED set (symmetric on both sides), with
    // grade-filtered content — never gated on the asymmetric full/reseed split.
    // Mesh-D4 roster leg (REQ-MESH-2): dialer sends first, then reads.
    exchange_roster_dialer(send, recv, roster, self_addr, &graded.full).await;
    // Mesh-D7 re-seed grace leg (REQ-MESH-4): push our current seed for subnets
    // where the peer proved only the prior generation; adopt any pushed to us.
    exchange_seed_dialer(send, recv, roster, &graded.reseed, by_name).await;
    let _ = send.finish();
    if graded.full.is_empty() {
        None // nothing fully proven — drop (a pure re-seed conn re-forms full)
    } else {
        Some(graded.full)
    }
}

#[allow(clippy::too_many_arguments)]
async fn prove_as_acceptor(
    send: &mut SendStream,
    recv: &mut RecvStream,
    by_name: &HashMap<&str, &SubnetCred>,
    dialer_pub: PublicKey,
    acceptor_pub: PublicKey,
    remote_hex: &str,
    roster: Option<&RosterExchange>,
    self_addr: &serde_json::Value,
) -> 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 gens = build_gens(&shared, by_name, dialer_pub, acceptor_pub, nonce_d, nonce_a)?;

    // Read the dialer's proof sets first, then send ours (mirror of the dialer's
    // send-then-read, so the ping-pong stays deadlock-free).
    let mut peer_sets = Vec::with_capacity(gens.len());
    for _ in &gens {
        match read_frame(recv, MAX_FRAME).await.and_then(|b| SeedProofFrame::decode(&b)) {
            Some(SeedProofFrame::ProofSet { proofs }) => peer_sets.push(proofs),
            _ => return None,
        }
    }
    for g in &gens {
        write_frame(send, &SeedProofFrame::ProofSet { proofs: g.outbound(ProofRole::Acceptor) }.encode())
            .await?;
    }
    let graded = grade_all(&gens, &peer_sets, ProofRole::Dialer, remote_hex, roster);

    // Mirror of the dialer: read-then-send on each leg (over the shared set).
    exchange_roster_acceptor(send, recv, roster, self_addr, &graded.full).await;
    exchange_seed_acceptor(send, recv, roster, &graded.reseed, by_name).await;
    let _ = send.finish();
    if graded.full.is_empty() {
        None
    } else {
        Some(graded.full)
    }
}


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

#[allow(clippy::too_many_arguments)]
async fn prove_as_dialer(
    send: &mut SendStream,
    recv: &mut RecvStream,
    creds: &[SubnetCred],
    by_name: &HashMap<&str, &SubnetCred>,
    dialer_pub: PublicKey,
    acceptor_pub: PublicKey,
    roster: Option<&RosterExchange>,
    self_addr: &serde_json::Value,
) -> 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 (_name, t, mk) in &transcripts {
        let tag = t.tag(mk, ProofRole::Dialer);
        write_frame(send, &SeedProofFrame::Proof { tag }.encode()).await?;
    }
    // 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());
    }
    // Mesh-D4 roster leg (REQ-MESH-2): dialer sends its slice first (it buffers
    // while the acceptor reads), then reads the acceptor's. Best-effort — never
    // fails the already-proven connection.
    exchange_roster_dialer(send, recv, roster, self_addr, &proven).await;
    let _ = send.finish();
    Some(proven)
}

#[allow(clippy::too_many_arguments)]
async fn prove_as_acceptor(
    send: &mut SendStream,
    recv: &mut RecvStream,
    by_name: &HashMap<&str, &SubnetCred>,
    dialer_pub: PublicKey,
    acceptor_pub: PublicKey,
    roster: Option<&RosterExchange>,
    self_addr: &serde_json::Value,
) -> 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?;
    }
    // Mesh-D4 roster leg (REQ-MESH-2): mirror of the dialer — read its slice
    // first, then send ours. Best-effort (see `prove_membership`).
    exchange_roster_acceptor(send, recv, roster, self_addr, &proven).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)
}

/// The graded outcome of an exchange: subnets that proved full, and subnets
/// where THIS node is the fresh side and must push its current seed.
struct Graded {
    full: HashSet<String>,
    reseed: Vec<String>,
}

/// Grade every shared subnet from the peer's proof sets + roster status. The
/// `full`/`reseed` split drives what the legs do; both are derived from the same
/// per-subnet [`grade_subnet`] truth table.
fn grade_all(
    gens: &[LocalGens],
    peer_sets: &[Vec<(u64, [u8; 32])>],
    prover: ProofRole,
    remote_hex: &str,
    roster: Option<&RosterExchange>,
) -> Graded {
    let mut full = HashSet::new();
    let mut reseed = Vec::new();
    for (g, peer_proofs) in gens.iter().zip(peer_sets) {
        let (mc, mp) = g.matches(peer_proofs, prover);
        match grade_subnet(mc, mp, peer_status(roster, &g.name, remote_hex)) {
            Grade::Full => {
                full.insert(g.name.clone());
            }
            Grade::ReseedOnly => reseed.push(g.name.clone()),
            Grade::Denied => {}
        }
    }
    Graded { full, reseed }
}

#[allow(clippy::too_many_arguments)]
async fn prove_as_dialer(
    send: &mut SendStream,
    recv: &mut RecvStream,
    creds: &[SubnetCred],
    by_name: &HashMap<&str, &SubnetCred>,
    dialer_pub: PublicKey,
    acceptor_pub: PublicKey,
    remote_hex: &str,
    roster: Option<&RosterExchange>,
    self_addr: &serde_json::Value,
) -> 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 gens = build_gens(&shared, by_name, dialer_pub, acceptor_pub, nonce_d, nonce_a)?;

    // Send all our (dialer-role) proof sets first — current + prior generation
    // per subnet (Mesh-D7), so a peer one epoch ahead or behind can still match.
    // They buffer while the acceptor reads, so the ping-pong never deadlocks.
    for g in &gens {
        write_frame(send, &SeedProofFrame::ProofSet { proofs: g.outbound(ProofRole::Dialer) }.encode())
            .await?;
    }
    // Then read the acceptor's proof sets (no early abort — grade after the full
    // read so the legs below stay symmetric with the acceptor; tombstone grading
    // is one-sided, so an aborting verify would desync the stream).
    let mut peer_sets = Vec::with_capacity(gens.len());
    for _ in &gens {
        match read_frame(recv, MAX_FRAME).await.and_then(|b| SeedProofFrame::decode(&b)) {
            Some(SeedProofFrame::ProofSet { proofs }) => peer_sets.push(proofs),
            _ => return None,
        }
    }
    let graded = grade_all(&gens, &peer_sets, ProofRole::Acceptor, remote_hex, roster);

    // The legs run over the SHARED set (symmetric on both sides), with
    // grade-filtered content — never gated on the asymmetric full/reseed split.
    // Mesh-D4 roster leg (REQ-MESH-2): dialer sends first, then reads.
    exchange_roster_dialer(send, recv, roster, self_addr, &graded.full).await;
    // Mesh-D7 re-seed grace leg (REQ-MESH-4): push our current seed for subnets
    // where the peer proved only the prior generation; adopt any pushed to us.
    exchange_seed_dialer(send, recv, roster, &graded.reseed, by_name).await;
    let _ = send.finish();
    if graded.full.is_empty() {
        None // nothing fully proven — drop (a pure re-seed conn re-forms full)
    } else {
        Some(graded.full)
    }
}

#[allow(clippy::too_many_arguments)]
async fn prove_as_acceptor(
    send: &mut SendStream,
    recv: &mut RecvStream,
    by_name: &HashMap<&str, &SubnetCred>,
    dialer_pub: PublicKey,
    acceptor_pub: PublicKey,
    remote_hex: &str,
    roster: Option<&RosterExchange>,
    self_addr: &serde_json::Value,
) -> 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 gens = build_gens(&shared, by_name, dialer_pub, acceptor_pub, nonce_d, nonce_a)?;

    // Read the dialer's proof sets first, then send ours (mirror of the dialer's
    // send-then-read, so the ping-pong stays deadlock-free).
    let mut peer_sets = Vec::with_capacity(gens.len());
    for _ in &gens {
        match read_frame(recv, MAX_FRAME).await.and_then(|b| SeedProofFrame::decode(&b)) {
            Some(SeedProofFrame::ProofSet { proofs }) => peer_sets.push(proofs),
            _ => return None,
        }
    }
    for g in &gens {
        write_frame(send, &SeedProofFrame::ProofSet { proofs: g.outbound(ProofRole::Acceptor) }.encode())
            .await?;
    }
    let graded = grade_all(&gens, &peer_sets, ProofRole::Dialer, remote_hex, roster);

    // Mirror of the dialer: read-then-send on each leg (over the shared set).
    exchange_roster_acceptor(send, recv, roster, self_addr, &graded.full).await;
    exchange_seed_acceptor(send, recv, roster, &graded.reseed, by_name).await;
    let _ = send.finish();
    if graded.full.is_empty() {
        None
    } else {
        Some(graded.full)
    }
}

+/// The graded outcome of an exchange: subnets that proved full, and subnets

+/// where THIS node is the fresh side and must push its current seed.

+/// Grade every shared subnet from the peer's proof sets + roster status. The

+/// `full`/`reseed` split drives what the legs do; both are derived from the same

+/// per-subnet [`grade_subnet`] truth table.

+    for (g, peer_proofs) in gens.iter().zip(peer_sets) {

+        let (mc, mp) = g.matches(peer_proofs, prover);

+        match grade_subnet(mc, mp, peer_status(roster, &g.name, remote_hex)) {

+                full.insert(g.name.clone());

+            Grade::ReseedOnly => reseed.push(g.name.clone()),

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

+    let gens = build_gens(&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 (_name, t, mk) in &transcripts {

-        let tag = t.tag(mk, ProofRole::Dialer);

-        write_frame(send, &SeedProofFrame::Proof { tag }.encode()).await?;

+    // Send all our (dialer-role) proof sets first — current + prior generation

+    // per subnet (Mesh-D7), so a peer one epoch ahead or behind can still match.

+    // They buffer while the acceptor reads, so the ping-pong never deadlocks.

+        write_frame(send, &SeedProofFrame::ProofSet { proofs: g.outbound(ProofRole::Dialer) }.encode())

-    // Then read + verify the acceptor's proofs.

-    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,

+    // Then read the acceptor's proof sets (no early abort — grade after the full

+    // read so the legs below stay symmetric with the acceptor; tombstone grading

+    // is one-sided, so an aborting verify would desync the stream).

+    let mut peer_sets = Vec::with_capacity(gens.len());

+        match read_frame(recv, MAX_FRAME).await.and_then(|b| SeedProofFrame::decode(&b)) {

+            Some(SeedProofFrame::ProofSet { proofs }) => peer_sets.push(proofs),

-        if !t.verify(mk, ProofRole::Acceptor, &tag) {

-            return None; // any agreed subnet failing ⇒ drop the whole connection

-    // Mesh-D4 roster leg (REQ-MESH-2): dialer sends its slice first (it buffers

-    // while the acceptor reads), then reads the acceptor's. Best-effort — never

-    // fails the already-proven connection.

-    exchange_roster_dialer(send, recv, roster, self_addr, &proven).await;

+    let graded = grade_all(&gens, &peer_sets, ProofRole::Acceptor, remote_hex, roster);

+    // The legs run over the SHARED set (symmetric on both sides), with

+    // grade-filtered content — never gated on the asymmetric full/reseed split.

+    // Mesh-D4 roster leg (REQ-MESH-2): dialer sends first, then reads.

+    exchange_roster_dialer(send, recv, roster, self_addr, &graded.full).await;

+    // Mesh-D7 re-seed grace leg (REQ-MESH-4): push our current seed for subnets

+    // where the peer proved only the prior generation; adopt any pushed to us.

+    exchange_seed_dialer(send, recv, roster, &graded.reseed, by_name).await;

+        None // nothing fully proven — drop (a pure re-seed conn re-forms full)

-    // 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).

-    for (name, t, mk) in &transcripts {

-            Some(SeedProofFrame::Proof { tag }) => tag,

+    // Read the dialer's proof sets first, then send ours (mirror of the dialer's

+    // send-then-read, so the ping-pong stays deadlock-free).

-        if !t.verify(mk, ProofRole::Dialer, &tag) {

-    for (_name, t, mk) in &transcripts {

-        let tag = t.tag(mk, ProofRole::Acceptor);

+        write_frame(send, &SeedProofFrame::ProofSet { proofs: g.outbound(ProofRole::Acceptor) }.encode())

-    // Mesh-D4 roster leg (REQ-MESH-2): mirror of the dialer — read its slice

-    // first, then send ours. Best-effort (see `prove_membership`).

-    exchange_roster_acceptor(send, recv, roster, self_addr, &proven).await;

+    let graded = grade_all(&gens, &peer_sets, ProofRole::Dialer, remote_hex, roster);

-/// 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).

-    by_name: &HashMap<&str, &SubnetCred>,

-) -> Option<Vec<(String, SeedProofTranscript, MembershipKey)>> {

-    let mut out = Vec::with_capacity(shared.len());

-        let cred = by_name.get(name.as_str())?;

-        let mk = MembershipKey::derive(&cred.seed, name, cred.epoch);

-        out.push((name.clone(), t, mk));

+    // Mirror of the dialer: read-then-send on each leg (over the shared set).

+    exchange_roster_acceptor(send, recv, roster, self_addr, &graded.full).await;

+    exchange_seed_acceptor(send, recv, roster, &graded.reseed, by_name).await;

/// Write a length-delimited frame (`u32` BE length prefix + body).


Now update the roster legs to always exchange a frame (symmetry), and add the seed-transfer legs. First the roster legs:


async fn exchange_roster_dialer(
    send: &mut SendStream,
    recv: &mut RecvStream,
    roster: Option<&RosterExchange>,
    self_addr: &serde_json::Value,
    proven: &HashSet<String>,
) {
    let Some(rx) = roster else { return };
    if proven.is_empty() {
        return;
    }
    let (entries, tombstones) = (rx.provider)(proven, self_addr);

async fn exchange_roster_dialer(
    send: &mut SendStream,
    recv: &mut RecvStream,
    roster: Option<&RosterExchange>,
    self_addr: &serde_json::Value,
    proven: &HashSet<String>,
) {
    // Gated only on the seam's presence (symmetric on both peers by config), NOT
    // on `proven` — Mesh-D7 grading is one-sided (a tombstone the peer lacks), so
    // an empty-proven side must still exchange the (empty) frame or the stream
    // desyncs against a peer that does. `proven` is empty ⇒ an empty slice.
    let Some(rx) = roster else { return };
    let (entries, tombstones) = (rx.provider)(proven, self_addr);


async fn exchange_roster_dialer(
    send: &mut SendStream,
    recv: &mut RecvStream,
    roster: Option<&RosterExchange>,
    self_addr: &serde_json::Value,
    proven: &HashSet<String>,
) {
    let Some(rx) = roster else { return };
    if proven.is_empty() {
        return;
    }
    let (entries, tombstones) = (rx.provider)(proven, self_addr);

async fn exchange_roster_dialer(
    send: &mut SendStream,
    recv: &mut RecvStream,
    roster: Option<&RosterExchange>,
    self_addr: &serde_json::Value,
    proven: &HashSet<String>,
) {
    // Gated only on the seam's presence (symmetric on both peers by config), NOT
    // on `proven` — Mesh-D7 grading is one-sided (a tombstone the peer lacks), so
    // an empty-proven side must still exchange the (empty) frame or the stream
    // desyncs against a peer that does. `proven` is empty ⇒ an empty slice.
    let Some(rx) = roster else { return };
    let (entries, tombstones) = (rx.provider)(proven, self_addr);

+    // Gated only on the seam's presence (symmetric on both peers by config), NOT

+    // on `proven` — Mesh-D7 grading is one-sided (a tombstone the peer lacks), so

+    // an empty-proven side must still exchange the (empty) frame or the stream

+    // desyncs against a peer that does. `proven` is empty ⇒ an empty slice.

if write_frame(send, &enc_roster(&entries, &tombstones)).await.is_none() {


    let Some(rx) = roster else { return };
    if proven.is_empty() {
        return;
    }
    if let Some(body) = read_frame(recv, MAX_FRAME).await {
        if let Some((rent, rtomb)) = dec_roster(&body) {
            (rx.sink)(rent, rtomb);
        }
    }
    let (entries, tombstones) = (rx.provider)(proven, self_addr);
    let _ = write_frame(send, &enc_roster(&entries, &tombstones)).await;
}

    let Some(rx) = roster else { return };
    if let Some(body) = read_frame(recv, MAX_FRAME).await {
        if let Some((rent, rtomb)) = dec_roster(&body) {
            (rx.sink)(rent, rtomb);
        }
    }
    let (entries, tombstones) = (rx.provider)(proven, self_addr);
    let _ = write_frame(send, &enc_roster(&entries, &tombstones)).await;
}

// ── Mesh-D7: the confidential re-seed grace leg (REQ-MESH-4) ─────────────────
//
// Runs after the roster leg, over the SAME member-auth'd control stream. Each
// side pushes its CURRENT `{seed, epoch}` for every subnet where it graded the
// peer `ReseedOnly` (the peer proved only the prior generation — it is a benign
// member one rotation behind) and adopts any seed the peer pushes to it. The
// seed rides ONLY this leg — never `roster.json`/registry gossip — so a leaked
// gossip record can never carry it (the REQ-MESH-4 secrecy invariant). Gated on
// the seam's presence (symmetric), like the roster leg; the frame is exchanged
// even when empty so the stream never desyncs.

/// One pushed generation: which subnet, the raw seed, and its epoch.
type SeedPush = (String, Vec<u8>, u64);

/// Build this node's push list from the reseed-graded subnets using its CURRENT
/// credential (the generation the behind peer is missing).
fn seed_pushes(reseed: &[String], by_name: &HashMap<&str, &SubnetCred>) -> Vec<SeedPush> {
    reseed
        .iter()
        .filter_map(|n| by_name.get(n.as_str()).map(|c| (n.clone(), c.seed.clone(), c.epoch)))
        .collect()
}

/// The dialer's seed leg: send our pushes, then read + adopt the peer's.
async fn exchange_seed_dialer(
    send: &mut SendStream,
    recv: &mut RecvStream,
    roster: Option<&RosterExchange>,
    reseed: &[String],
    by_name: &HashMap<&str, &SubnetCred>,
) {
    let Some(rx) = roster else { return };
    let pushes = seed_pushes(reseed, by_name);
    if write_frame(send, &enc_seedxfer(&pushes)).await.is_none() {
        return;
    }
    if let Some(body) = read_frame(recv, MAX_FRAME).await {
        if let Some(items) = dec_seedxfer(&body) {
            for (name, seed, epoch) in items {
                (rx.adopt_seed)(&name, &seed, epoch);
            }
        }
    }
}

/// The acceptor's seed leg: read + adopt the peer's, then send ours (mirror).
async fn exchange_seed_acceptor(
    send: &mut SendStream,
    recv: &mut RecvStream,
    roster: Option<&RosterExchange>,
    reseed: &[String],
    by_name: &HashMap<&str, &SubnetCred>,
) {
    let Some(rx) = roster else { return };
    if let Some(body) = read_frame(recv, MAX_FRAME).await {
        if let Some(items) = dec_seedxfer(&body) {
            for (name, seed, epoch) in items {
                (rx.adopt_seed)(&name, &seed, epoch);
            }
        }
    }
    let pushes = seed_pushes(reseed, by_name);
    let _ = write_frame(send, &enc_seedxfer(&pushes)).await;
}

/// Encode the seed-transfer frame: `u32 count` then per item `field(subnet) ‖
/// field(seed) ‖ u64 epoch`. Self-contained positional codec, same discipline as
/// [`enc_roster`]; the seed is raw bytes (it never appears anywhere else on the
/// wire). Counts are never pre-allocated on decode.
// [impl->REQ-MESH-4]
fn enc_seedxfer(items: &[SeedPush]) -> Vec<u8> {
    let mut b = Vec::new();
    b.extend_from_slice(&(items.len() as u32).to_be_bytes());
    for (subnet, seed, epoch) in items {
        push_field(&mut b, subnet.as_bytes());
        push_field(&mut b, seed);
        b.extend_from_slice(&epoch.to_be_bytes());
    }
    b
}

/// Decode a seed-transfer frame. Total: any truncation, non-UTF-8 subnet, or
/// trailing garbage ⇒ `None` (the leg then no-ops — never a partial adopt).
// [impl->REQ-MESH-4]
fn dec_seedxfer(buf: &[u8]) -> Option<Vec<SeedPush>> {
    let mut r = Rdr { b: buf, pos: 0 };
    let n = r.u32()? as usize;
    let mut out = Vec::new();
    for _ in 0..n {
        let subnet = r.utf8()?;
        let seed = r.field()?.to_vec();
        let epoch = r.u64()?;
        out.push((subnet, seed, epoch));
    }
    if r.pos != buf.len() {
        return None;
    }
    Some(out)
}


    let Some(rx) = roster else { return };
    if proven.is_empty() {
        return;
    }
    if let Some(body) = read_frame(recv, MAX_FRAME).await {
        if let Some((rent, rtomb)) = dec_roster(&body) {
            (rx.sink)(rent, rtomb);
        }
    }
    let (entries, tombstones) = (rx.provider)(proven, self_addr);
    let _ = write_frame(send, &enc_roster(&entries, &tombstones)).await;
}

    let Some(rx) = roster else { return };
    if let Some(body) = read_frame(recv, MAX_FRAME).await {
        if let Some((rent, rtomb)) = dec_roster(&body) {
            (rx.sink)(rent, rtomb);
        }
    }
    let (entries, tombstones) = (rx.provider)(proven, self_addr);
    let _ = write_frame(send, &enc_roster(&entries, &tombstones)).await;
}

// ── Mesh-D7: the confidential re-seed grace leg (REQ-MESH-4) ─────────────────
//
// Runs after the roster leg, over the SAME member-auth'd control stream. Each
// side pushes its CURRENT `{seed, epoch}` for every subnet where it graded the
// peer `ReseedOnly` (the peer proved only the prior generation — it is a benign
// member one rotation behind) and adopts any seed the peer pushes to it. The
// seed rides ONLY this leg — never `roster.json`/registry gossip — so a leaked
// gossip record can never carry it (the REQ-MESH-4 secrecy invariant). Gated on
// the seam's presence (symmetric), like the roster leg; the frame is exchanged
// even when empty so the stream never desyncs.

/// One pushed generation: which subnet, the raw seed, and its epoch.
type SeedPush = (String, Vec<u8>, u64);

/// Build this node's push list from the reseed-graded subnets using its CURRENT
/// credential (the generation the behind peer is missing).
fn seed_pushes(reseed: &[String], by_name: &HashMap<&str, &SubnetCred>) -> Vec<SeedPush> {
    reseed
        .iter()
        .filter_map(|n| by_name.get(n.as_str()).map(|c| (n.clone(), c.seed.clone(), c.epoch)))
        .collect()
}

/// The dialer's seed leg: send our pushes, then read + adopt the peer's.
async fn exchange_seed_dialer(
    send: &mut SendStream,
    recv: &mut RecvStream,
    roster: Option<&RosterExchange>,
    reseed: &[String],
    by_name: &HashMap<&str, &SubnetCred>,
) {
    let Some(rx) = roster else { return };
    let pushes = seed_pushes(reseed, by_name);
    if write_frame(send, &enc_seedxfer(&pushes)).await.is_none() {
        return;
    }
    if let Some(body) = read_frame(recv, MAX_FRAME).await {
        if let Some(items) = dec_seedxfer(&body) {
            for (name, seed, epoch) in items {
                (rx.adopt_seed)(&name, &seed, epoch);
            }
        }
    }
}

/// The acceptor's seed leg: read + adopt the peer's, then send ours (mirror).
async fn exchange_seed_acceptor(
    send: &mut SendStream,
    recv: &mut RecvStream,
    roster: Option<&RosterExchange>,
    reseed: &[String],
    by_name: &HashMap<&str, &SubnetCred>,
) {
    let Some(rx) = roster else { return };
    if let Some(body) = read_frame(recv, MAX_FRAME).await {
        if let Some(items) = dec_seedxfer(&body) {
            for (name, seed, epoch) in items {
                (rx.adopt_seed)(&name, &seed, epoch);
            }
        }
    }
    let pushes = seed_pushes(reseed, by_name);
    let _ = write_frame(send, &enc_seedxfer(&pushes)).await;
}

/// Encode the seed-transfer frame: `u32 count` then per item `field(subnet) ‖
/// field(seed) ‖ u64 epoch`. Self-contained positional codec, same discipline as
/// [`enc_roster`]; the seed is raw bytes (it never appears anywhere else on the
/// wire). Counts are never pre-allocated on decode.
// [impl->REQ-MESH-4]
fn enc_seedxfer(items: &[SeedPush]) -> Vec<u8> {
    let mut b = Vec::new();
    b.extend_from_slice(&(items.len() as u32).to_be_bytes());
    for (subnet, seed, epoch) in items {
        push_field(&mut b, subnet.as_bytes());
        push_field(&mut b, seed);
        b.extend_from_slice(&epoch.to_be_bytes());
    }
    b
}

/// Decode a seed-transfer frame. Total: any truncation, non-UTF-8 subnet, or
/// trailing garbage ⇒ `None` (the leg then no-ops — never a partial adopt).
// [impl->REQ-MESH-4]
fn dec_seedxfer(buf: &[u8]) -> Option<Vec<SeedPush>> {
    let mut r = Rdr { b: buf, pos: 0 };
    let n = r.u32()? as usize;
    let mut out = Vec::new();
    for _ in 0..n {
        let subnet = r.utf8()?;
        let seed = r.field()?.to_vec();
        let epoch = r.u64()?;
        out.push((subnet, seed, epoch));
    }
    if r.pos != buf.len() {
        return None;
    }
    Some(out)
}

+// ── Mesh-D7: the confidential re-seed grace leg (REQ-MESH-4) ─────────────────

+// Runs after the roster leg, over the SAME member-auth'd control stream. Each

+// side pushes its CURRENT `{seed, epoch}` for every subnet where it graded the

+// peer `ReseedOnly` (the peer proved only the prior generation — it is a benign

+// member one rotation behind) and adopts any seed the peer pushes to it. The

+// seed rides ONLY this leg — never `roster.json`/registry gossip — so a leaked

+// gossip record can never carry it (the REQ-MESH-4 secrecy invariant). Gated on

+// the seam's presence (symmetric), like the roster leg; the frame is exchanged

+// even when empty so the stream never desyncs.

+/// One pushed generation: which subnet, the raw seed, and its epoch.

+type SeedPush = (String, Vec<u8>, u64);

+/// Build this node's push list from the reseed-graded subnets using its CURRENT

+/// credential (the generation the behind peer is missing).

+fn seed_pushes(reseed: &[String], by_name: &HashMap<&str, &SubnetCred>) -> Vec<SeedPush> {

+        .filter_map(|n| by_name.get(n.as_str()).map(|c| (n.clone(), c.seed.clone(), c.epoch)))

+/// The dialer's seed leg: send our pushes, then read + adopt the peer's.

+    by_name: &HashMap<&str, &SubnetCred>,

+    let Some(rx) = roster else { return };

+    let pushes = seed_pushes(reseed, by_name);

+    if write_frame(send, &enc_seedxfer(&pushes)).await.is_none() {

+    if let Some(body) = read_frame(recv, MAX_FRAME).await {

+        if let Some(items) = dec_seedxfer(&body) {

+            for (name, seed, epoch) in items {

+                (rx.adopt_seed)(&name, &seed, epoch);

+/// The acceptor's seed leg: read + adopt the peer's, then send ours (mirror).

+    let _ = write_frame(send, &enc_seedxfer(&pushes)).await;

+/// Encode the seed-transfer frame: `u32 count` then per item `field(subnet) ‖

+/// field(seed) ‖ u64 epoch`. Self-contained positional codec, same discipline as

+/// [`enc_roster`]; the seed is raw bytes (it never appears anywhere else on the

+/// wire). Counts are never pre-allocated on decode.

+fn enc_seedxfer(items: &[SeedPush]) -> Vec<u8> {

+    b.extend_from_slice(&(items.len() as u32).to_be_bytes());

+    for (subnet, seed, epoch) in items {

+        push_field(&mut b, subnet.as_bytes());

+        b.extend_from_slice(&epoch.to_be_bytes());

+/// Decode a seed-transfer frame. Total: any truncation, non-UTF-8 subnet, or

+/// trailing garbage ⇒ `None` (the leg then no-ops — never a partial adopt).

+fn dec_seedxfer(buf: &[u8]) -> Option<Vec<SeedPush>> {

+    let mut r = Rdr { b: buf, pos: 0 };

+        let seed = r.field()?.to_vec();

+        out.push((subnet, seed, epoch));

/// Append a length-prefixed (`u32` len + bytes) field.

fn push_field(out: &mut Vec<u8>, bytes: &[u8]) {


Now extend the `RosterExchange` struct with the two new seams:


/// The injected roster-exchange seam wired onto a live member connection
/// (Mesh-D4, REQ-MESH-2). `None` (the default) disables the leg entirely — the
/// proof still runs, no roster is exchanged. Cheap to clone (Arc closures).
#[derive(Clone)]
pub struct RosterExchange {
    /// Produces our outbound slice (+ self-address stamp) for the proven subnets.
    pub provider: RosterProvider,
    /// Consumes the peer's inbound slice (merge + dial-cache gap-fill).
    pub sink: RosterSink,
}

/// Looks up a peer's [`MemberStatus`] for a subnet (Mesh-D7 — the re-seed
/// grace gate). Production: a `RosterStore` membership/tombstone lookup.
pub type MemberStatusFn = Arc<dyn Fn(&str, &str) -> MemberStatus + Send + Sync>;

/// Adopts a seed handed over the grace leg (Mesh-D7). Production:
/// `SubnetStore::adopt_rotation` (validates length, ignores non-newer epochs).
/// Returns whether anything was adopted (callers may log).
pub type AdoptSeedFn = Arc<dyn Fn(&str, &[u8], u64) -> bool + Send + Sync>;

/// The injected roster-exchange seam wired onto a live member connection
/// (Mesh-D4, REQ-MESH-2; Mesh-D7 added the grace seams). `None` (the default)
/// disables the roster + grace legs entirely — the proof still runs, graded on
/// epoch alone. Cheap to clone (Arc closures).
#[derive(Clone)]
pub struct RosterExchange {
    /// Produces our outbound slice (+ self-address stamp) for the proven subnets.
    pub provider: RosterProvider,
    /// Consumes the peer's inbound slice (merge + dial-cache gap-fill).
    pub sink: RosterSink,
    /// The peer's per-subnet roster status — gates the re-seed grace (a revoked
    /// peer is denied; an unknown one is never re-seeded). [`REQ-MESH-4`].
    pub member_status: MemberStatusFn,
    /// Adopts a seed the peer pushes over the grace leg (Mesh-D7). [`REQ-MESH-4`].
    pub adopt_seed: AdoptSeedFn,
}


/// The injected roster-exchange seam wired onto a live member connection
/// (Mesh-D4, REQ-MESH-2). `None` (the default) disables the leg entirely — the
/// proof still runs, no roster is exchanged. Cheap to clone (Arc closures).
#[derive(Clone)]
pub struct RosterExchange {
    /// Produces our outbound slice (+ self-address stamp) for the proven subnets.
    pub provider: RosterProvider,
    /// Consumes the peer's inbound slice (merge + dial-cache gap-fill).
    pub sink: RosterSink,
}

/// Looks up a peer's [`MemberStatus`] for a subnet (Mesh-D7 — the re-seed
/// grace gate). Production: a `RosterStore` membership/tombstone lookup.
pub type MemberStatusFn = Arc<dyn Fn(&str, &str) -> MemberStatus + Send + Sync>;

/// Adopts a seed handed over the grace leg (Mesh-D7). Production:
/// `SubnetStore::adopt_rotation` (validates length, ignores non-newer epochs).
/// Returns whether anything was adopted (callers may log).
pub type AdoptSeedFn = Arc<dyn Fn(&str, &[u8], u64) -> bool + Send + Sync>;

/// The injected roster-exchange seam wired onto a live member connection
/// (Mesh-D4, REQ-MESH-2; Mesh-D7 added the grace seams). `None` (the default)
/// disables the roster + grace legs entirely — the proof still runs, graded on
/// epoch alone. Cheap to clone (Arc closures).
#[derive(Clone)]
pub struct RosterExchange {
    /// Produces our outbound slice (+ self-address stamp) for the proven subnets.
    pub provider: RosterProvider,
    /// Consumes the peer's inbound slice (merge + dial-cache gap-fill).
    pub sink: RosterSink,
    /// The peer's per-subnet roster status — gates the re-seed grace (a revoked
    /// peer is denied; an unknown one is never re-seeded). [`REQ-MESH-4`].
    pub member_status: MemberStatusFn,
    /// Adopts a seed the peer pushes over the grace leg (Mesh-D7). [`REQ-MESH-4`].
    pub adopt_seed: AdoptSeedFn,
}

/// cache (gap-fill). Receives the peer's advertised entries + tombstones.

pub type RosterSink = Arc<dyn Fn(Vec<RosterEntry>, Vec<Tombstone>) + Send + Sync>;

+/// Looks up a peer's [`MemberStatus`] for a subnet (Mesh-D7 — the re-seed

+/// grace gate). Production: a `RosterStore` membership/tombstone lookup.

+pub type MemberStatusFn = Arc<dyn Fn(&str, &str) -> MemberStatus + Send + Sync>;

+/// Adopts a seed handed over the grace leg (Mesh-D7). Production:

+/// `SubnetStore::adopt_rotation` (validates length, ignores non-newer epochs).

+/// Returns whether anything was adopted (callers may log).

+pub type AdoptSeedFn = Arc<dyn Fn(&str, &[u8], u64) -> bool + Send + Sync>;

-/// (Mesh-D4, REQ-MESH-2). `None` (the default) disables the leg entirely — the

-/// proof still runs, no roster is exchanged. Cheap to clone (Arc closures).

+/// (Mesh-D4, REQ-MESH-2; Mesh-D7 added the grace seams). `None` (the default)

+/// disables the roster + grace legs entirely — the proof still runs, graded on

+/// epoch alone. Cheap to clone (Arc closures).

+    /// The peer's per-subnet roster status — gates the re-seed grace (a revoked

+    /// peer is denied; an unknown one is never re-seeded). [`REQ-MESH-4`].

+    /// Adopts a seed the peer pushes over the grace leg (Mesh-D7). [`REQ-MESH-4`].

/// The dialer's roster leg: send ours, then read + sink theirs. Best-effort —


Now wire `production_roster_exchange` with the two new seams:


        sink: Arc::new(move |entries: Vec<RosterEntry>, tombstones: Vec<Tombstone>| {
            let mut store = RosterStore::load();
            for e in &entries {
                store.merge_entry(e.clone());
            }
            for t in &tombstones {
                store.tombstone(&t.subnet, &t.pubkey_hex, &t.stamp);
            }
            let _ = store.save();
            let path = spt_store::peeraddrs::peer_addrs_file();
            let mut pa = PeerAddrStore::load_from(&path);
            if gapfill_peeraddrs(&mut pa, &self_hex, &entries) {
                let _ = pa.save_to(&path);
            }
        }),
    }
}

        sink: Arc::new(move |entries: Vec<RosterEntry>, tombstones: Vec<Tombstone>| {
            let mut store = RosterStore::load();
            for e in &entries {
                store.merge_entry(e.clone());
            }
            for t in &tombstones {
                store.tombstone(&t.subnet, &t.pubkey_hex, &t.stamp);
            }
            let _ = store.save();
            let path = spt_store::peeraddrs::peer_addrs_file();
            let mut pa = PeerAddrStore::load_from(&path);
            if gapfill_peeraddrs(&mut pa, &self_hex, &entries) {
                let _ = pa.save_to(&path);
            }
        }),
        // Mesh-D7 (REQ-MESH-4): the peer's roster status gates the grace —
        // tombstoned ⇒ revoked ⇒ denied; listed ⇒ present; otherwise absent.
        // Re-read per call so a just-propagated tombstone takes effect at once.
        member_status: Arc::new(move |subnet: &str, peer_hex: &str| {
            let store = RosterStore::load();
            if store.is_tombstoned(subnet, peer_hex) {
                MemberStatus::Tombstoned
            } else if store.find(subnet, peer_hex).is_some() {
                MemberStatus::Present
            } else {
                MemberStatus::Absent
            }
        }),
        // Mesh-D7 (REQ-MESH-4): adopt a pushed seed into the live subnet store —
        // a strictly-newer epoch only (idempotent, never a downgrade). The 20-byte
        // length is enforced here; a malformed push is ignored.
        adopt_seed: Arc::new(move |subnet: &str, seed: &[u8], epoch: u64| {
            let Ok(bytes) = <[u8; spt_store::subnet::TOTP_SEED_LEN]>::try_from(seed) else {
                return false;
            };
            let mut store = SubnetStore::load();
            match store.adopt_rotation(subnet, bytes, epoch) {
                Ok(true) => store.save().is_ok(),
                _ => false,
            }
        }),
    }
}


        sink: Arc::new(move |entries: Vec<RosterEntry>, tombstones: Vec<Tombstone>| {
            let mut store = RosterStore::load();
            for e in &entries {
                store.merge_entry(e.clone());
            }
            for t in &tombstones {
                store.tombstone(&t.subnet, &t.pubkey_hex, &t.stamp);
            }
            let _ = store.save();
            let path = spt_store::peeraddrs::peer_addrs_file();
            let mut pa = PeerAddrStore::load_from(&path);
            if gapfill_peeraddrs(&mut pa, &self_hex, &entries) {
                let _ = pa.save_to(&path);
            }
        }),
    }
}

        sink: Arc::new(move |entries: Vec<RosterEntry>, tombstones: Vec<Tombstone>| {
            let mut store = RosterStore::load();
            for e in &entries {
                store.merge_entry(e.clone());
            }
            for t in &tombstones {
                store.tombstone(&t.subnet, &t.pubkey_hex, &t.stamp);
            }
            let _ = store.save();
            let path = spt_store::peeraddrs::peer_addrs_file();
            let mut pa = PeerAddrStore::load_from(&path);
            if gapfill_peeraddrs(&mut pa, &self_hex, &entries) {
                let _ = pa.save_to(&path);
            }
        }),
        // Mesh-D7 (REQ-MESH-4): the peer's roster status gates the grace —
        // tombstoned ⇒ revoked ⇒ denied; listed ⇒ present; otherwise absent.
        // Re-read per call so a just-propagated tombstone takes effect at once.
        member_status: Arc::new(move |subnet: &str, peer_hex: &str| {
            let store = RosterStore::load();
            if store.is_tombstoned(subnet, peer_hex) {
                MemberStatus::Tombstoned
            } else if store.find(subnet, peer_hex).is_some() {
                MemberStatus::Present
            } else {
                MemberStatus::Absent
            }
        }),
        // Mesh-D7 (REQ-MESH-4): adopt a pushed seed into the live subnet store —
        // a strictly-newer epoch only (idempotent, never a downgrade). The 20-byte
        // length is enforced here; a malformed push is ignored.
        adopt_seed: Arc::new(move |subnet: &str, seed: &[u8], epoch: u64| {
            let Ok(bytes) = <[u8; spt_store::subnet::TOTP_SEED_LEN]>::try_from(seed) else {
                return false;
            };
            let mut store = SubnetStore::load();
            match store.adopt_rotation(subnet, bytes, epoch) {
                Ok(true) => store.save().is_ok(),
                _ => false,
            }
        }),
    }
}

+        // Mesh-D7 (REQ-MESH-4): the peer's roster status gates the grace —

+        // tombstoned ⇒ revoked ⇒ denied; listed ⇒ present; otherwise absent.

+        // Re-read per call so a just-propagated tombstone takes effect at once.

+        member_status: Arc::new(move |subnet: &str, peer_hex: &str| {

+            let store = RosterStore::load();

+            if store.is_tombstoned(subnet, peer_hex) {

+                MemberStatus::Tombstoned

+            } else if store.find(subnet, peer_hex).is_some() {

+        // Mesh-D7 (REQ-MESH-4): adopt a pushed seed into the live subnet store —

+        // a strictly-newer epoch only (idempotent, never a downgrade). The 20-byte

+        // length is enforced here; a malformed push is ignored.

+        adopt_seed: Arc::new(move |subnet: &str, seed: &[u8], epoch: u64| {

+            let Ok(bytes) = <[u8; spt_store::subnet::TOTP_SEED_LEN]>::try_from(seed) else {

+            let mut store = SubnetStore::load();

+            match store.adopt_rotation(subnet, bytes, epoch) {

+                Ok(true) => store.save().is_ok(),


Now fix the `rosterprop.rs` test constructor of `RosterExchange`:


40
41	/// An in-memory roster exchange over a shared store: the provider stamps our
42	/// live address onto our self-entry (so peers learn how to reach us) and returns
43	/// the proven subnets' slice; the sink merges the peer's slice in. Mirrors
44	/// `production_roster_exchange` minus the file/peeraddrs plumbing.
45	fn test_exchange(store: Arc<Mutex<RosterStore>>, self_hex: String) -> RosterExchange {
46	    let prov_store = Arc::clone(&store);
47	    RosterExchange {
48	        provider: Arc::new(move |proven: &HashSet<String>, self_addr: &serde_json::Value| {
49	            let mut s = prov_store.lock().unwrap();
50	            let addr = if self_addr.is_null() {
51	                None
52	            } else {
53	                Some(self_addr.clone())
54	            };
55	            for subnet in proven {
56	                s.upsert_self(subnet, &self_hex, "lbl", "mid", addr.clone(), "1700000000", 5);
57	            }
58	            let mut entries = Vec::new();
59	            let mut tombs = Vec::new();
60	            for subnet in proven {
61	                let (m, t) = s.roster_for(subnet);
62	                entries.extend(m);
63	                tombs.extend(t);
64	            }
65	            (entries, tombs)
66	        }),
67	        sink: Arc::new(move |entries, tombs| {
68	            let mut s = store.lock().unwrap();
69	            for e in &entries {
70	                s.merge_entry(e.clone());
71	            }
72	            for t in &tombs {
73	                s.tombstone(&t.subnet, &t.pubkey_hex, &t.stamp);
74	            }
75	        }),
76	    }
77	}
78
79	fn host(identity: Identity, creds: Vec<SubnetCred>) -> NetHost {
80	    NetHost::start(NetConfig {
81	        identity,
82	        relay: RelayPolicy::Disabled,
83	        discovery: LocalDiscovery::Off,
84	        scope: BindScope::Loopback,
85	        membership: Some(membership(creds)),
86	    })
87	    .expect("net host start")
88	}
89
90	fn wait_until(mut f: impl FnMut() -> bool) -> bool {
91	    for _ in 0..400 {
92	        if f() {
93	            return true;
94	        }
95	        std::thread::sleep(Duration::from_millis(5));
96	    }
97	    false
98	}
99
100	// [int->REQ-MESH-2] B knows an offline C; A connects B; A learns C (transitive
101	// directory growth) with an address (reach precondition), and B learns A — the
102	// roster converges over a proven member connection, no row ever relayed.
103	#[test]
104	fn roster_propagates_transitively_on_connect() {
105	    let a_id = Identity::generate();
106	    let b_id = Identity::generate();
107	    let a_hex = a_id.public_key().to_hex();
108	    let b_hex = b_id.public_key().to_hex();
109	    let c_hex = Identity::from_seed(&[0xC5; 32]).public_key().to_hex(); // offline third node
110
111	    let store_a = Arc::new(Mutex::new(RosterStore::default()));
112	    let store_b = Arc::new(Mutex::new(RosterStore::default()));
113
114	    // B already holds C in its roster (a member it paired with while A was
115	    // offline) — C carries a dialable address but is not connected here.
116	    let c_addr = serde_json::json!({"id": c_hex, "addrs": ["10.9.9.9:4711"]});
117	    store_b.lock().unwrap().merge_entry(RosterEntry {
118	        pubkey_hex: c_hex.clone(),
119	        subnet: "home".to_string(),
120	        label: "c-host".to_string(),
121	        machine_id: "c-mid".to_string(),
122	        address: Some(c_addr.clone()),
123	        last_seen: "1699999999".to_string(),
124	        lease_epoch: 2,
125	    });
126
127	    let a = host(a_id, vec![cred("home", 7, 1)]);
128	    let b = host(b_id, vec![cred("home", 7, 1)]);
129	    a.set_roster_exchange(test_exchange(Arc::clone(&store_a), a_hex.clone()));

/// An in-memory roster exchange over a shared store: the provider stamps our
/// live address onto our self-entry (so peers learn how to reach us) and returns
/// the proven subnets' slice; the sink merges the peer's slice in. Mirrors
/// `production_roster_exchange` minus the file/peeraddrs plumbing.
fn test_exchange(store: Arc<Mutex<RosterStore>>, self_hex: String) -> RosterExchange {
    let prov_store = Arc::clone(&store);
    RosterExchange {
        provider: Arc::new(move |proven: &HashSet<String>, self_addr: &serde_json::Value| {
            let mut s = prov_store.lock().unwrap();
            let addr = if self_addr.is_null() {
                None
            } else {
                Some(self_addr.clone())
            };
            for subnet in proven {
                s.upsert_self(subnet, &self_hex, "lbl", "mid", addr.clone(), "1700000000", 5);
            }
            let mut entries = Vec::new();
            let mut tombs = Vec::new();
            for subnet in proven {
                let (m, t) = s.roster_for(subnet);
                entries.extend(m);
                tombs.extend(t);
            }
            (entries, tombs)
        }),
        sink: Arc::new(move |entries, tombs| {
            let mut s = store.lock().unwrap();
            for e in &entries {
                s.merge_entry(e.clone());
            }
            for t in &tombs {
                s.tombstone(&t.subnet, &t.pubkey_hex, &t.stamp);
            }
        }),
    }
}

fn host(identity: Identity, creds: Vec<SubnetCred>) -> NetHost {
    NetHost::start(NetConfig {
        identity,
        relay: RelayPolicy::Disabled,
        discovery: LocalDiscovery::Off,
        scope: BindScope::Loopback,
        membership: Some(membership(creds)),
    })
    .expect("net host start")
}

fn wait_until(mut f: impl FnMut() -> bool) -> bool {
    for _ in 0..400 {
        if f() {
            return true;
        }
        std::thread::sleep(Duration::from_millis(5));
    }
    false
}

// [int->REQ-MESH-2] B knows an offline C; A connects B; A learns C (transitive
// directory growth) with an address (reach precondition), and B learns A — the
// roster converges over a proven member connection, no row ever relayed.
#[test]
fn roster_propagates_transitively_on_connect() {
    let a_id = Identity::generate();
    let b_id = Identity::generate();
    let a_hex = a_id.public_key().to_hex();
    let b_hex = b_id.public_key().to_hex();
    let c_hex = Identity::from_seed(&[0xC5; 32]).public_key().to_hex(); // offline third node

    let store_a = Arc::new(Mutex::new(RosterStore::default()));
    let store_b = Arc::new(Mutex::new(RosterStore::default()));

    // B already holds C in its roster (a member it paired with while A was
    // offline) — C carries a dialable address but is not connected here.
    let c_addr = serde_json::json!({"id": c_hex, "addrs": ["10.9.9.9:4711"]});
    store_b.lock().unwrap().merge_entry(RosterEntry {
        pubkey_hex: c_hex.clone(),
        subnet: "home".to_string(),
        label: "c-host".to_string(),
        machine_id: "c-mid".to_string(),
        address: Some(c_addr.clone()),
        last_seen: "1699999999".to_string(),
        lease_epoch: 2,
    });

    let a = host(a_id, vec![cred("home", 7, 1)]);
    let b = host(b_id, vec![cred("home", 7, 1)]);
    a.set_roster_exchange(test_exchange(Arc::clone(&store_a), a_hex.clone()));


29:fn cred(name: &str, s: u8, epoch: u64) -> SubnetCred {
30-    SubnetCred {
31-        name: name.to_string(),
32-        seed: seed(s),
33-        epoch,
34-    }
35-}
36-


fn cred(name: &str, s: u8, epoch: u64) -> SubnetCred {
    SubnetCred {
        name: name.to_string(),
        seed: seed(s),
        epoch,
    }
}

fn cred(name: &str, s: u8, epoch: u64) -> SubnetCred {
    SubnetCred {
        name: name.to_string(),
        seed: seed(s),
        epoch,
        prev: None,
    }
}


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

fn cred(name: &str, s: u8, epoch: u64) -> SubnetCred {
    SubnetCred {
        name: name.to_string(),
        seed: seed(s),
        epoch,
        prev: None,
    }
}

//! Mesh-D4 — on-connect roster propagation, the integration evidence
//! (REQ-MESH-2 int).
//!
//! Two in-process `NetHost`s dial over hermetic loopback QUIC with the seed-proof
//! membership source AND a roster exchange wired. The roster leg rides the
//! **same** proven control stream (Mesh-D2's), so it runs only after both sides
//! prove. We model the canonical mesh gap: B already knows an **offline** third
//! node C (subnet member, never directly paired to A). When A connects B, A must
//! **learn C** (the transitive directory growth REQ-MESH-2 owns) AND learn an
//! address for both B and C (the reach precondition Mesh-D6 consumes) — with B
//! never relaying a registry row. The store here is an in-memory `RosterStore`
//! per host (the file-backed `production_roster_exchange` is exercised by its own
//! unit tests); this test proves the *propagation over the wire*.

use std::collections::HashSet;
use std::sync::{Arc, Mutex};
use std::time::Duration;

use spt_daemon::nethost::{NetConfig, NetHost};
use spt_daemon::seedproofx::{MembershipSource, RosterExchange, SubnetCred};
use spt_net::net::endpoint::{BindScope, LocalDiscovery, RelayPolicy};
use spt_proto::identity::Identity;
use spt_store::roster::{RosterEntry, RosterStore};

fn seed(b: u8) -> Vec<u8> {
    vec![b; 20]
}

fn cred(name: &str, s: u8, epoch: u64) -> SubnetCred {
    SubnetCred {
        name: name.to_string(),
        seed: seed(s),
        epoch,
    }
}

fn membership(creds: Vec<SubnetCred>) -> MembershipSource {
    Arc::new(move || creds.clone())
}

/// An in-memory roster exchange over a shared store: the provider stamps our
/// live address onto our self-entry (so peers learn how to reach us) and returns
/// the proven subnets' slice; the sink merges the peer's slice in. Mirrors
/// `production_roster_exchange` minus the file/peeraddrs plumbing.
fn test_exchange(store: Arc<Mutex<RosterStore>>, self_hex: String) -> RosterExchange {
    let prov_store = Arc::clone(&store);
    RosterExchange {
        provider: Arc::new(move |proven: &HashSet<String>, self_addr: &serde_json::Value| {
            let mut s = prov_store.lock().unwrap();
            let addr = if self_addr.is_null() {
                None
            } else {
                Some(self_addr.clone())
            };
            for subnet in proven {
                s.upsert_self(subnet, &self_hex, "lbl", "mid", addr.clone(), "1700000000", 5);
            }
            let mut entries = Vec::new();
            let mut tombs = Vec::new();
            for subnet in proven {
                let (m, t) = s.roster_for(subnet);
                entries.extend(m);
                tombs.extend(t);
            }
            (entries, tombs)
        }),
        sink: Arc::new(move |entries, tombs| {
            let mut s = store.lock().unwrap();
            for e in &entries {
                s.merge_entry(e.clone());
            }
            for t in &tombs {
                s.tombstone(&t.subnet, &t.pubkey_hex, &t.stamp);
            }
        }),
    }
}

fn host(identity: Identity, creds: Vec<SubnetCred>) -> NetHost {
    NetHost::start(NetConfig {
        identity,
        relay: RelayPolicy::Disabled,
        discovery: LocalDiscovery::Off,
        scope: BindScope::Loopback,
        membership: Some(membership(creds)),
    })
    .expect("net host start")
}

fn wait_until(mut f: impl FnMut() -> bool) -> bool {
    for _ in 0..400 {
        if f() {
            return true;
        }
        std::thread::sleep(Duration::from_millis(5));
    }
    false
}

// [int->REQ-MESH-2] B knows an offline C; A connects B; A learns C (transitive
// directory growth) with an address (reach precondition), and B learns A — the
// roster converges over a proven member connection, no row ever relayed.
#[test]
fn roster_propagates_transitively_on_connect() {
    let a_id = Identity::generate();
    let b_id = Identity::generate();
    let a_hex = a_id.public_key().to_hex();
    let b_hex = b_id.public_key().to_hex();
    let c_hex = Identity::from_seed(&[0xC5; 32]).public_key().to_hex(); // offline third node

    let store_a = Arc::new(Mutex::new(RosterStore::default()));
    let store_b = Arc::new(Mutex::new(RosterStore::default()));

    // B already holds C in its roster (a member it paired with while A was
    // offline) — C carries a dialable address but is not connected here.
    let c_addr = serde_json::json!({"id": c_hex, "addrs": ["10.9.9.9:4711"]});
    store_b.lock().unwrap().merge_entry(RosterEntry {
        pubkey_hex: c_hex.clone(),
        subnet: "home".to_string(),
        label: "c-host".to_string(),
        machine_id: "c-mid".to_string(),
        address: Some(c_addr.clone()),
        last_seen: "1699999999".to_string(),
        lease_epoch: 2,
    });

    let a = host(a_id, vec![cred("home", 7, 1)]);
    let b = host(b_id, vec![cred("home", 7, 1)]);
    a.set_roster_exchange(test_exchange(Arc::clone(&store_a), a_hex.clone()));
    b.set_roster_exchange(test_exchange(Arc::clone(&store_b), b_hex.clone()));

    // A dials B: the proof verifies, then the roster leg runs on the same stream.
    // dial() blocks through the dialer leg, so A's sink has merged B's slice by
    // the time it returns.
    let (_conn_id, remote) = a.dial(b.addr()).expect("dial member");
    assert_eq!(remote, b_hex, "handshake proves B");

    let sa = store_a.lock().unwrap();
    assert!(
        sa.is_member("home", &c_hex),
        "A learned the offline C transitively via B's roster"
    );
    assert_eq!(
        sa.find("home", &c_hex).and_then(|e| e.address.clone()),
        Some(c_addr),
        "A learned C's advertised address (the Mesh-D6 reach precondition)"
    );
    assert!(sa.is_member("home", &b_hex), "A learned B (its self-entry)");
    assert!(
        sa.find("home", &b_hex)
            .map(|e| e.address.is_some())
            .unwrap_or(false),
        "B's entry carries B's live address (the self-address advertise flow)"
    );
    drop(sa);

    // The acceptor's roster leg runs on its spawned task — poll until B's store
    // has A (the reverse direction of the mutual exchange).
    assert!(
        wait_until(|| store_b.lock().unwrap().is_member("home", &a_hex)),
        "B learned A over the same proven connection"
    );
}

// [int->REQ-MESH-2] the leak defense end-to-end: a roster exchange only emits the
// PROVEN subnet's slice. A and B share only `home`; B also holds a `work` member
// D that A must never receive (A is not a `work` member, so `work` is not proven).
#[test]
fn roster_exchange_never_leaks_unproven_subnets() {
    let a_id = Identity::generate();
    let b_id = Identity::generate();
    let a_hex = a_id.public_key().to_hex();
    let b_hex = b_id.public_key().to_hex();
    let d_hex = Identity::from_seed(&[0xD4; 32]).public_key().to_hex();

    let store_a = Arc::new(Mutex::new(RosterStore::default()));
    let store_b = Arc::new(Mutex::new(RosterStore::default()));

    // B holds D in `work` — a subnet A is not a member of.
    store_b.lock().unwrap().merge_entry(RosterEntry {
        pubkey_hex: d_hex.clone(),
        subnet: "work".to_string(),
        label: "d".to_string(),
        machine_id: "d".to_string(),
        address: None,
        last_seen: "1699999999".to_string(),
        lease_epoch: 1,
    });

    // A: home only. B: home + work. Only `home` is mutually proven.
    let a = host(a_id, vec![cred("home", 7, 1)]);
    let b = host(b_id, vec![cred("home", 7, 1), cred("work", 8, 1)]);
    a.set_roster_exchange(test_exchange(Arc::clone(&store_a), a_hex));
    b.set_roster_exchange(test_exchange(Arc::clone(&store_b), b_hex.clone()));

    let _ = a.dial(b.addr()).expect("dial");

    let sa = store_a.lock().unwrap();
    assert!(sa.is_member("home", &b_hex), "A still learned B in home");
    assert!(
        sa.find("work", &d_hex).is_none(),
        "A never received the unproven `work` roster (no cross-subnet leak)"
    );
}

name: name.to_string(),
seed: seed(s),
epoch,
+        prev: None,
}
}


        sink: Arc::new(move |entries, tombs| {
            let mut s = store.lock().unwrap();
            for e in &entries {
                s.merge_entry(e.clone());
            }
            for t in &tombs {
                s.tombstone(&t.subnet, &t.pubkey_hex, &t.stamp);
            }
        }),
    }
}

        sink: Arc::new(move |entries, tombs| {
            let mut s = sink_store.lock().unwrap();
            for e in &entries {
                s.merge_entry(e.clone());
            }
            for t in &tombs {
                s.tombstone(&t.subnet, &t.pubkey_hex, &t.stamp);
            }
        }),
        // D7 grace seams — same-epoch tests never trigger a re-seed, so status
        // reads the shared store and adopt is a no-op.
        member_status: Arc::new(move |subnet: &str, hex: &str| {
            let s = status_store.lock().unwrap();
            if s.is_tombstoned(subnet, hex) {
                spt_daemon::seedproofx::MemberStatus::Tombstoned
            } else if s.find(subnet, hex).is_some() {
                spt_daemon::seedproofx::MemberStatus::Present
            } else {
                spt_daemon::seedproofx::MemberStatus::Absent
            }
        }),
        adopt_seed: Arc::new(|_subnet: &str, _seed: &[u8], _epoch: u64| false),
    }
}


        sink: Arc::new(move |entries, tombs| {
            let mut s = sink_store.lock().unwrap();
            for e in &entries {
                s.merge_entry(e.clone());
            }
            for t in &tombs {
                s.tombstone(&t.subnet, &t.pubkey_hex, &t.stamp);
            }
        }),
        // D7 grace seams — same-epoch tests never trigger a re-seed, so status
        // reads the shared store and adopt is a no-op.
        member_status: Arc::new(move |subnet: &str, hex: &str| {
            let s = status_store.lock().unwrap();
            if s.is_tombstoned(subnet, hex) {
                spt_daemon::seedproofx::MemberStatus::Tombstoned
            } else if s.find(subnet, hex).is_some() {
                spt_daemon::seedproofx::MemberStatus::Present
            } else {
                spt_daemon::seedproofx::MemberStatus::Absent
            }
        }),
        adopt_seed: Arc::new(|_subnet: &str, _seed: &[u8], _epoch: u64| false),
    }
}

//! Mesh-D4 — on-connect roster propagation, the integration evidence
//! (REQ-MESH-2 int).
//!
//! Two in-process `NetHost`s dial over hermetic loopback QUIC with the seed-proof
//! membership source AND a roster exchange wired. The roster leg rides the
//! **same** proven control stream (Mesh-D2's), so it runs only after both sides
//! prove. We model the canonical mesh gap: B already knows an **offline** third
//! node C (subnet member, never directly paired to A). When A connects B, A must
//! **learn C** (the transitive directory growth REQ-MESH-2 owns) AND learn an
//! address for both B and C (the reach precondition Mesh-D6 consumes) — with B
//! never relaying a registry row. The store here is an in-memory `RosterStore`
//! per host (the file-backed `production_roster_exchange` is exercised by its own
//! unit tests); this test proves the *propagation over the wire*.

use std::collections::HashSet;
use std::sync::{Arc, Mutex};
use std::time::Duration;

use spt_daemon::nethost::{NetConfig, NetHost};
use spt_daemon::seedproofx::{MembershipSource, RosterExchange, SubnetCred};
use spt_net::net::endpoint::{BindScope, LocalDiscovery, RelayPolicy};
use spt_proto::identity::Identity;
use spt_store::roster::{RosterEntry, RosterStore};

fn seed(b: u8) -> Vec<u8> {
    vec![b; 20]
}

fn cred(name: &str, s: u8, epoch: u64) -> SubnetCred {
    SubnetCred {
        name: name.to_string(),
        seed: seed(s),
        epoch,
        prev: None,
    }
}

fn membership(creds: Vec<SubnetCred>) -> MembershipSource {
    Arc::new(move || creds.clone())
}

/// An in-memory roster exchange over a shared store: the provider stamps our
/// live address onto our self-entry (so peers learn how to reach us) and returns
/// the proven subnets' slice; the sink merges the peer's slice in. Mirrors
/// `production_roster_exchange` minus the file/peeraddrs plumbing.
fn test_exchange(store: Arc<Mutex<RosterStore>>, self_hex: String) -> RosterExchange {
    let prov_store = Arc::clone(&store);
    RosterExchange {
        provider: Arc::new(move |proven: &HashSet<String>, self_addr: &serde_json::Value| {
            let mut s = prov_store.lock().unwrap();
            let addr = if self_addr.is_null() {
                None
            } else {
                Some(self_addr.clone())
            };
            for subnet in proven {
                s.upsert_self(subnet, &self_hex, "lbl", "mid", addr.clone(), "1700000000", 5);
            }
            let mut entries = Vec::new();
            let mut tombs = Vec::new();
            for subnet in proven {
                let (m, t) = s.roster_for(subnet);
                entries.extend(m);
                tombs.extend(t);
            }
            (entries, tombs)
        }),
        sink: Arc::new(move |entries, tombs| {
            let mut s = store.lock().unwrap();
            for e in &entries {
                s.merge_entry(e.clone());
            }
            for t in &tombs {
                s.tombstone(&t.subnet, &t.pubkey_hex, &t.stamp);
            }
        }),
    }
}

fn host(identity: Identity, creds: Vec<SubnetCred>) -> NetHost {
    NetHost::start(NetConfig {
        identity,
        relay: RelayPolicy::Disabled,
        discovery: LocalDiscovery::Off,
        scope: BindScope::Loopback,
        membership: Some(membership(creds)),
    })
    .expect("net host start")
}

fn wait_until(mut f: impl FnMut() -> bool) -> bool {
    for _ in 0..400 {
        if f() {
            return true;
        }
        std::thread::sleep(Duration::from_millis(5));
    }
    false
}

// [int->REQ-MESH-2] B knows an offline C; A connects B; A learns C (transitive
// directory growth) with an address (reach precondition), and B learns A — the
// roster converges over a proven member connection, no row ever relayed.
#[test]
fn roster_propagates_transitively_on_connect() {
    let a_id = Identity::generate();
    let b_id = Identity::generate();
    let a_hex = a_id.public_key().to_hex();
    let b_hex = b_id.public_key().to_hex();
    let c_hex = Identity::from_seed(&[0xC5; 32]).public_key().to_hex(); // offline third node

    let store_a = Arc::new(Mutex::new(RosterStore::default()));
    let store_b = Arc::new(Mutex::new(RosterStore::default()));

    // B already holds C in its roster (a member it paired with while A was
    // offline) — C carries a dialable address but is not connected here.
    let c_addr = serde_json::json!({"id": c_hex, "addrs": ["10.9.9.9:4711"]});
    store_b.lock().unwrap().merge_entry(RosterEntry {
        pubkey_hex: c_hex.clone(),
        subnet: "home".to_string(),
        label: "c-host".to_string(),
        machine_id: "c-mid".to_string(),
        address: Some(c_addr.clone()),
        last_seen: "1699999999".to_string(),
        lease_epoch: 2,
    });

    let a = host(a_id, vec![cred("home", 7, 1)]);
    let b = host(b_id, vec![cred("home", 7, 1)]);
    a.set_roster_exchange(test_exchange(Arc::clone(&store_a), a_hex.clone()));
    b.set_roster_exchange(test_exchange(Arc::clone(&store_b), b_hex.clone()));

    // A dials B: the proof verifies, then the roster leg runs on the same stream.
    // dial() blocks through the dialer leg, so A's sink has merged B's slice by
    // the time it returns.
    let (_conn_id, remote) = a.dial(b.addr()).expect("dial member");
    assert_eq!(remote, b_hex, "handshake proves B");

    let sa = store_a.lock().unwrap();
    assert!(
        sa.is_member("home", &c_hex),
        "A learned the offline C transitively via B's roster"
    );
    assert_eq!(
        sa.find("home", &c_hex).and_then(|e| e.address.clone()),
        Some(c_addr),
        "A learned C's advertised address (the Mesh-D6 reach precondition)"
    );
    assert!(sa.is_member("home", &b_hex), "A learned B (its self-entry)");
    assert!(
        sa.find("home", &b_hex)
            .map(|e| e.address.is_some())
            .unwrap_or(false),
        "B's entry carries B's live address (the self-address advertise flow)"
    );
    drop(sa);

    // The acceptor's roster leg runs on its spawned task — poll until B's store
    // has A (the reverse direction of the mutual exchange).
    assert!(
        wait_until(|| store_b.lock().unwrap().is_member("home", &a_hex)),
        "B learned A over the same proven connection"
    );
}

// [int->REQ-MESH-2] the leak defense end-to-end: a roster exchange only emits the
// PROVEN subnet's slice. A and B share only `home`; B also holds a `work` member
// D that A must never receive (A is not a `work` member, so `work` is not proven).
#[test]
fn roster_exchange_never_leaks_unproven_subnets() {
    let a_id = Identity::generate();
    let b_id = Identity::generate();
    let a_hex = a_id.public_key().to_hex();
    let b_hex = b_id.public_key().to_hex();
    let d_hex = Identity::from_seed(&[0xD4; 32]).public_key().to_hex();

    let store_a = Arc::new(Mutex::new(RosterStore::default()));
    let store_b = Arc::new(Mutex::new(RosterStore::default()));

    // B holds D in `work` — a subnet A is not a member of.
    store_b.lock().unwrap().merge_entry(RosterEntry {
        pubkey_hex: d_hex.clone(),
        subnet: "work".to_string(),
        label: "d".to_string(),
        machine_id: "d".to_string(),
        address: None,
        last_seen: "1699999999".to_string(),
        lease_epoch: 1,
    });

    // A: home only. B: home + work. Only `home` is mutually proven.
    let a = host(a_id, vec![cred("home", 7, 1)]);
    let b = host(b_id, vec![cred("home", 7, 1), cred("work", 8, 1)]);
    a.set_roster_exchange(test_exchange(Arc::clone(&store_a), a_hex));
    b.set_roster_exchange(test_exchange(Arc::clone(&store_b), b_hex.clone()));

    let _ = a.dial(b.addr()).expect("dial");

    let sa = store_a.lock().unwrap();
    assert!(sa.is_member("home", &b_hex), "A still learned B in home");
    assert!(
        sa.find("work", &d_hex).is_none(),
        "A never received the unproven `work` roster (no cross-subnet leak)"
    );
}

-            let mut s = store.lock().unwrap();

+            let mut s = sink_store.lock().unwrap();

+        // D7 grace seams — same-epoch tests never trigger a re-seed, so status

+        // reads the shared store and adopt is a no-op.

+        member_status: Arc::new(move |subnet: &str, hex: &str| {

+            let s = status_store.lock().unwrap();

+            if s.is_tombstoned(subnet, hex) {

+                spt_daemon::seedproofx::MemberStatus::Tombstoned

+            } else if s.find(subnet, hex).is_some() {

+                spt_daemon::seedproofx::MemberStatus::Present

+                spt_daemon::seedproofx::MemberStatus::Absent

+        adopt_seed: Arc::new(|_subnet: &str, _seed: &[u8], _epoch: u64| false),


fn test_exchange(store: Arc<Mutex<RosterStore>>, self_hex: String) -> RosterExchange {
    let prov_store = Arc::clone(&store);
    RosterExchange {

fn test_exchange(store: Arc<Mutex<RosterStore>>, self_hex: String) -> RosterExchange {
    let prov_store = Arc::clone(&store);
    let sink_store = Arc::clone(&store);
    let status_store = Arc::clone(&store);
    RosterExchange {