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 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}