radicle: fix diverging quorum

When calculating a quorum there was a case where two heads that were equal would
result in double-counting, allowing the quorum to pass a threshold higher than
the expected votes.

To prevent this, each head is immediately counted as a direct vote. Then, when
comparing the head to the rest of the set, if they are equal that iteration
would be skipped. This is because the merge base of two equal commits is that
commit, resulting in the double-counting.

Note that the `skip` can also skip the current head, so `i + 1` is used.

Signed-off-by: Fintan Halpenny <fintan.halpenny@gmail.com>
X-Clacks-Overhead: GNU Terry Pratchett
This commit is contained in:
Fintan Halpenny 2024-06-14 11:20:49 +02:00 committed by cloudhead
parent 589c375624
commit 3260046c67
No known key found for this signature in database
1 changed files with 31 additions and 3 deletions

View File

@ -925,11 +925,18 @@ pub fn quorum(
for (i, head) in heads.iter().enumerate() {
let head = Oid::from(*head);
for other in heads.iter().skip(i) {
// Add a direct vote for this head.
*candidates.entry(head).or_default() += 1;
// Compare this head to all other heads ahead of it in the list.
for other in heads.iter().skip(i + 1) {
// N.b. if heads are equal then skip it, otherwise it will end up as
// a double vote.
if *head == *other {
continue;
}
let base = repo.merge_base(*head, *other)?;
// Nb. This also handles `head` == `other`, which happens once for every head,
// as well as when there is more than one head with the same OID.
if base == *other || base == *head {
*candidates.entry(Oid::from(base)).or_default() += 1;
}
@ -1086,6 +1093,7 @@ mod tests {
let c0: git::Oid = c0.into();
let c1 = fixtures::commit("C1", &[*c0], &repo);
let c2 = fixtures::commit("C2", &[*c1], &repo);
let c3 = fixtures::commit("C3", &[*c1], &repo);
let b2 = fixtures::commit("B2", &[*c1], &repo);
let a1 = fixtures::commit("A1", &[*c0], &repo);
let m1 = fixtures::commit("M1", &[*c2, *b2], &repo);
@ -1094,6 +1102,7 @@ mod tests {
eprintln!("C0: {c0}");
eprintln!("C1: {c1}");
eprintln!("C2: {c2}");
eprintln!("C3: {c3}");
eprintln!("B2: {b2}");
eprintln!("A1: {a1}");
eprintln!("M1: {m1}");
@ -1145,6 +1154,25 @@ mod tests {
Err(QuorumError::NoQuorum)
);
// B2 C2 C3
// \ | /
// C1
// |
// C0
assert_matches!(quorum(&[*b2, *c2, *c2], 2, &repo), Ok(c2));
assert_matches!(
quorum(&[*b2, *c2, *c2], 3, &repo),
Err(QuorumError::NoQuorum)
);
assert_matches!(
quorum(&[*b2, *c2, *b2, *c2], 3, &repo),
Err(QuorumError::NoQuorum)
);
assert_matches!(
quorum(&[*c3, *b2, *c2, *b2, *c2, *c3], 3, &repo),
Err(QuorumError::NoQuorum)
);
// B2 C2
// \|
// A1 C1