heddle_object_model/object/thread_replication/
delegated_import.rs1use api::{
10 heddle::api::v1alpha2::{
11 ImportContentV1, ImportFrontierV1, ImportIdentityV1, SignedDelegatedImportOperationV1,
12 },
13 import_authority::{self, VerifiedImportDelegation},
14};
15
16use super::{GenesisOwner, ThreadGenesis, ThreadOperation, ThreadOperationBody, invalid};
17use crate::{error::Result, object::ContentHash};
18
19#[derive(Clone, Debug, PartialEq)]
22pub struct DelegatedImport {
23 signed: SignedDelegatedImportOperationV1,
24 converted: ThreadOperation,
25}
26
27impl DelegatedImport {
28 pub fn bind(
31 signed: &SignedDelegatedImportOperationV1,
32 delegation: &VerifiedImportDelegation,
33 genesis: &ThreadGenesis,
34 original_identity: &ImportIdentityV1,
35 converted: &ThreadOperation,
36 parents: &[ThreadOperation],
37 ) -> Result<Self> {
38 import_authority::verify_operation(signed, delegation).map_err(invalid)?;
39 let body = signed
40 .body
41 .as_ref()
42 .ok_or_else(|| invalid("import operation missing"))?;
43 let identity = delegation
44 .body()
45 .identity
46 .as_ref()
47 .ok_or_else(|| invalid("import identity missing"))?;
48 let account =
49 uuid::Uuid::from_slice(&original_identity.owner_account_uuid).map_err(invalid)?;
50 let ThreadOperationBody::Capture(capture) = &converted.body else {
51 return Err(invalid(
52 "delegated import requires converted Capture content",
53 ));
54 };
55 if !matches!(capture.author, super::SourceAuthor::LocalKey)
59 || genesis.owner != GenesisOwner::Account(account)
60 || identity.spool_uuid != original_identity.spool_uuid
61 || identity.spool_genesis_digest != original_identity.spool_genesis_digest
62 || genesis.spool
63 != uuid::Uuid::from_slice(&body.spool_uuid)
64 .map_err(invalid)?
65 .to_string()
66 || genesis.id()?.as_bytes().as_slice() != body.genesis_digest
67 || converted.thread.as_bytes().as_slice() != body.target_thread_id
68 {
69 return Err(invalid(
70 "import content differs from owner/genesis/job/target binding",
71 ));
72 }
73 let expected = ImportFrontierV1 {
74 format_version: 1,
75 thread_id: converted.thread.as_bytes().to_vec(),
76 operation_ids: converted
77 .parents
78 .iter()
79 .map(|id| id.as_bytes().to_vec())
80 .collect(),
81 };
82 let resulting = ImportFrontierV1 {
83 format_version: 1,
84 thread_id: converted.thread.as_bytes().to_vec(),
85 operation_ids: vec![converted.id()?.as_bytes().to_vec()],
86 };
87 let content = ImportContentV1 {
88 format_version: 1,
89 canonical_capture: rmp_serde::to_vec_named(&capture.result)?,
90 };
91 if import_authority::frontier_digest(&expected).map_err(invalid)?
92 != body.expected_frontier_digest
93 || import_authority::frontier_digest(&resulting).map_err(invalid)?
94 != body.resulting_frontier_digest
95 || import_authority::content_digest(&content).map_err(invalid)?
96 != body.resulting_content_digest
97 {
98 return Err(invalid(
99 "import content or complete frontier commitment differs",
100 ));
101 }
102 let bound = Self {
103 signed: signed.clone(),
104 converted: converted.clone(),
105 };
106 bound.validate_parents(genesis, parents)?;
107 Ok(bound)
108 }
109 pub fn validate_parents(
111 &self,
112 genesis: &ThreadGenesis,
113 parents: &[ThreadOperation],
114 ) -> Result<()> {
115 self.converted
116 .validate_parents_inner(genesis, parents, true)
117 }
118 pub fn signed(&self) -> &SignedDelegatedImportOperationV1 {
120 &self.signed
121 }
122 pub fn converted(&self) -> &ThreadOperation {
124 &self.converted
125 }
126 pub fn id(&self) -> Result<ContentHash> {
128 self.converted.id()
129 }
130}