heddle_object_model/object/thread_replication/
local_integration.rs1use std::collections::BTreeSet;
5
6use serde::{Deserialize, Serialize};
7
8use super::{ThreadGenesis, ThreadOperation, bounded, invalid};
9use crate::{
10 error::Result,
11 object::{ContentHash, State, StateId, VisibilityTier},
12};
13
14#[derive(Clone, Debug, PartialEq, Eq, Serialize, Deserialize)]
15#[serde(deny_unknown_fields)]
16pub struct LocalIntegration {
17 pub version: u16,
18 pub spool: uuid::Uuid,
19 pub device: [u8; 32],
20 pub author: super::SourceAuthor,
21 pub source_thread: ContentHash,
22 pub source_operation: ContentHash,
23 pub source_revision: StateId,
24 pub target_thread: ContentHash,
25 pub expected_target_frontier: BTreeSet<ContentHash>,
26 pub result: super::Capture,
27 pub result_visibility: VisibilityTier,
28 pub initiating_request_proof: ContentHash,
33 pub local_policy_version: ContentHash,
34 pub executed_at_ms: i64,
35}
36impl LocalIntegration {
37 pub fn encode(&self) -> Result<Vec<u8>> {
38 self.author.validate()?;
39 if matches!(&self.author, super::SourceAuthor::Account { spool, .. } if *spool != self.spool)
40 {
41 return Err(invalid("local integration author crosses Spool scope"));
42 }
43 if self.version != 1
44 || self.spool.is_nil()
45 || self.source_thread == self.target_thread
46 || self.expected_target_frontier.len() > 128
47 || self.executed_at_ms < 0
48 {
49 return Err(invalid("invalid local integration receipt"));
50 }
51 let state = self.result.validated_state()?;
52 if state.encode_current_msgpack()? != self.result.state {
53 return Err(invalid("non-canonical local integration State"));
54 }
55 let bytes = rmp_serde::to_vec_named(self)?;
56 bounded(&bytes)?;
57 Ok(bytes)
58 }
59 pub fn decode(bytes: &[u8]) -> Result<Self> {
60 bounded(bytes)?;
61 let receipt: Self = rmp_serde::from_slice(bytes)?;
62 if receipt.encode()? != bytes {
63 return Err(invalid("non-canonical local integration receipt"));
64 }
65 Ok(receipt)
66 }
67 pub fn resulting_state(&self) -> Result<State> {
68 self.result.validated_state()
69 }
70 pub fn validate_source(&self, source: &ThreadOperation) -> Result<()> {
73 if self.result.source_targets.is_none()
74 && source
75 .source_result()?
76 .is_some_and(|result| result.source_targets.is_some())
77 {
78 return Err(invalid("integration drops source reference closure"));
79 }
80 if source.thread != self.source_thread
81 || source.id()? != self.source_operation
82 || source
83 .source_state()?
84 .is_none_or(|state| state.id() != self.source_revision)
85 {
86 return Err(invalid(
87 "local integration source differs from original operation",
88 ));
89 }
90 Ok(())
91 }
92 pub(super) fn validate_operation(&self, operation: &ThreadOperation) -> Result<()> {
93 if operation.thread != self.target_thread
94 || operation.publisher != self.device
95 || operation.parents != self.expected_target_frontier
96 {
97 return Err(invalid(
98 "local integration changes device, target or observed frontier",
99 ));
100 }
101 Ok(())
102 }
103 pub(super) fn validate_parents(
104 &self,
105 genesis: &ThreadGenesis,
106 parents: &[ThreadOperation],
107 ) -> Result<()> {
108 if genesis.spool != self.spool.to_string() {
109 return Err(invalid("local integration belongs to another Spool"));
110 }
111 let state = self.resulting_state()?;
112 let mut expected = BTreeSet::from([self.source_revision]);
113 if parents.is_empty() {
114 expected.insert(genesis.base);
115 }
116 for parent in parents {
117 expected.insert(
118 parent
119 .source_state()?
120 .ok_or_else(|| invalid("local integration parent is not source"))?
121 .id(),
122 );
123 }
124 if state.parents.iter().copied().collect::<BTreeSet<_>>() != expected
127 || state.parents.len() != expected.len()
128 {
129 return Err(invalid("local integration drops source or target ancestry"));
130 }
131 Ok(())
132 }
133}
134
135pub fn intersect_visibility(
138 left: &VisibilityTier,
139 right: &VisibilityTier,
140) -> Result<VisibilityTier> {
141 use VisibilityTier::*;
142 if left == right {
143 return Ok(left.clone());
144 }
145 match (left, right) {
146 (Public, other) | (other, Public) => Ok(other.clone()),
147 (Internal, other) | (other, Internal) => Ok(other.clone()),
148 (Private { scope_label: a }, Restricted { scope_label: b })
149 | (Restricted { scope_label: b }, Private { scope_label: a })
150 if a == b =>
151 {
152 Ok(Private {
153 scope_label: a.clone(),
154 })
155 }
156 _ => Err(invalid("integration combines incomparable named audiences")),
157 }
158}