From c3bc26d52fb88c3cd6f3d851c324cb0b47c32a5e Mon Sep 17 00:00:00 2001 From: holger krekel Date: Fri, 31 Jul 2026 03:03:07 +0200 Subject: [PATCH] fix: ensure same-second primary transport change propagates correctly Without the fix, too fast transport changes are ignored at the receiver side. --- src/config.rs | 19 +++++--- src/transport/transport_tests.rs | 84 ++++++++++++++++++++++++++------ 2 files changed, 81 insertions(+), 22 deletions(-) diff --git a/src/config.rs b/src/config.rs index ca3024438..04308341f 100644 --- a/src/config.rs +++ b/src/config.rs @@ -788,16 +788,19 @@ impl Context { (addr,), )?; - // Update the timestamp for the primary transport - // so it becomes the first in `get_all_self_addrs()` list - // and the list of relays distributed in the public key. - // This ensures that messages will be sent - // to the primary relay by the contacts - // and will be fetched in background_fetch() - // which only fetches from the primary transport. + // `is_published=1`: an unpublished primary would be missing + // from the relay list in the public key, so contacts would + // never send to it. + // + // The timestamp must strictly increase because + // other devices ignore the row update otherwise, + // and contacts only adopt the re-signed key + // if its signature timestamp increases. transaction .execute( - "UPDATE transports SET add_timestamp=?, is_published=1 WHERE addr=?", + "UPDATE transports + SET add_timestamp=MAX(?, add_timestamp+1), is_published=1 + WHERE addr=?", (time(), addr), ) .context( diff --git a/src/transport/transport_tests.rs b/src/transport/transport_tests.rs index ec44a7bfd..136c14491 100644 --- a/src/transport/transport_tests.rs +++ b/src/transport/transport_tests.rs @@ -115,6 +115,19 @@ fn dummy_configured_login_param(addr: &str) -> ConfiguredLoginParam { } } +async fn add_dummy_transport(t: &TestContext, addr: &str) -> Result<()> { + dummy_configured_login_param(addr) + .save_to_transports_table( + t, + &EnteredLoginParam { + addr: addr.to_string(), + ..Default::default() + }, + time(), + ) + .await +} + #[tokio::test(flavor = "multi_thread", worker_threads = 2)] async fn test_is_published_flag() -> Result<()> { let mut tcm = TestContextManager::new(); @@ -138,16 +151,7 @@ async fn test_is_published_flag() -> Result<()> { ) .await; - dummy_configured_login_param("alice@otherprovider.com") - .save_to_transports_table( - alice, - &EnteredLoginParam { - addr: "alice@otherprovider.com".to_string(), - ..Default::default() - }, - time(), - ) - .await?; + add_dummy_transport(alice, "alice@otherprovider.com").await?; send_sync_transports(alice).await?; sync_and_check_recipients(alice, alice2, "alice@otherprovider.com alice@example.org").await; @@ -195,10 +199,7 @@ async fn test_is_published_flag() -> Result<()> { SystemTime::shift(Duration::from_secs(2)); - alice - .set_config(Config::ConfiguredAddr, Some("alice@otherprovider.com")) - .await?; - sync_and_check_recipients(alice, alice2, "alice@example.org alice@otherprovider.com").await; + promote_transport_and_check_success(alice, alice2, "alice@otherprovider.com").await?; check_addrs( alice, @@ -215,6 +216,61 @@ async fn test_is_published_flag() -> Result<()> { Ok(()) } +/// Tests that changing the primary transport propagates to other devices +/// even if the promoted transport was added within the same second. +#[tokio::test(flavor = "multi_thread", worker_threads = 2)] +async fn test_promote_transport_same_second() -> Result<()> { + let mut tcm = TestContextManager::new(); + let alice = &tcm.alice().await; + let alice2 = &tcm.alice().await; + for a in [alice, alice2] { + a.set_config_bool(Config::SyncMsgs, true).await?; + a.set_config_bool(Config::BccSelf, true).await?; + } + + add_dummy_transport(alice, "alice@otherprovider.com").await?; + send_sync_transports(alice).await?; + sync_and_check_recipients(alice, alice2, "alice@otherprovider.com alice@example.org").await; + + promote_transport_and_check_success(alice, alice2, "alice@otherprovider.com").await +} + +/// Promotes `addr` to primary on `alice` and checks the change syncs to `alice2`. +async fn promote_transport_and_check_success( + alice: &TestContext, + alice2: &TestContext, + addr: &str, +) -> Result<()> { + let old_timestamp = add_timestamp(alice2, addr).await; + alice.set_config(Config::ConfiguredAddr, Some(addr)).await?; + assert!(add_timestamp(alice, addr).await > old_timestamp); + + alice.send_sync_msg().await?.unwrap(); + let sync_msg = alice.pop_sent_msg().await; + assert_eq!(sync_msg.recipients, format!("alice@example.org {addr}")); + // Other devices switch their primary transport + // based on the From address of the sync message. + assert!(sync_msg.payload.contains(&format!("From: <{addr}>"))); + alice2.recv_msg_trash(&sync_msg).await; + + // add_timestamp must monotonically increase because + // other devices ignore the change otherwise. + assert!(add_timestamp(alice2, addr).await > old_timestamp); + assert_eq!( + alice2.get_config(Config::ConfiguredAddr).await?.as_deref(), + Some(addr) + ); + Ok(()) +} + +async fn add_timestamp(t: &TestContext, addr: &str) -> i64 { + t.sql + .query_get_value("SELECT add_timestamp FROM transports WHERE addr=?", (addr,)) + .await + .unwrap() + .unwrap() +} + struct Addresses { primary: &'static str, secondary_published: &'static [&'static str],