From 148a7a5a80ade732292ad4baa67f251a0fe4a305 Mon Sep 17 00:00:00 2001 From: Logan Johnson Date: Thu, 8 Oct 2026 16:09:17 -0400 Subject: [PATCH] test(relay): racing creates for one label resolve to the same channel One creator sends two kind:9007s for the same label at once. The relay keeps both channels, the #P + #t lookup finds both, and oldest_channel picks the same one in any input order, even after the older channel's kind:39000 is re-published by an edit. Signed-off-by: Logan Johnson Co-authored-by: Larry <627498bd4bd1f281a16431e3c6cce3b5c25b6692798c78672298aefbf2f8f8b5@buzz.block.builderlab.xyz> --- .../handlers/group_state_postgres_tests.rs | 97 +++++++++++++++++++ 1 file changed, 97 insertions(+) diff --git a/crates/buzz-relay/src/handlers/group_state_postgres_tests.rs b/crates/buzz-relay/src/handlers/group_state_postgres_tests.rs index b845efcd6a..d3d5315682 100644 --- a/crates/buzz-relay/src/handlers/group_state_postgres_tests.rs +++ b/crates/buzz-relay/src/handlers/group_state_postgres_tests.rs @@ -454,6 +454,103 @@ async fn members_cannot_change_labels() { ); } +/// Ensure race: one creator sends two kind:9007s for the same label at +/// once. The relay keeps both channels, a `#P` + `#t` lookup finds both, +/// and every reader picks the same one: the lowest stored creation time, +/// then the lowest ID. +/// Mutation: rank by the 39000's own created_at after an edit, or let the +/// lookup miss one channel → RED. +#[tokio::test] +#[ignore = "requires Postgres"] +async fn racing_creates_resolve_to_the_same_oldest_channel() { + let state = state().await; + let tenant = community(&state, "group-state-ensure-race").await; + let creator = Keys::generate(); + let extra: &[&[&str]] = &[&["t", "ensure-race"]]; + + let (first, second) = tokio::join!( + create_channel(&state, &tenant, &creator, extra), + create_channel(&state, &tenant, &creator, extra), + ); + assert_ne!( + first, second, + "both creates must succeed as separate channels" + ); + + // Edit whichever channel is older so its 39000 becomes the newest event. + // Readers must still pick it, because it was created first. + let created = |id: Uuid| { + let state = state.clone(); + let tenant = tenant.clone(); + async move { + state + .db + .get_channel_for_event_write(tenant.community(), id) + .await + .expect("channel") + .created_at + .timestamp() + } + }; + let mut ranked = [ + (created(first).await, first.to_string()), + (created(second).await, second.to_string()), + ]; + ranked.sort(); + let expected = ranked[0].1.clone(); + tokio::time::sleep(std::time::Duration::from_millis(1_100)).await; + edit_channel( + &state, + &tenant, + &creator, + expected.parse().unwrap(), + &[&["about", "edited after the race"]], + ) + .await + .expect("edit the older channel"); + + // Step 1 of Ensure: the creator's label lookup finds both channels. + let found = req_event_ids( + &state, + &tenant, + &creator, + serde_json::json!({ + "kinds": [39000], + "#P": [creator.public_key().to_hex()], + "#t": ["ensure-race"], + }), + ) + .await; + assert_eq!(found.len(), 2, "lookup must find both channels: {found:?}"); + + let mut events = Vec::new(); + for id in [first, second] { + let stored = state + .db + .query_events(&EventQuery { + kinds: Some(vec![39000]), + d_tag: Some(id.to_string()), + limit: Some(1), + ..EventQuery::for_community(tenant.community()) + }) + .await + .expect("query 39000"); + let event = stored.into_iter().next().expect("39000").event; + assert!( + found.contains(&event.id.to_hex()), + "lookup returned the current 39000" + ); + events.push(event); + } + let pick = |events: Vec<&nostr::Event>| { + buzz_core::channel::oldest_channel(events) + .and_then(|event| event.tags.identifier()) + .map(str::to_owned) + }; + assert_eq!(pick(vec![&events[0], &events[1]]), Some(expected.clone())); + assert_eq!(pick(vec![&events[1], &events[0]]), Some(expected)); +} + /// Run one historical REQ as `reader` and return the IDs of the events sent /// before EOSE. async fn req_event_ids(