aboutsummaryrefslogtreecommitdiffstats
path: root/src/simtest
diff options
context:
space:
mode:
authortv <tv@krebsco.de>2026-11-07 22:35:21 +0100
committertv <tv@krebsco.de>2026-11-07 22:35:21 +0100
commitf2db469dd9380414ba11c573695f305c9b8c0b34 (patch)
treebe532d28c371cf0a7c7efd10771f5b5689adba37 /src/simtest
parent5471e5285e94ce7576e6c86f260fea15ed9503d1 (diff)
simtest: signed changes
Every change vouched for on every node within a second; a forged signature vouches for nothing. Auth plan M5.
Diffstat (limited to 'src/simtest')
-rw-r--r--src/simtest/mod.rs3
-rw-r--r--src/simtest/signatures.rs113
2 files changed, 115 insertions, 1 deletions
diff --git a/src/simtest/mod.rs b/src/simtest/mod.rs
index 5acc886..cd0c2c7 100644
--- a/src/simtest/mod.rs
+++ b/src/simtest/mod.rs
@@ -16,6 +16,7 @@ mod identity;
mod reach;
mod relay;
mod remakes;
+mod signatures;
mod soak;
mod timing;
@@ -403,7 +404,7 @@ pub(crate) fn sim_key(id: &str) -> NodeKey {
NodeKey::from_seed(sha2::Sha256::digest(id.as_bytes()).into())
}
-fn path(path: &str) -> Vec<String> {
+pub(crate) fn path(path: &str) -> Vec<String> {
path.split('/').map(str::to_owned).collect()
}
diff --git a/src/simtest/signatures.rs b/src/simtest/signatures.rs
new file mode 100644
index 0000000..2425a16
--- /dev/null
+++ b/src/simtest/signatures.rs
@@ -0,0 +1,113 @@
+//! Signed changes end to end (`document::signatures`, `swarm::signatures`): nodes under
+//! the identity layer, whose node keys sign, write through the API as a user would,
+//! and every node comes to see the writes verified, by signatures that came with sync
+//! messages or on their own.
+
+use std::time::Duration;
+
+use serde_json::json;
+
+use super::{Sim, path};
+use crate::document::ActorId;
+use crate::document::signatures::{Sign, SignEvery};
+use crate::document::store;
+use crate::state::AppState;
+use crate::swarm::bootstrap::Start;
+
+const SOON: Duration = Duration::from_secs(20);
+
+/// Writes `value` at `at` on `node` as a write through the API does, signed at once if
+/// `signed` (`?signed=true`); the change's actor and seq.
+async fn write_as_user(
+ node: &AppState,
+ at: &str,
+ value: serde_json::Value,
+ signed: bool,
+) -> (ActorId, u64) {
+ let at = path(at);
+ let sign = Sign::Write {
+ path: at.clone(),
+ asked: signed,
+ };
+ node.mutate_doc_for(Some("alice".to_owned()), sign, move |doc| {
+ store::write_path(doc, &at, &value)
+ })
+ .await
+ .expect("write");
+ let replica = node.doc.read().await;
+ let change = replica.doc.get_last_local_change().expect("the write");
+ (change.actor_id().clone(), change.seq())
+}
+
+/// Whether a signature vouches for `actor`'s change `seq` on `node`.
+async fn verified(node: &AppState, actor: &ActorId, seq: u64) -> bool {
+ node.doc.read().await.signatures.verified(actor, seq)
+}
+
+/// A write no rule signs is signed a while after it (`--sign-every`, 1 s by default),
+/// and the signature reaches every node on its own (SIGNATURES): the change went before
+/// it was signed.
+#[tokio::test(flavor = "multi_thread", worker_threads = 4)]
+async fn unsigned_writes_are_verified_within_the_interval() {
+ let sim = Sim::with_identity(61);
+ let a = sim.start("a", &[], Start::Genesis);
+ let b = sim.start("b", &["a"], Start::Join);
+ let c = sim.start("c", &["a"], Start::Join);
+ sim.wait_ready(&[&a, &b, &c], SOON).await;
+
+ let (actor, seq) = write_as_user(&b, "metrics/cpu", json!(42), false).await;
+ sim.wait_converged(&[&a, &b, &c], SOON).await;
+ for node in [&a, &b, &c] {
+ let actor = actor.clone();
+ sim.wait_until("the write is verified", SOON, move || {
+ let actor = actor.clone();
+ async move { verified(node, &actor, seq).await }
+ })
+ .await;
+ }
+}
+
+/// With `--sign-every none`, a write no rule signs stays unverified; the node's next
+/// signed write vouches for it too, wherever its signature goes.
+#[tokio::test(flavor = "multi_thread", worker_threads = 4)]
+async fn a_signed_write_vouches_for_the_writes_before() {
+ let sim = Sim::with_identity(62);
+ let a = sim.start("a", &[], Start::Genesis);
+ let b = sim.start_with("b", &["a"], Start::Join, |config| {
+ config.sign_every = SignEvery::Never;
+ });
+ let c = sim.start("c", &["a"], Start::Join);
+ sim.wait_ready(&[&a, &b, &c], SOON).await;
+
+ let (actor, first) = write_as_user(&b, "metrics/cpu", json!(42), false).await;
+ sim.wait_converged(&[&a, &b, &c], SOON).await;
+ // Signatures go within a second: none came for it. Unless b wrote protocol data
+ // since (always signed), whose signature vouches for it too.
+ tokio::time::sleep(Duration::from_secs(3)).await;
+ let latest = b
+ .doc
+ .read()
+ .await
+ .doc
+ .get_last_local_change()
+ .map(|change| change.seq());
+ if latest == Some(first) {
+ for node in [&a, &b, &c] {
+ assert!(!verified(node, &actor, first).await);
+ }
+ }
+
+ let (same, second) = write_as_user(&b, "config/mode", json!("fast"), true).await;
+ assert_eq!(same, actor);
+ sim.wait_converged(&[&a, &b, &c], SOON).await;
+ for node in [&a, &b, &c] {
+ let actor = actor.clone();
+ sim.wait_until("both writes are verified", SOON, move || {
+ let actor = actor.clone();
+ async move {
+ verified(node, &actor, first).await && verified(node, &actor, second).await
+ }
+ })
+ .await;
+ }
+}