gossip: replicate account state only, keep channels node-local

Each event now has a scope: account identity is global and gossips to
every peer; channel state is local to the node that authored it. The
version vector, push, digest and compaction only carry global events,
and ingest refuses any non-global entry a peer sends. A node can no
longer be handed ownership of a channel registered on a network it was
never part of.
This commit is contained in:
Jean Chevronnet 2026-07-12 12:20:24 +00:00
parent e8b55bdb27
commit d4b9172455
No known key found for this signature in database
3 changed files with 132 additions and 17 deletions

View file

@ -327,6 +327,38 @@ mod tests {
assert!(got, "a post-connect write should push to B well under the 10s digest");
}
// Accounts federate, but channel registrations stay on the node that made
// them: A registers both; B receives the account and never the channel.
#[tokio::test]
async fn channels_stay_node_local() {
let (a, atx) = engine("A", "local-a");
let (b, btx) = engine("B", "local-b");
a.lock().await.test_register("alice");
a.lock().await.test_register_channel("#secret", "alice");
let (ca, cb) = tokio::io::duplex(64 * 1024);
let sa = tokio::spawn(session(ca, a.clone(), "s3cret".into(), "A".into(), atx));
let sb = tokio::spawn(session(cb, b.clone(), "s3cret".into(), "B".into(), btx));
// Wait for the account to converge (proves the link works), then assert
// the channel never crossed it.
let mut got_account = false;
for _ in 0..100 {
if b.lock().await.test_has_account("alice") {
got_account = true;
break;
}
tokio::time::sleep(Duration::from_millis(20)).await;
}
// Give any (erroneous) channel replication ample time to arrive too.
tokio::time::sleep(Duration::from_millis(200)).await;
let leaked_channel = b.lock().await.test_has_channel("#secret");
sa.abort();
sb.abort();
assert!(got_account, "the account should federate");
assert!(!leaked_channel, "the channel must NOT federate to a node that never saw it registered");
}
// A wrong secret is rejected before any state is exchanged.
#[tokio::test]
async fn bad_secret_is_rejected() {