feat(tickets): member-issued tickets so rooms outlive their creator (W7 P3)
The ticket a member sees/copies for sharing is now stamped with THEIR OWN live address + the room's topic, not the (possibly someone else's) ticket they joined with. So every member — not just the creator — hands out a working door that bootstraps newcomers off themselves; a room stays reachable as long as anyone inside can share a ticket, even after the creator leaves. Pure seam PeerSpeakTicket::restamp(ticket_str, my_addr): re-parse, swap host_addr to mine, keep topic_id; no-op for an unparseable string or when the addr is already mine. The core re-stamps only the DISPLAY copy sent in RoomJoined; the join/bootstrap ticket_str and the A8 retain logic are untouched, so this is non-breaking (same wire format, different addr). +3 unit tests (swaps addr/keeps topic, idempotent for same addr, passes through unparseable). 248 lib tests green, clippy clean, release builds. Tests-green; the end-to-end 'creator leaves, joiner's ticket still works' behaviour wants a 2-machine field test. Multi-bootstrap (Vec) + ticket encoding tightening deferred (both breaking wire changes — want daylight). Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
This commit is contained in:
+11
-1
@@ -1078,7 +1078,17 @@ async fn run_core_loop(
|
||||
};
|
||||
|
||||
let self_id = endpoint.id().to_string();
|
||||
let _ = ui_tx.send(UiEvent::RoomJoined { ticket: ticket_str, self_id }).await;
|
||||
// Member-issued ticket (W7 P3): the ticket we hand to the UI for
|
||||
// sharing is stamped with OUR OWN live address + the room's topic,
|
||||
// not the (possibly someone else's) ticket we joined with. So every
|
||||
// member — not just the creator — hands out a working door pointing
|
||||
// at themselves, which is what lets a room outlive its creator. We
|
||||
// keep `ticket_str` untouched for joining + the A8 retain logic; we
|
||||
// only re-stamp the *display* copy. Re-stamping is a no-op for the
|
||||
// creator (same addr+topic), and a no-op if the ticket can't be
|
||||
// parsed (a malformed join, which fails anyway).
|
||||
let share_ticket = PeerSpeakTicket::restamp(&ticket_str, endpoint.addr());
|
||||
let _ = ui_tx.send(UiEvent::RoomJoined { ticket: share_ticket, self_id }).await;
|
||||
active_session = Some(session);
|
||||
}
|
||||
|
||||
|
||||
@@ -84,6 +84,20 @@ pub struct PeerSpeakTicket {
|
||||
pub topic_id: [u8; 32],
|
||||
}
|
||||
|
||||
impl PeerSpeakTicket {
|
||||
/// Member-issued ticket (W7 P3): re-stamp an existing ticket string with our
|
||||
/// OWN address while keeping its room `topic_id`, so any member can hand out a
|
||||
/// working door that bootstraps off themselves — the mechanism that lets a
|
||||
/// room outlive its creator. A no-op (returns the input unchanged) if the
|
||||
/// string can't be parsed. Re-stamping with the same address is idempotent.
|
||||
pub fn restamp(ticket_str: &str, my_addr: iroh::EndpointAddr) -> String {
|
||||
match ticket_str.parse::<PeerSpeakTicket>() {
|
||||
Ok(t) => PeerSpeakTicket { host_addr: my_addr, topic_id: t.topic_id }.to_string(),
|
||||
Err(_) => ticket_str.to_string(),
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
impl std::fmt::Display for PeerSpeakTicket {
|
||||
fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
|
||||
// serde_json on a struct of String/[u8;32] fields is infallible in practice,
|
||||
@@ -223,6 +237,38 @@ mod tests {
|
||||
assert!(matches!(res3, Err(NetError::InvalidTicket(_))));
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn test_restamp_swaps_addr_keeps_topic() {
|
||||
// A ticket "from" the host, then re-stamped by another member.
|
||||
let host = SecretKey::generate().public();
|
||||
let member = SecretKey::generate().public();
|
||||
let topic_id = [42u8; 32];
|
||||
let original = PeerSpeakTicket { host_addr: EndpointAddr::from(host), topic_id }.to_string();
|
||||
|
||||
let restamped_str = PeerSpeakTicket::restamp(&original, EndpointAddr::from(member));
|
||||
let restamped = restamped_str.parse::<PeerSpeakTicket>().unwrap();
|
||||
// Same room, but the door now points at the member, not the host.
|
||||
assert_eq!(restamped.topic_id, topic_id);
|
||||
assert_eq!(restamped.host_addr.id, member);
|
||||
assert_ne!(restamped.host_addr.id, host);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn test_restamp_is_idempotent_for_same_addr() {
|
||||
let me = SecretKey::generate().public();
|
||||
let topic_id = [7u8; 32];
|
||||
let mine = PeerSpeakTicket { host_addr: EndpointAddr::from(me), topic_id }.to_string();
|
||||
// Re-stamping my own ticket with my own addr changes nothing.
|
||||
assert_eq!(PeerSpeakTicket::restamp(&mine, EndpointAddr::from(me)), mine);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn test_restamp_passes_through_unparseable() {
|
||||
let me = SecretKey::generate().public();
|
||||
// A malformed ticket is returned unchanged (the join will fail anyway).
|
||||
assert_eq!(PeerSpeakTicket::restamp("not-a-ticket", EndpointAddr::from(me)), "not-a-ticket");
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn test_peer_state_serde_round_trip() {
|
||||
let original = sample_peer_state();
|
||||
|
||||
Reference in New Issue
Block a user