diff options
Diffstat (limited to 'src/access')
| -rw-r--r-- | src/access/policy.rs | 28 | ||||
| -rw-r--r-- | src/access/screen.rs | 122 |
2 files changed, 140 insertions, 10 deletions
diff --git a/src/access/policy.rs b/src/access/policy.rs index 7ddef0e..f8ccfc2 100644 --- a/src/access/policy.rs +++ b/src/access/policy.rs @@ -4,7 +4,9 @@ //! by a user who may write protocol data, as any of it. //! //! `{"version": 1, "signatures": "optional"}`: rules enforced; a change is its claimed -//! committer's whether or not a signature proves it (`document::signatures`). A policy +//! committer's whether or not a signature proves it (`document::signatures`). +//! `"required"`: a change is applied once a signature by its committer vouches for it, +//! and waits until then; every node signs every change it makes. A policy //! this node doesn't know (another version, or a setting it lacks) stops it from taking //! writes through the API, and it receives without judging: it can't judge as the //! others do, so it writes nothing they might refuse, until it is upgraded. @@ -26,15 +28,21 @@ pub(crate) fn path() -> Vec<String> { pub(crate) enum Policy { /// None: changes aren't judged. Open, - /// Rules by path enforced. - Enforced, + /// Rules by path enforced; with signatures `required`, only on changes a signature + /// proves. + Enforced { required: bool }, /// One this node doesn't know, and why. Unknown(String), } impl Policy { pub(crate) fn enforced(&self) -> bool { - matches!(self, Policy::Enforced) + matches!(self, Policy::Enforced { .. }) + } + + /// Whether changes need a signature (`signatures: "required"`). + pub(crate) fn required(&self) -> bool { + matches!(self, Policy::Enforced { required: true }) } } @@ -55,7 +63,8 @@ pub(crate) fn of(doc: &impl ReadDoc) -> Policy { )); } match value.get("signatures").and_then(JsonValue::as_str) { - None | Some("optional") => Policy::Enforced, + None | Some("optional") => Policy::Enforced { required: false }, + Some("required") => Policy::Enforced { required: true }, Some(other) => Policy::Unknown(format!("signatures {other:?}")), } } @@ -78,10 +87,15 @@ mod tests { #[test] fn what_the_policy_says() { assert_eq!(of(&Document::new()), Policy::Open); - assert_eq!(with(json!({"version": 1})), Policy::Enforced); + let optional = Policy::Enforced { required: false }; + assert_eq!(with(json!({"version": 1})), optional); assert_eq!( with(json!({"version": 1, "signatures": "optional"})), - Policy::Enforced + optional + ); + assert_eq!( + with(json!({"version": 1, "signatures": "required"})), + Policy::Enforced { required: true } ); assert!(matches!(with(json!({"version": 2})), Policy::Unknown(_))); assert!(matches!( diff --git a/src/access/screen.rs b/src/access/screen.rs index 66b7fc8..507de1d 100644 --- a/src/access/screen.rs +++ b/src/access/screen.rs @@ -10,6 +10,9 @@ //! //! A change whose dependencies are neither in the document nor among those received //! waits (`Waiting`) until they come, a bounded number; Automerge never sees it before. +//! With signatures required (`access::policy`), so does one no signature by its +//! committer vouches for yet (`Signatures::vouch_batch`), and whatever builds on it, +//! until one comes, but an epoch's root, which no node signs. use std::collections::{BTreeSet, HashMap, HashSet}; @@ -19,6 +22,7 @@ use super::rules::{self, Refusal}; use crate::document::inspect::{Inspector, Path}; use crate::document::meta; use crate::document::replica::{Epoch, user_of}; +use crate::document::signatures::Signatures; use crate::document::{Change, ChangeHash, Document, ReadDoc}; /// Changes waiting for their dependencies, kept at most. @@ -102,11 +106,13 @@ pub(crate) struct Screened { /// Judges `received` (and those `waiting` before) and applies to `doc` (of `epoch`) /// those rules allow, what they change recorded in `log`. Those already in `doc` are /// left out; those whose dependencies are missing wait. +/// With `signatures` (required), those no signature vouches for wait too. pub(crate) fn apply( doc: &mut Document, epoch: Epoch, received: Vec<Change>, waiting: &mut Waiting, + mut signatures: Option<&mut Signatures>, log: &mut crate::document::PatchLog, ) -> Result<Screened> { let mut screened = Screened::default(); @@ -120,8 +126,8 @@ pub(crate) fn apply( .collect(); let (ready, unready) = in_order(doc, candidates); waiting.0 = unready; - waiting.0.truncate(MAX_WAITING); if ready.is_empty() { + waiting.0.truncate(MAX_WAITING); return Ok(screened); } @@ -150,10 +156,14 @@ pub(crate) fn apply( named }) .collect(); + if let Some(signatures) = signatures.as_deref_mut() { + signatures.vouch_batch(&ready, &committers); + } // The changes of the document each accepted one, not applied yet, builds on. let mut frontier: HashMap<ChangeHash, Vec<ChangeHash>> = HashMap::new(); let mut refused: HashSet<ChangeHash> = HashSet::new(); + let mut unsigned: HashSet<ChangeHash> = HashSet::new(); let mut accepted: Vec<Change> = Vec::new(); for (index, change) in ready.iter().enumerate() { let hash = change.hash(); @@ -173,6 +183,19 @@ pub(crate) fn apply( .push(refuse(format!("it builds on refused change {dep}"))); continue; } + // No signature vouches for it yet, or for what it builds on: it waits. + let vouched = |signatures: &Signatures| { + change.deps().is_empty() || signatures.verified(change.actor_id(), change.seq()) + }; + if signatures + .as_deref() + .is_some_and(|signatures| !vouched(signatures)) + || change.deps().iter().any(|dep| unsigned.contains(dep)) + { + unsigned.insert(hash); + waiting.0.push(change.clone()); + continue; + } let mut seen_at: BTreeSet<ChangeHash> = BTreeSet::new(); for dep in change.deps() { match frontier.get(dep) { @@ -222,6 +245,7 @@ pub(crate) fn apply( if !accepted.is_empty() { doc.apply_changes_log_patches(accepted, log)?; } + waiting.0.truncate(MAX_WAITING); Ok(screened) } @@ -229,16 +253,19 @@ pub(crate) fn apply( /// what `screening` keeps for document `epoch_seq` (of `epoch`). Bytes that don't load /// as changes, or with a misplaced author (`changes::misplaced_author`), are refused /// whole. +/// With `signatures` (required), those no signature vouches for wait too; and with no +/// bytes, those waiting are judged again (signatures came since). pub(crate) fn apply_bytes( doc: &mut Document, epoch: Epoch, epoch_seq: u32, bytes: &[u8], screening: &std::sync::Mutex<Screening>, + signatures: Option<&mut Signatures>, log: &mut crate::document::PatchLog, ) -> Result<usize> { use crate::locks::MutexExt; - if bytes.is_empty() { + if bytes.is_empty() && signatures.is_none() { return Ok(0); } if let Some(misplaced) = crate::document::changes::misplaced_author(bytes) { @@ -248,7 +275,10 @@ pub(crate) fn apply_bytes( .ok_or_else(|| anyhow::anyhow!("Refused changes that don't load"))?; let mut screening = screening.lock_or_recover(); let screening = screening.of(epoch_seq); - let screened = apply(doc, epoch, changes, &mut screening.waiting, log)?; + if changes.is_empty() && screening.waiting.len() == 0 { + return Ok(0); + } + let screened = apply(doc, epoch, changes, &mut screening.waiting, signatures, log)?; let applied = screened.applied; screening.record(screened.refused); Ok(applied) @@ -339,6 +369,7 @@ mod tests { Epoch::GENESIS, changes, waiting, + None, &mut crate::document::PatchLog::inactive(), ) .unwrap() @@ -450,4 +481,89 @@ mod tests { Some(json!(2)) ); } + + /// With signatures required, changes no signature vouches for wait, with what + /// builds on them, until one comes; one signature vouches for its actor's earlier + /// changes too. + #[test] + fn unsigned_changes_wait_for_a_signature() { + let key = crate::identity::NodeKey::from_seed([3; 32]); + let node = key.id(); + let mut theirs = Document::new(); + let alice = registry::entry(&node, 1, &[(path("notes"), &[Right::Write][..])]); + transact( + &mut theirs, + UnixMillis(1), + &Writer { + node: &node, + user: None, + }, + |doc| { + store::write_path(doc, &path("_continuum/swarm"), &json!("s"))?; + store::write_path(doc, &path("_continuum/users/alice"), &alice) + }, + ) + .unwrap(); + let root = theirs.get_heads(); + write( + &mut theirs, + &node, + None, + &format!("_continuum/members/{node}"), + json!({}), + ); + write(&mut theirs, &node, Some("alice"), "notes/x", json!(1)); + let last = theirs.get_heads()[0]; + + let mut ours = Document::new(); + let mut waiting = Waiting::default(); + let mut signatures = Signatures::default(); + let required = + |doc: &mut Document, waiting: &mut Waiting, signatures: &mut Signatures, changes| { + apply( + doc, + Epoch::GENESIS, + changes, + waiting, + Some(signatures), + &mut crate::document::PatchLog::inactive(), + ) + .unwrap() + }; + // Unsigned: only the root goes in (no node signs it). + let screened = required( + &mut ours, + &mut waiting, + &mut signatures, + received(&theirs, &[]), + ); + assert_eq!(screened.applied, 1); + assert_eq!(ours.get_heads(), root); + assert_eq!(waiting.len(), 2); + // A forged signature vouches for nothing. + let forged = crate::identity::NodeKey::from_seed([4; 32]); + signatures.offer( + &ours, + vec![( + last, + forged.sign(&crate::document::signatures::message(&last)), + )], + ); + let screened = required(&mut ours, &mut waiting, &mut signatures, Vec::new()); + assert_eq!(screened.applied, 0); + assert_eq!(waiting.len(), 2); + // The last one's: both in. + signatures.offer( + &ours, + vec![(last, key.sign(&crate::document::signatures::message(&last)))], + ); + let screened = required(&mut ours, &mut waiting, &mut signatures, Vec::new()); + assert_eq!(screened.applied, 2); + assert!(screened.refused.is_empty()); + assert_eq!(waiting.len(), 0); + assert_eq!( + store::read_path(&ours, &path("notes/x")).unwrap(), + Some(json!(1)) + ); + } } |
