Skip to main content

heddle_thread_api/replication/
ownership.rs

1//! Portable claims retain both original signatures and optionally matched first
2//! admission testimony. Structural verification never enrolls an issuer.
3use std::collections::BTreeMap;
4
5use crypto::{
6    thread_authority_admission::SignedAuthorityAdmission,
7    thread_ownership_claim::SignedOwnershipClaim,
8    thread_ownership_resolution::SignedOwnershipResolution,
9};
10use heddle_object_model::object::{
11    ContentHash,
12    thread_replication::{ThreadGenesis, integration::TrustedHostedExecutor},
13};
14
15use crate::{contract::ThreadGenesisRecord, transport::Error};
16
17pub struct OriginalClaim {
18    pub original: SignedOwnershipClaim,
19    pub authority_admission: Option<SignedAuthorityAdmission>,
20}
21pub struct OriginalResolution {
22    pub original: SignedOwnershipResolution,
23    pub authority_admission: Option<SignedAuthorityAdmission>,
24}
25pub fn verify_resolutions(
26    record: &ThreadGenesisRecord,
27    genesis: &ThreadGenesis,
28) -> Result<Vec<OriginalResolution>, Error> {
29    if record.ownership_resolutions.len() > 1 || record.ownership_resolution_admissions.len() > 1 {
30        return Err(Error::Protocol(
31            "ownership resolution witness exceeds bound",
32        ));
33    }
34    let claims = verify_claims(record, genesis)?;
35    let mut evidence = crate::boundary_acceptance::wrapper_evidence(record)?;
36    let admission = record
37        .ownership_resolution_admissions
38        .first()
39        .map(|wire| {
40            let mut receipt = crate::authority_admission::decode(wire)?;
41            let statement = receipt
42                .verify_signature()
43                .map_err(|_| Error::Protocol("invalid resolution admission signature"))?;
44            receipt.boundary_acceptance = evidence.matched(&statement.basis)?;
45            Ok::<_, Error>((receipt, statement))
46        })
47        .transpose()?;
48    let Some(wire) = record.ownership_resolutions.first() else {
49        if admission.is_some() {
50            return Err(Error::Protocol("unmatched resolution admission"));
51        }
52        return Ok(Vec::new());
53    };
54    let original = crate::thread_ownership::decode_resolution(wire)?;
55    let value = heddle_object_model::object::thread_replication::ownership_resolution::ThreadOwnershipResolution::decode(&original.canonical)
56        .map_err(|_| Error::Protocol("invalid ownership resolution"))?;
57    value
58        .validate_genesis(genesis)
59        .map_err(|_| Error::Protocol("resolution differs from immutable genesis"))?;
60    let claim_ids = claims
61        .iter()
62        .map(|claim| {
63            claim
64                .original
65                .verify()
66                .and_then(|value| value.id().map_err(Into::into))
67        })
68        .collect::<Result<std::collections::BTreeSet<_>, _>>()
69        .map_err(|_| Error::Protocol("invalid ownership claims"))?;
70    if claim_ids != value.conflicting_claims {
71        return Err(Error::Protocol(
72            "resolution differs from exact bundled claim set",
73        ));
74    }
75    let winner = claims
76        .iter()
77        .find(|claim| {
78            claim
79                .original
80                .verify()
81                .and_then(|value| value.id().map_err(Into::into))
82                .ok()
83                == Some(value.winning_claim)
84        })
85        .ok_or(Error::Protocol("winning claim absent"))?;
86    original
87        .verify(
88            &winner
89                .original
90                .verify()
91                .map_err(|_| Error::Protocol("invalid winning claim"))?,
92        )
93        .map_err(|_| Error::Protocol("invalid dual resolution signatures"))?;
94    let authority_admission = admission
95        .map(|(receipt, statement)| {
96            let trust = TrustedHostedExecutor {
97                spool: statement.spool,
98                spool_genesis: statement.spool_genesis,
99                executor: statement.executor,
100            };
101            receipt
102                .verify_resolution(
103                    &original,
104                    &winner
105                        .original
106                        .verify()
107                        .map_err(|_| Error::Protocol("invalid winning claim"))?,
108                    genesis,
109                    &trust,
110                )
111                .map_err(|_| Error::Protocol("resolution admission differs from original"))?;
112            Ok::<_, Error>(receipt)
113        })
114        .transpose()?;
115    evidence.finish()?;
116    Ok(vec![OriginalResolution {
117        original,
118        authority_admission,
119    }])
120}
121/// Conflicts are preserved, never selected by arrival order. The caller admits
122/// each claim independently and treats multiple accepted claims as unavailable.
123pub fn verify_claims(
124    record: &ThreadGenesisRecord,
125    genesis: &ThreadGenesis,
126) -> Result<Vec<OriginalClaim>, Error> {
127    if record.ownership_claims.len() > 2 || record.ownership_claim_admissions.len() > 2 {
128        return Err(Error::Protocol("ownership claim witness exceeds bound"));
129    }
130    let mut evidence = crate::boundary_acceptance::wrapper_evidence(record)?;
131    let mut receipts = BTreeMap::new();
132    for wire in &record.ownership_claim_admissions {
133        let mut receipt = crate::authority_admission::decode(wire)?;
134        let statement = receipt
135            .verify_signature()
136            .map_err(|_| Error::Protocol("invalid claim admission signature"))?;
137        receipt.boundary_acceptance = evidence.matched(&statement.basis)?;
138        let id = statement.subject.claim_id().ok_or(Error::Protocol(
139            "claim witness cannot carry operation admission",
140        ))?;
141        if receipts.insert(id, (receipt, statement)).is_some() {
142            return Err(Error::Protocol("duplicate claim admission"));
143        }
144    }
145    let mut ids = std::collections::BTreeSet::<ContentHash>::new();
146    let mut output = Vec::new();
147    for wire in &record.ownership_claims {
148        let crate::thread_ownership::ClaimProof::Complete(original) =
149            crate::thread_ownership::decode(wire)?
150        else {
151            return Err(Error::Protocol(
152                "transferred claim requires both original signatures",
153            ));
154        };
155        let claim = original
156            .verify()
157            .map_err(|_| Error::Protocol("invalid original claim"))?;
158        claim
159            .validate_genesis(genesis)
160            .map_err(|_| Error::Protocol("claim differs from immutable genesis"))?;
161        let id = claim
162            .id()
163            .map_err(|_| Error::Protocol("invalid claim identity"))?;
164        if !ids.insert(id) {
165            return Err(Error::Protocol("duplicate original ownership claim"));
166        }
167        let admission = receipts
168            .remove(&id)
169            .map(
170                |(receipt, statement)| -> Result<SignedAuthorityAdmission, Error> {
171                    let trust = TrustedHostedExecutor {
172                        spool: statement.spool,
173                        spool_genesis: statement.spool_genesis,
174                        executor: statement.executor,
175                    };
176                    // Only compare the immutable subject here. Independent receiver pins
177                    // remain mandatory in the storage admission path.
178                    receipt
179                        .verify_claim(&original, genesis, &trust)
180                        .map_err(|_| Error::Protocol("claim admission differs from original"))?;
181                    Ok(receipt)
182                },
183            )
184            .transpose()?;
185        output.push(OriginalClaim {
186            original,
187            authority_admission: admission,
188        });
189    }
190    if !receipts.is_empty() {
191        return Err(Error::Protocol("unmatched ownership claim admission"));
192    }
193    evidence.finish()?;
194    Ok(output)
195}