Skip to main content

repo/thread_replication/
prefix.rs

1//! Prefix projection changes only carrier arrays and unsigned selectors. All
2//! original, admission and publication bytes remain exactly as authenticated.
3use std::collections::BTreeSet;
4
5use api::{
6    heddle::api::v1alpha2 as wire,
7    hybrid_codec::{self, Reject},
8    import_authority as import,
9};
10use prost::Message;
11
12use super::authority::PublicProof;
13
14impl PublicProof {
15    pub fn foreign_dependencies(&self) -> &[wire::ForeignDependencyV1] {
16        match self {
17            Self::Import(b) => &b.foreign_dependencies,
18            Self::Native(b) => &b.foreign_dependencies,
19        }
20    }
21
22    pub fn foreign_original(
23        &self,
24        reference: &wire::ForeignDependencyV1,
25    ) -> Result<&wire::SignedRecord, Reject> {
26        let (authority, landing) = match self {
27            Self::Import(b) => (&b.authority_witnesses, &b.landing_witnesses),
28            Self::Native(b) => (&b.authority_witnesses, &b.landing_witnesses),
29        };
30        authority
31            .iter()
32            .flat_map(|p| &p.dependencies)
33            .chain(
34                landing
35                    .iter()
36                    .flat_map(|p| p.source_operation.iter().chain(&p.review_evidence)),
37            )
38            .find(|r| {
39                import::signed_native_digest(r).is_ok_and(|d| d == reference.signed_native_digest)
40            })
41            .ok_or(Reject::Scope)
42    }
43    pub fn prefix_original(
44        &self,
45        reference: &wire::ForeignDependencyV1,
46    ) -> Result<&wire::SignedRecord, Reject> {
47        let (genesis, authority, landing) = match self {
48            Self::Import(b) => (
49                b.genesis_witnesses
50                    .iter()
51                    .filter_map(|p| p.original_genesis.as_ref())
52                    .collect::<Vec<_>>(),
53                &b.authority_witnesses,
54                &b.landing_witnesses,
55            ),
56            Self::Native(b) => (
57                b.genesis_witnesses
58                    .iter()
59                    .filter_map(|p| p.original_genesis.as_ref())
60                    .collect::<Vec<_>>(),
61                &b.authority_witnesses,
62                &b.landing_witnesses,
63            ),
64        };
65        genesis
66            .iter()
67            .copied()
68            .chain(
69                authority
70                    .iter()
71                    .flat_map(|p| p.original.iter().chain(&p.dependencies)),
72            )
73            .chain(landing.iter().flat_map(|p| {
74                p.execution
75                    .iter()
76                    .chain(p.source_operation.iter())
77                    .chain(&p.review_evidence)
78            }))
79            .find(|r| {
80                import::signed_native_digest(r).is_ok_and(|d| d == reference.signed_native_digest)
81            })
82            .ok_or(Reject::Scope)
83    }
84
85    // A complete acceptance may select an original admitted after this cutoff.
86    // Until the contract defines proof-only originals, that prefix cannot travel.
87    fn require_boundary_originals(&self) -> Result<(), Reject> {
88        use objects::object::{
89            original_boundary_acceptance::{
90                OriginalBoundaryAcceptance, OriginalPublicationManifest, PublicationIntent,
91            },
92            thread_replication::{
93                self as native, ownership_claim::ThreadOwnershipClaim,
94                ownership_resolution::ThreadOwnershipResolution,
95            },
96        };
97        let (geneses, authority, landing, boundaries) = match self {
98            Self::Native(b) => (
99                b.genesis_witnesses
100                    .iter()
101                    .filter_map(|p| p.original_genesis.as_ref())
102                    .collect::<Vec<_>>(),
103                &b.authority_witnesses,
104                &b.landing_witnesses,
105                b.genesis_witnesses
106                    .iter()
107                    .filter_map(|p| p.boundary_acceptance.as_ref())
108                    .collect::<Vec<_>>(),
109            ),
110            Self::Import(b) => (
111                b.genesis_witnesses
112                    .iter()
113                    .filter_map(|p| p.original_genesis.as_ref())
114                    .collect::<Vec<_>>(),
115                &b.authority_witnesses,
116                &b.landing_witnesses,
117                b.genesis_witnesses
118                    .iter()
119                    .filter_map(|p| p.boundary_acceptance.as_ref())
120                    .collect::<Vec<_>>(),
121            ),
122        };
123        let originals = geneses
124            .into_iter()
125            .chain(
126                authority
127                    .iter()
128                    .flat_map(|p| p.original.iter().chain(&p.dependencies)),
129            )
130            .chain(landing.iter().flat_map(|p| {
131                p.execution
132                    .iter()
133                    .chain(p.source_operation.iter())
134                    .chain(&p.review_evidence)
135            }));
136        let mut ids = BTreeSet::new();
137        for original in originals {
138            let id = match original.format.as_str() {
139                native::GENESIS_FORMAT => crypto::import_authority::verify_native_genesis(original)
140                    .map_err(|_| Reject::Scope)?
141                    .1
142                    .id(),
143                native::OPERATION_FORMAT => {
144                    crypto::import_authority::verify_native_operation(original)
145                        .map_err(|_| Reject::Scope)?
146                        .1
147                        .id()
148                }
149                native::ownership_claim::FORMAT => {
150                    ThreadOwnershipClaim::decode(&original.canonical_record)
151                        .map_err(|_| Reject::Scope)?
152                        .id()
153                }
154                native::ownership_resolution::FORMAT => {
155                    ThreadOwnershipResolution::decode(&original.canonical_record)
156                        .map_err(|_| Reject::Scope)?
157                        .id()
158                }
159                _ => continue,
160            }
161            .map_err(|_| Reject::Scope)?;
162            ids.insert(id);
163        }
164        for boundary in boundaries
165            .into_iter()
166            .chain(authority.iter().flat_map(|p| &p.boundary_acceptances))
167        {
168            let acceptance = OriginalBoundaryAcceptance::decode(
169                &boundary
170                    .signed_acceptance
171                    .as_ref()
172                    .ok_or(Reject::Scope)?
173                    .canonical_record,
174            )
175            .map_err(|_| Reject::Scope)?;
176            let manifest = OriginalPublicationManifest::decode(&boundary.originals_manifest)
177                .map_err(|_| Reject::Scope)?;
178            let intent = PublicationIntent::decode(&boundary.publication_intent)
179                .map_err(|_| Reject::Scope)?;
180            if acceptance
181                .selected(&intent, &manifest)
182                .map_err(|_| Reject::Scope)?
183                .iter()
184                .any(|entry| !ids.contains(&entry.subject.id()))
185            {
186                return Err(Reject::Scope);
187            }
188        }
189        Ok(())
190    }
191
192    /// Select the requested own-origin prefix before traversing its foreign
193    /// obligations. The receiver still verifies the exact installed endpoint
194    /// and derived cutoff inside its transaction.
195    pub fn prefix(&self, reference: &wire::ForeignDependencyV1) -> Result<Self, Reject> {
196        if reference.format_version != 1
197            || reference.prefix_admission_order == 0
198            || reference.thread_genesis_digest.len() != 32
199            || reference.signed_native_digest.len() != 32
200        {
201            return Err(Reject::Scope);
202        }
203        let cutoff = reference.prefix_admission_order;
204        let mut projected = self.clone();
205        match &mut projected {
206            Self::Native(b) if reference.origin == 2 => {
207                api::native_witness::validate_public_bundle(b)?;
208                b.statements
209                    .retain(|s| s.body.as_ref().is_some_and(|s| s.admission_order <= cutoff));
210                let payloads = payloads(&b.statements)?;
211                retain(&mut b.genesis_witnesses, &payloads)?;
212                retain(&mut b.authority_witnesses, &payloads)?;
213                retain(&mut b.landing_witnesses, &payloads)?;
214                retain_foreign(
215                    &mut b.foreign_dependencies,
216                    &b.authority_witnesses,
217                    &b.landing_witnesses,
218                )?;
219                api::native_witness::validate_public_bundle(b)?;
220            }
221            Self::Import(b) if reference.origin == 1 => {
222                import::validate_public_bundle(b)?;
223                b.statements
224                    .retain(|s| s.body.as_ref().is_some_and(|s| s.admission_order <= cutoff));
225                let payloads = payloads(&b.statements)?;
226                retain(&mut b.genesis_witnesses, &payloads)?;
227                retain(&mut b.authority_witnesses, &payloads)?;
228                retain(&mut b.landing_witnesses, &payloads)?;
229                let mut selected = BTreeSet::new();
230                let mut manifests = BTreeSet::new();
231                for operation in &b.operations {
232                    for manifest in &b.manifests {
233                        let publication = import::publication_payload(operation, manifest)?;
234                        if payloads.contains(&hybrid_codec::canonical(&publication)?) {
235                            selected.insert(import::signed_operation_digest(operation)?);
236                            manifests.insert(import::manifest_digest(manifest)?);
237                        }
238                    }
239                }
240                b.operations.retain(|o| {
241                    import::signed_operation_digest(o).is_ok_and(|d| selected.contains(&d))
242                });
243                b.manifests
244                    .retain(|m| import::manifest_digest(m).is_ok_and(|d| manifests.contains(&d)));
245                b.terminal_manifest = b.manifests.iter().max_by_key(|m| m.slots.len()).cloned();
246                retain_foreign(
247                    &mut b.foreign_dependencies,
248                    &b.authority_witnesses,
249                    &b.landing_witnesses,
250                )?;
251                import::validate_public_bundle(b)?;
252            }
253            _ => return Err(Reject::Scope),
254        }
255        projected.require_boundary_originals()?;
256        Ok(projected)
257    }
258}
259fn payloads(
260    statements: &[api::heddle::api::common::SignedHostedWitnessStatementV1],
261) -> Result<BTreeSet<Vec<u8>>, Reject> {
262    statements
263        .iter()
264        .map(|s| {
265            Ok(s.body
266                .as_ref()
267                .ok_or(Reject::Canonical)?
268                .canonical_payload
269                .clone())
270        })
271        .collect()
272}
273fn retain<T: Message + hybrid_codec::Canonical>(
274    records: &mut Vec<T>,
275    payloads: &BTreeSet<Vec<u8>>,
276) -> Result<(), Reject> {
277    let mut keep = Vec::with_capacity(records.len());
278    for record in records.drain(..) {
279        if payloads.contains(&hybrid_codec::canonical(&record)?) {
280            keep.push(record);
281        }
282    }
283    *records = keep;
284    Ok(())
285}
286fn retain_foreign(
287    references: &mut Vec<wire::ForeignDependencyV1>,
288    authority: &[wire::ImportAuthorityWitnessV1],
289    landing: &[wire::HostedLandingWitnessV1],
290) -> Result<(), Reject> {
291    let used = authority
292        .iter()
293        .flat_map(|p| &p.dependencies)
294        .chain(
295            landing
296                .iter()
297                .flat_map(|p| p.source_operation.iter().chain(&p.review_evidence)),
298        )
299        .map(import::signed_native_digest)
300        .collect::<Result<BTreeSet<_>, _>>()?;
301    references.retain(|r| used.contains(&r.signed_native_digest));
302    Ok(())
303}