Skip to main content

coven_protocol/circle_control/
access.rs

1use super::*;
2
3/// Exact Circle database image offered when one recipient becomes active.
4#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize)]
5#[serde(deny_unknown_fields)]
6pub struct CircleBootstrapRef {
7    pub coverage: CommitFrontier,
8    pub schema_version: u32,
9    pub sync_routing_hash: ObjectHash,
10    pub image: SnapshotImageRef,
11    pub blobs: Vec<crate::blob::RowBlobRef>,
12}
13
14impl CircleBootstrapRef {
15    pub(crate) fn verify_for_access(&self, access: &CircleAccessLeaf) -> bool {
16        if crate::store_commit::validate_commit_frontier(&self.coverage).is_err() {
17            return false;
18        }
19        let blobs_are_canonical = self.blobs.windows(2).all(|pair| {
20            serde_json::to_vec(&pair[0]).expect("row blob reference serialization cannot fail")
21                < serde_json::to_vec(&pair[1])
22                    .expect("row blob reference serialization cannot fail")
23        });
24        if !blobs_are_canonical
25            || self.blobs.iter().any(|blob| {
26                !matches!(
27                    blob.authority(),
28                    crate::blob::RowBlobAuthority::Remote(
29                        crate::audience_package::PackageAudience::Circle {
30                            circle_id,
31                            ..
32                        }
33                    ) if *circle_id == access.circle_id
34                ) || blob.stored().is_none_or(|stored| {
35                    stored.locator().audience()
36                        != crate::blob::locator::RemoteAudience::Circle(access.circle_id)
37                })
38            })
39        {
40            return false;
41        }
42        let semantic_prefix = crate::store_commit::circle_bootstrap_image_semantic_prefix(
43            access.circle_id,
44            access.candidate_family,
45            &access.owner_pubkey,
46            access.epoch_id,
47            &access.recipient_slot,
48            self.image.image_hash,
49        );
50        self.image.object.slot().logical_key() == format!("{semantic_prefix}.db")
51    }
52}
53
54/// The exact retained bootstrap coverage a recipient device's live Circle
55/// projection was seeded from: the activating Store commit, the control it
56/// activated under, and the bootstrap reference (its exact cut and image hash
57/// live inside that reference, not re-declared here). Names one row of
58/// `circle_bootstrap_coverage`.
59#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize)]
60#[serde(deny_unknown_fields)]
61pub struct CircleBootstrapCoverageRef {
62    pub circle_id: CircleId,
63    pub control: CircleControlCoord,
64    pub activation_commit: StoreBatchCommitRef,
65    pub bootstrap: CircleBootstrapRef,
66}
67
68#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize)]
69#[serde(rename_all = "snake_case", deny_unknown_fields)]
70pub enum CircleAccessDisposition {
71    Active {
72        keyring: String,
73        key_fingerprint: KeyFingerprint,
74        roster: CircleRosterStateRef,
75        bootstrap: Option<CircleBootstrapRef>,
76    },
77    Inactive,
78}
79
80/// The wire body of one recipient's Circle access leaf. Every field here is
81/// signed.
82#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize)]
83#[serde(deny_unknown_fields)]
84pub struct CircleAccessLeafBody {
85    pub store_root_hash: ObjectHash,
86    pub candidate_family: crate::store_commit::CandidateFamilyId,
87    pub circle_id: CircleId,
88    pub epoch_id: CircleEpochId,
89    pub leaf_id: AccessLeafId,
90    pub owner_pubkey: String,
91    pub recipient_pubkey: String,
92    pub recipient_slot: String,
93    pub disposition: CircleAccessDisposition,
94    pub store_membership: StoreMembershipStateRef,
95}
96
97impl SignedBody for CircleAccessLeafBody {
98    const DOMAIN: &'static [u8] = ACCESS_DOMAIN;
99}
100
101pub type CircleAccessLeaf = Signed<CircleAccessLeafBody>;
102
103impl CircleAccessLeaf {
104    pub fn verify_signature(&self) -> bool {
105        self.verify_by(&self.owner_pubkey).is_ok()
106    }
107
108    pub(crate) fn verify_for_control(
109        &self,
110        control: &PreparedCircleControl,
111        candidate_family: crate::store_commit::CandidateFamilyId,
112    ) -> bool {
113        self.verify_signature()
114            && self.store_root_hash == control.value.store_root_hash
115            && self.candidate_family == candidate_family
116            && self.circle_id == control.value.circle_id
117            && self.epoch_id == control.value.epoch_id()
118            && self.store_membership == control.value.store_membership_state_ref()
119            && match &self.disposition {
120                CircleAccessDisposition::Active {
121                    keyring,
122                    key_fingerprint,
123                    roster,
124                    bootstrap,
125                } => {
126                    roster == &control.value.roster_state_ref()
127                        && *key_fingerprint == control.value.key_fingerprint()
128                        && MasterKeyring::from_serialized(keyring).is_ok_and(|keyring| {
129                            EncryptionService::from(keyring).seal_key_fingerprint()
130                                == *key_fingerprint
131                        })
132                        && bootstrap
133                            .as_ref()
134                            .is_none_or(|bootstrap| bootstrap.verify_for_access(self))
135                }
136                CircleAccessDisposition::Inactive => true,
137            }
138            && self.owner_pubkey == control.value.author_pubkey
139    }
140}
141
142#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize)]
143#[serde(rename_all = "snake_case", deny_unknown_fields)]
144pub enum MerkleStep {
145    Left(ObjectHash),
146    Right(ObjectHash),
147}
148
149fn merkle_parent(left: ObjectHash, right: ObjectHash) -> ObjectHash {
150    let mut bytes = Vec::with_capacity(1 + 64);
151    bytes.push(1);
152    bytes.extend_from_slice(left.as_bytes());
153    bytes.extend_from_slice(right.as_bytes());
154    ObjectHash::digest(&bytes)
155}
156
157pub fn verify_merkle_proof(mut hash: ObjectHash, proof: &[MerkleStep], root: ObjectHash) -> bool {
158    for step in proof {
159        hash = match step {
160            MerkleStep::Left(left) => merkle_parent(*left, hash),
161            MerkleStep::Right(right) => merkle_parent(hash, *right),
162        };
163    }
164    hash == root
165}
166
167pub fn merkle_root_and_proofs(hashes: &[ObjectHash]) -> (ObjectHash, Vec<Vec<MerkleStep>>) {
168    assert!(
169        !hashes.is_empty(),
170        "a circle control has at least one access leaf"
171    );
172    let mut indexed = hashes
173        .iter()
174        .copied()
175        .enumerate()
176        .collect::<Vec<(usize, ObjectHash)>>();
177    indexed.sort_by_key(|(index, hash)| (*hash, *index));
178    let mut proofs = vec![Vec::new(); hashes.len()];
179    let mut layer = indexed
180        .into_iter()
181        .map(|(index, hash)| (hash, vec![index]))
182        .collect::<Vec<_>>();
183    while layer.len() > 1 {
184        let mut next = Vec::with_capacity(layer.len().div_ceil(2));
185        for pair in layer.chunks(2) {
186            let (left_hash, left_indices) = &pair[0];
187            if let Some((right_hash, right_indices)) = pair.get(1) {
188                for index in left_indices {
189                    proofs[*index].push(MerkleStep::Right(*right_hash));
190                }
191                for index in right_indices {
192                    proofs[*index].push(MerkleStep::Left(*left_hash));
193                }
194                let mut indices = left_indices.clone();
195                indices.extend(right_indices);
196                next.push((merkle_parent(*left_hash, *right_hash), indices));
197            } else {
198                for index in left_indices {
199                    proofs[*index].push(MerkleStep::Right(*left_hash));
200                }
201                next.push((merkle_parent(*left_hash, *left_hash), left_indices.clone()));
202            }
203        }
204        layer = next;
205    }
206    (layer[0].0, proofs)
207}