Skip to main content

heddle_thread_api/hybrid/
history.rs

1//! Proof-only recovery for retained originals. Lookup is public metadata and
2//! cannot restore content access or authorize an installation.
3use std::future::Future;
4
5use api::{
6    heddle::api::common::{HostedWitnessHistoryProofV1, SignedHostedWitnessStatementV1},
7    hybrid_codec::{Reject, canonical},
8    witness_trust::{self, VerifiedWitnessSet},
9};
10use prost::Message;
11
12use crate::contract::{GetHostedWitnessHistoryProofRequest, GetHostedWitnessHistoryProofResponse};
13
14#[derive(Debug, thiserror::Error)]
15pub enum Error<E: std::error::Error> {
16    #[error("HYBRID history proof rejected: {0}")]
17    Rejected(#[from] Reject),
18    #[error("history proof lookup failed: {0}")]
19    Lookup(E),
20    #[error("history proof is unavailable")]
21    NotFound,
22}
23
24/// Implementations return uniform misses without originals or resource metadata.
25/// Requests and responses are bounded before archive I/O and protobuf decoding.
26pub trait HistoryProofLookup: Send + Sync {
27    type Error: std::error::Error + Send + Sync + 'static;
28    fn lookup(
29        &self,
30        request: &GetHostedWitnessHistoryProofRequest,
31    ) -> impl Future<Output = Result<Option<GetHostedWitnessHistoryProofResponse>, Self::Error>> + Send;
32}
33
34pub fn request(
35    signed: &SignedHostedWitnessStatementV1,
36) -> Result<GetHostedWitnessHistoryProofRequest, Reject> {
37    let statement = signed.body.as_ref().ok_or(Reject::Canonical)?;
38    let request = GetHostedWitnessHistoryProofRequest {
39        executor_id: statement.executor_id.clone(),
40        statement_leaf_digest: witness_trust::leaf_digest(
41            statement.purpose,
42            &canonical(statement)?,
43            &signed.signature,
44        )?,
45    };
46    witness_trust::validate_lookup(&request)?;
47    Ok(request)
48}
49
50/// Refresh public receiver metadata without replacing any original authority,
51/// operation or accepted witness. This does not authorize installation: the
52/// receiver still resolves the complete closure under its mutation lock.
53pub fn replace_receiver_metadata(
54    original: &mut crate::contract::ImportPublicProofBundleV1,
55    refreshed: crate::contract::ImportPublicProofBundleV1,
56) -> Result<(), Reject> {
57    api::import_authority::validate_public_bundle(&refreshed)?;
58    let mut unchanged = refreshed.clone();
59    unchanged.witness_set = original.witness_set.clone();
60    unchanged.history_proofs = original.history_proofs.clone();
61    if &unchanged != original {
62        return Err(Reject::Scope);
63    }
64    *original = refreshed;
65    Ok(())
66}
67
68/// Recover and verify the exact original's path against independently selected
69/// fresh trust. A neighboring statement's valid proof never satisfies this one.
70pub async fn retrieve<L: HistoryProofLookup>(
71    lookup: &L,
72    set: &VerifiedWitnessSet,
73    signed: &SignedHostedWitnessStatementV1,
74    now_ms: i64,
75) -> Result<HostedWitnessHistoryProofV1, Error<L::Error>> {
76    let request = request(signed)?;
77    let response = lookup
78        .lookup(&request)
79        .await
80        .map_err(Error::Lookup)?
81        .ok_or(Error::NotFound)?;
82    if response.encoded_len() > witness_trust::MAX_PROOF_BYTES {
83        return Err(Reject::Bounds.into());
84    }
85    let proof = response.proof.ok_or(Reject::Proof)?;
86    if proof.siblings.len() > witness_trust::MAX_SIBLINGS {
87        return Err(Reject::Bounds.into());
88    }
89    witness_trust::resolve_statement(set, signed, Some(&proof), false, now_ms)?;
90    Ok(proof)
91}
92
93/// Fill missing paths after retirement without fetching content or rewriting
94/// any original. Existing paths must match exact originals; substitution and
95/// unreferenced proof entries reject instead of being hidden by retrieval.
96pub async fn complete_bundle<L: HistoryProofLookup>(
97    lookup: &L,
98    set: &VerifiedWitnessSet,
99    bundle: &mut crate::contract::ImportPublicProofBundleV1,
100    now_ms: i64,
101) -> Result<(), Error<L::Error>> {
102    api::import_authority::validate_public_bundle(bundle)?;
103    let proofs = complete_proofs(
104        lookup,
105        set,
106        &bundle.statements,
107        &bundle.history_proofs,
108        now_ms,
109    )
110    .await?;
111    let mut completed = bundle.clone();
112    completed.history_proofs = proofs;
113    api::import_authority::validate_public_bundle(&completed)?;
114    *bundle = completed;
115    Ok(())
116}
117
118pub fn replace_native_receiver_metadata(
119    original: &mut crate::contract::NativePublicProofBundleV1,
120    refreshed: crate::contract::NativePublicProofBundleV1,
121) -> Result<(), Reject> {
122    api::native_witness::validate_public_bundle(&refreshed)?;
123    let mut unchanged = refreshed.clone();
124    unchanged.witness_set = original.witness_set.clone();
125    unchanged.history_proofs = original.history_proofs.clone();
126    if &unchanged != original {
127        return Err(Reject::Scope);
128    }
129    *original = refreshed;
130    Ok(())
131}
132pub async fn complete_native_bundle<L: HistoryProofLookup>(
133    lookup: &L,
134    set: &VerifiedWitnessSet,
135    bundle: &mut crate::contract::NativePublicProofBundleV1,
136    now_ms: i64,
137) -> Result<(), Error<L::Error>> {
138    api::native_witness::validate_public_bundle(bundle)?;
139    let proofs = complete_proofs(
140        lookup,
141        set,
142        &bundle.statements,
143        &bundle.history_proofs,
144        now_ms,
145    )
146    .await?;
147    let mut completed = bundle.clone();
148    completed.history_proofs = proofs;
149    api::native_witness::validate_public_bundle(&completed)?;
150    *bundle = completed;
151    Ok(())
152}
153async fn complete_proofs<L: HistoryProofLookup>(
154    lookup: &L,
155    set: &VerifiedWitnessSet,
156    statements: &[SignedHostedWitnessStatementV1],
157    original_proofs: &[HostedWitnessHistoryProofV1],
158    now_ms: i64,
159) -> Result<Vec<HostedWitnessHistoryProofV1>, Error<L::Error>> {
160    let mut proofs = original_proofs.to_vec();
161    let mut used = std::collections::BTreeSet::new();
162    for signed in statements {
163        let body = signed.body.as_ref().ok_or(Reject::Canonical)?;
164        match witness_trust::resolve_statement(set, signed, None, false, now_ms) {
165            Ok(_) => continue,
166            Err(Reject::Proof) => {}
167            Err(error) => return Err(error.into()),
168        }
169        let mut matched = None;
170        for (index, proof) in proofs.iter().enumerate().filter(|(_, proof)| {
171            proof.executor_id == body.executor_id && proof.purpose == body.purpose
172        }) {
173            if witness_trust::resolve_statement(set, signed, Some(proof), false, now_ms).is_ok()
174                && matched.replace(index).is_some()
175            {
176                return Err(Reject::Proof.into());
177            }
178        }
179        let index = if let Some(index) = matched {
180            index
181        } else {
182            let proof = retrieve(lookup, set, signed, now_ms).await?;
183            proofs.push(proof);
184            if proofs.len() > 1024 {
185                return Err(Reject::Bounds.into());
186            }
187            proofs.len() - 1
188        };
189        used.insert(index);
190    }
191    if used.len() != proofs.len() {
192        return Err(Reject::Proof.into());
193    }
194    Ok(proofs)
195}