coven_protocol/store_commit/
membership_rollup.rs1use super::*;
2
3#[derive(Debug, Clone, PartialEq, Eq, PartialOrd, Ord, Serialize, Deserialize)]
10#[serde(deny_unknown_fields)]
11pub struct MembershipRollupRef {
12 pub rollup_hash: ObjectHash,
13 pub object: ExactObjectRef,
14}
15
16#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize)]
18#[serde(deny_unknown_fields)]
19pub struct MembershipRollupHead {
20 pub head: MembershipHeadRef,
21 pub head_value: AuthorHead,
22 pub entry: MembershipEntryRef,
23 pub entry_value: MembershipEntry,
24}
25
26#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize)]
28#[serde(deny_unknown_fields)]
29pub struct MembershipRollupResolution {
30 pub resolution: StoreMembershipConflictResolutionRef,
31 pub resolution_value: StoreMembershipConflictResolution,
32}
33
34#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize)]
36#[serde(deny_unknown_fields)]
37pub struct MembershipRollupStream {
38 pub author_pubkey: String,
39 pub author_owner_grant: MembershipGrantId,
40 pub stream_id: AuthorStreamId,
41 pub heads: Vec<MembershipRollupHead>,
42}
43
44#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize)]
68#[serde(deny_unknown_fields)]
69pub struct MembershipRollupBody {
70 pub store_root_hash: ObjectHash,
71 pub author_registration: StoreDeviceRegistrationRef,
72 pub streams: Vec<MembershipRollupStream>,
73 pub resolutions: Vec<MembershipRollupResolution>,
74}
75
76impl SignedBody for MembershipRollupBody {
77 const DOMAIN: &'static [u8] = MEMBERSHIP_ROLLUP_DOMAIN;
78}
79
80pub type MembershipRollup = Signed<MembershipRollupBody>;
81
82impl MembershipRollup {
83 pub fn signed(
84 store_root_hash: ObjectHash,
85 author_registration: StoreDeviceRegistrationRef,
86 streams: Vec<MembershipRollupStream>,
87 resolutions: Vec<MembershipRollupResolution>,
88 device_signer: &UserKeypair,
89 ) -> Result<Self, StoreProtocolError> {
90 let rollup = Signed::sign(
91 MembershipRollupBody {
92 store_root_hash,
93 author_registration,
94 streams,
95 resolutions,
96 },
97 device_signer,
98 );
99 rollup.validate_shape()?;
100 Ok(rollup)
101 }
102
103 pub fn validate_shape(&self) -> Result<(), StoreProtocolError> {
114 if self
115 .streams
116 .windows(2)
117 .any(|pair| stream_key(&pair[0]) >= stream_key(&pair[1]))
118 {
119 return Err(StoreProtocolError::Malformed(
120 "membership rollup streams are not canonical".to_string(),
121 ));
122 }
123 for stream in &self.streams {
124 if stream.heads.is_empty() {
125 return Err(StoreProtocolError::Malformed(
126 "membership rollup carries an empty author stream".to_string(),
127 ));
128 }
129 for (index, carried) in stream.heads.iter().enumerate() {
130 let sequence = u64::try_from(index)
131 .ok()
132 .and_then(|index| index.checked_add(1))
133 .ok_or_else(|| {
134 StoreProtocolError::Malformed(
135 "membership rollup sequence overflow".to_string(),
136 )
137 })?;
138 carried.validate_at(stream, sequence)?;
139 }
140 }
141 for carried in &self.resolutions {
142 let value = &carried.resolution_value;
143 if value.store_root_hash != self.store_root_hash
144 || !value.verify_signature()
145 || value.resolution_ref(carried.resolution.object.clone()) != carried.resolution
146 {
147 return Err(StoreProtocolError::Malformed(
148 "membership rollup carries an unauthentic conflict resolution".to_string(),
149 ));
150 }
151 carried
152 .resolution
153 .object
154 .verify(&serde_json::to_vec(value)?)?;
155 }
156 Ok(())
157 }
158
159 pub fn parse_at(
160 bytes: &[u8],
161 expected_store_root_hash: ObjectHash,
162 expected: &MembershipRollupRef,
163 author: &StoreDeviceRegistration,
164 ) -> Result<Self, StoreProtocolError> {
165 let rollup: Self = crate::objects::decode_protocol_object(bytes)?;
166 rollup.require_version()?;
167 crate::objects::verify_store_root(expected_store_root_hash, rollup.store_root_hash)?;
168 rollup.author_registration.verify_registration(author)?;
169 rollup.validate_shape()?;
170 rollup.verify_by(&author.device_signing_pubkey)?;
171 let actual = ObjectHash::digest(bytes);
172 if actual != expected.rollup_hash {
173 return Err(StoreProtocolError::ObjectHashMismatch {
174 expected: expected.rollup_hash,
175 actual,
176 });
177 }
178 Ok(rollup)
179 }
180}
181
182impl MembershipRollupHead {
183 fn validate_at(
184 &self,
185 stream: &MembershipRollupStream,
186 sequence: u64,
187 ) -> Result<(), StoreProtocolError> {
188 let coord = self.head_value.entry_coord();
189 if coord != self.head.coord
190 || coord.author_pubkey != stream.author_pubkey
191 || coord.author_owner_grant != stream.author_owner_grant
192 || coord.stream_id != stream.stream_id
193 || coord.seq != sequence
194 || self.head.head_hash != self.head_value.head_hash()
195 || self.head_value.body.entry != self.entry
196 || self.entry.coord != self.entry_value.coord()
197 || !verify_membership_entry(&self.entry_value)
198 {
199 return Err(StoreProtocolError::Malformed(format!(
200 "membership rollup head {}/{}/{sequence} does not match its own reference",
201 stream.author_pubkey, stream.stream_id
202 )));
203 }
204 self.head
205 .object
206 .verify(&serde_json::to_vec(&self.head_value)?)?;
207 self.entry
208 .object
209 .verify(&serde_json::to_vec(&self.entry_value)?)?;
210 Ok(())
211 }
212}
213
214fn stream_key(stream: &MembershipRollupStream) -> (&str, &MembershipGrantId, AuthorStreamId) {
215 (
216 &stream.author_pubkey,
217 &stream.author_owner_grant,
218 stream.stream_id,
219 )
220}