From 2f1aaa45b2657d51ef5683f2acc86e050c268d51 Mon Sep 17 00:00:00 2001 From: Fintan Halpenny Date: Thu, 2 Jul 2026 16:43:09 +0000 Subject: [PATCH] radicle/cob/identity: Add sibling-accept invariant to property tests --- .../radicle/src/cob/identity/test/property.rs | 57 +++++++++++++++++++ 1 file changed, 57 insertions(+) diff --git a/crates/radicle/src/cob/identity/test/property.rs b/crates/radicle/src/cob/identity/test/property.rs index 7d313c26..3e79d096 100644 --- a/crates/radicle/src/cob/identity/test/property.rs +++ b/crates/radicle/src/cob/identity/test/property.rs @@ -540,6 +540,7 @@ impl Harness { self.assert_failed_parents_have_failed_children(identity); self.assert_rejection_reasons_are_accurate(identity); self.assert_verdicts_match_parent_delegates(identity); + self.assert_no_duplicate_sibling_accepts(identity); } /// Strong eventual consistency. @@ -962,4 +963,60 @@ impl Harness { } } } + + /// No delegate has Accept verdicts on two Active siblings. + /// + /// A delegate may hold at most one Accept across all Active children of + /// the same parent. If two siblings both accumulate majority accepts, the + /// `adopt()` function could encounter an already-rejected revision during + /// re-evaluation, which would violate internal assumptions. + /// + /// PASS: + /// [Parent] → [Child A: Alice accepts] [Child B: Bob accepts] + /// + /// FAIL: + /// [Parent] → [Child A: Alice accepts] [Child B: Alice accepts] + fn assert_no_duplicate_sibling_accepts(&self, identity: &Identity) { + for id in &identity.timeline { + let Some(rev) = identity.revision(id) else { + continue; + }; + if !rev.is_active() { + continue; + } + let Some(parent_id) = rev.parent else { + continue; + }; + + for (voter_key, verdict) in rev.verdicts() { + if !matches!(verdict, cob::identity::Verdict::Accept(_)) { + continue; + } + let voter_did = Did::from(*voter_key); + + // Check that no Active sibling also has an accept from this voter. + for sibling_id in identity + .revision(&parent_id) + .map(|p| &p.children) + .into_iter() + .flatten() + { + if sibling_id == id { + continue; + } + if let Some(sibling) = identity.revision(sibling_id) + && sibling.is_active() + { + assert!( + !sibling.accepted().any(|did| did == voter_did), + "Delegate {} has Accept verdicts on two active siblings: {} and {}", + voter_did, + id, + sibling_id, + ); + } + } + } + } + } }