/// Decode the trailing roster block of a `Seed` frame (REQ-MESH-2): a u32 member
/// count + entries, then a u32 tombstone count + entries. Entries carry an empty
/// `subnet` (implicit; the joiner stamps it). Counts are never pre-allocated — a
/// hostile count exhausts the buffer on the first short field and errors
/// "truncated", not a huge allocation. Every field is bounds-checked by [`Cursor`].
fn dec_roster(c: &mut Cursor) -> Result<(Vec<RosterEntry>, Vec<Tombstone>), PairWireError> {
    let member_count = u32::from_be_bytes(c.take(4)?.try_into().expect("took 4"));
    let mut roster = Vec::new();
    for _ in 0..member_count {
        let pubkey_hex = dec_utf8(c.field()?, "roster pubkey")?;
        let label = dec_utf8(c.field()?, "roster label")?;
        let machine_id = dec_utf8(c.field()?, "roster machine_id")?;
        let addr_bytes = c.field()?;
        let address = if addr_bytes.is_empty() {
            None
        } else {
            Some(
                serde_json::from_slice(addr_bytes)
                    .map_err(|_| PairWireError::Protocol("roster address not json".into()))?,
            )
        };
        let last_seen = dec_utf8(c.field()?, "roster last_seen")?;
        let lease_epoch = c.u64()?;
        roster.push(RosterEntry {
            pubkey_hex,
            subnet: String::new(), // implicit — stamped by the joiner on adopt
            label,
            machine_id,
            address,
            last_seen,
            lease_epoch,
        });
    }
    let tomb_count = u32::from_be_bytes(c.take(4)?.try_into().expect("took 4"));
    let mut tombstones = Vec::new();
    for _ in 0..tomb_count {
        let pubkey_hex = dec_utf8(c.field()?, "tombstone pubkey")?;
        let stamp = dec_utf8(c.field()?, "tombstone stamp")?;
        tombstones.push(Tombstone {
            pubkey_hex,
            subnet: String::new(),
            stamp,
        });
    }
    Ok((roster, tombstones))
}

/// Decode a length-prefixed field as UTF-8, mapping a bad encoding to a labelled
/// protocol error (never a panic — the input is attacker-reachable).
fn dec_utf8(bytes: &[u8], what: &str) -> Result<String, PairWireError> {
    String::from_utf8(bytes.to_vec())
        .map_err(|_| PairWireError::Protocol(format!("{what} not utf-8")))
}

fn enc_abort(reason: u8, retry_after: u64) -> Vec<u8> {

+/// Decode the trailing roster block of a `Seed` frame (REQ-MESH-2): a u32 member

+/// count + entries, then a u32 tombstone count + entries. Entries carry an empty

+/// `subnet` (implicit; the joiner stamps it). Counts are never pre-allocated — a

+/// hostile count exhausts the buffer on the first short field and errors

+/// "truncated", not a huge allocation. Every field is bounds-checked by [`Cursor`].

+fn dec_roster(c: &mut Cursor) -> Result<(Vec<RosterEntry>, Vec<Tombstone>), PairWireError> {

+    let member_count = u32::from_be_bytes(c.take(4)?.try_into().expect("took 4"));

+        let pubkey_hex = dec_utf8(c.field()?, "roster pubkey")?;

+        let label = dec_utf8(c.field()?, "roster label")?;

+        let machine_id = dec_utf8(c.field()?, "roster machine_id")?;

+        let address = if addr_bytes.is_empty() {

+                serde_json::from_slice(addr_bytes)

+                    .map_err(|_| PairWireError::Protocol("roster address not json".into()))?,

+        let last_seen = dec_utf8(c.field()?, "roster last_seen")?;

+            subnet: String::new(), // implicit — stamped by the joiner on adopt

+    let tomb_count = u32::from_be_bytes(c.take(4)?.try_into().expect("took 4"));

+        let pubkey_hex = dec_utf8(c.field()?, "tombstone pubkey")?;

+        let stamp = dec_utf8(c.field()?, "tombstone stamp")?;

+/// Decode a length-prefixed field as UTF-8, mapping a bad encoding to a labelled

+/// protocol error (never a panic — the input is attacker-reachable).

+fn dec_utf8(bytes: &[u8], what: &str) -> Result<String, PairWireError> {

+        .map_err(|_| PairWireError::Protocol(format!("{what} not utf-8")))


Wire the responder leg — `Admitted` carries the roster slice; `run_responder` takes `&RosterStore`.


Edit

    /// This seed-holder's own node pubkey (the transcript's responder side).
    local_pub: PublicKey,
    /// This node's clock, for the ±1 step window. Injected for deterministic tests.
    now: u64,
}

    /// This seed-holder's own node pubkey (the transcript's responder side).
    local_pub: PublicKey,
    /// This node's clock, for the ±1 step window. Injected for deterministic tests.
    now: u64,
    /// The seed-holder's current roster members for this subnet — handed to the
    /// joiner in the `Seed` frame on confirm-success (REQ-MESH-2).
    roster_members: &'a [RosterEntry],
    /// The subnet's revocation tombstones, transferred alongside the members.
    roster_tombstones: &'a [Tombstone],
}


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

    /// This seed-holder's own node pubkey (the transcript's responder side).
    local_pub: PublicKey,
    /// This node's clock, for the ±1 step window. Injected for deterministic tests.
    now: u64,
}

    /// This seed-holder's own node pubkey (the transcript's responder side).
    local_pub: PublicKey,
    /// This node's clock, for the ±1 step window. Injected for deterministic tests.
    now: u64,
    /// The seed-holder's current roster members for this subnet — handed to the
    /// joiner in the `Seed` frame on confirm-success (REQ-MESH-2).
    roster_members: &'a [RosterEntry],
    /// The subnet's revocation tombstones, transferred alongside the members.
    roster_tombstones: &'a [Tombstone],
}

+    /// The seed-holder's current roster members for this subnet — handed to the

+    /// joiner in the `Seed` frame on confirm-success (REQ-MESH-2).

+    /// The subnet's revocation tombstones, transferred alongside the members.

+    roster_tombstones: &'a [Tombstone],


            let seed = p.seed.expect("seed present on the confirm-success path");
            write_frame(send, &enc_seed(&seed, p.epoch)).await?;

            let seed = p.seed.expect("seed present on the confirm-success path");
            write_frame(
                send,
                &enc_seed(&seed, p.epoch, p.roster_members, p.roster_tombstones),
            )
            .await?;


            let seed = p.seed.expect("seed present on the confirm-success path");
            write_frame(send, &enc_seed(&seed, p.epoch)).await?;

            let seed = p.seed.expect("seed present on the confirm-success path");
            write_frame(
                send,
                &enc_seed(&seed, p.epoch, p.roster_members, p.roster_tombstones),
            )
            .await?;

// ONLY on confirm-success, over the iroh-authenticated TLS stream to the

// now-trust-pinned peer. `seed` is Some here (a None seed aborted upstream).

-            write_frame(send, &enc_seed(&seed, p.epoch)).await?;

+                &enc_seed(&seed, p.epoch, p.roster_members, p.roster_tombstones),


Now `run_responder` signature + build the roster slice:


