diff options
| author | tv <tv@krebsco.de> | 2026-10-27 01:31:58 +0100 |
|---|---|---|
| committer | tv <tv@krebsco.de> | 2026-10-27 01:31:58 +0100 |
| commit | 4cdece6d0f8e25ac154c3fd0f30bca604b08c16a (patch) | |
| tree | 10a6aab1793d1c4fd0bd53c16b600f1adf986cb9 /src/simtest/identity.rs | |
| parent | 5fd686dcd8ddfbdf276c044e5acf03f744e03193 (diff) | |
add identity layer
Diffstat (limited to 'src/simtest/identity.rs')
| -rw-r--r-- | src/simtest/identity.rs | 128 |
1 files changed, 128 insertions, 0 deletions
diff --git a/src/simtest/identity.rs b/src/simtest/identity.rs new file mode 100644 index 0000000..a33783f --- /dev/null +++ b/src/simtest/identity.rs @@ -0,0 +1,128 @@ +//! Nodes under the identity layer (`transport::identity`): named by node IDs, found +//! through addresses (seeds, HELLO_REPLYs, the member list), and believed only once a +//! node proved itself at an address. + +use std::time::Duration; + +use serde_json::json; + +use super::{Sim, read, sim_key, write}; +use crate::identity::{self, NodeKey}; +use crate::state::AppState; +use crate::swarm::bootstrap::Start; +use crate::swarm::members; +use crate::transport::identity::{WHO_LABEL, signed}; +use crate::transport::{HeadsSummary, IdentityProof, Keepalive, ParsedMessage}; + +const SOON: Duration = Duration::from_secs(20); + +async fn peers_are_node_ids(node: &AppState) { + for (peer, _) in node.peers_snapshot().await { + assert!(identity::is_node_id(&peer), "peer {peer}"); + } +} + +/// Seeds by name (resolved by the net) and by address; the third node finds the second +/// only through what the first lists in its HELLO_REPLY. +#[tokio::test(flavor = "multi_thread", worker_threads = 4)] +async fn nodes_are_their_node_ids() { + let sim = Sim::with_identity(51); + let a = sim.start("a", &[], Start::Genesis); + let b = sim.start("b", &["a"], Start::Join); + let c = sim.start("c", &["sim:a"], Start::Join); + sim.wait_ready(&[&a, &b, &c], SOON).await; + assert_eq!(a.local_id(), sim_key("a").id()); + + write(&b, "x", json!(1)).await; + sim.wait_converged(&[&a, &b, &c], SOON).await; + assert_eq!(read(&c, "x").await, Some(json!(1))); + for node in [&a, &b, &c] { + peers_are_node_ids(node).await; + } + let (b_id, c_id) = (b.local_id().to_owned(), c.local_id().to_owned()); + let a = &a; + sim.wait_until( + "b and c are registered with their addresses", + SOON, + move || { + let (b_id, c_id) = (b_id.clone(), c_id.clone()); + async move { + let replica = a.doc.read().await; + members::addresses(&replica.doc, &b_id) == ["sim:b"] + && members::addresses(&replica.doc, &c_id) == ["sim:c"] + } + }, + ) + .await; +} + +/// With two neighbours each, most pairs of six nodes never meet through seeds: they +/// find each other's addresses in the member list. +#[tokio::test(flavor = "multi_thread", worker_threads = 4)] +async fn members_find_each_other_through_the_member_list() { + let sim = Sim::with_identity(52); + let two = |config: &mut crate::node::NodeConfig| config.neighbours_k = 2; + let a = sim.start_with("a", &[], Start::Genesis, two); + let others: Vec<AppState> = ["b", "c", "d", "e", "f"] + .into_iter() + .map(|id| sim.start_with(id, &["a"], Start::Join, two)) + .collect(); + let mut all: Vec<&AppState> = vec![&a]; + all.extend(others.iter()); + sim.wait_ready(&all, SOON).await; + + write(&others[4], "from/f", json!("f")).await; + write(&others[0], "from/b", json!("b")).await; + sim.wait_converged(&all, SOON).await; + for node in &all { + assert_eq!(read(node, "from/f").await, Some(json!("f"))); + assert_eq!(read(node, "from/b").await, Some(json!("b"))); + peers_are_node_ids(node).await; + } +} + +fn ping(node: &AppState) -> ParsedMessage { + ParsedMessage::Ping(Keepalive { + heads: HeadsSummary::of(Vec::new()), + epoch: node.local_epoch(), + epoch_seq: 0, + ready: false, + syncs_with_you: false, + }) +} + +fn who(key: &NodeKey, from: &str, to: &str) -> ParsedMessage { + ParsedMessage::Who(IdentityProof { + key: key.public(), + signature: key.sign(&signed(WHO_LABEL, from, to)), + }) +} + +/// A proof counts only for the address it was made at: messages from an address with +/// a proof made elsewhere are dropped, and the node never hears of them. +#[tokio::test(flavor = "multi_thread", worker_threads = 4)] +async fn a_proof_holds_only_at_its_address() { + let sim = Sim::with_identity(53); + let a = sim.start("a", &[], Start::Genesis); + sim.wait_ready(&[&a], SOON).await; + let ghost = NodeKey::from_seed([9; 32]); + + // Made for another sender address, or for another recipient. + for forged in [ + who(&ghost, "sim:elsewhere", "sim:a"), + who(&ghost, "sim:ghost", "sim:b"), + ] { + sim.net.inject("ghost", "a", &forged).await.unwrap(); + sim.net.inject("ghost", "a", &ping(&a)).await.unwrap(); + assert!(a.get_peer_state(&ghost.id()).await.is_none()); + } + + sim.net + .inject("ghost", "a", &who(&ghost, "sim:ghost", "sim:a")) + .await + .unwrap(); + sim.net.inject("ghost", "a", &ping(&a)).await.unwrap(); + assert!(a.get_peer_state(&ghost.id()).await.is_some()); + // Known by its node ID, not by its address. + assert!(a.get_peer_state("ghost").await.is_none()); +} |
