aboutsummaryrefslogtreecommitdiffstats
path: root/src
diff options
context:
space:
mode:
authortv <tv@krebsco.de>2026-11-12 01:13:54 +0000
committertv <tv@krebsco.de>2026-11-12 02:47:08 +0100
commit81dc0ca6012372ff293e19549053fda84bca1eb0 (patch)
tree883f867ee70072d38e26b907ce4734cad40f0ba8 /src
parent5b13f5e9d83a11b8a60f2d4711065360ba20f70c (diff)
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.
Diffstat (limited to 'src')
-rw-r--r--src/access/screen.rs32
-rw-r--r--src/document/signatures.rs8
2 files changed, 27 insertions, 13 deletions
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<u8> = 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`.