aboutsummaryrefslogtreecommitdiffstats
diff options
context:
space:
mode:
-rw-r--r--src/simtest/mod.rs1
-rw-r--r--src/simtest/policy.rs164
2 files changed, 165 insertions, 0 deletions
diff --git a/src/simtest/mod.rs b/src/simtest/mod.rs
index cd0c2c7..1dc60ea 100644
--- a/src/simtest/mod.rs
+++ b/src/simtest/mod.rs
@@ -13,6 +13,7 @@ mod bench;
mod deltas;
mod hyparview;
mod identity;
+mod policy;
mod reach;
mod relay;
mod remakes;
diff --git a/src/simtest/policy.rs b/src/simtest/policy.rs
new file mode 100644
index 0000000..b56766b
--- /dev/null
+++ b/src/simtest/policy.rs
@@ -0,0 +1,164 @@
+//! A swarm whose policy has every node judge every change (`access::policy`,
+//! `access::screen`), with signatures required: honest writes go everywhere, a joining
+//! node registers, a write a node lets through for a user the registry doesn't have
+//! stays where it was made, and a remake carries the swarm over without refusing
+//! anything.
+
+use std::time::Duration;
+
+use serde_json::json;
+
+use super::{Sim, path};
+use crate::document::signatures::Sign;
+use crate::document::store;
+use crate::state::AppState;
+use crate::swarm::bootstrap::Start;
+
+const SOON: Duration = Duration::from_secs(30);
+
+/// Writes `value` at `at` on `node` for `user`, as the API does after its checks, or
+/// without them.
+async fn write_as(node: &AppState, user: &str, at: &str, value: serde_json::Value) {
+ let at = path(at);
+ let sign = Sign::Write {
+ path: at.clone(),
+ asked: false,
+ };
+ node.mutate_doc_for(Some(user.to_owned()), sign, move |doc| {
+ store::write_path(doc, &at, &value)
+ })
+ .await
+ .expect("write");
+}
+
+async fn holds(node: &AppState, at: &str) -> bool {
+ let at = path(at);
+ let replica = node.doc.read().await;
+ store::read_path(&replica.doc, &at).ok().flatten().is_some()
+}
+
+async fn refused(node: &AppState) -> usize {
+ use crate::locks::MutexExt;
+ node.screening.lock_or_recover().refused().count()
+}
+
+#[tokio::test(flavor = "multi_thread", worker_threads = 4)]
+async fn a_swarm_judging_every_change() {
+ let sim = Sim::with_identity(71);
+ let a = sim.start_with("a", &[], Start::Genesis, |config| {
+ config.first_users = vec!["alice".to_owned()];
+ });
+ let b = sim.start("b", &["a"], Start::Join);
+ let c = sim.start("c", &["a"], Start::Join);
+ sim.wait_ready(&[&a, &b, &c], SOON).await;
+
+ write_as(
+ &a,
+ "alice",
+ "_continuum/access/policy",
+ json!({"version": 1, "signatures": "required"}),
+ )
+ .await;
+ sim.wait_converged(&[&a, &b, &c], SOON).await;
+
+ // Honest writes, on any node: everywhere.
+ write_as(&b, "alice", "notes/x", json!(1)).await;
+ write_as(&c, "alice", "notes/y", json!(2)).await;
+ sim.wait_converged(&[&a, &b, &c], SOON).await;
+ for node in [&a, &b, &c] {
+ assert!(holds(node, "notes/x").await && holds(node, "notes/y").await);
+ }
+
+ // A node joining under the policy: it registers, and its writes go everywhere.
+ let d = sim.start("d", &["a"], Start::Join);
+ sim.wait_ready(&[&d], SOON).await;
+ write_as(&d, "alice", "notes/z", json!(3)).await;
+ sim.wait_converged(&[&a, &b, &c, &d], SOON).await;
+ for node in [&a, &b, &c, &d] {
+ assert_eq!(refused(node).await, 0, "an honest change was refused");
+ assert!(holds(node, "notes/z").await);
+ }
+
+ // A write c lets through for a user the registry doesn't have: refused by the
+ // others, and so is what c builds on it.
+ write_as(&c, "mallory", "notes/forged", json!(4)).await;
+ for node in [&a, &b, &d] {
+ sim.wait_until("the forged write is refused", SOON, move || async move {
+ refused(node).await > 0
+ })
+ .await;
+ assert!(!holds(node, "notes/forged").await);
+ }
+}
+
+/// A compaction under the policy: every node switches, the data and what was written
+/// while it ran come along (replayed, signed at once after the switch), and nothing an
+/// honest node wrote is refused, before or after.
+#[tokio::test(flavor = "multi_thread", worker_threads = 4)]
+async fn a_remake_under_the_policy() {
+ let sim = Sim::with_identity(72);
+ let a = sim.start_with("a", &[], Start::Genesis, |config| {
+ config.first_users = vec!["alice".to_owned()];
+ });
+ 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 nodes = [a.clone(), b.clone(), c.clone()];
+ sim.wait_until("every node is registered", SOON, || {
+ let nodes = nodes.clone();
+ async move {
+ for node in &nodes {
+ if crate::swarm::members::members(&node.doc.read().await.doc).len() < 3 {
+ return false;
+ }
+ }
+ true
+ }
+ })
+ .await;
+ write_as(
+ &a,
+ "alice",
+ "_continuum/access/policy",
+ json!({"version": 1, "signatures": "required"}),
+ )
+ .await;
+ write_as(&b, "alice", "notes/before", json!(1)).await;
+ sim.wait_converged(&[&a, &b, &c], SOON).await;
+
+ let (status, report) =
+ crate::remake::testing::start(a.clone(), json!({ "kind": "compact" }), None).await;
+ assert!(status.is_success(), "proposal refused: {status} {report}");
+ // While it runs: replayed into the new epoch.
+ write_as(&c, "alice", "notes/during", json!(2)).await;
+ for node in [&a, &b, &c] {
+ assert_eq!(refused(node).await, 0, "an honest change was refused");
+ }
+ sim.wait_until(
+ "every node is on generation 1",
+ Duration::from_secs(60),
+ || {
+ let nodes = nodes.clone();
+ async move {
+ for node in &nodes {
+ if node.local_epoch().generation != 1 || !node.is_ready().await {
+ return false;
+ }
+ }
+ true
+ }
+ },
+ )
+ .await;
+
+ write_as(&b, "alice", "notes/after", json!(3)).await;
+ for node in [&a, &b, &c] {
+ sim.wait_until("every write is everywhere", SOON, move || async move {
+ holds(node, "notes/before").await
+ && holds(node, "notes/during").await
+ && holds(node, "notes/after").await
+ })
+ .await;
+ assert_eq!(refused(node).await, 0, "an honest change was refused");
+ }
+}