From 81dc0ca6012372ff293e19549053fda84bca1eb0 Mon Sep 17 00:00:00 2001 From: tv Date: Thu, 12 Nov 2026 01:13:54 +0000 Subject: screen: refuse a change at a seq this node made under its own actor A node receiving a change of the actor it writes as, one it doesn't hold, writes on as a fresh actor (another node writes as it, from a copy of its files). Where the other node made it at a seq this node made a change at too, the change was taken to Automerge, which refuses the whole batch (duplicate seq): the message failed, and with it everything else it carried. Signatures::forks found it forged only where a signature verified a change of the actor at that seq, and this node signs its own latest change only as it sends it. forks now also finds a change forged where its actor is the one the document writes as and the document's latest change of it (get_last_local_change) is at its seq or later. apply_bytes switches to the fresh actor after judging the batch, not before, so the check still sees the actor; also where judging fails. --- src/access/screen.rs | 32 +++++++++++++++++++++----------- src/document/signatures.rs | 8 ++++++-- 2 files changed, 27 insertions(+), 13 deletions(-) (limited to 'src') diff --git a/src/access/screen.rs b/src/access/screen.rs index 010ef49..8fd2c61 100644 --- a/src/access/screen.rs +++ b/src/access/screen.rs @@ -716,24 +716,29 @@ pub(crate) fn apply_bytes( // A change of the actor this node writes as, and not one it made: another node writes // as it too (two started from one document file with `--keep-actor`, a copied data // directory). Each would make changes the other's peers refuse as forged; the node - // writes on as a fresh actor at once. The change is judged as any other. - let fresh_actor = changes + // writes on as a fresh actor once these are judged. They are judged as any other, + // under the actor still: one at a seq the node made a change at is forged + // (`Signatures::forks`). + let twin = changes .iter() .find(|change| { change.actor_id() == doc.get_actor() && !crate::document::changes::holds(&*doc, &change.hash()) }) - .map(|change| { + .map(Change::seq); + let switch = |doc: &mut Document| { + twin.map(|seq| { let actor = crate::document::ActorId::random(); tracing::warn!( actor = %doc.get_actor(), - seq = change.seq(), + seq, fresh = %actor, "A change of this node's actor that it didn't make: another node writes as it (its files copied?); writing as a fresh actor" ); doc.set_actor(actor.clone()); actor - }); + }) + }; let mut screening = screening.lock_or_recover(); let screening = screening.of(epoch_seq); // Into an empty document of the first epoch, a root only as pinned: one given or @@ -777,7 +782,7 @@ pub(crate) fn apply_bytes( screening.record(refused_roots); if changes.is_empty() && screening.waiting.len() == 0 { return Ok(Outcome { - fresh_actor, + fresh_actor: switch(doc), ..Outcome::default() }); } @@ -792,7 +797,9 @@ pub(crate) fn apply_bytes( &mut screening.refusals, signatures, log, - )?; + ); + let fresh_actor = switch(doc); + let screened = screened?; let outcome = Outcome { refused_again: received > 0 && screened.refused_again == received, roots: screened.roots.iter().copied().collect(), @@ -1128,14 +1135,16 @@ mod tests { assert!(building.refused[0].why.contains("builds on refused change")); } - /// A change of the node's own actor that it didn't make: it writes on as a fresh - /// actor. Its own changes coming back change nothing. + /// A change of the node's own actor that it didn't make: refused as forged where the + /// node made one at its seq, and it writes on as a fresh actor. Its own changes + /// coming back change nothing. #[test] fn another_writing_as_this_node() { let mut theirs = genesis(); let mut ours = theirs.fork().with_actor(theirs.get_actor().clone()); let screening = std::sync::Mutex::new(Screening::default()); - let take = |doc: &mut Document, from: &Document, since: &[ChangeHash]| { + let mut signatures = Signatures::default(); + let mut take = |doc: &mut Document, from: &Document, since: &[ChangeHash]| { let bytes: Vec = received(from, since) .iter() .flat_map(|change| change.raw_bytes().to_vec()) @@ -1146,7 +1155,7 @@ mod tests { 0, &bytes, &screening, - None, + Some(&mut signatures), &mut crate::document::PatchLog::inactive(), ) .unwrap() @@ -1166,6 +1175,7 @@ mod tests { json!(2), ); let outcome = take(&mut ours, &theirs, &before); + assert_eq!(outcome.forged, 1); assert_eq!(outcome.fresh_actor.as_ref(), Some(ours.get_actor())); assert_ne!(ours.get_actor(), &actor); } diff --git a/src/document/signatures.rs b/src/document/signatures.rs index 4f34ecb..79c8569 100644 --- a/src/document/signatures.rs +++ b/src/document/signatures.rs @@ -603,11 +603,15 @@ impl Signatures { /// Whether `change`, not in `doc`, forks its actor's changes: `doc` holds a change /// a signature verified of that actor at a seq as late, so its real change at /// `change`'s seq too. Anyone can make a change under any actor; only its node can - /// sign one. + /// sign one. Or the actor is the one `doc` writes as, and it made a change at a seq + /// as late: another node writing as it too made this one. pub(crate) fn forks(&self, doc: &Document, change: &super::Change) -> bool { self.latest.get(change.actor_id()).is_some_and(|latest| { latest.seq >= change.seq() && super::changes::holds(doc, &latest.hash) - }) + }) || (change.actor_id() == doc.get_actor() + && doc + .get_last_local_change() + .is_some_and(|own| own.seq() >= change.seq())) } /// Whether a signature vouches for `actor`'s change `seq`. -- cgit v1.3.1