2026-09-10 02:43:45 -04:00
|
|
|
//! M4 live: F4 rotate onion locator; identity stays; online peer updates.
|
|
|
|
|
|
|
|
|
|
use std::time::{Duration, Instant};
|
|
|
|
|
|
|
|
|
|
use onionwire::Store;
|
|
|
|
|
use onionwire::node::Node;
|
|
|
|
|
|
|
|
|
|
const AFTER: &[u8] = b"after rotate";
|
|
|
|
|
|
|
|
|
|
#[tokio::test(flavor = "multi_thread")]
|
|
|
|
|
#[ignore = "needs live Tor network"]
|
|
|
|
|
async fn rotate_changes_onion_not_identity_and_notifies_online_peer() {
|
|
|
|
|
let _ = tracing_subscriber::fmt()
|
|
|
|
|
.with_env_filter(
|
|
|
|
|
tracing_subscriber::EnvFilter::try_from_default_env().unwrap_or_else(|_| "info".into()),
|
|
|
|
|
)
|
|
|
|
|
.with_test_writer()
|
|
|
|
|
.try_init();
|
|
|
|
|
|
|
|
|
|
let root = tempfile::tempdir().expect("tempdir");
|
|
|
|
|
let alice_home = root.path().join("alice");
|
|
|
|
|
let bob_home = root.path().join("bob");
|
|
|
|
|
let carol_home = root.path().join("carol");
|
|
|
|
|
|
|
|
|
|
eprintln!("starting alice + bob…");
|
2026-09-10 17:19:50 -04:00
|
|
|
unsafe {
|
|
|
|
|
std::env::set_var("ONIONWIRE_STORE_PASSPHRASE", "onionwire-test");
|
|
|
|
|
}
|
2026-09-10 02:43:45 -04:00
|
|
|
let (alice, bob) = tokio::join!(
|
|
|
|
|
Node::start(alice_home.clone()),
|
|
|
|
|
Node::start(bob_home.clone())
|
|
|
|
|
);
|
|
|
|
|
let alice = alice.expect("alice");
|
|
|
|
|
let bob = bob.expect("bob");
|
|
|
|
|
|
|
|
|
|
let a_qr = alice.qr_payload().expect("alice qr");
|
|
|
|
|
let b_qr = bob.qr_payload().expect("bob qr");
|
|
|
|
|
alice.add_friend_from_qr(&b_qr).expect("alice adds bob");
|
|
|
|
|
bob.add_friend_from_qr(&a_qr).expect("bob adds alice");
|
|
|
|
|
|
2026-09-10 17:19:50 -04:00
|
|
|
let carol = Store::open_at_with_passphrase(&carol_home, "onionwire-test").expect("carol store");
|
2026-09-10 02:43:45 -04:00
|
|
|
let a_pk = alice.identity_pk();
|
|
|
|
|
carol
|
|
|
|
|
.upsert_friend(&a_pk, &alice.onion(), Some("alice"))
|
|
|
|
|
.expect("carol knows alice");
|
|
|
|
|
carol
|
|
|
|
|
.set_friend_prekey(&a_pk, &[0u8; 32])
|
|
|
|
|
.expect("carol prekey placeholder");
|
|
|
|
|
|
|
|
|
|
let fp_before = alice.identity_pk();
|
|
|
|
|
let onion_before = alice.onion();
|
|
|
|
|
eprintln!("alice onion before={onion_before}");
|
|
|
|
|
|
|
|
|
|
let outcome = alice.rotate().await.expect("rotate");
|
|
|
|
|
let onion_after = alice.onion();
|
|
|
|
|
eprintln!(
|
|
|
|
|
"alice onion after={onion_after} notified={}/{}",
|
|
|
|
|
outcome.notified, outcome.friends
|
|
|
|
|
);
|
|
|
|
|
|
|
|
|
|
assert_eq!(alice.identity_pk(), fp_before, "identity key never changes");
|
|
|
|
|
assert_ne!(onion_after, onion_before, "onion locator must change");
|
|
|
|
|
assert_eq!(outcome.friends, 1, "only bob is a friend");
|
|
|
|
|
assert_eq!(outcome.notified, 1, "online bob got loc");
|
|
|
|
|
|
|
|
|
|
let deadline = Instant::now() + Duration::from_secs(30);
|
|
|
|
|
let bob_row = loop {
|
|
|
|
|
let row = bob.friend(&fp_before).expect("bob row");
|
|
|
|
|
if row.onion == onion_after {
|
|
|
|
|
break row;
|
|
|
|
|
}
|
|
|
|
|
if Instant::now() >= deadline {
|
|
|
|
|
panic!(
|
|
|
|
|
"bob locator not updated; have {} want {onion_after}",
|
|
|
|
|
row.onion
|
|
|
|
|
);
|
|
|
|
|
}
|
|
|
|
|
tokio::time::sleep(Duration::from_millis(200)).await;
|
|
|
|
|
};
|
|
|
|
|
assert_eq!(bob_row.pubkey, fp_before);
|
|
|
|
|
assert_eq!(bob_row.fingerprint, to_hex(&fp_before));
|
|
|
|
|
|
|
|
|
|
bob.send(&fp_before, AFTER)
|
|
|
|
|
.await
|
|
|
|
|
.expect("bob dials new locator");
|
|
|
|
|
|
|
|
|
|
let deadline = Instant::now() + Duration::from_secs(30);
|
|
|
|
|
loop {
|
|
|
|
|
let msgs = alice.list_messages(&bob.identity_pk()).expect("alice msgs");
|
|
|
|
|
if msgs.iter().any(|m| m.dir == "in" && m.plaintext == AFTER) {
|
|
|
|
|
break;
|
|
|
|
|
}
|
|
|
|
|
if Instant::now() >= deadline {
|
|
|
|
|
panic!("alice missing post-rotate message; got {msgs:?}");
|
|
|
|
|
}
|
|
|
|
|
tokio::time::sleep(Duration::from_millis(200)).await;
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
assert!(
|
|
|
|
|
alice.connect_onion(&onion_before).await.is_err(),
|
|
|
|
|
"old HS must be hard-cut"
|
|
|
|
|
);
|
|
|
|
|
|
|
|
|
|
let c_row = carol.get_friend(&a_pk).unwrap().unwrap();
|
|
|
|
|
assert_eq!(c_row.onion, onion_before, "offline carol stays stale");
|
|
|
|
|
assert_eq!(c_row.petname.as_deref(), Some("alice"));
|
|
|
|
|
|
|
|
|
|
let new_qr = alice.qr_payload().expect("new qr");
|
|
|
|
|
let p = onionwire::qr::decode(&new_qr).expect("decode");
|
|
|
|
|
carol
|
|
|
|
|
.upsert_friend(&p.pubkey, &p.onion, None)
|
|
|
|
|
.expect("rescan");
|
|
|
|
|
assert_eq!(carol.friend_count().unwrap(), 1);
|
|
|
|
|
let c2 = carol.get_friend(&a_pk).unwrap().unwrap();
|
|
|
|
|
assert_eq!(c2.onion, onion_after);
|
|
|
|
|
assert_eq!(c2.petname.as_deref(), Some("alice"));
|
|
|
|
|
assert_eq!(c2.fingerprint, c_row.fingerprint);
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
fn to_hex(bytes: &[u8]) -> String {
|
|
|
|
|
bytes.iter().map(|b| format!("{b:02x}")).collect()
|
|
|
|
|
}
|