radicle/cob/identity: Strip implicit author accept for sibling revisions in `action`
When a delegate creates a new `Revision`, their signature is implicitly added to the revision. Prevent the delegate from an accept verdict on two active, sibling revisions by: 1. Checking that the `state` is `Active`, and then checking `has_active_sibling_accept` 2. If there is an existing verdict, then it the verdict from the new revision is stripped. The creation of the `Revision` must still happen, as opposed to throwing an error, in case of existing histories.
This commit is contained in:
parent
82dc3fe20c
commit
654218e6a5
|
|
@ -696,6 +696,14 @@ impl Identity {
|
||||||
State::Active => State::Active,
|
State::Active => State::Active,
|
||||||
};
|
};
|
||||||
|
|
||||||
|
// Check BEFORE inserting the new revision, so it doesn't count
|
||||||
|
// as its own sibling. If the author already has an accept on an
|
||||||
|
// Active sibling, their implicit author accept will be stripped
|
||||||
|
// after creation.
|
||||||
|
let should_strip_author_accept = matches!(state, State::Active)
|
||||||
|
.then(|| self.has_active_sibling_accept(&parent_id, &did))
|
||||||
|
.flatten();
|
||||||
|
|
||||||
let revision = Revision::new(
|
let revision = Revision::new(
|
||||||
id,
|
id,
|
||||||
title,
|
title,
|
||||||
|
|
@ -712,6 +720,14 @@ impl Identity {
|
||||||
self.revisions.insert(id, revision);
|
self.revisions.insert(id, revision);
|
||||||
self.revision_mut(&parent_id)?.children.push(id);
|
self.revision_mut(&parent_id)?.children.push(id);
|
||||||
|
|
||||||
|
if let Some(revision_id) = should_strip_author_accept {
|
||||||
|
log::debug!(
|
||||||
|
"Stripping implicit accept from revision {id} by {did}: \
|
||||||
|
already accepted the active revision {revision_id}.",
|
||||||
|
);
|
||||||
|
self.revision_mut(&id)?.verdicts.remove(&author);
|
||||||
|
}
|
||||||
|
|
||||||
if state == State::Active {
|
if state == State::Active {
|
||||||
self.adopt(id);
|
self.adopt(id);
|
||||||
}
|
}
|
||||||
|
|
@ -2986,4 +3002,79 @@ mod test {
|
||||||
"Alice's accept on child_a should have been skipped (she already accepted sibling child_b)"
|
"Alice's accept on child_a should have been skipped (she already accepted sibling child_b)"
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/// When replaying an old history where a delegate created two sibling
|
||||||
|
/// revisions, the second revision's implicit author accept is stripped.
|
||||||
|
#[test]
|
||||||
|
fn evaluation_strips_author_accept_for_sibling_creation() {
|
||||||
|
let network = Network::default();
|
||||||
|
let alice = &network.alice;
|
||||||
|
let bob = &network.bob;
|
||||||
|
|
||||||
|
let mut alice_identity = Identity::load_mut(&*alice.repo, &alice.signer).unwrap();
|
||||||
|
let mut alice_doc = alice_identity.doc().clone().edit();
|
||||||
|
alice_doc.delegate(bob.signer.public_key().into());
|
||||||
|
let _a1 = alice_identity
|
||||||
|
.update(
|
||||||
|
cob::Title::new("Add Bob").unwrap(),
|
||||||
|
"",
|
||||||
|
&alice_doc.verified().unwrap(),
|
||||||
|
)
|
||||||
|
.unwrap();
|
||||||
|
|
||||||
|
// Alice proposes first child (gets implicit accept).
|
||||||
|
let mut doc1 = alice_identity.doc().clone().edit();
|
||||||
|
doc1.visibility = Visibility::private([]);
|
||||||
|
let child1 = alice_identity
|
||||||
|
.update(
|
||||||
|
cob::Title::new("Child 1").unwrap(),
|
||||||
|
"",
|
||||||
|
&doc1.clone().verified().unwrap(),
|
||||||
|
)
|
||||||
|
.unwrap();
|
||||||
|
|
||||||
|
// Alice proposes second child via transaction (simulating old history).
|
||||||
|
let mut doc2 = doc1.clone();
|
||||||
|
doc2.visibility = Visibility::private([alice.signer.public_key().into()]);
|
||||||
|
let current = alice_identity.current;
|
||||||
|
let child2 = alice_identity
|
||||||
|
.transaction("Child 2", |tx, repo| {
|
||||||
|
*tx = Transaction::new_revision(
|
||||||
|
cob::Title::new("Child 2").unwrap(),
|
||||||
|
"",
|
||||||
|
&doc2.verified().unwrap(),
|
||||||
|
Some(current),
|
||||||
|
repo,
|
||||||
|
&alice.signer,
|
||||||
|
)?;
|
||||||
|
Ok(())
|
||||||
|
})
|
||||||
|
.unwrap();
|
||||||
|
|
||||||
|
let alice_did: Did = alice.signer.public_key().into();
|
||||||
|
|
||||||
|
// child1 has Alice's accept (she was the first to propose under this parent).
|
||||||
|
assert!(
|
||||||
|
alice_identity
|
||||||
|
.revision(&child1)
|
||||||
|
.unwrap()
|
||||||
|
.accepted()
|
||||||
|
.any(|did| did == alice_did),
|
||||||
|
"child1 should have Alice's accept"
|
||||||
|
);
|
||||||
|
// child2's implicit accept was stripped because Alice already accepted child1.
|
||||||
|
assert!(
|
||||||
|
!alice_identity
|
||||||
|
.revision(&child2)
|
||||||
|
.unwrap()
|
||||||
|
.accepted()
|
||||||
|
.any(|did| did == alice_did),
|
||||||
|
"child2 should NOT have Alice's accept"
|
||||||
|
);
|
||||||
|
// child2 still exists and is Active (just has 0 accept votes).
|
||||||
|
assert_eq!(
|
||||||
|
alice_identity.revision(&child2).unwrap().state,
|
||||||
|
State::Active
|
||||||
|
);
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
|
||||||
Loading…
Reference in New Issue