1#[path = "original_boundary_preflight.rs"]
4mod preflight;
5
6use std::collections::BTreeSet;
7
8use serde::{Deserialize, Serialize};
9use uuid::Uuid;
10
11use super::{
12 ContentHash, StateId,
13 thread_authority_admission::OriginalAuthorityBinding,
14 thread_replication::{
15 GenesisOwner, SourceAuthor, ThreadGenesis, ThreadOperation, metadata::AUTHORITY_FORMAT,
16 ownership_claim::ThreadOwnershipClaim, ownership_resolution::ThreadOwnershipResolution,
17 },
18};
19use crate::error::{HeddleError, Result};
20
21pub const FORMAT: &str = "heddle-original-boundary-acceptance-v1";
22pub const MANIFEST_FORMAT: &str = "heddle-original-publication-manifest-v1";
23pub const INTENT_FORMAT: &str = "heddle-original-publication-intent-v1";
24pub const MAX_RECORDS: usize = 10_384;
25pub const MAX_MANIFEST_BYTES: usize = 16 * 1024 * 1024;
26pub const MAX_ACCEPTANCE_BYTES: usize = 96 * 1024;
27
28#[derive(Clone, Debug, Eq, PartialEq, Ord, PartialOrd, Serialize, Deserialize)]
31pub enum ManifestSubject {
32 Genesis(ContentHash),
33 Source(ContentHash),
34 OtherOperation(ContentHash),
35 OwnershipClaim(ContentHash),
36 OwnershipResolution(ContentHash),
37}
38impl ManifestSubject {
39 pub fn id(&self) -> ContentHash {
40 match self {
41 Self::Genesis(id)
42 | Self::Source(id)
43 | Self::OtherOperation(id)
44 | Self::OwnershipClaim(id)
45 | Self::OwnershipResolution(id) => *id,
46 }
47 }
48}
49#[derive(Clone, Debug, Eq, PartialEq, Serialize, Deserialize)]
50#[serde(deny_unknown_fields)]
51pub struct OriginalManifestEntry {
52 pub subject: ManifestSubject,
53 pub thread: ContentHash,
54 pub publisher: [u8; 32],
55 pub authority: Option<OriginalAuthorityBinding>,
56}
57impl OriginalManifestEntry {
58 pub fn from_operation(operation: &ThreadOperation) -> Result<Self> {
60 let id = operation.id()?;
61 Ok(Self {
62 subject: if operation.source_result()?.is_some() {
63 ManifestSubject::Source(id)
64 } else {
65 ManifestSubject::OtherOperation(id)
66 },
67 thread: operation.thread,
68 publisher: operation.publisher,
69 authority: OriginalAuthorityBinding::from_operation(operation)?,
70 })
71 }
72 pub fn from_genesis(genesis: &ThreadGenesis, creator_authority: &[u8]) -> Result<Self> {
76 let authority = match genesis.owner {
77 GenesisOwner::Account(account) => {
78 if creator_authority.is_empty() || creator_authority.len() > 64 * 1024 {
79 return Err(invalid(
80 "account genesis requires bounded creator authority",
81 ));
82 }
83 Some(OriginalAuthorityBinding {
84 spool: Uuid::parse_str(&genesis.spool)
85 .map_err(|_| invalid("invalid genesis Spool"))?,
86 actor: super::CollaborationActor {
87 principal_id: account,
88 agent_id: None,
89 },
90 authority_digest: ContentHash::compute_typed(
91 AUTHORITY_FORMAT,
92 creator_authority,
93 ),
94 })
95 }
96 GenesisOwner::LocalKey(_) => {
97 if !creator_authority.is_empty() {
98 return Err(invalid("local genesis has no account authority"));
99 }
100 None
101 }
102 };
103 let id = genesis.id()?;
104 Ok(Self {
105 subject: ManifestSubject::Genesis(id),
106 thread: id,
107 publisher: genesis.creator,
108 authority,
109 })
110 }
111 pub fn from_claim(claim: &ThreadOwnershipClaim) -> Result<Self> {
113 claim.encode()?;
114 let SourceAuthor::Account {
115 spool,
116 actor,
117 authority_digest,
118 ..
119 } = &claim.acceptance
120 else {
121 return Err(invalid("claim requires explicit account authority"));
122 };
123 Ok(Self {
124 subject: ManifestSubject::OwnershipClaim(claim.id()?),
125 thread: claim.thread,
126 publisher: claim.accepting_publisher,
127 authority: Some(OriginalAuthorityBinding {
128 spool: *spool,
129 actor: actor.clone(),
130 authority_digest: *authority_digest,
131 }),
132 })
133 }
134 pub fn from_resolution(resolution: &ThreadOwnershipResolution) -> Result<Self> {
135 resolution.encode()?;
136 let SourceAuthor::Account {
137 spool,
138 actor,
139 authority_digest,
140 ..
141 } = &resolution.acceptance
142 else {
143 return Err(invalid("resolution requires explicit account acceptance"));
144 };
145 Ok(Self {
146 subject: ManifestSubject::OwnershipResolution(resolution.id()?),
147 thread: resolution.thread,
148 publisher: resolution.accepting_publisher,
149 authority: Some(OriginalAuthorityBinding {
150 spool: *spool,
151 actor: actor.clone(),
152 authority_digest: *authority_digest,
153 }),
154 })
155 }
156 fn validate(&self) -> Result<()> {
157 if self.publisher == [0; 32]
158 || self.subject.id().as_bytes() == &[0; 32]
159 || self.thread.as_bytes() == &[0; 32]
160 {
161 return Err(invalid("invalid original manifest identity"));
162 }
163 if matches!(self.subject, ManifestSubject::Genesis(_))
164 && (self.subject.id() != self.thread
165 || self
166 .authority
167 .as_ref()
168 .is_some_and(|binding| binding.actor.agent_id.is_some()))
169 {
170 return Err(invalid("genesis manifest cannot assert an unsigned agent"));
171 }
172 if let Some(binding) = &self.authority
173 && (binding.spool.is_nil()
174 || binding.actor.principal_id.is_nil()
175 || binding.actor.agent_id.as_ref().is_some_and(|id| {
176 id.is_empty() || id.len() > 256 || id.chars().any(char::is_control)
177 }))
178 {
179 return Err(invalid("invalid original manifest authority"));
180 }
181 Ok(())
182 }
183}
184#[derive(Clone, Debug, Eq, PartialEq, Serialize, Deserialize)]
185#[serde(deny_unknown_fields)]
186pub struct OriginalPublicationManifest {
187 pub version: u16,
188 pub entries: Vec<OriginalManifestEntry>,
189}
190impl OriginalPublicationManifest {
191 pub fn new(mut entries: Vec<OriginalManifestEntry>) -> Result<Self> {
192 if entries.len() > MAX_RECORDS {
193 return Err(invalid("original manifest record bound exceeded"));
194 }
195 entries.sort_by(|a, b| a.subject.cmp(&b.subject));
196 let value = Self {
197 version: 1,
198 entries,
199 };
200 value.encode()?;
201 Ok(value)
202 }
203 pub fn encode(&self) -> Result<Vec<u8>> {
204 if self.version != 1 || self.entries.is_empty() || self.entries.len() > MAX_RECORDS {
205 return Err(invalid("original manifest record bound exceeded"));
206 }
207 let mut ids = BTreeSet::new();
208 for entry in &self.entries {
209 entry.validate()?;
210 let domain = match entry.subject {
212 ManifestSubject::Genesis(_) => 0,
213 ManifestSubject::Source(_) | ManifestSubject::OtherOperation(_) => 1,
214 ManifestSubject::OwnershipClaim(_) => 2,
215 ManifestSubject::OwnershipResolution(_) => 3,
216 };
217 if !ids.insert((domain, entry.subject.id())) {
218 return Err(invalid("duplicate original manifest identity"));
219 }
220 }
221 if self
222 .entries
223 .windows(2)
224 .any(|pair| pair[0].subject >= pair[1].subject)
225 {
226 return Err(invalid("original manifest is not sorted"));
227 }
228 let bytes = rmp_serde::to_vec_named(self)?;
229 if bytes.len() > MAX_MANIFEST_BYTES {
230 return Err(invalid("original manifest byte bound exceeded"));
231 }
232 Ok(bytes)
233 }
234 pub fn decode(bytes: &[u8]) -> Result<Self> {
235 if bytes.is_empty() || bytes.len() > MAX_MANIFEST_BYTES {
236 return Err(invalid("original manifest byte bound exceeded"));
237 }
238 let value: Self =
239 preflight::decode(bytes, true, |bytes| Ok(rmp_serde::from_slice(bytes)?))?;
240 if value.encode()? != bytes {
241 return Err(invalid("noncanonical original manifest"));
242 }
243 Ok(value)
244 }
245 pub fn id(&self) -> Result<ContentHash> {
246 Ok(ContentHash::compute_typed(MANIFEST_FORMAT, &self.encode()?))
247 }
248}
249#[derive(Clone, Copy, Debug, Eq, PartialEq, Ord, PartialOrd, Serialize, Deserialize)]
250pub enum BoundaryOriginalKind {
251 Source,
252 AccountGenesis,
253 OwnershipClaim,
254 OwnershipResolution,
255}
256#[derive(Clone, Debug, Eq, PartialEq, Serialize, Deserialize)]
257#[serde(deny_unknown_fields)]
258pub struct PublicationIntent {
259 pub spool: Uuid,
260 pub spool_genesis: ContentHash,
261 pub thread: ContentHash,
262 pub revision: StateId,
263 pub inventory: ContentHash,
265 pub sharing_policy: Option<ContentHash>,
266 pub source: [u8; 32],
267 pub destination: [u8; 32],
268 pub client_operation_id: Uuid,
269}
270impl PublicationIntent {
271 pub fn id(&self) -> Result<ContentHash> {
272 if self.spool.is_nil()
273 || self.client_operation_id.is_nil()
274 || self.source == [0; 32]
275 || self.destination == [0; 32]
276 {
277 return Err(invalid("invalid boundary publication intent"));
278 }
279 Ok(ContentHash::compute_typed(
280 INTENT_FORMAT,
281 &rmp_serde::to_vec_named(self)?,
282 ))
283 }
284}
285#[derive(Clone, Debug, Eq, PartialEq, Serialize, Deserialize)]
286#[serde(deny_unknown_fields)]
287pub struct OriginalBoundaryAcceptance {
288 pub version: u16,
289 pub publication_intent: ContentHash,
290 pub originals_manifest: ContentHash,
291 pub original_account: Uuid,
292 pub kinds: BTreeSet<BoundaryOriginalKind>,
293 pub accepting_publisher: [u8; 32],
294 pub accepting_author: SourceAuthor,
296}
297impl OriginalBoundaryAcceptance {
298 pub fn encode(&self) -> Result<Vec<u8>> {
299 self.accepting_author.validate()?;
300 let SourceAuthor::Account { actor, .. } = &self.accepting_author else {
301 return Err(invalid(
302 "boundary acceptance requires explicit account authority",
303 ));
304 };
305 if self.version != 1
306 || self.kinds.is_empty()
307 || self.original_account.is_nil()
308 || actor.principal_id != self.original_account
309 || self.accepting_publisher == [0; 32]
310 {
311 return Err(invalid("invalid boundary acceptance identity"));
312 }
313 let bytes = rmp_serde::to_vec_named(self)?;
314 if bytes.len() > MAX_ACCEPTANCE_BYTES {
315 return Err(invalid("boundary acceptance byte bound exceeded"));
316 }
317 Ok(bytes)
318 }
319 pub fn decode(bytes: &[u8]) -> Result<Self> {
320 if bytes.is_empty() || bytes.len() > MAX_ACCEPTANCE_BYTES {
321 return Err(invalid("boundary acceptance byte bound exceeded"));
322 }
323 let value: Self =
324 preflight::decode(bytes, false, |bytes| Ok(rmp_serde::from_slice(bytes)?))?;
325 if value.encode()? != bytes {
326 return Err(invalid("noncanonical boundary acceptance"));
327 }
328 Ok(value)
329 }
330 pub fn id(&self) -> Result<ContentHash> {
331 Ok(ContentHash::compute_typed(FORMAT, &self.encode()?))
332 }
333 pub fn selected<'a>(
336 &self,
337 intent: &PublicationIntent,
338 manifest: &'a OriginalPublicationManifest,
339 ) -> Result<Vec<&'a OriginalManifestEntry>> {
340 self.encode()?;
341 let SourceAuthor::Account { spool, .. } = &self.accepting_author else {
342 return Err(invalid("account acceptance required"));
343 };
344 if *spool != intent.spool
345 || self.publication_intent != intent.id()?
346 || self.originals_manifest != manifest.id()?
347 {
348 return Err(invalid(
349 "boundary acceptance differs from exact publication",
350 ));
351 }
352 let selected: Vec<_> = manifest
353 .entries
354 .iter()
355 .filter(|entry| {
356 let Some(authority) = &entry.authority else {
357 return false;
358 };
359 if authority.spool != intent.spool
360 || authority.actor.principal_id != self.original_account
361 {
362 return false;
363 }
364 let kind = match entry.subject {
365 ManifestSubject::Genesis(_) => BoundaryOriginalKind::AccountGenesis,
366 ManifestSubject::Source(_) => BoundaryOriginalKind::Source,
367 ManifestSubject::OwnershipClaim(_) => BoundaryOriginalKind::OwnershipClaim,
368 ManifestSubject::OwnershipResolution(_) => {
369 BoundaryOriginalKind::OwnershipResolution
370 }
371 ManifestSubject::OtherOperation(_) => return false,
372 };
373 self.kinds.contains(&kind)
374 })
375 .collect();
376 if selected.is_empty() {
377 return Err(invalid("boundary acceptance selects no original"));
378 }
379 Ok(selected)
380 }
381}
382#[derive(Clone, Debug, Eq, PartialEq, Serialize, Deserialize)]
385pub enum AdmissionBasis {
386 OriginalAuthority,
387 BoundaryAcceptance { acceptance: ContentHash },
388}
389impl AdmissionBasis {
390 pub fn authorize_evidence(
393 &self,
394 evidence: Option<&OriginalBoundaryAcceptance>,
395 spool: Uuid,
396 account: Uuid,
397 kind: Option<BoundaryOriginalKind>,
398 ) -> Result<()> {
399 match (self, evidence) {
400 (Self::OriginalAuthority, None) => Ok(()),
401 (Self::BoundaryAcceptance { acceptance }, Some(value)) => {
402 let SourceAuthor::Account {
403 spool: accepting_spool,
404 ..
405 } = &value.accepting_author
406 else {
407 return Err(invalid("boundary receipt requires account acceptance"));
408 };
409 if value.id()? != *acceptance
410 || *accepting_spool != spool
411 || value.original_account != account
412 || !kind.is_some_and(|kind| value.kinds.contains(&kind))
413 {
414 return Err(invalid(
415 "boundary receipt evidence differs from original authority scope",
416 ));
417 }
418 Ok(())
419 }
420 _ => Err(invalid(
421 "receipt requires exactly its matched admission basis evidence",
422 )),
423 }
424 }
425}
426fn invalid(message: &str) -> HeddleError {
427 HeddleError::InvalidObject(message.into())
428}
429
430#[cfg(test)]
431#[path = "original_boundary_acceptance_tests.rs"]
432mod tests;