1use crypto::thread_operation::SignedGenesis;
4use heddle_object_model::object::{
5 ContentHash, StateId,
6 thread_replication::integration::{SPOOL_GENESIS_TRUST_FORMAT, TrustedHostedExecutor},
7};
8use objects::store::ObjectStore;
9use prost::Message;
10use repo::{Repository, thread_replication::ThreadReplica};
11
12use super::{Error, StagedSource};
13use crate::contract::EndpointKind;
14
15pub struct OwnedDeviceBinding<'a> {
18 pub attachment: &'a crate::contract::RootAttachment,
19 pub credential: &'a [u8],
20}
21
22impl StagedSource {
23 pub fn install(self, repository: &Repository, now_unix_seconds: i64) -> Result<StateId, Error> {
27 let endpoint = self
28 .ready
29 .endpoint
30 .as_ref()
31 .ok_or(Error::Invalid("endpoint absent"))?;
32 if endpoint.kind != EndpointKind::Weft as i32 {
33 return Err(Error::Invalid(
34 "hosted installation requires the selected Weft endpoint",
35 ));
36 }
37 let executor = endpoint
38 .public_key
39 .as_slice()
40 .try_into()
41 .map_err(|_| Error::Invalid("invalid hosted endpoint key"))?;
42 let spool = uuid::Uuid::parse_str(
43 &self
44 .ready
45 .thread
46 .as_ref()
47 .and_then(|t| t.spool.as_ref())
48 .ok_or(Error::Invalid("Spool absent"))?
49 .id,
50 )
51 .map_err(preparation)?;
52 let genesis = self
53 .ready
54 .owner_genesis
55 .as_ref()
56 .ok_or(Error::Invalid("owner genesis absent"))?;
57 let owner = self
58 .ready
59 .ownership
60 .as_ref()
61 .ok_or(Error::Invalid("owner history absent"))?;
62 let verified =
63 repo::verify_spool_owner_observation(genesis, owner, spool, now_unix_seconds)
64 .map_err(preparation)?;
65 let body = genesis
66 .genesis
67 .as_ref()
68 .ok_or(Error::Invalid("owner genesis body absent"))?;
69 let trust = TrustedHostedExecutor {
70 spool,
71 spool_genesis: ContentHash::compute_typed(
72 SPOOL_GENESIS_TRUST_FORMAT,
73 &body.encode_to_vec(),
74 ),
75 executor,
76 };
77 for signed in &self.operations {
78 let operation = signed.verify().map_err(preparation)?;
79 require_source_operation(&operation).map_err(preparation)?;
80 if operation
81 .hosted_execution_binding()
82 .map_err(preparation)?
83 .is_some()
84 {
85 trust.authorize(&operation).map_err(preparation)?;
86 }
87 }
88 repository
89 .verify_and_pin_owner_observation(
90 genesis,
91 owner,
92 spool,
93 &verified.wire().canonical_spool_path_segments,
94 now_unix_seconds,
95 )
96 .map_err(preparation)?;
97 self.install_replicas(repository, Some(&trust), None, "", now_unix_seconds)?;
98 Ok(self.state.id())
99 }
100 pub fn install_owned_device(
105 self,
106 repository: &Repository,
107 authority: &repo::device_authority::DeviceAuthority,
108 binding: OwnedDeviceBinding<'_>,
109 spool_path: &str,
110 now_unix_seconds: i64,
111 ) -> Result<StateId, Error> {
112 let endpoint = self
113 .ready
114 .endpoint
115 .as_ref()
116 .ok_or(Error::Invalid("endpoint absent"))?;
117 if endpoint.kind != EndpointKind::Device as i32 {
118 return Err(Error::Invalid(
119 "owned-device installation requires device endpoint",
120 ));
121 }
122 let owner = repo::verify_account_owner_observation(&authority.owner, now_unix_seconds)
123 .map_err(preparation)?;
124 let account = owner
125 .signed_root()
126 .root
127 .as_ref()
128 .ok_or(Error::Invalid("account owner root absent"))?;
129 let account_id = uuid::Uuid::from_slice(&account.account_uuid)
130 .map_err(preparation)?
131 .to_string();
132 authority
133 .verify_mint_root(&binding.attachment.root_public_key, now_unix_seconds)
134 .map_err(preparation)?;
135 authority
136 .verify_publisher(&binding.attachment.subject_public_key)
137 .map_err(preparation)?;
138 authority
139 .verify_publisher(&endpoint.public_key)
140 .map_err(preparation)?;
141 let roots = biscuit_verifier::parse_ed25519_public_keys_hex(
142 &hex::encode(&binding.attachment.root_public_key),
143 1,
144 )
145 .map_err(preparation)?;
146 let verified = crate::root_attachment::verify(
147 binding.attachment,
148 binding.credential,
149 &roots,
150 &account_id,
151 endpoint,
152 chrono::DateTime::from_timestamp(now_unix_seconds, 0)
153 .ok_or(Error::Invalid("invalid endpoint verification time"))?,
154 )?;
155 if verified
156 .credential_revocation_ids()
157 .iter()
158 .any(|id| authority.revoked_ids.contains(id))
159 {
160 return Err(Error::Invalid(
161 "endpoint binding credential is explicitly revoked",
162 ));
163 }
164 let spool = self
165 .ready
166 .thread
167 .as_ref()
168 .and_then(|thread| thread.spool.as_ref())
169 .ok_or(Error::Invalid("Spool absent"))?;
170 repository
171 .install_native_spool_id(spool.id.parse().map_err(preparation)?)
172 .map_err(preparation)?;
173 self.install_replicas(
174 repository,
175 None,
176 Some(authority),
177 spool_path,
178 now_unix_seconds,
179 )?;
180 Ok(self.state.id())
181 }
182 fn install_replicas(
183 &self,
184 repository: &Repository,
185 trust: Option<&TrustedHostedExecutor>,
186 authority: Option<&repo::device_authority::DeviceAuthority>,
187 spool_path: &str,
188 now: i64,
189 ) -> Result<(), Error> {
190 let main = self
191 .ready
192 .thread_genesis
193 .as_ref()
194 .ok_or(Error::Invalid("Thread genesis absent"))?;
195 let mut replicas = std::collections::BTreeMap::new();
196 let mut claims = std::collections::BTreeMap::<ContentHash, Vec<PendingClaim>>::new();
197 let mut resolutions = std::collections::BTreeMap::<
198 ContentHash,
199 crate::replication::ownership::OriginalResolution,
200 >::new();
201 for wrapper in std::iter::once(main).chain(&self.dependencies) {
202 let original = wrapper
203 .genesis
204 .as_ref()
205 .ok_or(Error::Invalid("original signed genesis absent"))?;
206 let [signature] = original.signatures.as_slice() else {
207 return Err(Error::Invalid("one original creator signature required"));
208 };
209 let signed = SignedGenesis {
210 canonical: original.canonical_record.clone(),
211 signature: signature.signature.clone(),
212 };
213 let genesis = signed.verify().map_err(preparation)?;
214 let replica = match &genesis.owner {
215 heddle_object_model::object::thread_replication::GenesisOwner::LocalKey(_) => {
216 if !wrapper.creator_authority.is_empty() || wrapper.admission.is_some() {
217 return Err(Error::Invalid(
218 "local ownership cannot carry implicit account admission",
219 ));
220 }
221 ThreadReplica::create(repository.heddle_dir(), &signed).map_err(preparation)?
222 }
223 heddle_object_model::object::thread_replication::GenesisOwner::Account(_) => {
224 if let Some(receipt) = &wrapper.admission {
225 if receipt.format
226 != heddle_object_model::object::thread_genesis_admission::FORMAT
227 {
228 return Err(Error::Invalid("unknown genesis admission format"));
229 }
230 let [_signature] = receipt.signatures.as_slice() else {
231 return Err(Error::Invalid(
232 "one hosted genesis admission signature required",
233 ));
234 };
235 let admission = crate::boundary_acceptance::genesis_admission(wrapper)?
236 .ok_or(Error::Invalid("genesis admission absent"))?;
237 match trust {
238 Some(trust) => ThreadReplica::create_from_genesis_admission(
239 repository.heddle_dir(),
240 &signed,
241 &wrapper.creator_authority,
242 &admission,
243 trust,
244 ),
245 None => ThreadReplica::create_from_pinned_genesis_admission(
246 repository.heddle_dir(),
247 &signed,
248 &wrapper.creator_authority,
249 &admission,
250 ),
251 }
252 .map_err(preparation)?
253 } else {
254 let authority = authority.ok_or(Error::Invalid(
255 "original account genesis admission required",
256 ))?;
257 ThreadReplica::create_authorized(
258 repository.heddle_dir(),
259 &signed,
260 &wrapper.creator_authority,
261 authority,
262 spool_path,
263 "/heddle.api.v1alpha2.ThreadService/StartThread",
264 now,
265 )
266 .map_err(preparation)?
267 }
268 }
269 };
270 if let Some(trust) = trust {
271 let executor = trust.executor;
272 repository
273 .pin_thread_hosted_executor(&replica, executor)
274 .map_err(preparation)?;
275 }
276 let mut pending = Vec::new();
277 let retained = replica.ownership_claims().map_err(preparation)?;
278 for claim in crate::replication::ownership::verify_claims(wrapper, &genesis)? {
279 let value = claim.original.verify().map_err(preparation)?;
280 if let Some(receipt) = &claim.authority_admission {
281 let trust = replica
282 .authority_admission_trust(receipt)
283 .map_err(preparation)?;
284 receipt
285 .verify_claim(&claim.original, &genesis, &trust)
286 .map_err(preparation)?;
287 } else if !retained.contains(&claim.original) {
288 let authority = authority.ok_or(Error::Invalid("new claim requires current original acceptance or independently pinned admission"))?;
289 repo::thread_replication::ownership_claim::verify_claim_authority(
290 &claim.original,
291 &genesis,
292 authority,
293 spool_path,
294 now,
295 )
296 .map_err(preparation)?;
297 }
298 pending.push(PendingClaim {
299 original: claim,
300 remaining: value.source_frontier,
301 });
302 }
303 for resolution in crate::replication::ownership::verify_resolutions(wrapper, &genesis)?
304 {
305 if resolutions
306 .insert(replica.thread_id(), resolution)
307 .is_some()
308 {
309 return Err(Error::Invalid("duplicate ownership resolution"));
310 }
311 }
312 claims.insert(replica.thread_id(), pending);
313 replicas.insert(replica.thread_id(), replica);
314 }
315 for signed in &self.operations {
319 let operation = signed.verify().map_err(preparation)?;
320 let replica = replicas
321 .get(&operation.thread)
322 .ok_or(Error::Invalid("source dependency replica absent"))?;
323 if let Some(receipt) = self
324 .authority_admissions
325 .get(&operation.id().map_err(preparation)?)
326 {
327 replica
328 .require_authority_admission(signed, receipt)
329 .map_err(preparation)?;
330 }
331 }
332 self.install_source_objects(repository)?;
333 for (thread, pending) in &mut claims {
334 install_ready_claims(
335 replicas
336 .get(thread)
337 .ok_or(Error::Invalid("claim replica absent"))?,
338 pending,
339 authority,
340 spool_path,
341 now,
342 )?;
343 }
344 for (thread, resolution) in &resolutions {
345 install_ready_resolution(
346 replicas
347 .get(thread)
348 .ok_or(Error::Invalid("resolution replica absent"))?,
349 resolution,
350 authority,
351 spool_path,
352 now,
353 )?;
354 }
355 for signed in &self.operations {
356 let operation = signed.verify().map_err(preparation)?;
357 let replica = replicas
358 .get(&operation.thread)
359 .ok_or(Error::Invalid("source dependency replica absent"))?;
360 let id = operation.id().map_err(preparation)?;
361 if !self.authority_admissions.contains_key(&id) {
362 let prior = replica
363 .operation_with_authority_admission(&id)
364 .map_err(preparation)?;
365 if !prior.is_some_and(|prior| {
366 prior.original == *signed
367 && prior.status == objects::object::thread_replication::Admission::Accepted
368 }) && let Some(author) = operation.source_author().map_err(preparation)?
369 {
370 match author {
371 objects::object::thread_replication::SourceAuthor::LocalKey => replica
372 .verify_local_source_owner(&operation)
373 .map_err(preparation)?,
374 objects::object::thread_replication::SourceAuthor::Account { .. } => {
375 replica.verify_source_authority(&operation, authority.ok_or(Error::Invalid("fresh source requires original authority or retained admission"))?, spool_path, now).map_err(preparation)?;
376 }
377 }
378 }
379 }
380 let admission = if !self.is_complete() {
381 replica.receive_source_metadata(
382 signed,
383 repository.store(),
384 self.authority_admissions.get(&id),
385 require_source_operation,
386 )
387 } else if let Some(receipt) = self
388 .authority_admissions
389 .get(&operation.id().map_err(preparation)?)
390 {
391 replica.receive_with_authority_admission(
392 signed,
393 receipt,
394 repository.store(),
395 require_source_operation,
396 )
397 } else {
398 replica.receive(signed, repository.store(), require_source_operation)
399 }
400 .map_err(preparation)?;
401 if admission != objects::object::thread_replication::Admission::Accepted {
402 return Err(Error::Invalid(
403 "source proof did not settle in dependency order",
404 ));
405 }
406 if let Some(pending) = claims.get_mut(&operation.thread) {
407 for claim in pending.iter_mut() {
408 claim.remaining.remove(&id);
409 }
410 install_ready_claims(replica, pending, authority, spool_path, now)?;
411 }
412 if let Some(resolution) = resolutions.get(&operation.thread) {
413 install_ready_resolution(replica, resolution, authority, spool_path, now)?;
414 }
415 }
416 if claims.values().any(|claims| !claims.is_empty()) {
417 return Err(Error::Invalid(
418 "ownership cutoff did not settle before source completion",
419 ));
420 }
421 for (thread, resolution) in &resolutions {
422 let replica = replicas
423 .get(thread)
424 .ok_or(Error::Invalid("resolution replica absent"))?;
425 install_ready_resolution(replica, resolution, authority, spool_path, now)?;
426 if replica
427 .ownership_resolution()
428 .map_err(preparation)?
429 .is_none()
430 {
431 return Err(Error::Invalid(
432 "ownership resolution frontier did not settle",
433 ));
434 }
435 }
436 for thread in claims
437 .keys()
438 .filter(|thread| !resolutions.contains_key(*thread))
439 {
440 replicas
441 .get(thread)
442 .ok_or(Error::Invalid("claim replica absent"))?
443 .effective_owner()
444 .map_err(preparation)?;
445 }
446 let main_id = crate::replication::opening::verify_genesis(
447 main.genesis
448 .as_ref()
449 .ok_or(Error::Invalid("signed genesis absent"))?,
450 self.ready
451 .thread
452 .as_ref()
453 .ok_or(Error::Invalid("Thread absent"))?,
454 )?
455 .id()
456 .map_err(preparation)?;
457 let selected = replicas
458 .get(&main_id)
459 .ok_or(Error::Invalid("selected replica absent"))?;
460 if self.operations.is_empty() {
461 let genesis = selected.genesis().map_err(preparation)?;
462 let canonical = self.state.encode_current_msgpack().map_err(preparation)?;
463 objects::object::thread_replication::hosted_import::initial_base_state(
464 &genesis, &canonical,
465 )
466 .map_err(preparation)?;
467 } else {
468 let mut source = selected;
469 let mut proved = false;
470 let mut possession = Vec::new();
471 for _ in 0..128 {
472 if source
473 .accepted_source_revision(self.state.id())
474 .map_err(preparation)?
475 .is_some()
476 {
477 possession.push(source);
478 proved = true;
479 break;
480 }
481 let genesis = source.genesis().map_err(preparation)?;
482 if genesis.base != self.state.id() {
483 break;
484 }
485 possession.push(source);
486 let Some(parent) = genesis.parent else { break };
487 let Some(next) = replicas.get(&parent) else {
488 break;
489 };
490 if next.genesis().map_err(preparation)?.spool != genesis.spool {
491 break;
492 }
493 source = next;
494 }
495 if !proved {
496 return Err(Error::Invalid("selected source proof did not settle"));
497 }
498 if self.is_complete() {
499 for replica in possession {
500 replica
501 .record_source_possession(self.state.id())
502 .map_err(preparation)?;
503 }
504 }
505 }
506 if self.is_complete() {
507 selected
508 .record_source_possession(self.state.id())
509 .map_err(preparation)?;
510 }
511 Ok(())
512 }
513
514 pub(super) fn install_source_objects(&self, repository: &Repository) -> Result<(), Error> {
515 let pack = self.directory.path().join("source.pack");
516 let index = self.directory.path().join("source.idx");
517 if self.is_complete() {
518 return repository
519 .store()
520 .install_pack_streaming(&pack, &index)
521 .map(|_| ())
522 .map_err(preparation);
523 }
524 let visible_pack = self.directory.path().join("visible.pack");
527 let visible_index = self.directory.path().join("visible.idx");
528 let output = std::fs::OpenOptions::new()
529 .read(true)
530 .write(true)
531 .create_new(true)
532 .open(&visible_pack)?;
533 let mut builder = heddle_pack::store::pack::StreamingPackBuilder::new(
534 output,
535 visible_index.clone(),
536 Default::default(),
537 self.directory.path().join("visible-buckets"),
538 )
539 .map_err(preparation)?;
540 let reader =
541 heddle_pack::store::pack::PackReader::open(&pack, &index).map_err(preparation)?;
542 reader
543 .visit_objects(|id, kind, bytes| {
544 if kind != heddle_pack::store::pack::ObjectType::Tree
545 || !objects::object::is_redacted_tree(bytes)
546 {
547 builder.add_id(id, kind, bytes)?;
548 }
549 Ok(())
550 })
551 .map_err(preparation)?;
552 let (output, _) = builder.finalize().map_err(preparation)?;
553 drop(output);
554 for partial in &self.partial_trees {
555 let bytes =
556 objects::object::encode_redacted_projection(partial).map_err(preparation)?;
557 repository
558 .store()
559 .put_partial_tree(&partial.declared_root(), &bytes)
560 .map_err(preparation)?;
561 }
562 repository
563 .store()
564 .install_pack_streaming(&visible_pack, &visible_index)
565 .map(|_| ())
566 .map_err(preparation)
567 }
568}
569struct PendingClaim {
570 original: crate::replication::ownership::OriginalClaim,
571 remaining: std::collections::BTreeSet<ContentHash>,
572}
573fn install_ready_resolution(
574 replica: &ThreadReplica,
575 resolution: &crate::replication::ownership::OriginalResolution,
576 authority: Option<&repo::device_authority::DeviceAuthority>,
577 spool_path: &str,
578 now: i64,
579) -> Result<(), Error> {
580 if let Some(existing) = replica.ownership_resolution().map_err(preparation)? {
581 if existing == resolution.original {
582 return Ok(());
583 }
584 return Err(Error::Invalid(
585 "incoming ownership resolution conflicts with retained history",
586 ));
587 }
588 let value = heddle_object_model::object::thread_replication::ownership_resolution::ThreadOwnershipResolution::decode(&resolution.original.canonical)
589 .map_err(preparation)?;
590 if replica.ownership_claims().map_err(preparation)?.len() != value.conflicting_claims.len() {
591 return Ok(());
592 }
593 for head in &value.frontier {
594 if replica
595 .operation(head)
596 .map_err(preparation)?
597 .is_none_or(|(_, status)| {
598 status != objects::object::thread_replication::Admission::Accepted
599 })
600 {
601 return Ok(());
602 }
603 }
604 if let Some(receipt) = &resolution.authority_admission {
605 replica
606 .resolve_ownership_with_admission(&resolution.original, receipt)
607 .map_err(preparation)?;
608 } else {
609 replica
610 .resolve_ownership(
611 &resolution.original,
612 authority.ok_or(Error::Invalid(
613 "new ownership resolution requires current recipient authority",
614 ))?,
615 spool_path,
616 now,
617 )
618 .map_err(preparation)?;
619 }
620 Ok(())
621}
622fn install_ready_claims(
623 replica: &ThreadReplica,
624 pending: &mut Vec<PendingClaim>,
625 authority: Option<&repo::device_authority::DeviceAuthority>,
626 spool_path: &str,
627 now: i64,
628) -> Result<(), Error> {
629 let mut index = 0;
630 while index < pending.len() {
631 if !pending[index].remaining.is_empty() {
632 index += 1;
633 continue;
634 }
635 let claim = pending.remove(index).original;
636 if let Some(receipt) = &claim.authority_admission {
637 replica
638 .claim_ownership_with_admission(&claim.original, receipt)
639 .map_err(preparation)?;
640 } else if !replica
641 .ownership_claims()
642 .map_err(preparation)?
643 .contains(&claim.original)
644 {
645 replica
646 .claim_ownership_for_import(
647 &claim.original,
648 authority.ok_or(Error::Invalid("new claim authority absent"))?,
649 spool_path,
650 now,
651 )
652 .map_err(preparation)?;
653 }
654 }
655 Ok(())
656}
657fn preparation(error: impl std::fmt::Display) -> Error {
658 Error::Preparation(error.to_string())
659}
660
661fn require_source_operation(
664 operation: &heddle_object_model::object::thread_replication::ThreadOperation,
665) -> repo::thread_replication::Result<()> {
666 if operation.facet() != heddle_object_model::object::thread_replication::ThreadFacet::Source {
667 return Err(repo::thread_replication::Error::Invalid(
668 "source installation cannot admit non-source authority".into(),
669 ));
670 }
671 Ok(())
672}
673
674#[cfg(test)]
675mod tests {
676 use crypto::{Ed25519Signer, Signer, thread_operation::SignedOperation};
677 use heddle_object_model::object::{
678 CollaborationActor,
679 thread_replication::{
680 ThreadGenesis, ThreadOperation, ThreadOperationBody,
681 metadata::{AUTHORITY_FORMAT, Control, ThreadControl},
682 },
683 };
684
685 use super::*;
686 #[test]
687 fn source_install_gate_cannot_create_metadata_original_authority() {
688 let directory = tempfile::tempdir().expect("repository");
689 let repository = Repository::init_default(directory.path()).expect("repo");
690 let signer = Ed25519Signer::from_seed(&[56; 32]).expect("signer");
691 let spool = uuid::Uuid::from_u128(11);
692 let genesis = ThreadGenesis {
693 owner: objects::object::thread_replication::GenesisOwner::LocalKey(
694 signer.public_key().try_into().expect("key"),
695 ),
696 version: 1,
697 spool: spool.to_string(),
698 parent: None,
699 base: repository.head().expect("head").expect("base"),
700 name: "source import".into(),
701 intent: "original authority".into(),
702 creator: signer.public_key().try_into().expect("key"),
703 nonce: vec![5],
704 };
705 let replica = ThreadReplica::create(
706 repository.heddle_dir(),
707 &SignedGenesis::sign(&genesis, &signer).expect("original genesis"),
708 )
709 .expect("replica");
710 let proof = b"unverified author evidence must not establish admission".to_vec();
711 let control = ThreadControl {
712 version: 1,
713 spool,
714 actor: CollaborationActor {
715 principal_id: uuid::Uuid::from_u128(22),
716 agent_id: None,
717 },
718 authority_digest: ContentHash::compute_typed(AUTHORITY_FORMAT, &proof),
719 authority_envelope: proof,
720 client_operation_id: uuid::Uuid::now_v7(),
721 occurred_at_ms: 0,
722 control: Control::Name("unproved author".into()),
723 };
724 let operation = ThreadOperation {
725 version: 1,
726 thread: replica.thread_id(),
727 parents: Default::default(),
728 publisher: signer.public_key().try_into().expect("key"),
729 body: ThreadOperationBody::Metadata(control.encode().expect("valid canonical control")),
730 };
731 let signed = SignedOperation::sign(&operation, &signer).expect("valid original signature");
732 signed.verify().expect("signature itself is valid");
733 let failure = replica
734 .receive(&signed, repository.store(), require_source_operation)
735 .expect_err("source-only gate denies unproved Metadata");
736 assert!(failure.to_string().contains("non-source authority"));
737 assert!(
738 replica
739 .operation(&operation.id().expect("ID"))
740 .expect("stored operation")
741 .is_none(),
742 "denial precedes immutable persistence"
743 );
744 assert!(
745 !replica
746 .original_authority_admitted(&signed)
747 .expect("admission marker"),
748 "source trust cannot manufacture author admission"
749 );
750 }
751}