pub async fn run_responder(
    conn: &Connection,
    local_pub: PublicKey,
    subnets: &SubnetStore,
    rate: &mut PairingRateLimiter,
    trust: &mut TrustStore,
    now: u64,
) -> Result<ResponderOutcome, PairWireError> {

#[allow(clippy::too_many_arguments)]
pub async fn run_responder(
    conn: &Connection,
    local_pub: PublicKey,
    subnets: &SubnetStore,
    roster: &RosterStore,
    rate: &mut PairingRateLimiter,
    trust: &mut TrustStore,
    now: u64,
) -> Result<ResponderOutcome, PairWireError> {


pub async fn run_responder(
    conn: &Connection,
    local_pub: PublicKey,
    subnets: &SubnetStore,
    rate: &mut PairingRateLimiter,
    trust: &mut TrustStore,
    now: u64,
) -> Result<ResponderOutcome, PairWireError> {

#[allow(clippy::too_many_arguments)]
pub async fn run_responder(
    conn: &Connection,
    local_pub: PublicKey,
    subnets: &SubnetStore,
    roster: &RosterStore,
    rate: &mut PairingRateLimiter,
    trust: &mut TrustStore,
    now: u64,
) -> Result<ResponderOutcome, PairWireError> {

/// failure when a ceremony was admitted (charging backoff), or not at all when admission


    let params = Admitted {
        subnet_name: &subnet_name,
        peer_step,
        epoch: record.epoch,
        seed: record.seed_bytes(),
        initiator_pub,
        local_pub,
        now,
    };
    let result = responder_admitted(&mut send, &mut recv, &params, trust).await;

    let (roster_members, roster_tombstones) = roster.roster_for(&subnet_name);
    let params = Admitted {
        subnet_name: &subnet_name,
        peer_step,
        epoch: record.epoch,
        seed: record.seed_bytes(),
        initiator_pub,
        local_pub,
        now,
        roster_members: &roster_members,
        roster_tombstones: &roster_tombstones,
    };
    let result = responder_admitted(&mut send, &mut recv, &params, trust).await;


    let params = Admitted {
        subnet_name: &subnet_name,
        peer_step,
        epoch: record.epoch,
        seed: record.seed_bytes(),
        initiator_pub,
        local_pub,
        now,
    };
    let result = responder_admitted(&mut send, &mut recv, &params, trust).await;

    let (roster_members, roster_tombstones) = roster.roster_for(&subnet_name);
    let params = Admitted {
        subnet_name: &subnet_name,
        peer_step,
        epoch: record.epoch,
        seed: record.seed_bytes(),
        initiator_pub,
        local_pub,
        now,
        roster_members: &roster_members,
        roster_tombstones: &roster_tombstones,
    };
    let result = responder_admitted(&mut send, &mut recv, &params, trust).await;

// holds if every admitted-but-unsuccessful ceremony is charged, not just clean

// confirmation mismatches). Decoding seed + epoch here drops the `subnets` borrow

+    let (roster_members, roster_tombstones) = roster.roster_for(&subnet_name);

+        roster_members: &roster_members,

+        roster_tombstones: &roster_tombstones,


Now the initiator. Read its current signature + Seed-receive site.


373	/// just after binds to the wrong step and fails as "wrong code" — the ±1
374	/// responder window ([`accepts_step`](super::totp)) tolerates an announced
375	/// `step-1`, letting the caller retry the SAME typed code against the
376	/// previous step before re-prompting the user (the late-code retry, field
377	/// report 2026-06-06).
378	// [impl->REQ-PAIR-1] [impl->REQ-PAIR-5]
379	#[allow(clippy::too_many_arguments)]
380	pub async fn run_initiator(
381	    conn: &Connection,
382	    local_pub: PublicKey,
383	    subnet_name: &str,
384	    code: &str,
385	    trust: &mut TrustStore,
386	    subnets: &mut SubnetStore,
387	    now: u64,
388	    step_back: u64,
389	    intro: Option<&NodeIntro>,
390	) -> Result<TrustOutcome, PairWireError> {
391	    let (mut send, mut recv) = conn
392	        .open_bi()
393	        .await
394	        .map_err(|e| PairWireError::Io(format!("open_bi: {e}")))?;
395	    let responder_pub = remote_pub(conn)?;
396	    let step = time_step(now).saturating_sub(step_back);
397
398	    // F1: Hello — opens the stream (the dialer must write first for accept_bi
399	    // to fire). Carries the optional self-intro (M8 decision 13).
400	    write_frame(&mut send, &enc_hello(subnet_name, step, intro)).await?;
401
402	    // F2: Announce(epoch) — or an Abort if the responder refused admission / step / name.
403	    let epoch = match read_frame(&mut recv).await? {
404	        Frame::Announce { epoch } => epoch,
405	        Frame::Abort {
406	            reason,
407	            retry_after,
408	        } => return Err(map_abort(reason, retry_after, subnet_name)),
409	        other => return Err(unexpected("Announce", other)),
410	    };
411
412	    // Now the joiner can bind the full transcript and run SPAKE2 side A.
413	    let transcript = PairingTranscript {
414	        subnet_name: subnet_name.to_string(),
415	        epoch,
416	        step,
417	        initiator_pub: local_pub,
418	        responder_pub,
419	    };
420	    let (initiator, msg_a) = Initiator::start(transcript, code);
421
422	    // F3: Spake1 — msg_a.
423	    write_frame(&mut send, &enc_spake1(&msg_a)).await?;
424
425	    // F4: Spake2 — msg_b + the responder's confirmation tag.
426	    let (msg_b, responder_tag) = match read_frame(&mut recv).await? {
427	        Frame::Spake2 { msg_b, tag } => (msg_b, tag),
428	        Frame::Abort {
429	            reason,
430	            retry_after,
431	        } => return Err(map_abort(reason, retry_after, subnet_name)),
432	        other => return Err(unexpected("Spake2", other)),
433	    };
434
435	    // Finish the PAKE and verify the responder's tag. On failure, signal the responder
436	    // with an Abort so it doesn't wait out the ceremony timeout, then surface the error.
437	    let (paired, initiator_tag) = match initiator.finish(&msg_b, &responder_tag) {
438	        Ok(v) => v,
439	        Err(e) => {
440	            abort(&mut send, A_CONFIRM_FAIL, 0).await?;
441	            let _ = send.finish();
442	            return Err(PairWireError::Pairing(e));
443	        }
444	    };
445
446	    // F5: Confirm — the joiner's own tag (proves to the responder it holds the same key).
447	    write_frame(&mut send, &enc_confirm(&initiator_tag)).await?;
448	    let _ = send.finish();
449
450	    // F6: Done — the responder's verdict on our tag.
451	    match read_frame(&mut recv).await? {
452	        Frame::Done { ok: true } => {
453	            // Pin the responder (its pubkey was proven live by the confirmation round).
454	            let outcome = trust.record(subnet_name, &paired.peer_pub.to_hex(), None, None);
455	            // F7: receive the subnet seed and become a full member / future seed-holder.
456	            // Read it on the same confirmed stream; a missing/short Seed frame is a
457	            // protocol error (the responder always sends it on success).
458	            match read_frame(&mut recv).await? {
459	                Frame::Seed { seed, epoch } => {
460	                    // `add_joined` errors `Exists` for an idempotent re-pair (we already
461	                    // hold this subnet) — that is success, not failure: keep our record.
462	                    match subnets.add_joined(subnet_name, seed, epoch) {
463	                        Ok(_) | Err(SubnetError::Exists(_)) => {}
464	                        Err(e) => return Err(PairWireError::Protocol(format!("seed store: {e}"))),
465	                    }
466	                }
467	                other => return Err(unexpected("Seed", other)),
468	            }
469	            Ok(outcome)
470	        }
471	        Frame::Done { ok: false } => Err(PairWireError::Pairing(PairError::Confirm)),
472	        Frame::Abort {

/// just after binds to the wrong step and fails as "wrong code" — the ±1
/// responder window ([`accepts_step`](super::totp)) tolerates an announced
/// `step-1`, letting the caller retry the SAME typed code against the
/// previous step before re-prompting the user (the late-code retry, field
/// report 2026-06-06).
// [impl->REQ-PAIR-1] [impl->REQ-PAIR-5]
#[allow(clippy::too_many_arguments)]
pub async fn run_initiator(
    conn: &Connection,
    local_pub: PublicKey,
    subnet_name: &str,
    code: &str,
    trust: &mut TrustStore,
    subnets: &mut SubnetStore,
    now: u64,
    step_back: u64,
    intro: Option<&NodeIntro>,
) -> Result<TrustOutcome, PairWireError> {
    let (mut send, mut recv) = conn
        .open_bi()
        .await
        .map_err(|e| PairWireError::Io(format!("open_bi: {e}")))?;
    let responder_pub = remote_pub(conn)?;
    let step = time_step(now).saturating_sub(step_back);

    // F1: Hello — opens the stream (the dialer must write first for accept_bi
    // to fire). Carries the optional self-intro (M8 decision 13).
    write_frame(&mut send, &enc_hello(subnet_name, step, intro)).await?;

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

    // Now the joiner can bind the full transcript and run SPAKE2 side A.
    let transcript = PairingTranscript {
        subnet_name: subnet_name.to_string(),
        epoch,
        step,
        initiator_pub: local_pub,
        responder_pub,
    };
    let (initiator, msg_a) = Initiator::start(transcript, code);

    // F3: Spake1 — msg_a.
    write_frame(&mut send, &enc_spake1(&msg_a)).await?;

    // F4: Spake2 — msg_b + the responder's confirmation tag.
    let (msg_b, responder_tag) = match read_frame(&mut recv).await? {
        Frame::Spake2 { msg_b, tag } => (msg_b, tag),
        Frame::Abort {
            reason,
            retry_after,
        } => return Err(map_abort(reason, retry_after, subnet_name)),
        other => return Err(unexpected("Spake2", other)),
    };

    // Finish the PAKE and verify the responder's tag. On failure, signal the responder
    // with an Abort so it doesn't wait out the ceremony timeout, then surface the error.
    let (paired, initiator_tag) = match initiator.finish(&msg_b, &responder_tag) {
        Ok(v) => v,
        Err(e) => {
            abort(&mut send, A_CONFIRM_FAIL, 0).await?;
            let _ = send.finish();
            return Err(PairWireError::Pairing(e));
        }
    };

    // F5: Confirm — the joiner's own tag (proves to the responder it holds the same key).
    write_frame(&mut send, &enc_confirm(&initiator_tag)).await?;
    let _ = send.finish();

    // F6: Done — the responder's verdict on our tag.
    match read_frame(&mut recv).await? {
        Frame::Done { ok: true } => {
            // Pin the responder (its pubkey was proven live by the confirmation round).
            let outcome = trust.record(subnet_name, &paired.peer_pub.to_hex(), None, None);
            // F7: receive the subnet seed and become a full member / future seed-holder.
            // Read it on the same confirmed stream; a missing/short Seed frame is a
            // protocol error (the responder always sends it on success).
            match read_frame(&mut recv).await? {
                Frame::Seed { seed, epoch } => {
                    // `add_joined` errors `Exists` for an idempotent re-pair (we already
                    // hold this subnet) — that is success, not failure: keep our record.
                    match subnets.add_joined(subnet_name, seed, epoch) {
                        Ok(_) | Err(SubnetError::Exists(_)) => {}
                        Err(e) => return Err(PairWireError::Protocol(format!("seed store: {e}"))),
                    }
                }
                other => return Err(unexpected("Seed", other)),
            }
            Ok(outcome)
        }
        Frame::Done { ok: false } => Err(PairWireError::Pairing(PairError::Confirm)),
        Frame::Abort {


    code: &str,
    trust: &mut TrustStore,
    subnets: &mut SubnetStore,
    now: u64,
    step_back: u64,
    intro: Option<&NodeIntro>,
) -> Result<TrustOutcome, PairWireError> {

    code: &str,
    trust: &mut TrustStore,
    subnets: &mut SubnetStore,
    roster: &mut RosterStore,
    now: u64,
    step_back: u64,
    intro: Option<&NodeIntro>,
) -> Result<TrustOutcome, PairWireError> {


    code: &str,
    trust: &mut TrustStore,
    subnets: &mut SubnetStore,
    now: u64,
    step_back: u64,
    intro: Option<&NodeIntro>,
) -> Result<TrustOutcome, PairWireError> {

    code: &str,
    trust: &mut TrustStore,
    subnets: &mut SubnetStore,
    roster: &mut RosterStore,
    now: u64,
    step_back: u64,
    intro: Option<&NodeIntro>,
) -> Result<TrustOutcome, PairWireError> {

code: &str,
trust: &mut TrustStore,
subnets: &mut SubnetStore,
+    roster: &mut RosterStore,
now: u64,
step_back: u64,
intro: Option<&NodeIntro>,


            match read_frame(&mut recv).await? {
                Frame::Seed { seed, epoch } => {
                    // `add_joined` errors `Exists` for an idempotent re-pair (we already
                    // hold this subnet) — that is success, not failure: keep our record.
                    match subnets.add_joined(subnet_name, seed, epoch) {
                        Ok(_) | Err(SubnetError::Exists(_)) => {}
                        Err(e) => return Err(PairWireError::Protocol(format!("seed store: {e}"))),
                    }
                }
                other => return Err(unexpected("Seed", other)),
            }
            Ok(outcome)

            match read_frame(&mut recv).await? {
                Frame::Seed {
                    seed,
                    epoch,
                    roster: members,
                    tombstones,
                } => {
                    // `add_joined` errors `Exists` for an idempotent re-pair (we already
                    // hold this subnet) — that is success, not failure: keep our record.
                    match subnets.add_joined(subnet_name, seed, epoch) {
                        Ok(_) | Err(SubnetError::Exists(_)) => {}
                        Err(e) => return Err(PairWireError::Protocol(format!("seed store: {e}"))),
                    }
                    // Adopt the seed-holder's roster (REQ-MESH-2): stamp the
                    // implicit subnet onto each wire entry (defends against a
                    // cross-subnet inject — every adopted row is forced into the
                    // subnet we are joining) and union-merge it in.
                    adopt_roster(roster, subnet_name, members, tombstones);
                }
                other => return Err(unexpected("Seed", other)),
            }
            Ok(outcome)


            match read_frame(&mut recv).await? {
                Frame::Seed { seed, epoch } => {
                    // `add_joined` errors `Exists` for an idempotent re-pair (we already
                    // hold this subnet) — that is success, not failure: keep our record.
                    match subnets.add_joined(subnet_name, seed, epoch) {
                        Ok(_) | Err(SubnetError::Exists(_)) => {}
                        Err(e) => return Err(PairWireError::Protocol(format!("seed store: {e}"))),
                    }
                }
                other => return Err(unexpected("Seed", other)),
            }
            Ok(outcome)

            match read_frame(&mut recv).await? {
                Frame::Seed {
                    seed,
                    epoch,
                    roster: members,
                    tombstones,
                } => {
                    // `add_joined` errors `Exists` for an idempotent re-pair (we already
                    // hold this subnet) — that is success, not failure: keep our record.
                    match subnets.add_joined(subnet_name, seed, epoch) {
                        Ok(_) | Err(SubnetError::Exists(_)) => {}
                        Err(e) => return Err(PairWireError::Protocol(format!("seed store: {e}"))),
                    }
                    // Adopt the seed-holder's roster (REQ-MESH-2): stamp the
                    // implicit subnet onto each wire entry (defends against a
                    // cross-subnet inject — every adopted row is forced into the
                    // subnet we are joining) and union-merge it in.
                    adopt_roster(roster, subnet_name, members, tombstones);
                }
                other => return Err(unexpected("Seed", other)),
            }
            Ok(outcome)

// Read it on the same confirmed stream; a missing/short Seed frame is a

// protocol error (the responder always sends it on success).

-                Frame::Seed { seed, epoch } => {

+                    // Adopt the seed-holder's roster (REQ-MESH-2): stamp the

+                    // implicit subnet onto each wire entry (defends against a

+                    // cross-subnet inject — every adopted row is forced into the

+                    // subnet we are joining) and union-merge it in.

+                    adopt_roster(roster, subnet_name, members, tombstones);


Add the `adopt_roster` helper:


/// Decode a length-prefixed field as UTF-8, mapping a bad encoding to a labelled
/// protocol error (never a panic — the input is attacker-reachable).
fn dec_utf8(bytes: &[u8], what: &str) -> Result<String, PairWireError> {
    String::from_utf8(bytes.to_vec())
        .map_err(|_| PairWireError::Protocol(format!("{what} not utf-8")))
}

/// Decode a length-prefixed field as UTF-8, mapping a bad encoding to a labelled
/// protocol error (never a panic — the input is attacker-reachable).
fn dec_utf8(bytes: &[u8], what: &str) -> Result<String, PairWireError> {
    String::from_utf8(bytes.to_vec())
        .map_err(|_| PairWireError::Protocol(format!("{what} not utf-8")))
}

/// Adopt a received roster into the joiner's store (REQ-MESH-2): force each wire
/// entry/tombstone into `subnet` (they arrive with an empty implicit subnet —
/// stamping it here is what defends against a cross-subnet inject) and
/// union-merge strictly-greater-wins. Pure store mutation — the caller saves.
fn adopt_roster(
    roster: &mut RosterStore,
    subnet: &str,
    members: Vec<RosterEntry>,
    tombstones: Vec<Tombstone>,
) {
    for mut e in members {
        e.subnet = subnet.to_string();
        roster.merge_entry(e);
    }
    for t in tombstones {
        roster.tombstone(subnet, &t.pubkey_hex, &t.stamp);
    }
}


/// Decode a length-prefixed field as UTF-8, mapping a bad encoding to a labelled
/// protocol error (never a panic — the input is attacker-reachable).
fn dec_utf8(bytes: &[u8], what: &str) -> Result<String, PairWireError> {
    String::from_utf8(bytes.to_vec())
        .map_err(|_| PairWireError::Protocol(format!("{what} not utf-8")))
}

/// Adopt a received roster into the joiner's store (REQ-MESH-2): force each wire
/// entry/tombstone into `subnet` (they arrive with an empty implicit subnet —
/// stamping it here is what defends against a cross-subnet inject) and
/// union-merge strictly-greater-wins. Pure store mutation — the caller saves.
fn adopt_roster(
    roster: &mut RosterStore,
    subnet: &str,
    members: Vec<RosterEntry>,
    tombstones: Vec<Tombstone>,
) {
    for mut e in members {
        e.subnet = subnet.to_string();
        roster.merge_entry(e);
    }
    for t in tombstones {
        roster.tombstone(subnet, &t.pubkey_hex, &t.stamp);
    }
}

+/// Adopt a received roster into the joiner's store (REQ-MESH-2): force each wire

+/// entry/tombstone into `subnet` (they arrive with an empty implicit subnet —

+/// stamping it here is what defends against a cross-subnet inject) and

+/// union-merge strictly-greater-wins. Pure store mutation — the caller saves.

+        roster.tombstone(subnet, &t.pubkey_hex, &t.stamp);

fn enc_abort(reason: u8, retry_after: u64) -> Vec<u8> {


Now fix the test call to `enc_seed` + update the existing wire `#[cfg(test)]` callers of `run_responder`/`run_initiator`. Let me find all callers/tests.


172:pub async fn run_responder(
249:/// NOT touch the rate limiter: [`run_responder`] owns the slot and releases it once based
253:/// The admitted-ceremony parameters [`run_responder`] hands to [`responder_admitted`]
348:                &enc_seed(&seed, p.epoch, p.roster_members, p.roster_tombstones),
380:pub async fn run_initiator(
460:                Frame::Seed {
578:            Frame::Seed { .. } => "Seed",
626:fn enc_seed(
833:            Frame::Seed {
994:            let outcome = run_responder(&conn, resp_pub, &subnets, &mut rate, &mut trust, NOW)
1007:        let init_outcome = run_initiator(
1076:            let result = run_responder(&conn, resp_pub, &subnets, &mut rate, &mut trust, NOW).await;
1089:        let init_result = run_initiator(
1147:            let result = run_responder(&conn, resp_pub, &subnets, &mut rate, &mut trust, NOW).await;
1158:        let init_result = run_initiator(
1209:            let result = run_responder(&conn, resp_pub, &subnets, &mut rate, &mut trust, NOW).await;
1220:        let init_result = run_initiator(
1266:            let result = run_responder(&conn, resp_pub, &subnets, &mut rate, &mut trust, NOW).await;
1278:        let init_result = run_initiator(
1331:            run_responder(&conn, resp_pub, &subnets, &mut rate, &mut trust, NOW)
1343:        run_initiator(
1388:            let outcome = run_responder(&conn, resp_pub, &subnets, &mut rate, &mut trust, NOW)
1406:        let init_outcome = run_initiator(
1497:            enc_seed(&[5u8; TOTP_SEED_LEN], 9),


Many test call-sites. Read the test harness to update them correctly.


820	        T_SEED => {
821	            let seed: [u8; TOTP_SEED_LEN] = c
822	                .take(TOTP_SEED_LEN)?
823	                .try_into()
824	                .expect("took TOTP_SEED_LEN");
825	            let epoch = c.u64()?;
826	            // Trailing + additive roster (REQ-MESH-2): a legacy short Seed body
827	            // (seed+epoch only) leaves nothing here ⇒ empty roster.
828	            let (roster, tombstones) = if c.remaining() > 0 {
829	                dec_roster(&mut c)?
830	            } else {
831	                (Vec::new(), Vec::new())
832	            };
833	            Frame::Seed {
834	                seed,
835	                epoch,
836	                roster,
837	                tombstones,
838	            }
839	        }
840	        T_ABORT => {
841	            let reason = c.u8()?;
842	            let retry_after = c.u64()?;
843	            Frame::Abort {
844	                reason,
845	                retry_after,
846	            }
847	        }
848	        other => {
849	            return Err(PairWireError::Protocol(format!(
850	                "unknown frame tag {other}"
851	            )))
852	        }
853	    };
854	    Ok(frame)
855	}
856
857	/// A minimal big-endian field reader over a frame body — every accessor bounds-checks so
858	/// a truncated or oversized field is a [`Protocol`](PairWireError::Protocol) error, never
859	/// a panic (the input is pre-trust attacker-reachable).
860	struct Cursor<'a> {
861	    buf: &'a [u8],
862	    pos: usize,
863	}
864
865	impl<'a> Cursor<'a> {
866	    fn new(buf: &'a [u8]) -> Cursor<'a> {
867	        Cursor { buf, pos: 0 }
868	    }
869
870	    fn take(&mut self, n: usize) -> Result<&'a [u8], PairWireError> {
871	        let end = self
872	            .pos
873	            .checked_add(n)
874	            .filter(|e| *e <= self.buf.len())
875	            .ok_or_else(|| PairWireError::Protocol("frame truncated".into()))?;
876	        let slice = &self.buf[self.pos..end];
877	        self.pos = end;
878	        Ok(slice)
879	    }

        T_SEED => {
            let seed: [u8; TOTP_SEED_LEN] = c
                .take(TOTP_SEED_LEN)?
                .try_into()
                .expect("took TOTP_SEED_LEN");
            let epoch = c.u64()?;
            // Trailing + additive roster (REQ-MESH-2): a legacy short Seed body
            // (seed+epoch only) leaves nothing here ⇒ empty roster.
            let (roster, tombstones) = if c.remaining() > 0 {
                dec_roster(&mut c)?
            } else {
                (Vec::new(), Vec::new())
            };
            Frame::Seed {
                seed,
                epoch,
                roster,
                tombstones,
            }
        }
        T_ABORT => {
            let reason = c.u8()?;
            let retry_after = c.u64()?;
            Frame::Abort {
                reason,
                retry_after,
            }
        }
        other => {
            return Err(PairWireError::Protocol(format!(
                "unknown frame tag {other}"
            )))
        }
    };
    Ok(frame)
}

/// A minimal big-endian field reader over a frame body — every accessor bounds-checks so
/// a truncated or oversized field is a [`Protocol`](PairWireError::Protocol) error, never
/// a panic (the input is pre-trust attacker-reachable).
struct Cursor<'a> {
    buf: &'a [u8],
    pos: usize,
}

impl<'a> Cursor<'a> {
    fn new(buf: &'a [u8]) -> Cursor<'a> {
        Cursor { buf, pos: 0 }
    }

    fn take(&mut self, n: usize) -> Result<&'a [u8], PairWireError> {
        let end = self
            .pos
            .checked_add(n)
            .filter(|e| *e <= self.buf.len())
            .ok_or_else(|| PairWireError::Protocol("frame truncated".into()))?;
        let slice = &self.buf[self.pos..end];
        self.pos = end;
        Ok(slice)
    }


985	            let incoming = responder.accept().await.expect("incoming");
986	            let conn = incoming
987	                .accept()
988	                .expect("accept")
989	                .await
990	                .expect("connecting");
991	            let subnets = subnet_store("home", seed, 3);
992	            let mut rate = PairingRateLimiter::new();
993	            let mut trust = TrustStore::default();
994	            let outcome = run_responder(&conn, resp_pub, &subnets, &mut rate, &mut trust, NOW)
995	                .await
996	                .expect("responder pairs");
997	            conn.closed().await;
998	            (trust, outcome)
999	        });
1000
1001	        let conn = initiator
1002	            .connect_with_alpn(resp_addr, SPT_PAIR_ALPN)
1003	            .await
1004	            .expect("connect");
1005	        let mut init_trust = TrustStore::default();
1006	        let mut init_subnets = SubnetStore::default();
1007	        let init_outcome = run_initiator(
1008	            &conn,
1009	            init_pub,
1010	            "home",
1011	            &code,
1012	            &mut init_trust,
1013	            &mut init_subnets,
1014	            NOW,
1015	            0,    // step_back: the normal attempt
1016	            None, // intro: pre-M8 joiner shape
1017	        )
1018	        .await
1019	        .expect("initiator pairs");
1020	        conn.close(0u32.into(), b"done");
1021
1022	        let (resp_trust, resp_outcome) = resp_task.await.expect("responder task");
1023
1024	        assert_eq!(
1025	            init_outcome,
1026	            TrustOutcome::Pinned,
1027	            "initiator pins responder"
1028	        );
1029	        assert_eq!(

            let incoming = responder.accept().await.expect("incoming");
            let conn = incoming
                .accept()
                .expect("accept")
                .await
                .expect("connecting");
            let subnets = subnet_store("home", seed, 3);
            let mut rate = PairingRateLimiter::new();
            let mut trust = TrustStore::default();
            let outcome = run_responder(&conn, resp_pub, &subnets, &mut rate, &mut trust, NOW)
                .await
                .expect("responder pairs");
            conn.closed().await;
            (trust, outcome)
        });

        let conn = initiator
            .connect_with_alpn(resp_addr, SPT_PAIR_ALPN)
            .await
            .expect("connect");
        let mut init_trust = TrustStore::default();
        let mut init_subnets = SubnetStore::default();
        let init_outcome = run_initiator(
            &conn,
            init_pub,
            "home",
            &code,
            &mut init_trust,
            &mut init_subnets,
            NOW,
            0,    // step_back: the normal attempt
            None, // intro: pre-M8 joiner shape
        )
        .await
        .expect("initiator pairs");
        conn.close(0u32.into(), b"done");

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

        assert_eq!(
            init_outcome,
            TrustOutcome::Pinned,
            "initiator pins responder"
        );
        assert_eq!(


Responder calls share an identical tail — replace_all. First check initiator call variable names.


Grep

&mut .*subnets,|&mut .*trust,|let mut .*subnets = |let mut .*roster|run_initiator\(


380:pub async fn run_initiator(
668:    let mut roster = Vec::new();
994:            let outcome = run_responder(&conn, resp_pub, &subnets, &mut rate, &mut trust, NOW)
1006:        let mut init_subnets = SubnetStore::default();
1007:        let init_outcome = run_initiator(
1012:            &mut init_trust,
1013:            &mut init_subnets,
1076:            let result = run_responder(&conn, resp_pub, &subnets, &mut rate, &mut trust, NOW).await;
1088:        let mut init_subnets = SubnetStore::default();
1089:        let init_result = run_initiator(
1094:            &mut init_trust,
1095:            &mut init_subnets,
1147:            let result = run_responder(&conn, resp_pub, &subnets, &mut rate, &mut trust, NOW).await;
1157:        let mut init_subnets = SubnetStore::default();
1158:        let init_result = run_initiator(
1163:            &mut init_trust,
1164:            &mut init_subnets,
1209:            let result = run_responder(&conn, resp_pub, &subnets, &mut rate, &mut trust, NOW).await;
1219:        let mut init_subnets = SubnetStore::default();
1220:        let init_result = run_initiator(
1225:            &mut init_trust,
1226:            &mut init_subnets,
1266:            let result = run_responder(&conn, resp_pub, &subnets, &mut rate, &mut trust, NOW).await;
1276:        let mut init_subnets = SubnetStore::default();
1278:        let init_result = run_initiator(
1283:            &mut init_trust,
1284:            &mut init_subnets,
1314:        let mut subnets = SubnetStore::default();
1331:            run_responder(&conn, resp_pub, &subnets, &mut rate, &mut trust, NOW)
1342:        let mut init_subnets = SubnetStore::default();
1343:        run_initiator(
1348:            &mut init_trust,
1349:            &mut init_subnets,
1388:            let outcome = run_responder(&conn, resp_pub, &subnets, &mut rate, &mut trust, NOW)
1400:        let mut init_subnets = subnet_store("home", existing_seed, 7);
1406:        let init_outcome = run_initiator(
1411:            &mut init_trust,
1412:            &mut init_subnets,


Update wire.rs test call-sites. Responder tail (replace_all), initiator arg insertion (replace_all), bindings, and the `enc_seed` codec-test call.


Edit

run_responder(&conn, resp_pub, &subnets, &mut rate, &mut trust, NOW)

run_responder(&conn, resp_pub, &subnets, &RosterStore::default(), &mut rate, &mut trust, NOW)


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

let subnets = subnet_store("home", seed, 3);

let mut rate = PairingRateLimiter::new();

-            let outcome = run_responder(&conn, resp_pub, &subnets, &mut rate, &mut trust, NOW)

+            let outcome = run_responder(&conn, resp_pub, &subnets, &RosterStore::default(), &mut rate, &mut trust, NOW)

-            let result = run_responder(&conn, resp_pub, &subnets, &mut rate, &mut trust, NOW).await;

+            let result = run_responder(&conn, resp_pub, &subnets, &RosterStore::default(), &mut rate, &mut trust, NOW).await;

// After a failed ceremony the slot is backed off (charged a failure).

// A different ceremony already holds the slot.

let subnets = subnet_store("home", seed, 1); // only "home" exists

-            run_responder(&conn, resp_pub, &subnets, &mut rate, &mut trust, NOW)

+            run_responder(&conn, resp_pub, &subnets, &RosterStore::default(), &mut rate, &mut trust, NOW)

let subnets = subnet_store("home", seed, 2);


            &mut init_trust,
            &mut init_subnets,
            NOW,

            &mut init_trust,
            &mut init_subnets,
            &mut init_roster,
            NOW,


            &mut init_trust,
            &mut init_subnets,
            NOW,

            &mut init_trust,
            &mut init_subnets,
            &mut init_roster,
            NOW,


let mut init_subnets = SubnetStore::default();

        let mut init_subnets = SubnetStore::default();
        let mut init_roster = RosterStore::default();


        let mut init_subnets = SubnetStore::default();
        let mut init_roster = RosterStore::default();

let mut init_trust = TrustStore::default();

+        let mut init_roster = RosterStore::default();

// The joiner asks for "work", which the responder doesn't have.


let mut init_subnets = subnet_store("home", existing_seed, 7);

        let mut init_subnets = subnet_store("home", existing_seed, 7);
        let mut init_roster = RosterStore::default();


        let mut init_subnets = subnet_store("home", existing_seed, 7);
        let mut init_roster = RosterStore::default();

init_trust.record("home", &resp_pub.to_hex(), None, None);


-            enc_seed(&[5u8; TOTP_SEED_LEN], 9),


Add the test-module import, then wire pairhost.rs (the daemon callers).


    use spt_proto::identity::Identity;
    use spt_store::subnet::{SubnetRecord, SubnetStore};
    use spt_store::trust::TrustStore;

    use spt_proto::identity::Identity;
    use spt_store::roster::RosterStore;
    use spt_store::subnet::{SubnetRecord, SubnetStore};
    use spt_store::trust::TrustStore;


    use spt_proto::identity::Identity;
    use spt_store::subnet::{SubnetRecord, SubnetStore};
    use spt_store::trust::TrustStore;

    use spt_proto::identity::Identity;
    use spt_store::roster::RosterStore;
    use spt_store::subnet::{SubnetRecord, SubnetStore};
    use spt_store::trust::TrustStore;

use crate::net::endpoint::{BindScope, LocalDiscovery, NetEndpoint, RelayPolicy};

+    use spt_store::roster::RosterStore;


Check meet.rs + twohost.rs callers before building.


245-/// losers are dropped; racing pre-trust meet ids is harmless, module docs).
246-/// Each derived id is dialed by **id alone** (relay + n0 DNS / mDNS resolve
247-/// it — no address ticket); the winner's reply is the responder's real
248-/// address, dialed over the ceremony ALPN. The returned connection is ready
249:/// for [`run_initiator`](super::wire::run_initiator) — the ceremony itself
250-/// is unchanged.
251-// [impl->REQ-PAIR-5]
252-// [impl->REQ-SUBNET-2]
253-pub async fn dial_via_rendezvous(
--
395-            .connect_with_alpn(fetched, SPT_PAIR_ALPN)
396-            .await
397-            .expect("real dial");
398-        // The ceremony connection authenticates the responder's REAL key —
399:        // what run_initiator's transcript + trust pinning consume.
400-        assert_eq!(conn.remote_id().as_bytes(), &real_pub.to_bytes());
401-        conn.close(0u32.into(), b"done");
402-
403-        serve.await.expect("responder task");


565-    println!("TWOHOST role B: drive session {sid} ready");
566-
567-    \ ── Rung 1 — pair (responder): the PRODUCT surface (M7 D5). The
568-    // daemon-hosted ALWAYS-ON responder — the broker NetHost speaking the
569-    // pre-trust pairing ALPN beside the trusted surface (pairhost) — answers
570-    \ A's CLI-path ceremony. This test arms NOTHING: no dedicated endpoint,
571:    // no manual run_responder loop; the rung passing proves a stock member
572-    // daemon pairs a joiner. Trust lands in the canonical store the
573-    // dispatcher gates read.
574-    // [int->REQ-PAIR-1] [int->REQ-SUBNET-2]
575-    rig_wait(
576-        "pairing: A pinned via the daemon-hosted responder",
577-        rig.wait,
--
779-    let broker_name = format!("spt-twohost-a-{}.sock", std::process::id());
780-    let _broker = rig_broker(&broker_name, rig.id_a.clone(), rig.port_a, scratch.path());
781-
782-    \ ── Rung 1 — pair (initiator): the PRODUCT surface (M7 D5) — the exact
783-    // `spt subnet join` path (brain `pair-join` IPC → broker joiner leg →
784-    // the unchanged SPAKE2 ceremony), replacing the test-harness-driven
785:    // run_initiator as the pairing evidence. A holds NO subnet record yet:
786-    // the 6-digit code is read off the shared TOTP seed (the operator
787-    // channel) and success transfers the subnet seed (A becomes a full
788-    // member / future seed-holder). The explicit responder address is the
789-    \ static-`ip:port` rig seam (no relay, no discovery — rendezvous can't
790-    // resolve here).
791-    // [int->REQ-PAIR-1] [int->REQ-PAIR-5] [int->REQ-SUBNET-2]


Only pairhost.rs actually calls them (meet/twohost are comments). Check how the daemon reads its node epoch + label for self-authoring.


1://! Per-node monotonic epoch counter (D3b) — the ordering authority behind the
4://! A single strictly-increasing `u64` persisted at [`epoch_file`](crate::perch::epoch_file).
6://! context snapshot) stamps the value [`next_epoch`](EpochSource::next_epoch) returns. Comparing two
20:use crate::perch::epoch_file;
22:/// A node's persisted monotonic epoch counter.
29:    /// Load (or initialize) the counter from the canonical [`epoch_file`].
30:    pub fn load() -> EpochSource {
31:        Self::load_from(&epoch_file())
35:    /// (first [`next_epoch`] yields `1`); a corrupt/unparsable file ALSO degrades to `0`
37:    /// from 0 is safe-by-construction: see [`next_epoch`]'s monotonicity note.
38:    pub fn load_from(path: &Path) -> EpochSource {
49:    /// The last issued epoch without advancing (peek). `0` means none issued yet.
50:    pub fn current(&self) -> u64 {
54:    /// Advance and return the next epoch, persisting it **before** updating the
56:    /// returned, no epoch is consumed). Strictly greater than every prior return on
58:    pub fn next_epoch(&mut self) -> std::io::Result<u64> {
62:            .expect("epoch counter overflowed u64 — unreachable in any real lifetime");
81:    fn next_is_strictly_increasing_from_one() {
83:        let mut src = EpochSource::load_from(&dir.path().join("epoch"));
84:        let a = src.next_epoch().unwrap();
85:        let b = src.next_epoch().unwrap();
86:        let c = src.next_epoch().unwrap();
90:            "epoch is a small counter, not a unix timestamp"
95:    // up where the prior source left off, never reissuing a consumed epoch.
99:        let path = dir.path().join("epoch");
102:            assert_eq!(src.next_epoch().unwrap(), 1);
103:            assert_eq!(src.next_epoch().unwrap(), 2);
108:            reloaded.next_epoch().unwrap(),
122:        let corrupt = dir.path().join("epoch");
126:        assert_eq!(src.next_epoch().unwrap(), 1, "recovers without panic");


4://! D3 built the registry **model** (`spt_net::net::registry`: the epoch
16://!   [`EpochSource`] epoch — the per-node monotonic lease key, never
37:use spt_net::net::replicate::{NodeLabelUpdate, RegistryFeedRecord, RegistryUpdate};
38:use spt_store::epoch::EpochSource;
86:/// epoch source. Shared (`Arc`) between the dispatcher's registry arm and
94:    /// epoch-bumped updates. Display/addressing sugar; `node_hex` stays the
96:    node_label: Option<String>,
110:    /// epoch on every locally-authored instance row.
111:    epoch: Mutex<EpochSource>,
117:    /// Stand the host up for this node, with the canonical epoch counter and
126:        .with_node_label(spt_store::hostlabel::os_hostname())
132:    /// stay hostname-independent; chain [`with_node_label`](Self::with_node_label).
133:    pub fn new_at(node_hex: &str, epoch: EpochSource, snapshot_dir: PathBuf) -> RegistryHost {
136:            node_label: None,
140:            epoch: Mutex::new(epoch),
147:    pub fn with_node_label(mut self, label: Option<String>) -> RegistryHost {
148:        self.node_label = label;
165:    /// Run `f` with this host's **canonical per-node epoch** — the ONE
166:    /// monotonic counter (identity/epoch.json) behind every locally-authored
168:    /// from the same source, or `node:epoch`-shaped ids collide: a notif_id is
169:    /// `node_hex:epoch`, so the pump's consent-notif producer and the CLI
179:    pub fn with_epoch<R>(&self, f: impl FnOnce(&mut EpochSource) -> R) -> R {
180:        f(&mut self.epoch.lock().unwrap())
200:    /// daemon advertises transitions epoch-bumped), which rests a local
210:    /// flip). The lease has already dropped stale/equal epochs before any
238:                // The epoch lease (4.8) decides; an own-row echo from a peer
239:                // is dropped Stale by construction (our epoch is newer or
272:    /// [`SubnetRegistry::merge_node_label`] (epoch lease) and stamps the
277:    pub fn apply_node_labels(
280:        updates: &[NodeLabelUpdate],

[Showing results with pagination = limit: 30]


Epoch model clear (`EpochSource::load().current()` = peek). Now wire pairhost.rs. Imports first:


use spt_proto::identity::PublicKey;
use spt_store::subnet::SubnetStore;
use spt_store::trust::TrustStore;

use spt_proto::identity::PublicKey;
use spt_store::roster::{RosterEntry, RosterStore};
use spt_store::subnet::SubnetStore;
use spt_store::trust::TrustStore;


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

use spt_proto::identity::PublicKey;
use spt_store::subnet::SubnetStore;
use spt_store::trust::TrustStore;

use spt_proto::identity::PublicKey;
use spt_store::roster::{RosterEntry, RosterStore};
use spt_store::subnet::SubnetStore;
use spt_store::trust::TrustStore;

use spt_net::net::pairing::wire::{run_initiator, run_responder, PairWireError};


    let mut subnets = SubnetStore::load();
    spt_store::attachment::AttachmentStore::load().filter_serving(&mut subnets);
    let mut trust = TrustStore::load();
    match tokio::time::timeout(
        CEREMONY_TIMEOUT,
        run_responder(
            &conn,
            local_pub,
            &subnets,
            &mut rate,
            &mut trust,
            now_secs(),
        ),
    )
    .await
    {

    let mut subnets = SubnetStore::load();
    spt_store::attachment::AttachmentStore::load().filter_serving(&mut subnets);
    let mut trust = TrustStore::load();
    // Roster (REQ-MESH-2): refresh this seed-holder's own entry for every
    // served subnet so the joiner always learns at least us, then hand it the
    // whole current roster in the Seed frame.
    let mut roster = RosterStore::load();
    refresh_self_roster(&mut roster, &subnets, &local_pub.to_hex());
    match tokio::time::timeout(
        CEREMONY_TIMEOUT,
        run_responder(
            &conn,
            local_pub,
            &subnets,
            &roster,
            &mut rate,
            &mut trust,
            now_secs(),
        ),
    )
    .await
    {


    let mut subnets = SubnetStore::load();
    spt_store::attachment::AttachmentStore::load().filter_serving(&mut subnets);
    let mut trust = TrustStore::load();
    match tokio::time::timeout(
        CEREMONY_TIMEOUT,
        run_responder(
            &conn,
            local_pub,
            &subnets,
            &mut rate,
            &mut trust,
            now_secs(),
        ),
    )
    .await
    {

    let mut subnets = SubnetStore::load();
    spt_store::attachment::AttachmentStore::load().filter_serving(&mut subnets);
    let mut trust = TrustStore::load();
    // Roster (REQ-MESH-2): refresh this seed-holder's own entry for every
    // served subnet so the joiner always learns at least us, then hand it the
    // whole current roster in the Seed frame.
    let mut roster = RosterStore::load();
    refresh_self_roster(&mut roster, &subnets, &local_pub.to_hex());
    match tokio::time::timeout(
        CEREMONY_TIMEOUT,
        run_responder(
            &conn,
            local_pub,
            &subnets,
            &roster,
            &mut rate,
            &mut trust,
            now_secs(),
        ),
    )
    .await
    {

+    // Roster (REQ-MESH-2): refresh this seed-holder's own entry for every

+    // served subnet so the joiner always learns at least us, then hand it the

+    // whole current roster in the Seed frame.

+    let mut roster = RosterStore::load();

+    refresh_self_roster(&mut roster, &subnets, &local_pub.to_hex());


Now the responder success arm — add the joiner to R's roster + save:


            if let Some(intro) = &outcome.intro {
                crate::registryhost::repair_evict_superseded(
                    &mut trust,
                    &outcome.subnet,
                    &outcome.joiner_hex,
                    intro,
                    &spt_store::perch::identity_dir().join("registry"),
                );
            }
            match trust.save() {
                Ok(()) => eprintln!("PAIRED: joiner pinned ({:?})", outcome.trust),
                Err(e) => eprintln!("PAIRING_TRUST_SAVE_FAIL: {e}"),
            }

            if let Some(intro) = &outcome.intro {
                crate::registryhost::repair_evict_superseded(
                    &mut trust,
                    &outcome.subnet,
                    &outcome.joiner_hex,
                    intro,
                    &spt_store::perch::identity_dir().join("registry"),
                );
            }
            // Grow the seed-holder's roster by the new member (REQ-MESH-2): the
            // joiner's identity (its self-intro) + the address observed on the
            // live ceremony connection. A completed re-pair of a tombstoned key
            // clears the tombstone (decision 10) — the deliberate elevated act.
            // [impl->REQ-MESH-2]
            roster.clear_tombstone(&outcome.subnet, &outcome.joiner_hex);
            record_roster_peer(
                &mut roster,
                &outcome.subnet,
                &outcome.joiner_hex,
                outcome.intro.as_ref(),
                &conn,
            );
            match trust.save() {
                Ok(()) => eprintln!("PAIRED: joiner pinned ({:?})", outcome.trust),
                Err(e) => eprintln!("PAIRING_TRUST_SAVE_FAIL: {e}"),
            }
            if let Err(e) = roster.save() {
                eprintln!("PAIRING_ROSTER_SAVE_FAIL: {e}");
            }


            if let Some(intro) = &outcome.intro {
                crate::registryhost::repair_evict_superseded(
                    &mut trust,
                    &outcome.subnet,
                    &outcome.joiner_hex,
                    intro,
                    &spt_store::perch::identity_dir().join("registry"),
                );
            }
            match trust.save() {
                Ok(()) => eprintln!("PAIRED: joiner pinned ({:?})", outcome.trust),
                Err(e) => eprintln!("PAIRING_TRUST_SAVE_FAIL: {e}"),
            }

            if let Some(intro) = &outcome.intro {
                crate::registryhost::repair_evict_superseded(
                    &mut trust,
                    &outcome.subnet,
                    &outcome.joiner_hex,
                    intro,
                    &spt_store::perch::identity_dir().join("registry"),
                );
            }
            // Grow the seed-holder's roster by the new member (REQ-MESH-2): the
            // joiner's identity (its self-intro) + the address observed on the
            // live ceremony connection. A completed re-pair of a tombstoned key
            // clears the tombstone (decision 10) — the deliberate elevated act.
            // [impl->REQ-MESH-2]
            roster.clear_tombstone(&outcome.subnet, &outcome.joiner_hex);
            record_roster_peer(
                &mut roster,
                &outcome.subnet,
                &outcome.joiner_hex,
                outcome.intro.as_ref(),
                &conn,
            );
            match trust.save() {
                Ok(()) => eprintln!("PAIRED: joiner pinned ({:?})", outcome.trust),
                Err(e) => eprintln!("PAIRING_TRUST_SAVE_FAIL: {e}"),
            }
            if let Err(e) = roster.save() {
                eprintln!("PAIRING_ROSTER_SAVE_FAIL: {e}");
            }

+            // Grow the seed-holder's roster by the new member (REQ-MESH-2): the

+            // joiner's identity (its self-intro) + the address observed on the

+            // live ceremony connection. A completed re-pair of a tombstoned key

+            // clears the tombstone (decision 10) — the deliberate elevated act.

+            roster.clear_tombstone(&outcome.subnet, &outcome.joiner_hex);

+                outcome.intro.as_ref(),

+            if let Err(e) = roster.save() {

+                eprintln!("PAIRING_ROSTER_SAVE_FAIL: {e}");

// A failed ceremony is normal traffic (wrong code, impostor probe,

// vanished peer) — logged, never fatal; the limiter charged it.


Now the joiner leg. Add roster load + thread into both `run_initiator` calls:


    let mut trust = TrustStore::load();
    let mut subnets = SubnetStore::load();
    // The joiner's self-intro (M8 decision 13, REQ-SUBNET-7): label +

    let mut trust = TrustStore::load();
    let mut subnets = SubnetStore::load();
    // Roster (REQ-MESH-2): run_initiator merges the seed-holder's transferred
    // roster into this store; we add our own self-entry + the seed-holder's
    // address after a successful ceremony.
    let mut roster = RosterStore::load();
    // The joiner's self-intro (M8 decision 13, REQ-SUBNET-7): label +


    let mut trust = TrustStore::load();
    let mut subnets = SubnetStore::load();
    // The joiner's self-intro (M8 decision 13, REQ-SUBNET-7): label +

    let mut trust = TrustStore::load();
    let mut subnets = SubnetStore::load();
    // Roster (REQ-MESH-2): run_initiator merges the seed-holder's transferred
    // roster into this store; we add our own self-entry + the seed-holder's
    // address after a successful ceremony.
    let mut roster = RosterStore::load();
    // The joiner's self-intro (M8 decision 13, REQ-SUBNET-7): label +

+    // Roster (REQ-MESH-2): run_initiator merges the seed-holder's transferred

+    // roster into this store; we add our own self-entry + the seed-holder's

+    // address after a successful ceremony.

+    let mut roster = RosterStore::load();

// hashed machine id — what lets the seed-holder recognize this MACHINE

// re-pairing under a fresh key and evict the superseded identity.


        run_initiator(
            &conn,
            endpoint.node_id(),
            subnet_name,
            code,
            &mut trust,
            &mut subnets,
            now_secs(),
            0,
            intro.as_ref(),
        ),
    )
    .await
    .map_err(|_| JoinFail::Other(format!("ceremony exceeded {CEREMONY_TIMEOUT_SECS}s")))?;

        run_initiator(
            &conn,
            endpoint.node_id(),
            subnet_name,
            code,
            &mut trust,
            &mut subnets,
            &mut roster,
            now_secs(),
            0,
            intro.as_ref(),
        ),
    )
    .await
    .map_err(|_| JoinFail::Other(format!("ceremony exceeded {CEREMONY_TIMEOUT_SECS}s")))?;


        run_initiator(
            &conn,
            endpoint.node_id(),
            subnet_name,
            code,
            &mut trust,
            &mut subnets,
            now_secs(),
            0,
            intro.as_ref(),
        ),
    )
    .await
    .map_err(|_| JoinFail::Other(format!("ceremony exceeded {CEREMONY_TIMEOUT_SECS}s")))?;

        run_initiator(
            &conn,
            endpoint.node_id(),
            subnet_name,
            code,
            &mut trust,
            &mut subnets,
            &mut roster,
            now_secs(),
            0,
            intro.as_ref(),
        ),
    )
    .await
    .map_err(|_| JoinFail::Other(format!("ceremony exceeded {CEREMONY_TIMEOUT_SECS}s")))?;


                run_initiator(
                    &retry_conn,
                    endpoint.node_id(),
                    subnet_name,
                    code,
                    &mut trust,
                    &mut subnets,
                    now_secs(),
                    1,
                    intro.as_ref(),
                ),

                run_initiator(
                    &retry_conn,
                    endpoint.node_id(),
                    subnet_name,
                    code,
                    &mut trust,
                    &mut subnets,
                    &mut roster,
                    now_secs(),
                    1,
                    intro.as_ref(),
                ),


                run_initiator(
                    &retry_conn,
                    endpoint.node_id(),
                    subnet_name,
                    code,
                    &mut trust,
                    &mut subnets,
                    now_secs(),
                    1,
                    intro.as_ref(),
                ),

                run_initiator(
                    &retry_conn,
                    endpoint.node_id(),
                    subnet_name,
                    code,
                    &mut trust,
                    &mut subnets,
                    &mut roster,
                    now_secs(),
                    1,
                    intro.as_ref(),
                ),


Joiner success arms — stamp the responder's address + author self-entry + save:


            // [impl->REQ-CONV-1]
            record_ceremony_peer_addr(&conn, &remote_hex(&conn));
            o
        }

            // [impl->REQ-CONV-1]
            record_ceremony_peer_addr(&conn, &remote_hex(&conn));
            // [impl->REQ-MESH-2]
            stamp_roster_responder(&mut roster, subnet_name, &conn);
            o
        }


            // [impl->REQ-CONV-1]
            record_ceremony_peer_addr(&conn, &remote_hex(&conn));
            o
        }

            // [impl->REQ-CONV-1]
            record_ceremony_peer_addr(&conn, &remote_hex(&conn));
            // [impl->REQ-MESH-2]
            stamp_roster_responder(&mut roster, subnet_name, &conn);
            o
        }

// directly instead of re-finding the peer from scratch.

+            stamp_roster_responder(&mut roster, subnet_name, &conn);

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


                    // [impl->REQ-CONV-1]
                    record_ceremony_peer_addr(&retry_conn, &remote_hex(&retry_conn));
                    o
                }

                    // [impl->REQ-CONV-1]
                    record_ceremony_peer_addr(&retry_conn, &remote_hex(&retry_conn));
                    // [impl->REQ-MESH-2]
                    stamp_roster_responder(&mut roster, subnet_name, &retry_conn);
                    o
                }


                    // [impl->REQ-CONV-1]
                    record_ceremony_peer_addr(&retry_conn, &remote_hex(&retry_conn));
                    o
                }

                    // [impl->REQ-CONV-1]
                    record_ceremony_peer_addr(&retry_conn, &remote_hex(&retry_conn));
                    // [impl->REQ-MESH-2]
                    stamp_roster_responder(&mut roster, subnet_name, &retry_conn);
                    o
                }

// The retry ran on a FRESH connection — seed from it.

+                    // [impl->REQ-MESH-2]

+                    stamp_roster_responder(&mut roster, subnet_name, &retry_conn);


    trust
        .save()
        .map_err(|e| JoinFail::Other(format!("trust save: {e}")))?;
    subnets
        .save()
        .map_err(|e| JoinFail::Other(format!("seed save: {e}")))?;
    Ok(format!("{outcome:?}"))

    trust
        .save()
        .map_err(|e| JoinFail::Other(format!("trust save: {e}")))?;
    subnets
        .save()
        .map_err(|e| JoinFail::Other(format!("seed save: {e}")))?;
    // Author our own roster self-entry for the now-joined subnet, then persist
    // the adopted roster (REQ-MESH-2). `subnets` now holds the joined subnet, so
    // refresh covers it.
    // [impl->REQ-MESH-2]
    refresh_self_roster(&mut roster, &subnets, &endpoint.node_id().to_hex());
    roster
        .save()
        .map_err(|e| JoinFail::Other(format!("roster save: {e}")))?;
    Ok(format!("{outcome:?}"))


    trust
        .save()
        .map_err(|e| JoinFail::Other(format!("trust save: {e}")))?;
    subnets
        .save()
        .map_err(|e| JoinFail::Other(format!("seed save: {e}")))?;
    Ok(format!("{outcome:?}"))

    trust
        .save()
        .map_err(|e| JoinFail::Other(format!("trust save: {e}")))?;
    subnets
        .save()
        .map_err(|e| JoinFail::Other(format!("seed save: {e}")))?;
    // Author our own roster self-entry for the now-joined subnet, then persist
    // the adopted roster (REQ-MESH-2). `subnets` now holds the joined subnet, so
    // refresh covers it.
    // [impl->REQ-MESH-2]
    refresh_self_roster(&mut roster, &subnets, &endpoint.node_id().to_hex());
    roster
        .save()
        .map_err(|e| JoinFail::Other(format!("roster save: {e}")))?;
    Ok(format!("{outcome:?}"))

+    // Author our own roster self-entry for the now-joined subnet, then persist

+    // the adopted roster (REQ-MESH-2). `subnets` now holds the joined subnet, so

+    refresh_self_roster(&mut roster, &subnets, &endpoint.node_id().to_hex());

+        .map_err(|e| JoinFail::Other(format!("roster save: {e}")))?;


Add the three roster helpers near the existing ceremony-addr writer:


/// Unix seconds — the ceremony clock (TOTP steps + limiter time). This is

/// Author this node's own roster self-entry for every subnet it holds
/// (REQ-MESH-2), so a joiner receiving the roster always learns at least us.
/// Identity only — the self address is captured from live member connections
/// (Mesh-D4), not here. Lease = the node's current label epoch (a peek, never a
/// consume, so the pairing path can't disturb registry ordering).
// [impl->REQ-MESH-2]
fn refresh_self_roster(roster: &mut RosterStore, subnets: &SubnetStore, self_hex: &str) {
    let label = spt_store::hostlabel::os_hostname().unwrap_or_default();
    let machine_id = crate::machineid::machine_id_hash().unwrap_or_default();
    let lease = spt_store::epoch::EpochSource::load().current();
    let now = now_secs().to_string();
    for s in &subnets.subnets {
        roster.upsert_self(&s.name, self_hex, &label, &machine_id, None, &now, lease);
    }
}

/// Record a freshly-paired member into the seed-holder's roster (REQ-MESH-2):
/// the joiner's self-intro identity + the address observed on the live ceremony
/// connection. Merged strictly-greater-wins like any peer entry (lease 0 — the
/// member re-authors at its real lease over a mesh connection, Mesh-D4).
// [impl->REQ-MESH-2]
fn record_roster_peer(
    roster: &mut RosterStore,
    subnet: &str,
    peer_hex: &str,
    intro: Option<&spt_net::net::pairing::wire::NodeIntro>,
    conn: &Connection,
) {
    let (label, machine_id) = match intro {
        Some(i) => (i.label.clone(), i.machine_id.clone()),
        None => (String::new(), String::new()),
    };
    roster.merge_entry(RosterEntry {
        pubkey_hex: peer_hex.to_string(),
        subnet: subnet.to_string(),
        label,
        machine_id,
        address: conn_addr_json(conn),
        last_seen: now_secs().to_string(),
        lease_epoch: 0,
    });
}

/// Stamp the seed-holder's observed address onto its (already-adopted) roster
/// entry on the joiner side (REQ-MESH-2) — the joiner learned the responder's
/// identity from the transferred roster, but not its address (a self-entry
/// carries none); the live ceremony connection supplies it.
// [impl->REQ-MESH-2]
fn stamp_roster_responder(roster: &mut RosterStore, subnet: &str, conn: &Connection) {
    let peer_hex = remote_hex(conn);
    let Some(addr) = conn_addr_json(conn) else {
        return;
    };
    if let Some(existing) = roster
        .members
        .iter_mut()
        .find(|m| m.subnet == subnet && m.pubkey_hex == peer_hex)
    {
        existing.address = Some(addr);
    } else {
        roster.merge_entry(RosterEntry {
            pubkey_hex: peer_hex,
            subnet: subnet.to_string(),
            label: String::new(),
            machine_id: String::new(),
            address: Some(addr),
            last_seen: now_secs().to_string(),
            lease_epoch: 0,
        });
    }
}

/// The connection's observed remote address as opaque JSON (the roster's
/// address field, same posture as `peer-addrs.json`). `None` if the address is
/// pathless or won't serialize.
fn conn_addr_json(conn: &Connection) -> Option<serde_json::Value> {
    let addr = spt_net::net::endpoint::conn_remote_addr(conn);
    if addr.is_empty() {
        return None;
    }
    serde_json::to_value(addr).ok()
}

/// Unix seconds — the ceremony clock (TOTP steps + limiter time). This is


/// Author this node's own roster self-entry for every subnet it holds
/// (REQ-MESH-2), so a joiner receiving the roster always learns at least us.
/// Identity only — the self address is captured from live member connections
/// (Mesh-D4), not here. Lease = the node's current label epoch (a peek, never a
/// consume, so the pairing path can't disturb registry ordering).
// [impl->REQ-MESH-2]
fn refresh_self_roster(roster: &mut RosterStore, subnets: &SubnetStore, self_hex: &str) {
    let label = spt_store::hostlabel::os_hostname().unwrap_or_default();
    let machine_id = crate::machineid::machine_id_hash().unwrap_or_default();
    let lease = spt_store::epoch::EpochSource::load().current();
    let now = now_secs().to_string();
    for s in &subnets.subnets {
        roster.upsert_self(&s.name, self_hex, &label, &machine_id, None, &now, lease);
    }
}

/// Record a freshly-paired member into the seed-holder's roster (REQ-MESH-2):
/// the joiner's self-intro identity + the address observed on the live ceremony
/// connection. Merged strictly-greater-wins like any peer entry (lease 0 — the
/// member re-authors at its real lease over a mesh connection, Mesh-D4).
// [impl->REQ-MESH-2]
fn record_roster_peer(
    roster: &mut RosterStore,
    subnet: &str,
    peer_hex: &str,
    intro: Option<&spt_net::net::pairing::wire::NodeIntro>,
    conn: &Connection,
) {
    let (label, machine_id) = match intro {
        Some(i) => (i.label.clone(), i.machine_id.clone()),
        None => (String::new(), String::new()),
    };
    roster.merge_entry(RosterEntry {
        pubkey_hex: peer_hex.to_string(),
        subnet: subnet.to_string(),
        label,
        machine_id,
        address: conn_addr_json(conn),
        last_seen: now_secs().to_string(),
        lease_epoch: 0,
    });
}

/// Stamp the seed-holder's observed address onto its (already-adopted) roster
/// entry on the joiner side (REQ-MESH-2) — the joiner learned the responder's
/// identity from the transferred roster, but not its address (a self-entry
/// carries none); the live ceremony connection supplies it.
// [impl->REQ-MESH-2]
fn stamp_roster_responder(roster: &mut RosterStore, subnet: &str, conn: &Connection) {
    let peer_hex = remote_hex(conn);
    let Some(addr) = conn_addr_json(conn) else {
        return;
    };
    if let Some(existing) = roster
        .members
        .iter_mut()
        .find(|m| m.subnet == subnet && m.pubkey_hex == peer_hex)
    {
        existing.address = Some(addr);
    } else {
        roster.merge_entry(RosterEntry {
            pubkey_hex: peer_hex,
            subnet: subnet.to_string(),
            label: String::new(),
            machine_id: String::new(),
            address: Some(addr),
            last_seen: now_secs().to_string(),
            lease_epoch: 0,
        });
    }
}

/// The connection's observed remote address as opaque JSON (the roster's
/// address field, same posture as `peer-addrs.json`). `None` if the address is
/// pathless or won't serialize.
fn conn_addr_json(conn: &Connection) -> Option<serde_json::Value> {
    let addr = spt_net::net::endpoint::conn_remote_addr(conn);
    if addr.is_empty() {
        return None;
    }
    serde_json::to_value(addr).ok()
}

/// Unix seconds — the ceremony clock (TOTP steps + limiter time). This is

+/// Author this node's own roster self-entry for every subnet it holds

+/// (REQ-MESH-2), so a joiner receiving the roster always learns at least us.

+/// Identity only — the self address is captured from live member connections

+/// (Mesh-D4), not here. Lease = the node's current label epoch (a peek, never a

+/// consume, so the pairing path can't disturb registry ordering).

+fn refresh_self_roster(roster: &mut RosterStore, subnets: &SubnetStore, self_hex: &str) {

+    let label = spt_store::hostlabel::os_hostname().unwrap_or_default();

+    let machine_id = crate::machineid::machine_id_hash().unwrap_or_default();

+    let lease = spt_store::epoch::EpochSource::load().current();

+        roster.upsert_self(&s.name, self_hex, &label, &machine_id, None, &now, lease);

+/// Record a freshly-paired member into the seed-holder's roster (REQ-MESH-2):

+/// the joiner's self-intro identity + the address observed on the live ceremony

+/// connection. Merged strictly-greater-wins like any peer entry (lease 0 — the

+/// member re-authors at its real lease over a mesh connection, Mesh-D4).

+    intro: Option<&spt_net::net::pairing::wire::NodeIntro>,

+    let (label, machine_id) = match intro {

+        Some(i) => (i.label.clone(), i.machine_id.clone()),

+        None => (String::new(), String::new()),

+        pubkey_hex: peer_hex.to_string(),

+        last_seen: now_secs().to_string(),

+/// Stamp the seed-holder's observed address onto its (already-adopted) roster

+/// entry on the joiner side (REQ-MESH-2) — the joiner learned the responder's

+/// identity from the transferred roster, but not its address (a self-entry

+/// carries none); the live ceremony connection supplies it.

+fn stamp_roster_responder(roster: &mut RosterStore, subnet: &str, conn: &Connection) {

+    let Some(addr) = conn_addr_json(conn) else {

+        .find(|m| m.subnet == subnet && m.pubkey_hex == peer_hex)

+        roster.merge_entry(RosterEntry {

+            subnet: subnet.to_string(),

+            last_seen: now_secs().to_string(),

+/// The connection's observed remote address as opaque JSON (the roster's

+/// address field, same posture as `peer-addrs.json`). `None` if the address is

+fn conn_addr_json(conn: &Connection) -> Option<serde_json::Value> {

+    let addr = spt_net::net::endpoint::conn_remote_addr(conn);

/// the NTP-corrected clock (M8 decision 18, REQ-PAIR-8): the system clock

/// plus a lazily-queried in-process offset, so a node whose OS clock is


Now build spt-net + spt-daemon to catch errors.


Bash

cd C:/Users/decid/Documents/projects/spt-core && cargo build -p spt-net -p spt-daemon 2>&1 | tail -40

Build spt-net + spt-daemon


   Compiling spt-store v0.2.0 (C:\Users\decid\Documents\projects\spt-core\crates\spt-store)
   Compiling spt-msg v0.2.0 (C:\Users\decid\Documents\projects\spt-core\crates\spt-msg)
   Compiling spt-runtime v0.2.0 (C:\Users\decid\Documents\projects\spt-core\crates\spt-runtime)
   Compiling spt-net v0.2.0 (C:\Users\decid\Documents\projects\spt-core\crates\spt-net)
   Compiling spt-term v0.2.0 (C:\Users\decid\Documents\projects\spt-core\crates\spt-term)
   Compiling spt-live v0.2.0 (C:\Users\decid\Documents\projects\spt-core\crates\spt-live)
   Compiling spt-daemon v0.2.0 (C:\Users\decid\Documents\projects\spt-core\crates\spt-daemon)
    Finished `dev` profile [unoptimized + debuginfo] target(s) in 9.86s


Clean build. Now add wire unit tests. Read the happy-path ceremony test to extend it for roster transfer.


958	        .expect("responder bind");
959	        let initiator = NetEndpoint::bind_scoped(
960	            &init_id,
961	            RelayPolicy::Disabled,
962	            LocalDiscovery::Off,
963	            vec![SPT_PAIR_ALPN.to_vec()],
964	            BindScope::Loopback,
965	        )
966	        .await
967	        .expect("initiator bind");
968	        (responder, initiator, resp_id, init_id)
969	    }
970
971	    // [unit->REQ-PAIR-1] HAPPY PATH over the wire: two endpoints, one TOTP code, complete
972	    // the ceremony on the pairing ALPN and each pins the other's pubkey in its trust
973	    // store — "two machines pair via one TOTP code" at the component level (loopback,
974	    // relays disabled). The two-host WAN form is the int stage at D9.
975	    #[tokio::test]
976	    async fn loopback_pairs_and_pins_both_sides() {
977	        let (responder, initiator, resp_id, init_id) = pair_endpoints().await;
978	        let resp_pub = resp_id.public_key();
979	        let init_pub = init_id.public_key();
980	        let seed = [7u8; 20];
981	        let code = code_for(seed);
982	        let resp_addr = responder.addr();
983
984	        // Responder task: accept, run the ceremony, return its trust store + outcome.
985	        let resp_task = tokio::spawn(async move {
986	            let incoming = responder.accept().await.expect("incoming");
987	            let conn = incoming
988	                .accept()
989	                .expect("accept")
990	                .await
991	                .expect("connecting");
992	            let subnets = subnet_store("home", seed, 3);
993	            let mut rate = PairingRateLimiter::new();
994	            let mut trust = TrustStore::default();
995	            let outcome = run_responder(&conn, resp_pub, &subnets, &RosterStore::default(), &mut rate, &mut trust, NOW)
996	                .await
997	                .expect("responder pairs");
998	            conn.closed().await;
999	            (trust, outcome)
1000	        });
1001
1002	        let conn = initiator
1003	            .connect_with_alpn(resp_addr, SPT_PAIR_ALPN)
1004	            .await
1005	            .expect("connect");
1006	        let mut init_trust = TrustStore::default();
1007	        let mut init_subnets = SubnetStore::default();
1008	        let mut init_roster = RosterStore::default();
1009	        let init_outcome = run_initiator(
1010	            &conn,
1011	            init_pub,
1012	            "home",
1013	            &code,
1014	            &mut init_trust,
1015	            &mut init_subnets,
1016	            &mut init_roster,
1017	            NOW,
1018	            0,    // step_back: the normal attempt
1019	            None, // intro: pre-M8 joiner shape
1020	        )
1021	        .await
1022	        .expect("initiator pairs");
1023	        conn.close(0u32.into(), b"done");
1024
1025	        let (resp_trust, resp_outcome) = resp_task.await.expect("responder task");
1026
1027	        assert_eq!(
1028	            init_outcome,
1029	            TrustOutcome::Pinned,
1030	            "initiator pins responder"
1031	        );
1032	        assert_eq!(
1033	            resp_outcome.trust,
1034	            TrustOutcome::Pinned,
1035	            "responder pins initiator"
1036	        );
1037	        // The outcome reports the ceremony facts the M8 re-pair overwrite
1038	        // consumes: the joiner's authenticated identity + the subnet. No
1039	        // intro was sent on this pre-M8-shaped call.
1040	        assert_eq!(resp_outcome.joiner_hex, init_pub.to_hex());
1041	        assert_eq!(resp_outcome.subnet, "home");
1042	        assert!(resp_outcome.intro.is_none(), "no intro sent");
1043	        // Each side trusts the OTHER's real pubkey within the subnet.
1044	        assert!(init_trust.is_trusted("home", &resp_pub.to_hex()));
1045	        assert!(resp_trust.is_trusted("home", &init_pub.to_hex()));
1046	        assert!(
1047	            !init_trust.is_trusted("home", &init_pub.to_hex()),

        .expect("responder bind");
        let initiator = NetEndpoint::bind_scoped(
            &init_id,
            RelayPolicy::Disabled,
            LocalDiscovery::Off,
            vec![SPT_PAIR_ALPN.to_vec()],
            BindScope::Loopback,
        )
        .await
        .expect("initiator bind");
        (responder, initiator, resp_id, init_id)
    }

    // [unit->REQ-PAIR-1] HAPPY PATH over the wire: two endpoints, one TOTP code, complete
    // the ceremony on the pairing ALPN and each pins the other's pubkey in its trust
    // store — "two machines pair via one TOTP code" at the component level (loopback,
    // relays disabled). The two-host WAN form is the int stage at D9.
    #[tokio::test]
    async fn loopback_pairs_and_pins_both_sides() {
        let (responder, initiator, resp_id, init_id) = pair_endpoints().await;
        let resp_pub = resp_id.public_key();
        let init_pub = init_id.public_key();
        let seed = [7u8; 20];
        let code = code_for(seed);
        let resp_addr = responder.addr();

        // Responder task: accept, run the ceremony, return its trust store + outcome.
        let resp_task = tokio::spawn(async move {
            let incoming = responder.accept().await.expect("incoming");
            let conn = incoming
                .accept()
                .expect("accept")
                .await
                .expect("connecting");
            let subnets = subnet_store("home", seed, 3);
            let mut rate = PairingRateLimiter::new();
            let mut trust = TrustStore::default();
            let outcome = run_responder(&conn, resp_pub, &subnets, &RosterStore::default(), &mut rate, &mut trust, NOW)
                .await
                .expect("responder pairs");
            conn.closed().await;
            (trust, outcome)
        });

        let conn = initiator
            .connect_with_alpn(resp_addr, SPT_PAIR_ALPN)
            .await
            .expect("connect");
        let mut init_trust = TrustStore::default();
        let mut init_subnets = SubnetStore::default();
        let mut init_roster = RosterStore::default();
        let init_outcome = run_initiator(
            &conn,
            init_pub,
            "home",
            &code,
            &mut init_trust,
            &mut init_subnets,
            &mut init_roster,
            NOW,
            0,    // step_back: the normal attempt
            None, // intro: pre-M8 joiner shape
        )
        .await
        .expect("initiator pairs");
        conn.close(0u32.into(), b"done");

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

        assert_eq!(
            init_outcome,
            TrustOutcome::Pinned,
            "initiator pins responder"
        );
        assert_eq!(
            resp_outcome.trust,
            TrustOutcome::Pinned,
            "responder pins initiator"
        );
        // The outcome reports the ceremony facts the M8 re-pair overwrite
        // consumes: the joiner's authenticated identity + the subnet. No
        // intro was sent on this pre-M8-shaped call.
        assert_eq!(resp_outcome.joiner_hex, init_pub.to_hex());
        assert_eq!(resp_outcome.subnet, "home");
        assert!(resp_outcome.intro.is_none(), "no intro sent");
        // Each side trusts the OTHER's real pubkey within the subnet.
        assert!(init_trust.is_trusted("home", &resp_pub.to_hex()));
        assert!(resp_trust.is_trusted("home", &init_pub.to_hex()));
        assert!(
            !init_trust.is_trusted("home", &init_pub.to_hex()),


Extend the happy-path test to exercise roster transfer:


Edit

            let subnets = subnet_store("home", seed, 3);
            let mut rate = PairingRateLimiter::new();
            let mut trust = TrustStore::default();
            let outcome = run_responder(&conn, resp_pub, &subnets, &RosterStore::default(), &mut rate, &mut trust, NOW)
                .await
                .expect("responder pairs");
            conn.closed().await;
            (trust, outcome)
        });

            let subnets = subnet_store("home", seed, 3);
            let mut rate = PairingRateLimiter::new();
            let mut trust = TrustStore::default();
            // A non-empty roster to transfer: one live member + one tombstoned
            // (REQ-MESH-2 — the joiner must adopt both).
            let mut resp_roster = RosterStore::default();
            resp_roster.upsert_self("home", "alpha", "host-a", "mid-a", None, "10", 4);
            resp_roster.tombstone("home", "ghost", "20");
            let outcome = run_responder(&conn, resp_pub, &subnets, &resp_roster, &mut rate, &mut trust, NOW)
                .await
                .expect("responder pairs");
            conn.closed().await;
            (trust, outcome)
        });


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

            let subnets = subnet_store("home", seed, 3);
            let mut rate = PairingRateLimiter::new();
            let mut trust = TrustStore::default();
            let outcome = run_responder(&conn, resp_pub, &subnets, &RosterStore::default(), &mut rate, &mut trust, NOW)
                .await
                .expect("responder pairs");
            conn.closed().await;
            (trust, outcome)
        });

            let subnets = subnet_store("home", seed, 3);
            let mut rate = PairingRateLimiter::new();
            let mut trust = TrustStore::default();
            // A non-empty roster to transfer: one live member + one tombstoned
            // (REQ-MESH-2 — the joiner must adopt both).
            let mut resp_roster = RosterStore::default();
            resp_roster.upsert_self("home", "alpha", "host-a", "mid-a", None, "10", 4);
            resp_roster.tombstone("home", "ghost", "20");
            let outcome = run_responder(&conn, resp_pub, &subnets, &resp_roster, &mut rate, &mut trust, NOW)
                .await
                .expect("responder pairs");
            conn.closed().await;
            (trust, outcome)
        });

-            let outcome = run_responder(&conn, resp_pub, &subnets, &RosterStore::default(), &mut rate, &mut trust, NOW)

+            // A non-empty roster to transfer: one live member + one tombstoned

+            // (REQ-MESH-2 — the joiner must adopt both).

+            let mut resp_roster = RosterStore::default();

+            resp_roster.upsert_self("home", "alpha", "host-a", "mid-a", None, "10", 4);

+            resp_roster.tombstone("home", "ghost", "20");

+            let outcome = run_responder(&conn, resp_pub, &subnets, &resp_roster, &mut rate, &mut trust, NOW)


        // Each side trusts the OTHER's real pubkey within the subnet.
        assert!(init_trust.is_trusted("home", &resp_pub.to_hex()));
        assert!(resp_trust.is_trusted("home", &init_pub.to_hex()));

        // The joiner ADOPTED the seed-holder's roster (REQ-MESH-2): the live
        // member is now its member, the tombstone propagated, and both were
        // stamped into the subnet it joined (not the empty wire subnet).
        assert!(init_roster.is_member("home", "alpha"), "adopted live member");
        assert!(init_roster.is_tombstoned("home", "ghost"), "adopted tombstone");
        assert!(!init_roster.is_member("home", "ghost"), "tombstone dominates");
        assert_eq!(
            init_roster.find("home", "alpha").unwrap().label,
            "host-a",
            "entry fields transferred"
        );
        // Each side trusts the OTHER's real pubkey within the subnet.
        assert!(init_trust.is_trusted("home", &resp_pub.to_hex()));
        assert!(resp_trust.is_trusted("home", &init_pub.to_hex()));


        // The joiner ADOPTED the seed-holder's roster (REQ-MESH-2): the live
        // member is now its member, the tombstone propagated, and both were
        // stamped into the subnet it joined (not the empty wire subnet).
        assert!(init_roster.is_member("home", "alpha"), "adopted live member");
        assert!(init_roster.is_tombstoned("home", "ghost"), "adopted tombstone");
        assert!(!init_roster.is_member("home", "ghost"), "tombstone dominates");
        assert_eq!(
            init_roster.find("home", "alpha").unwrap().label,
            "host-a",
            "entry fields transferred"
        );
        // Each side trusts the OTHER's real pubkey within the subnet.
        assert!(init_trust.is_trusted("home", &resp_pub.to_hex()));
        assert!(resp_trust.is_trusted("home", &init_pub.to_hex()));

assert!(resp_outcome.intro.is_none(), "no intro sent");

+        // The joiner ADOPTED the seed-holder's roster (REQ-MESH-2): the live

+        // member is now its member, the tombstone propagated, and both were

+        // stamped into the subnet it joined (not the empty wire subnet).

+        assert!(init_roster.is_member("home", "alpha"), "adopted live member");

+        assert!(init_roster.is_tombstoned("home", "ghost"), "adopted tombstone");

+        assert!(!init_roster.is_member("home", "ghost"), "tombstone dominates");

+            init_roster.find("home", "alpha").unwrap().label,


Add a dedicated codec unit test for the additive Seed frame:


        // Unknown tag is rejected.
        assert!(matches!(
            decode_frame(&[0xFE]),
            Err(PairWireError::Protocol(_))
        ));
    }
}

        // Unknown tag is rejected.
        assert!(matches!(
            decode_frame(&[0xFE]),
            Err(PairWireError::Protocol(_))
        ));
    }

    // [unit->REQ-MESH-2] the Seed frame carries the roster additively: a
    // members+tombstones payload round-trips (subnet implicit ⇒ empty on the
    // wire; address optional), an empty roster round-trips, a LEGACY short body
    // (seed+epoch only) decodes to an empty roster, and a malformed trailing
    // count is a protocol error — never a panic.
    #[test]
    fn seed_frame_roster_is_additive_and_roundtrips() {
        let members = vec![
            RosterEntry {
                pubkey_hex: "aa".into(),
                subnet: "home".into(), // enc ignores subnet (implicit)
                label: "host-a".into(),
                machine_id: "mid-a".into(),
                address: Some(serde_json::json!({"addr": "1.2.3.4:5"})),
                last_seen: "5".into(),
                lease_epoch: 7,
            },
            RosterEntry {
                pubkey_hex: "bb".into(),
                subnet: "home".into(),
                label: String::new(),
                machine_id: String::new(),
                address: None,
                last_seen: "6".into(),
                lease_epoch: 0,
            },
        ];
        let tombs = vec![Tombstone {
            pubkey_hex: "cc".into(),
            subnet: "home".into(),
            stamp: "9".into(),
        }];

        match decode_frame(&enc_seed(&[1u8; TOTP_SEED_LEN], 3, &members, &tombs)).expect("decode") {
            Frame::Seed {
                seed,
                epoch,
                roster,
                tombstones,
            } => {
                assert_eq!(seed, [1u8; TOTP_SEED_LEN]);
                assert_eq!(epoch, 3);
                assert_eq!(roster.len(), 2);
                assert_eq!(roster[0].subnet, "", "subnet implicit on the wire");
                assert_eq!(roster[0].pubkey_hex, "aa");
                assert_eq!(roster[0].label, "host-a");
                assert_eq!(roster[0].address, Some(serde_json::json!({"addr": "1.2.3.4:5"})));
                assert_eq!(roster[0].lease_epoch, 7);
                assert_eq!(roster[1].address, None, "absent address stays None");
                assert_eq!(tombstones.len(), 1);
                assert_eq!(tombstones[0].pubkey_hex, "cc");
            }
            _ => panic!("expected Seed"),
        }

        // Empty roster round-trips.
        match decode_frame(&enc_seed(&[2u8; TOTP_SEED_LEN], 1, &[], &[])).expect("decode") {
            Frame::Seed {
                roster, tombstones, ..
            } => {
                assert!(roster.is_empty() && tombstones.is_empty());
            }
            _ => panic!("expected Seed"),
        }

        // A LEGACY short body (seed+epoch only) decodes to an empty roster —
        // the additive guarantee for a pre-mesh peer.
        let mut legacy = vec![T_SEED];
        legacy.extend_from_slice(&[3u8; TOTP_SEED_LEN]);
        legacy.extend_from_slice(&9u64.to_be_bytes());
        match decode_frame(&legacy).expect("legacy decodes") {
            Frame::Seed { epoch, roster, .. } => {
                assert_eq!(epoch, 9);
                assert!(roster.is_empty());
            }
            _ => panic!("expected Seed"),
        }

        // Malformed trailing: claims one member but no entry follows ⇒ truncated.
        let mut bad = vec![T_SEED];
        bad.extend_from_slice(&[4u8; TOTP_SEED_LEN]);
        bad.extend_from_slice(&1u64.to_be_bytes());
        bad.extend_from_slice(&1u32.to_be_bytes());
        assert!(matches!(decode_frame(&bad), Err(PairWireError::Protocol(_))));
    }
}


        // Unknown tag is rejected.
        assert!(matches!(
            decode_frame(&[0xFE]),
            Err(PairWireError::Protocol(_))
        ));
    }
}

        // Unknown tag is rejected.
        assert!(matches!(
            decode_frame(&[0xFE]),
            Err(PairWireError::Protocol(_))
        ));
    }

    // [unit->REQ-MESH-2] the Seed frame carries the roster additively: a
    // members+tombstones payload round-trips (subnet implicit ⇒ empty on the
    // wire; address optional), an empty roster round-trips, a LEGACY short body
    // (seed+epoch only) decodes to an empty roster, and a malformed trailing
    // count is a protocol error — never a panic.
    #[test]
    fn seed_frame_roster_is_additive_and_roundtrips() {
        let members = vec![
            RosterEntry {
                pubkey_hex: "aa".into(),
                subnet: "home".into(), // enc ignores subnet (implicit)
                label: "host-a".into(),
                machine_id: "mid-a".into(),
                address: Some(serde_json::json!({"addr": "1.2.3.4:5"})),
                last_seen: "5".into(),
                lease_epoch: 7,
            },
            RosterEntry {
                pubkey_hex: "bb".into(),
                subnet: "home".into(),
                label: String::new(),
                machine_id: String::new(),
                address: None,
                last_seen: "6".into(),
                lease_epoch: 0,
            },
        ];
        let tombs = vec![Tombstone {
            pubkey_hex: "cc".into(),
            subnet: "home".into(),
            stamp: "9".into(),
        }];

        match decode_frame(&enc_seed(&[1u8; TOTP_SEED_LEN], 3, &members, &tombs)).expect("decode") {
            Frame::Seed {
                seed,
                epoch,
                roster,
                tombstones,
            } => {
                assert_eq!(seed, [1u8; TOTP_SEED_LEN]);
                assert_eq!(epoch, 3);
                assert_eq!(roster.len(), 2);
                assert_eq!(roster[0].subnet, "", "subnet implicit on the wire");
                assert_eq!(roster[0].pubkey_hex, "aa");
                assert_eq!(roster[0].label, "host-a");
                assert_eq!(roster[0].address, Some(serde_json::json!({"addr": "1.2.3.4:5"})));
                assert_eq!(roster[0].lease_epoch, 7);
                assert_eq!(roster[1].address, None, "absent address stays None");
                assert_eq!(tombstones.len(), 1);
                assert_eq!(tombstones[0].pubkey_hex, "cc");
            }
            _ => panic!("expected Seed"),
        }

        // Empty roster round-trips.
        match decode_frame(&enc_seed(&[2u8; TOTP_SEED_LEN], 1, &[], &[])).expect("decode") {
            Frame::Seed {
                roster, tombstones, ..
            } => {
                assert!(roster.is_empty() && tombstones.is_empty());
            }
            _ => panic!("expected Seed"),
        }

        // A LEGACY short body (seed+epoch only) decodes to an empty roster —
        // the additive guarantee for a pre-mesh peer.
        let mut legacy = vec![T_SEED];
        legacy.extend_from_slice(&[3u8; TOTP_SEED_LEN]);
        legacy.extend_from_slice(&9u64.to_be_bytes());
        match decode_frame(&legacy).expect("legacy decodes") {
            Frame::Seed { epoch, roster, .. } => {
                assert_eq!(epoch, 9);
                assert!(roster.is_empty());
            }
            _ => panic!("expected Seed"),
        }

        // Malformed trailing: claims one member but no entry follows ⇒ truncated.
        let mut bad = vec![T_SEED];
        bad.extend_from_slice(&[4u8; TOTP_SEED_LEN]);
        bad.extend_from_slice(&1u64.to_be_bytes());
        bad.extend_from_slice(&1u32.to_be_bytes());
        assert!(matches!(decode_frame(&bad), Err(PairWireError::Protocol(_))));
    }
}

+    // [unit->REQ-MESH-2] the Seed frame carries the roster additively: a

+    // members+tombstones payload round-trips (subnet implicit ⇒ empty on the

+    // wire; address optional), an empty roster round-trips, a LEGACY short body

+    // (seed+epoch only) decodes to an empty roster, and a malformed trailing

+    // count is a protocol error — never a panic.

+    fn seed_frame_roster_is_additive_and_roundtrips() {

+                pubkey_hex: "aa".into(),

+                subnet: "home".into(), // enc ignores subnet (implicit)

+                label: "host-a".into(),

+                machine_id: "mid-a".into(),

+                address: Some(serde_json::json!({"addr": "1.2.3.4:5"})),

+                pubkey_hex: "bb".into(),

+                machine_id: String::new(),

+        match decode_frame(&enc_seed(&[1u8; TOTP_SEED_LEN], 3, &members, &tombs)).expect("decode") {

+                assert_eq!(seed, [1u8; TOTP_SEED_LEN]);

+                assert_eq!(roster.len(), 2);

+                assert_eq!(roster[0].subnet, "", "subnet implicit on the wire");

+                assert_eq!(roster[0].pubkey_hex, "aa");

+                assert_eq!(roster[0].label, "host-a");

+                assert_eq!(roster[0].address, Some(serde_json::json!({"addr": "1.2.3.4:5"})));

+                assert_eq!(roster[0].lease_epoch, 7);

+                assert_eq!(roster[1].address, None, "absent address stays None");

+                assert_eq!(tombstones.len(), 1);

+                assert_eq!(tombstones[0].pubkey_hex, "cc");

+            _ => panic!("expected Seed"),

+        match decode_frame(&enc_seed(&[2u8; TOTP_SEED_LEN], 1, &[], &[])).expect("decode") {

+                assert!(roster.is_empty() && tombstones.is_empty());

+        // A LEGACY short body (seed+epoch only) decodes to an empty roster —

+        // the additive guarantee for a pre-mesh peer.

+        legacy.extend_from_slice(&[3u8; TOTP_SEED_LEN]);

+        legacy.extend_from_slice(&9u64.to_be_bytes());

+        match decode_frame(&legacy).expect("legacy decodes") {

+            Frame::Seed { epoch, roster, .. } => {

+                assert!(roster.is_empty());

+        // Malformed trailing: claims one member but no entry follows ⇒ truncated.

+        bad.extend_from_slice(&[4u8; TOTP_SEED_LEN]);

+        bad.extend_from_slice(&1u64.to_be_bytes());

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

+        assert!(matches!(decode_frame(&bad), Err(PairWireError::Protocol(_))));


Run the wire + daemon pairing tests.


Bash

cd C:/Users/decid/Documents/projects/spt-core && cargo test -p spt-net pairing::wire 2>&1 | tail -35

Run wire pairing tests