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) -> Result<StreamActivationId, StoreProtocolError> {
22 let first_slot = ObjectSlot::logical(format!(
23 "{}.json",
24 circle_snapshot_slot_prefix(circle_id, &author_registration.device_id.to_string(), 0)
25 ))?;
26 Ok(StreamActivation::device_authorized(
27 store_root_hash,
28 author_registration.clone(),
29 DeviceStreamAnchor::CircleSnapshots {
30 circle_id,
31 first_slot,
32 },
33 )
34 .activation_id())
35}
36
37#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize)]
40#[serde(deny_unknown_fields)]
41pub struct CircleSnapshotSuccessorLink {
42 pub predecessor: Option<CircleSnapshotRef>,
43 pub next_slot: ObjectSlot,
44}
45
46#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize)]
53#[serde(deny_unknown_fields)]
54pub struct CircleSnapshotMetaBody {
55 pub store_root_hash: ObjectHash,
56 pub circle_id: CircleId,
57 pub author_registration: StoreDeviceRegistrationRef,
58 pub control: CircleControlCoord,
59 pub epoch_id: CircleEpochId,
60 pub key_fingerprint: KeyFingerprint,
61 pub generation: u64,
62 pub bootstrap: CircleBootstrapRef,
66 pub created_at: String,
67 pub successor: CircleSnapshotSuccessorLink,
68}
69
70impl SignedBody for CircleSnapshotMetaBody {
71 const DOMAIN: &'static [u8] = CIRCLE_SNAPSHOT_DOMAIN;
72}
73
74pub type CircleSnapshotMeta = Signed<CircleSnapshotMetaBody>;
75
76impl CircleSnapshotMeta {
77 #[allow(clippy::too_many_arguments)]
78 pub fn signed(
79 store_root_hash: ObjectHash,
80 circle_id: CircleId,
81 author_registration: StoreDeviceRegistrationRef,
82 control: CircleControlCoord,
83 epoch_id: CircleEpochId,
84 key_fingerprint: KeyFingerprint,
85 generation: u64,
86 bootstrap: CircleBootstrapRef,
87 created_at: String,
88 successor: CircleSnapshotSuccessorLink,
89 device_signer: &UserKeypair,
90 ) -> Result<Self, StoreProtocolError> {
91 validate_circle_snapshot_generation(generation, successor.predecessor.as_ref())?;
92 validate_circle_snapshot_state(&control, &bootstrap.coverage)?;
93 Ok(Signed::sign(
94 CircleSnapshotMetaBody {
95 store_root_hash,
96 circle_id,
97 author_registration,
98 control,
99 epoch_id,
100 key_fingerprint,
101 generation,
102 bootstrap,
103 created_at,
104 successor,
105 },
106 device_signer,
107 ))
108 }
109
110 pub fn snapshot_hash(&self) -> ObjectHash {
111 self.hash()
112 }
113
114 pub fn semantic_hash_from_bytes(bytes: &[u8]) -> Result<ObjectHash, StoreProtocolError> {
115 let meta: Self = crate::objects::decode_protocol_object(bytes)?;
116 Ok(meta.snapshot_hash())
117 }
118
119 pub fn parse_at(
124 bytes: &[u8],
125 expected_store_root_hash: ObjectHash,
126 expected: &CircleSnapshotRef,
127 author: &StoreDeviceRegistration,
128 ) -> Result<Self, StoreProtocolError> {
129 let meta: Self = crate::objects::decode_protocol_object(bytes)?;
130 meta.require_version()?;
131 crate::objects::verify_store_root(expected_store_root_hash, meta.store_root_hash)?;
132 crate::objects::verify_store_root(
133 expected_store_root_hash,
134 author.store_root.store_root_hash,
135 )?;
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}