coven_protocol/store_commit/
circle_snapshot.rs1use super::*;
2
3#[derive(Debug, Clone, PartialEq, Eq, PartialOrd, Ord, Serialize, Deserialize)]
6#[serde(deny_unknown_fields)]
7pub struct CircleSnapshotRef {
8 pub generation: u64,
9 pub snapshot_hash: ObjectHash,
10 pub object: ExactObjectRef,
11}
12
13pub fn circle_snapshot_stream_activation(
18 store_root_hash: ObjectHash,
19 author_registration: &StoreDeviceRegistrationRef,
20 circle_id: CircleId,
21 device_id: &str,
22) -> Result<StreamActivationId, StoreProtocolError> {
23 let first_slot = ObjectSlot::logical(format!(
24 "{}.json",
25 circle_snapshot_slot_prefix(circle_id, device_id, 0)
26 ))?;
27 Ok(StreamActivation::device_authorized(
28 store_root_hash,
29 author_registration.clone(),
30 DeviceStreamAnchor::CircleSnapshots {
31 circle_id,
32 first_slot,
33 },
34 )
35 .activation_id())
36}
37
38#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize)]
41#[serde(deny_unknown_fields)]
42pub struct CircleSnapshotSuccessorLink {
43 pub activation: StreamActivationId,
44 pub predecessor: Option<CircleSnapshotRef>,
45 pub next_slot: ObjectSlot,
46}
47
48#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize)]
55#[serde(deny_unknown_fields)]
56pub struct CircleSnapshotMetaBody {
57 pub store_root_hash: ObjectHash,
58 pub circle_id: CircleId,
59 pub author_registration: StoreDeviceRegistrationRef,
60 pub control: CircleControlCoord,
61 pub epoch_id: CircleEpochId,
62 pub key_fingerprint: KeyFingerprint,
63 pub generation: u64,
64 pub bootstrap: CircleBootstrapRef,
68 pub created_at: String,
69 pub successor: CircleSnapshotSuccessorLink,
70}
71
72impl SignedBody for CircleSnapshotMetaBody {
73 const DOMAIN: &'static [u8] = CIRCLE_SNAPSHOT_DOMAIN;
74}
75
76pub type CircleSnapshotMeta = Signed<CircleSnapshotMetaBody>;
77
78impl CircleSnapshotMeta {
79 #[allow(clippy::too_many_arguments)]
80 pub fn signed(
81 store_root_hash: ObjectHash,
82 circle_id: CircleId,
83 author_registration: StoreDeviceRegistrationRef,
84 control: CircleControlCoord,
85 epoch_id: CircleEpochId,
86 key_fingerprint: KeyFingerprint,
87 generation: u64,
88 bootstrap: CircleBootstrapRef,
89 created_at: String,
90 successor: CircleSnapshotSuccessorLink,
91 device_signer: &UserKeypair,
92 ) -> Result<Self, StoreProtocolError> {
93 validate_circle_snapshot_generation(generation, successor.predecessor.as_ref())?;
94 validate_circle_snapshot_state(&control, &bootstrap.coverage)?;
95 Ok(Signed::sign(
96 CircleSnapshotMetaBody {
97 store_root_hash,
98 circle_id,
99 author_registration,
100 control,
101 epoch_id,
102 key_fingerprint,
103 generation,
104 bootstrap,
105 created_at,
106 successor,
107 },
108 device_signer,
109 ))
110 }
111
112 pub fn snapshot_hash(&self) -> ObjectHash {
113 self.hash()
114 }
115
116 pub fn semantic_hash_from_bytes(bytes: &[u8]) -> Result<ObjectHash, StoreProtocolError> {
117 let meta: Self = crate::objects::decode_protocol_object(bytes)?;
118 Ok(meta.snapshot_hash())
119 }
120
121 pub fn parse_at(
128 bytes: &[u8],
129 expected_store_root_hash: ObjectHash,
130 expected: &CircleSnapshotRef,
131 author: &StoreDeviceRegistration,
132 ) -> Result<Self, StoreProtocolError> {
133 let meta: Self = crate::objects::decode_protocol_object(bytes)?;
134 meta.require_version()?;
135 crate::objects::verify_store_root(expected_store_root_hash, meta.store_root_hash)?;
136 meta.author_registration.verify_registration(author)?;
137 if meta.generation != expected.generation {
138 return Err(StoreProtocolError::RelocatedSlot {
139 expected: circle_snapshot_slot_prefix(
140 meta.circle_id,
141 &author.device_id.to_string(),
142 expected.generation,
143 ),
144 actual: circle_snapshot_slot_prefix(
145 meta.circle_id,
146 &author.device_id.to_string(),
147 meta.generation,
148 ),
149 });
150 }
151 validate_circle_snapshot_generation(meta.generation, meta.successor.predecessor.as_ref())?;
152 validate_circle_snapshot_state(&meta.control, &meta.bootstrap.coverage)?;
153 meta.verify_by(&author.device_signing_pubkey)?;
154 let actual = meta.snapshot_hash();
155 if actual != expected.snapshot_hash {
156 return Err(StoreProtocolError::ObjectHashMismatch {
157 expected: expected.snapshot_hash,
158 actual,
159 });
160 }
161 Ok(meta)
162 }
163}
164
165fn validate_circle_snapshot_state(
166 control: &CircleControlCoord,
167 coverage: &CommitFrontier,
168) -> Result<(), StoreProtocolError> {
169 control.validate()?;
170 super::validation::validate_commit_frontier(coverage)
171}
172
173fn validate_circle_snapshot_generation(
174 generation: u64,
175 predecessor: Option<&CircleSnapshotRef>,
176) -> Result<(), StoreProtocolError> {
177 match (generation, predecessor) {
178 (0, None) => Ok(()),
179 (0, Some(_)) | (_, None) => Err(StoreProtocolError::Malformed(
180 "Circle snapshot generation and predecessor disagree".to_string(),
181 )),
182 (generation, Some(predecessor)) => {
183 let expected = predecessor.generation.checked_add(1).ok_or_else(|| {
184 StoreProtocolError::Malformed("Circle snapshot generation overflow".to_string())
185 })?;
186 if generation != expected {
187 return Err(StoreProtocolError::Malformed(
188 "Circle snapshot generation does not follow its predecessor".to_string(),
189 ));
190 }
191 Ok(())
192 }
193 }
194}