Skip to main content

heddle_thread_api/
boundary_acceptance.rs

1//! Structural acceptance evidence matching, never executor trust or fresh authority.
2use std::collections::{BTreeMap, BTreeSet};
3
4use crypto::original_boundary_acceptance::SignedBoundaryAcceptance;
5use heddle_object_model::object::{
6    ContentHash,
7    original_boundary_acceptance::{AdmissionBasis, FORMAT, MAX_ACCEPTANCE_BYTES},
8};
9
10use crate::{contract as wire, transport::Error};
11
12pub const MAX_ACCEPTANCES: usize = 128;
13pub fn decode(record: &wire::SignedRecord) -> Result<SignedBoundaryAcceptance, Error> {
14    if record.format != FORMAT
15        || record.canonical_record.is_empty()
16        || record.canonical_record.len() > MAX_ACCEPTANCE_BYTES
17    {
18        return Err(Error::Protocol(
19            "invalid boundary acceptance format or bound",
20        ));
21    }
22    let [signature] = record.signatures.as_slice() else {
23        return Err(Error::Protocol(
24            "one boundary acceptance signature required",
25        ));
26    };
27    if signature.public_key.len() != 32 || signature.signature.len() != 64 {
28        return Err(Error::Protocol(
29            "invalid boundary acceptance signature shape",
30        ));
31    }
32    let signed = SignedBoundaryAcceptance {
33        canonical: record.canonical_record.clone(),
34        signature: signature.signature.clone(),
35    };
36    let value = signed
37        .verify_signature()
38        .map_err(|_| Error::Protocol("invalid boundary acceptance signature"))?;
39    if signature.public_key != value.accepting_publisher {
40        return Err(Error::Protocol("boundary acceptance signer differs"));
41    }
42    Ok(signed)
43}
44pub fn encode(signed: &SignedBoundaryAcceptance) -> Result<wire::SignedRecord, Error> {
45    let value = signed
46        .verify_signature()
47        .map_err(|_| Error::Protocol("invalid boundary acceptance signature"))?;
48    Ok(wire::SignedRecord {
49        format: FORMAT.into(),
50        canonical_record: signed.canonical.clone(),
51        signatures: vec![wire::RecordSignature {
52            public_key: value.accepting_publisher.to_vec(),
53            signature: signed.signature.clone(),
54        }],
55    })
56}
57#[derive(Default)]
58pub struct Evidence {
59    values: BTreeMap<ContentHash, std::sync::Arc<SignedBoundaryAcceptance>>,
60    used: BTreeSet<ContentHash>,
61}
62impl Evidence {
63    pub fn add(&mut self, records: &[wire::SignedRecord]) -> Result<(), Error> {
64        if records.len() > MAX_ACCEPTANCES {
65            return Err(Error::Protocol("boundary evidence count exceeded"));
66        }
67        let mut local = BTreeSet::new();
68        for record in records {
69            let signed = decode(record)?;
70            let id = ContentHash::compute_typed(FORMAT, &signed.canonical);
71            if !local.insert(id) {
72                return Err(Error::Protocol("duplicate boundary evidence in carrier"));
73            }
74            if let Some(prior) = self.values.get(&id) {
75                if prior.as_ref() != &signed {
76                    return Err(Error::Protocol("conflicting boundary evidence"));
77                }
78            } else {
79                if self.values.len() >= MAX_ACCEPTANCES {
80                    return Err(Error::Protocol("boundary evidence count exceeded"));
81                }
82                self.values.insert(id, std::sync::Arc::new(signed));
83            }
84        }
85        Ok(())
86    }
87    pub fn matched(
88        &mut self,
89        basis: &AdmissionBasis,
90    ) -> Result<Option<std::sync::Arc<SignedBoundaryAcceptance>>, Error> {
91        match basis {
92            AdmissionBasis::OriginalAuthority => Ok(None),
93            AdmissionBasis::BoundaryAcceptance { acceptance } => {
94                let value = self
95                    .values
96                    .get(acceptance)
97                    .ok_or(Error::Protocol("missing matched boundary acceptance"))?
98                    .clone();
99                self.used.insert(*acceptance);
100                Ok(Some(value))
101            }
102        }
103    }
104    pub fn finish(self) -> Result<(), Error> {
105        if self.values.len() != self.used.len() {
106            return Err(Error::Protocol("unreferenced boundary acceptance evidence"));
107        }
108        Ok(())
109    }
110}
111/// All evidence references in a genesis/claim carrier share one bounded matcher.
112pub fn wrapper_evidence(record: &wire::ThreadGenesisRecord) -> Result<Evidence, Error> {
113    use prost::Message;
114    if record.ownership_claims.len() > 2
115        || record.ownership_claim_admissions.len() > 2
116        || record.ownership_resolutions.len() > 1
117        || record.ownership_resolution_admissions.len() > 1
118        || record.encoded_len() > 256 * 1024
119    {
120        return Err(Error::Protocol("genesis evidence wrapper exceeds bounds"));
121    }
122    let mut evidence = Evidence::default();
123    evidence.add(&record.boundary_acceptances)?;
124    if let Some(receipt) = &record.admission {
125        let value =
126            heddle_object_model::object::thread_genesis_admission::ThreadGenesisAdmission::decode(
127                &receipt.canonical_record,
128            )
129            .map_err(|_| Error::Protocol("invalid genesis admission"))?;
130        evidence.matched(&value.basis)?;
131    }
132    for receipt in &record.ownership_claim_admissions {
133        evidence.matched(&crate::authority_admission::verify_signature(receipt)?.basis)?;
134    }
135    for receipt in &record.ownership_resolution_admissions {
136        evidence.matched(&crate::authority_admission::verify_signature(receipt)?.basis)?;
137    }
138    Ok(evidence)
139}
140/// Wire sidecar for an already matched receipt; full original/pin verification is
141/// still required at the receiver before persistence.
142pub fn authority_evidence(
143    receipt: Option<&crate::authority_admission::SignedAuthorityAdmission>,
144) -> Result<Vec<wire::SignedRecord>, Error> {
145    receipt
146        .and_then(|receipt| receipt.boundary_acceptance.as_deref())
147        .map(encode)
148        .transpose()
149        .map(|value| value.into_iter().collect())
150}
151/// Decode a genesis receipt and match its evidence, without deriving issuer trust.
152pub fn genesis_admission(
153    wrapper: &wire::ThreadGenesisRecord,
154) -> Result<Option<crypto::thread_genesis_admission::SignedGenesisAdmission>, Error> {
155    let mut evidence = wrapper_evidence(wrapper)?;
156    let Some(record) = &wrapper.admission else {
157        evidence.finish()?;
158        return Ok(None);
159    };
160    if record.format != heddle_object_model::object::thread_genesis_admission::FORMAT {
161        return Err(Error::Protocol("invalid genesis receipt format"));
162    }
163    let [signature] = record.signatures.as_slice() else {
164        return Err(Error::Protocol("one genesis receipt signature required"));
165    };
166    let mut signed = crypto::thread_genesis_admission::SignedGenesisAdmission {
167        canonical: record.canonical_record.clone(),
168        signature: signature.signature.clone(),
169        boundary_acceptance: None,
170    };
171    let value = signed
172        .verify_signature()
173        .map_err(|_| Error::Protocol("invalid genesis receipt signature"))?;
174    if signature.public_key != value.executor {
175        return Err(Error::Protocol("genesis receipt signer differs"));
176    }
177    signed.boundary_acceptance = evidence.matched(&value.basis)?;
178    evidence.finish()?;
179    Ok(Some(signed))
180}
181
182#[cfg(test)]
183#[path = "boundary_acceptance_tests.rs"]
184mod tests;