heddle_thread_api/
boundary_acceptance.rs1use 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}
111pub 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}
140pub 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}
151pub 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;