summaryrefslogtreecommitdiffstats
path: root/src/simtest/identity.rs
diff options
context:
space:
mode:
authortv <tv@krebsco.de>2026-10-27 01:31:58 +0100
committertv <tv@krebsco.de>2026-10-27 01:31:58 +0100
commit4cdece6d0f8e25ac154c3fd0f30bca604b08c16a (patch)
tree10a6aab1793d1c4fd0bd53c16b600f1adf986cb9 /src/simtest/identity.rs
parent5fd686dcd8ddfbdf276c044e5acf03f744e03193 (diff)
add identity layer
Diffstat (limited to 'src/simtest/identity.rs')
-rw-r--r--src/simtest/identity.rs128
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());
+}