coven_protocol/store_commit/
circle_ack.rs1use super::validation::{validate_commit_frontier, validate_successor_sequence};
2use super::*;
3
4#[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 pub store_cut: CommitFrontier,
17 pub control: CircleControlCoord,
20 pub epoch_id: CircleEpochId,
21 pub key_fingerprint: KeyFingerprint,
22 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 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 ack.registration.verify_registration(author)?;
108 if ack.registration != expected.registration {
109 return Err(StoreProtocolError::DeviceRegistrationRefMismatch {
110 device_id: expected.registration.device_id.to_string(),
111 expected: expected.registration.registration_hash,
112 actual: ack.registration.registration_hash,
113 });
114 }
115 if ack.circle_id != expected.circle_id {
116 return Err(StoreProtocolError::Malformed(
117 "Circle acknowledgement names another Circle".to_string(),
118 ));
119 }
120 if ack.control != expected.control {
121 return Err(StoreProtocolError::Malformed(
122 "Circle acknowledgement names another control".to_string(),
123 ));
124 }
125 if ack.sequence != expected.sequence {
126 return Err(StoreProtocolError::RelocatedSlot {
127 expected: circle_ack_slot_prefix(
128 expected.circle_id,
129 &author.device_id.to_string(),
130 expected.sequence,
131 ),
132 actual: circle_ack_slot_prefix(
133 ack.circle_id,
134 &author.device_id.to_string(),
135 ack.sequence,
136 ),
137 });
138 }
139 validate_successor_sequence(ack.sequence, &ack.successor)?;
140 validate_circle_ack_state(
141 &ack.store_cut,
142 &ack.control,
143 &ack.seeded_from,
144 ack.circle_id,
145 )?;
146 ack.verify_by(&author.device_signing_pubkey)?;
147 if ack.ack_hash() != expected.ack_hash {
148 return Err(StoreProtocolError::ObjectHashMismatch {
149 expected: expected.ack_hash,
150 actual: ack.ack_hash(),
151 });
152 }
153 Ok(ack)
154 }
155}
156
157fn validate_circle_ack_state(
158 store_cut: &CommitFrontier,
159 control: &CircleControlCoord,
160 seeded_from: &Option<CircleBootstrapCoverageRef>,
161 circle_id: CircleId,
162) -> Result<(), StoreProtocolError> {
163 validate_commit_frontier(store_cut)?;
164 control.validate()?;
165 if let Some(seeded_from) = seeded_from {
166 if seeded_from.circle_id != circle_id {
167 return Err(StoreProtocolError::Malformed(
168 "Circle acknowledgement seed coverage names another Circle".to_string(),
169 ));
170 }
171 }
172 Ok(())
173}
174
175#[cfg(test)]
176mod tests {
177 use super::*;
178
179 #[test]
180 fn circle_ack_reference_names_its_encryption_control() {
181 let control = CircleControlCoord {
182 device_id: "circle-ack-author".to_string(),
183 stream_id: crate::causal_grants::AuthorStreamId::from_digest(ObjectHash::digest(
184 b"circle-ack-stream",
185 )),
186 author_pubkey: "circle-ack-author-pubkey".to_string(),
187 author_owner_grant: crate::causal_grants::MembershipGrantId::from_test_label(
188 "circle-ack-owner",
189 ),
190 seq: 1,
191 control_hash: ObjectHash::digest(b"circle-ack-control"),
192 };
193 let object = ExactObjectRef::new(
194 crate::objects::ObjectSlot::logical(
195 "circles/ack-test/acknowledgements/device/1.json".to_string(),
196 )
197 .expect("valid acknowledgement slot"),
198 1,
199 ObjectHash::digest(b"ack"),
200 );
201 let reference = CircleAckRef {
202 registration: StoreDeviceRegistrationRef {
203 device_id: ObjectHash::digest(b"circle-ack-device")
204 .to_string()
205 .parse()
206 .expect("digest is a valid device id"),
207 registration_hash: ObjectHash::digest(b"circle-ack-registration"),
208 object: object.clone(),
209 },
210 circle_id: CircleId::from_bytes([1; 16]),
211 control: control.clone(),
212 sequence: 1,
213 ack_hash: ObjectHash::digest(b"circle-ack"),
214 object,
215 };
216
217 let encoded = serde_json::to_value(reference).expect("serialize acknowledgement ref");
218 assert_eq!(
219 encoded.get("control"),
220 Some(&serde_json::to_value(control).expect("serialize control"))
221 );
222 }
223}