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 pub predecessor_acceptance: Option<crate::membership::MembershipHeadAcceptance>,
25}
26
27#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize)]
29#[serde(deny_unknown_fields)]
30pub struct MembershipRollupStream {
31 pub author_pubkey: String,
32 pub author_owner_grant: MembershipGrantId,
33 pub stream_id: AuthorStreamId,
34 pub heads: Vec<MembershipRollupHead>,
35}
36
37#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize)]
61#[serde(deny_unknown_fields)]
62pub struct MembershipRollupBody {
63 pub store_root_hash: ObjectHash,
64 pub author_registration: StoreDeviceRegistrationRef,
65 pub streams: Vec<MembershipRollupStream>,
66}
67
68impl SignedBody for MembershipRollupBody {
69 const DOMAIN: &'static [u8] = MEMBERSHIP_ROLLUP_DOMAIN;
70}
71
72pub type MembershipRollup = Signed<MembershipRollupBody>;
73
74impl MembershipRollup {
75 pub fn signed(
76 store_root_hash: ObjectHash,
77 author_registration: StoreDeviceRegistrationRef,
78 streams: Vec<MembershipRollupStream>,
79 device_signer: &UserKeypair,
80 ) -> Result<Self, StoreProtocolError> {
81 let rollup = Signed::sign(
82 MembershipRollupBody {
83 store_root_hash,
84 author_registration,
85 streams,
86 },
87 device_signer,
88 );
89 rollup.validate_shape()?;
90 Ok(rollup)
91 }
92
93 pub fn validate_shape(&self) -> Result<(), StoreProtocolError> {
104 if self
105 .streams
106 .windows(2)
107 .any(|pair| stream_key(&pair[0]) >= stream_key(&pair[1]))
108 {
109 return Err(StoreProtocolError::Malformed(
110 "membership rollup streams are not canonical".to_string(),
111 ));
112 }
113 for stream in &self.streams {
114 if stream.heads.is_empty() {
115 return Err(StoreProtocolError::Malformed(
116 "membership rollup carries an empty author stream".to_string(),
117 ));
118 }
119 for (index, carried) in stream.heads.iter().enumerate() {
120 let sequence = u64::try_from(index)
121 .ok()
122 .and_then(|index| index.checked_add(1))
123 .ok_or_else(|| {
124 StoreProtocolError::Malformed(
125 "membership rollup sequence overflow".to_string(),
126 )
127 })?;
128 carried.validate_at(stream, sequence)?;
129 }
130 }
131
132 Ok(())
133 }
134
135 pub fn parse_at(
136 bytes: &[u8],
137 expected_store_root_hash: ObjectHash,
138 expected: &MembershipRollupRef,
139 author: &StoreDeviceRegistration,
140 ) -> Result<Self, StoreProtocolError> {
141 let rollup: Self = crate::objects::decode_protocol_object(bytes)?;
142 rollup.require_version()?;
143 crate::objects::verify_store_root(expected_store_root_hash, rollup.store_root_hash)?;
144 crate::objects::verify_store_root(
145 expected_store_root_hash,
146 author.store_root.store_root_hash,
147 )?;
148 rollup.author_registration.verify_registration(author)?;
149 rollup.validate_shape()?;
150 rollup.verify_by(&author.device_signing_pubkey)?;
151 let actual = ObjectHash::digest(bytes);
152 if actual != expected.rollup_hash {
153 return Err(StoreProtocolError::ObjectHashMismatch {
154 expected: expected.rollup_hash,
155 actual,
156 });
157 }
158 Ok(rollup)
159 }
160}
161
162impl MembershipRollupHead {
163 fn validate_at(
164 &self,
165 stream: &MembershipRollupStream,
166 sequence: u64,
167 ) -> Result<(), StoreProtocolError> {
168 let coord = self.head_value.entry_coord();
169 if coord != self.head.coord
170 || coord.author_pubkey != stream.author_pubkey
171 || coord.author_owner_grant != stream.author_owner_grant
172 || coord.stream_id != stream.stream_id
173 || coord.seq != sequence
174 || self.head.head_hash != self.head_value.head_hash()
175 || self.head_value.body.entry != self.entry
176 || self.entry.coord != self.entry_value.coord()
177 || !verify_membership_entry(&self.entry_value)
178 {
179 return Err(StoreProtocolError::Malformed(format!(
180 "membership rollup head {}/{}/{sequence} does not match its own reference",
181 stream.author_pubkey, stream.stream_id
182 )));
183 }
184 match (
185 self.head_value
186 .body
187 .predecessor
188 .as_ref()
189 .and_then(|previous| previous.acceptance()),
190 &self.predecessor_acceptance,
191 ) {
192 (Some(object), Some(result)) => {
193 object.verify(&result.to_bytes())?;
194 if Some(&result.head) != self.head_value.body.predecessor_head() {
195 return Err(StoreProtocolError::Malformed(
196 "membership rollup carries another predecessor's acceptance".into(),
197 ));
198 }
199 }
200 (None, None) => {}
201 _ => {
202 return Err(StoreProtocolError::Malformed(
203 "membership rollup omits or adds a predecessor acceptance".into(),
204 ));
205 }
206 }
207 self.head
208 .object
209 .verify(&serde_json::to_vec(&self.head_value)?)?;
210 self.entry
211 .object
212 .verify(&serde_json::to_vec(&self.entry_value)?)?;
213 Ok(())
214 }
215}
216
217fn stream_key(stream: &MembershipRollupStream) -> (&str, &MembershipGrantId, AuthorStreamId) {
218 (
219 &stream.author_pubkey,
220 &stream.author_owner_grant,
221 stream.stream_id,
222 )
223}