Skip to main content

coven_protocol/store_commit/
circle_ack.rs

1use super::validation::{validate_commit_frontier, validate_successor_sequence};
2use super::*;
3
4/// One device's signed acknowledgement of the exact private Circle history it
5/// currently holds, encrypted to the Circle epoch key it names. Store members
6/// outside the Circle observe only the object's shape and timing.
7#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize)]
8#[serde(deny_unknown_fields)]
9pub struct CircleAckBody {
10    pub store_root_hash: ObjectHash,
11    pub circle_id: CircleId,
12    pub registration: StoreDeviceRegistrationRef,
13    pub sequence: u64,
14    /// The device's accepted Store frontier at staging time — Circle packages
15    /// are activated by Store commits, so Circle coverage IS a Store frontier.
16    pub store_cut: CommitFrontier,
17    /// The exact activated control and epoch the device's live projection
18    /// derives from.
19    pub control: CircleControlCoord,
20    pub epoch_id: CircleEpochId,
21    pub key_fingerprint: KeyFingerprint,
22    /// The exact coverage the device's projection was seeded from: the retained
23    /// bootstrap coverage row (control, activating commit, exact cut, image
24    /// hash). `None` exactly for a founder/source device whose projection never
25    /// came from an image.
26    pub seeded_from: Option<CircleBootstrapCoverageRef>,
27    pub last_sync: String,
28    pub successor: SuccessorLink,
29}
30
31impl SignedBody for CircleAckBody {
32    const DOMAIN: &'static [u8] = CIRCLE_ACK_DOMAIN;
33}
34
35pub type CircleAck = Signed<CircleAckBody>;
36
37#[derive(Debug, Clone, PartialEq, Eq, PartialOrd, Ord, Serialize, Deserialize)]
38#[serde(deny_unknown_fields)]
39pub struct CircleAckRef {
40    pub registration: StoreDeviceRegistrationRef,
41    pub circle_id: CircleId,
42    pub control: CircleControlCoord,
43    pub sequence: u64,
44    pub ack_hash: ObjectHash,
45    pub object: ExactObjectRef,
46}
47
48impl CircleAck {
49    #[allow(clippy::too_many_arguments)]
50    pub fn signed(
51        store_root_hash: ObjectHash,
52        circle_id: CircleId,
53        registration: StoreDeviceRegistrationRef,
54        sequence: u64,
55        store_cut: CommitFrontier,
56        control: CircleControlCoord,
57        epoch_id: CircleEpochId,
58        key_fingerprint: KeyFingerprint,
59        seeded_from: Option<CircleBootstrapCoverageRef>,
60        last_sync: String,
61        successor: SuccessorLink,
62        device_signer: &UserKeypair,
63    ) -> Result<Self, StoreProtocolError> {
64        validate_successor_sequence(sequence, &successor)?;
65        validate_circle_ack_state(&store_cut, &control, &seeded_from, circle_id)?;
66        Ok(Signed::sign(
67            CircleAckBody {
68                store_root_hash,
69                circle_id,
70                registration,
71                sequence,
72                store_cut,
73                control,
74                epoch_id,
75                key_fingerprint,
76                seeded_from,
77                last_sync,
78                successor,
79            },
80            device_signer,
81        ))
82    }
83
84    pub fn ack_hash(&self) -> ObjectHash {
85        self.hash()
86    }
87
88    /// Verify one exact Circle acknowledgement against its expected reference and
89    /// author registration. The successor's stream activation is not recomputed
90    /// here: a Circle-acknowledgement stream's first slot is not carried by the
91    /// author's registration (unlike a Store-acknowledgement stream), so only
92    /// the author that holds it can reproduce the activation. A reader trusts
93    /// the Store commit that named this acknowledgement as the sole activation
94    /// authority, and checks the predecessor/sequence chain for ordering.
95    pub fn parse_at(
96        bytes: &[u8],
97        expected_store_root: &StoreRootRef,
98        expected: &CircleAckRef,
99        author: &StoreDeviceRegistration,
100    ) -> Result<Self, StoreProtocolError> {
101        let ack: Self = crate::objects::decode_protocol_object(bytes)?;
102        ack.require_version()?;
103        crate::objects::verify_store_root(
104            expected_store_root.store_root_hash,
105            ack.store_root_hash,
106        )?;
107        crate::objects::verify_store_root(
108            expected_store_root.store_root_hash,
109            author.store_root.store_root_hash,
110        )?;
111        ack.registration.verify_registration(author)?;
112        if ack.registration != expected.registration {
113            return Err(StoreProtocolError::DeviceRegistrationRefMismatch {
114                device_id: expected.registration.device_id.to_string(),
115                expected: expected.registration.registration_hash,
116                actual: ack.registration.registration_hash,
117            });
118        }
119        if ack.circle_id != expected.circle_id {
120            return Err(StoreProtocolError::Malformed(
121                "Circle acknowledgement names another Circle".to_string(),
122            ));
123        }
124        if ack.control != expected.control {
125            return Err(StoreProtocolError::Malformed(
126                "Circle acknowledgement names another control".to_string(),
127            ));
128        }
129        if ack.sequence != expected.sequence {
130            return Err(StoreProtocolError::RelocatedSlot {
131                expected: circle_ack_slot_prefix(
132                    expected.circle_id,
133                    &author.device_id.to_string(),
134                    expected.sequence,
135                ),
136                actual: circle_ack_slot_prefix(
137                    ack.circle_id,
138                    &author.device_id.to_string(),
139                    ack.sequence,
140                ),
141            });
142        }
143        validate_successor_sequence(ack.sequence, &ack.successor)?;
144        validate_circle_ack_state(
145            &ack.store_cut,
146            &ack.control,
147            &ack.seeded_from,
148            ack.circle_id,
149        )?;
150        ack.verify_by(&author.device_signing_pubkey)?;
151        if ack.ack_hash() != expected.ack_hash {
152            return Err(StoreProtocolError::ObjectHashMismatch {
153                expected: expected.ack_hash,
154                actual: ack.ack_hash(),
155            });
156        }
157        Ok(ack)
158    }
159}
160
161fn validate_circle_ack_state(
162    store_cut: &CommitFrontier,
163    control: &CircleControlCoord,
164    seeded_from: &Option<CircleBootstrapCoverageRef>,
165    circle_id: CircleId,
166) -> Result<(), StoreProtocolError> {
167    validate_commit_frontier(store_cut)?;
168    control.validate()?;
169    if let Some(seeded_from) = seeded_from {
170        if seeded_from.circle_id != circle_id {
171            return Err(StoreProtocolError::Malformed(
172                "Circle acknowledgement seed coverage names another Circle".to_string(),
173            ));
174        }
175    }
176    Ok(())
177}
178
179#[cfg(test)]
180mod tests {
181    use super::*;
182
183    #[test]
184    fn circle_ack_reference_names_its_encryption_control() {
185        let control = CircleControlCoord {
186            device_id: "circle-ack-author".to_string(),
187            stream_id: crate::causal_grants::AuthorStreamId::from_digest(ObjectHash::digest(
188                b"circle-ack-stream",
189            )),
190            author_pubkey: "circle-ack-author-pubkey".to_string(),
191            author_owner_grant: crate::causal_grants::MembershipGrantId::from_test_label(
192                "circle-ack-owner",
193            ),
194            seq: 1,
195            control_hash: ObjectHash::digest(b"circle-ack-control"),
196        };
197        let object = ExactObjectRef::new(
198            crate::objects::ObjectSlot::logical(
199                "circles/ack-test/acknowledgements/device/1.json".to_string(),
200            )
201            .expect("valid acknowledgement slot"),
202            1,
203            ObjectHash::digest(b"ack"),
204        );
205        let reference = CircleAckRef {
206            registration: StoreDeviceRegistrationRef {
207                device_id: ObjectHash::digest(b"circle-ack-device")
208                    .to_string()
209                    .parse()
210                    .expect("digest is a valid device id"),
211                registration_hash: ObjectHash::digest(b"circle-ack-registration"),
212                object: object.clone(),
213            },
214            circle_id: CircleId::from_bytes([1; 16]),
215            control: control.clone(),
216            sequence: 1,
217            ack_hash: ObjectHash::digest(b"circle-ack"),
218            object,
219        };
220
221        let encoded = serde_json::to_value(reference).expect("serialize acknowledgement ref");
222        assert_eq!(
223            encoded.get("control"),
224            Some(&serde_json::to_value(control).expect("serialize control"))
225        );
226    }
227}