1use std::collections::{BTreeMap, BTreeSet};
4use std::sync::Arc;
5
6use super::apply::{
7 apply_changeset_strict_on, resolve_and_apply_changeset_with_schema_on, ValidatedChangeset,
8};
9use super::audience_package::{AudiencePackage, PackageAudience};
10use super::circle_control::StoreMembershipStateRef;
11use super::conflict::TableSchema;
12use super::membership::{MembershipChain, MembershipStatus, SerialAuthorizationState};
13use super::pull::{
14 advance_max_updated_at, cache_eager_blobs, download_blobs, local_blob_cleanup_intents,
15};
16use super::session::SyncedTable;
17use super::storage::{
18 CoordinationError, CoordinationStorage, ProtocolObjectContext, ProtocolObjectDomain,
19 StorageError, SyncStorage,
20};
21use super::store_commit::{
22 head_slot_prefix, serial_head_key, ActivatedStoreDeviceRegistrationRef, CommitFrontier,
23 DeviceJoinAttempt, DeviceJoinOutcomeBody, DeviceStreamAnchor, ObjectHash, OwnerRecoveryNode,
24 OwnerRecoveryNodeRef, ResolvedStoreDeviceState, StoreBatchCommit, StoreBatchCommitRef,
25 StoreCommitAnchor, StoreCommitCoord, StoreDeviceHead, StoreDeviceRegistration,
26 StoreDeviceRegistrationActivation, StoreDeviceRegistrationActivationRef,
27 StoreDeviceRegistrationOrigin, StoreDeviceRegistrationRef, StoreDeviceStateRef,
28 StoreHistoryCut, StoreProtocolError, StoreRootRef, StoreSerialHead, StoreSerialHeadState,
29 StoreSerialPredecessor, StreamActivationId, SERIAL_STREAM_ID,
30};
31use super::store_objects::{
32 load_commit_ref, load_device_join_attempt_ref, load_device_join_outcome_ref,
33 load_founder_registration, load_owner_recovery_node_ref, load_registration_ref,
34 load_store_ack_ref, load_store_package, load_store_protocol_root, StoreObjectError,
35};
36use crate::blob::local_cleanup::{self, LocalBlobCleanupIntent};
37use crate::changeset::RowChange;
38use crate::database::{BlobActivation, Database, DbError};
39use crate::store_dir::StoreDir;
40
41#[derive(Debug, Clone, PartialEq, Eq)]
42pub enum HeldStorePositionReason {
43 MissingCommit,
44 MissingPackage,
45 MissingDeviceRegistration {
46 device_id: String,
47 revision: u64,
48 registration_hash: ObjectHash,
49 },
50 MissingPredecessor(StoreBatchCommitRef),
51 MissingDependency {
52 device_id: String,
53 commit: StoreBatchCommitRef,
54 },
55 NewerSchema {
56 local: u32,
57 required: u32,
58 },
59 Unauthorized,
60 InvalidChangeset(String),
61 InvalidRowIdentity {
62 table: String,
63 reason: String,
64 },
65 BlobDownloadFailed,
66 ForeignKeyDependency,
67 ConstraintConflict(Vec<String>),
68 HashMismatch {
69 referenced_device_id: String,
70 referenced_commit: StoreBatchCommitRef,
71 materialized_hash: ObjectHash,
72 },
73 InvalidSignature,
74 WrongSlot(String),
75 ObjectCollision(String),
76 ObjectUnreadable {
77 key: String,
78 detail: String,
79 },
80 InvalidObject(String),
81}
82
83#[derive(Debug, Clone, PartialEq, Eq)]
84pub enum HeldStoreCoordinate {
85 Head {
86 device_id: String,
87 seq: u64,
88 head_hash: ObjectHash,
89 },
90 Commit {
91 device_id: String,
92 commit: StoreBatchCommitRef,
93 },
94 Package {
95 device_id: String,
96 seq: u64,
97 package_hash: ObjectHash,
98 },
99 Dependency {
100 dependent_device_id: String,
101 dependent_commit: StoreBatchCommitRef,
102 required_device_id: String,
103 required_commit: StoreBatchCommitRef,
104 },
105}
106
107impl HeldStoreCoordinate {
108 pub fn device_id(&self) -> &str {
109 match self {
110 Self::Head { device_id, .. }
111 | Self::Commit { device_id, .. }
112 | Self::Package { device_id, .. } => device_id,
113 Self::Dependency {
114 dependent_device_id,
115 ..
116 } => dependent_device_id,
117 }
118 }
119
120 pub fn seq(&self) -> u64 {
121 match self {
122 Self::Head { seq, .. } | Self::Package { seq, .. } => *seq,
123 Self::Commit { commit, .. } => commit.coord.sequence(),
124 Self::Dependency {
125 dependent_commit, ..
126 } => dependent_commit.coord.sequence(),
127 }
128 }
129}
130
131#[derive(Debug, Clone, PartialEq, Eq)]
132pub struct HeldStorePosition {
133 pub coordinate: HeldStoreCoordinate,
134 pub reason: HeldStorePositionReason,
135}
136
137#[derive(Debug)]
138pub struct StorePullResult {
139 pub changesets_applied: u64,
140 pub devices_pulled: u64,
141 pub held_positions: Vec<HeldStorePosition>,
142 pub visible_heads: Vec<VerifiedStoreDeviceHead>,
143 pub serial_head: Option<StoreSerialHead>,
144 pub row_changes: Vec<RowChange>,
145 pub asset_downloads_failed: bool,
146 pub local_blob_cleanup_pending: bool,
147 pub frontier: BTreeMap<String, StoreBatchCommitRef>,
148}
149
150#[derive(Debug, Clone)]
151pub struct VerifiedStoreDeviceHead {
152 pub head: StoreDeviceHead,
153 pub author: StoreDeviceRegistration,
154}
155
156#[derive(Debug, thiserror::Error)]
157pub enum StorePullError {
158 #[error("{0}")]
159 Object(#[from] StoreObjectError),
160 #[error("database: {0}")]
161 Database(String),
162 #[error("membership: {0}")]
163 Membership(#[source] StorePullMembershipError),
164 #[error("Serial Store: {0}")]
165 Serial(String),
166 #[error("Serial coordination: {0}")]
167 Coordination(#[source] CoordinationError),
168 #[error("{0}")]
169 BlobDownloads(#[source] super::pull::BlobDownloadFailures),
170}
171
172#[derive(Debug, thiserror::Error)]
173pub enum StorePullMembershipError {
174 #[error("{0}")]
175 Object(#[source] StoreObjectError),
176 #[error("{0}")]
177 Chain(#[source] super::membership_ops::AnchoredChainError),
178 #[error("{0}")]
179 Message(String),
180}
181
182impl From<DbError> for StorePullError {
183 fn from(error: DbError) -> Self {
184 Self::Database(error.into_message())
185 }
186}
187
188#[derive(Clone)]
189struct Candidate {
190 commit_ref: StoreBatchCommitRef,
191 commit: StoreBatchCommit,
192 author: StoreDeviceRegistration,
193 package: Option<Vec<u8>>,
194 registrations: Vec<(StoreDeviceRegistration, StoreDeviceRegistrationActivation)>,
195 circle_activations: Vec<super::circle_ops::VerifiedCircleReference>,
196}
197
198fn parse_candidate_store_package(
199 candidate: &Candidate,
200 bytes: &[u8],
201) -> Result<AudiencePackage, String> {
202 let package = AudiencePackage::parse(bytes)
203 .map_err(|error| format!("invalid Store audience package: {error}"))?;
204 if !matches!(package.audience(), PackageAudience::Store)
205 || package.store_root_hash() != candidate.commit.store_root_hash
206 || package.write_id() != &candidate.commit.write_id
207 || package.commit_coord() != &candidate.commit_ref.coord
208 || package.candidate_family() != candidate.commit.candidate_family()
209 || candidate
210 .commit
211 .store_package()
212 .as_ref()
213 .is_none_or(|reference| package.schema_version() != reference.schema_version)
214 {
215 return Err("Store audience package differs from its exact commit".to_string());
216 }
217 Ok(package)
218}
219
220struct AuthorizedSerialCommit {
221 commit_ref: StoreBatchCommitRef,
222 commit: StoreBatchCommit,
223 author: StoreDeviceRegistration,
224 registrations: Vec<(StoreDeviceRegistration, StoreDeviceRegistrationActivation)>,
225 authorization_after: SerialAuthorizationState,
226}
227
228enum RegistrationLoadError {
229 Object(StoreObjectError),
230 Invalid(String),
231}
232
233enum RegistrationPredecessorAuthority<'a> {
234 MergeConcurrent(&'a MembershipChain),
235 Serial {
236 authorization: &'a SerialAuthorizationState,
237 position: super::store_commit::SerialStorePosition,
238 },
239}
240
241impl RegistrationPredecessorAuthority<'_> {
242 fn verifies_owner(
243 &self,
244 membership: &StoreMembershipStateRef,
245 owner_pubkey: &str,
246 owner_grant: &super::membership::MembershipGrantId,
247 ) -> bool {
248 match self {
249 Self::MergeConcurrent(chain) => {
250 let MembershipStatus::Resolved(resolved) = chain.status() else {
251 return false;
252 };
253 StoreMembershipStateRef::merge_concurrent(
254 chain.head_refs().to_vec(),
255 chain.resolution_refs().to_vec(),
256 membership.recovery().to_vec(),
257 resolved.state_hash,
258 )
259 .is_ok_and(|expected| membership == &expected)
260 && chain.active_owner_grant(owner_pubkey).as_ref() == Some(owner_grant)
261 }
262 Self::Serial {
263 authorization,
264 position,
265 } => {
266 StoreMembershipStateRef::serial(
267 position.clone(),
268 membership.recovery().to_vec(),
269 authorization,
270 )
271 .is_ok_and(|expected| membership == &expected)
272 && authorization
273 .membership
274 .authorizes_owner_grant_id(owner_pubkey, owner_grant)
275 }
276 }
277 }
278
279 fn verifies_active_owner(&self, owner_pubkey: &str) -> bool {
280 match self {
281 Self::MergeConcurrent(chain) => chain.is_owner_now(owner_pubkey),
282 Self::Serial { authorization, .. } => authorization.membership.is_owner(owner_pubkey),
283 }
284 }
285
286 fn verifies_provider_administrator(
287 &self,
288 grant_id: &super::provider::ProviderAdminGrantId,
289 executor: &StoreDeviceRegistrationRef,
290 expected: &super::provider::ProviderAdminGrantRecord,
291 ) -> bool {
292 let state = match self {
293 Self::MergeConcurrent(chain) => {
294 let super::membership::MembershipStatus::Resolved(resolved) = chain.status() else {
295 return false;
296 };
297 resolved.provider_admin.combined_state()
298 }
299 Self::Serial { authorization, .. } => &authorization.provider_admin,
300 };
301 state.authorizes(grant_id, executor)
302 && state
303 .records()
304 .get(grant_id)
305 .is_some_and(|record| record == expected)
306 }
307}
308
309async fn load_merge_predecessor_membership(
310 storage: &dyn SyncStorage,
311 root: &StoreRootRef,
312 state: &StoreMembershipStateRef,
313) -> Result<MembershipChain, RegistrationLoadError> {
314 let StoreMembershipStateRef::MergeConcurrent {
315 heads, resolutions, ..
316 } = state
317 else {
318 return Err(RegistrationLoadError::Invalid(
319 "Merge registration lifecycle commit carries Serial membership state".to_string(),
320 ));
321 };
322 let root_value = Box::pin(load_store_protocol_root(storage, root))
323 .await
324 .map_err(RegistrationLoadError::Object)?
325 .value;
326 Box::pin(super::membership_ops::load_anchored_chain_at_exact_heads(
327 storage,
328 root,
329 &root_value.descriptor.founder_pubkey,
330 heads,
331 resolutions,
332 ))
333 .await
334 .map_err(|error| RegistrationLoadError::Invalid(error.to_string()))
335}
336
337pub(crate) enum DeviceJoinBootstrapAuthorization {
338 MergeConcurrent {
339 state: StoreMembershipStateRef,
340 chain: MembershipChain,
341 },
342 Serial {
343 state: StoreMembershipStateRef,
344 position: super::store_commit::SerialStorePosition,
345 authorization: SerialAuthorizationState,
346 },
347}
348
349pub(crate) async fn load_device_join_authorization(
350 storage: &dyn SyncStorage,
351 root: &StoreRootRef,
352 state: &StoreMembershipStateRef,
353) -> Result<DeviceJoinBootstrapAuthorization, StorePullError> {
354 match state {
355 StoreMembershipStateRef::MergeConcurrent { .. } => {
356 let chain = load_merge_predecessor_membership(storage, root, state)
357 .await
358 .map_err(|error| match error {
359 RegistrationLoadError::Object(error) => StorePullError::Object(error),
360 RegistrationLoadError::Invalid(error) => StorePullError::Database(error),
361 })?;
362 Ok(DeviceJoinBootstrapAuthorization::MergeConcurrent {
363 state: state.clone(),
364 chain,
365 })
366 }
367 StoreMembershipStateRef::Serial {
368 position, recovery, ..
369 } => {
370 let reference = match position {
371 super::store_commit::SerialStorePosition::Genesis { .. } => None,
372 super::store_commit::SerialStorePosition::Commit(reference) => {
373 Some(reference.clone())
374 }
375 };
376 let authorization =
377 load_serial_authorization_at_position(storage, root, reference).await?;
378 let expected =
379 StoreMembershipStateRef::serial(position.clone(), recovery.clone(), &authorization)
380 .map_err(|error| StorePullError::Database(error.to_string()))?;
381 if &expected != state {
382 return Err(StorePullError::Database(
383 "Serial device join membership state differs from its exact authorization"
384 .to_string(),
385 ));
386 }
387 Ok(DeviceJoinBootstrapAuthorization::Serial {
388 state: expected,
389 position: position.clone(),
390 authorization,
391 })
392 }
393 }
394}
395
396async fn load_commit_registrations(
397 storage: &dyn SyncStorage,
398 root: &StoreRootRef,
399 commit: &StoreBatchCommit,
400 activating_author: &StoreDeviceRegistration,
401 predecessor: Option<&RegistrationPredecessorAuthority<'_>>,
402) -> Result<Vec<(StoreDeviceRegistration, StoreDeviceRegistrationActivation)>, RegistrationLoadError>
403{
404 Box::pin(validate_commit_join_attempts(
405 storage,
406 root,
407 commit,
408 activating_author,
409 predecessor,
410 ))
411 .await?;
412 Box::pin(validate_commit_join_outcomes(
413 storage,
414 root,
415 commit,
416 activating_author,
417 predecessor,
418 ))
419 .await?;
420 Box::pin(validate_commit_join_abandonments(
421 storage,
422 root,
423 commit,
424 activating_author,
425 predecessor,
426 ))
427 .await?;
428 Box::pin(validate_commit_join_cleanup_receipts(
429 storage,
430 root,
431 commit,
432 activating_author,
433 predecessor,
434 ))
435 .await?;
436 let mut registrations = Vec::with_capacity(commit.device_registrations().len());
437 for activated in commit.device_registrations() {
438 let registration = load_registration_ref(storage, root, &activated.registration)
439 .await
440 .map_err(RegistrationLoadError::Object)?
441 .value;
442 let predecessor = predecessor.ok_or_else(|| {
443 RegistrationLoadError::Invalid(
444 "registration activation has no exact predecessor membership authority".to_string(),
445 )
446 })?;
447 let authority = registration_activation(
448 storage,
449 root,
450 activated,
451 ®istration,
452 activating_author,
453 &commit.order,
454 commit.serial_recovery_activation(),
455 predecessor,
456 )
457 .await?;
458 registrations.push((registration, authority));
459 }
460 for retirement in commit.device_retirements() {
461 if retirement.target != commit.author_registration {
462 return Err(RegistrationLoadError::Invalid(
463 "self-retirement targets a different exact registration".to_string(),
464 ));
465 }
466 let context = ProtocolObjectContext::store(
467 root.store_root_hash,
468 ProtocolObjectDomain::StoreDeviceSelfRetirement,
469 );
470 let bytes = storage
471 .read_protocol_object(
472 &context,
473 &retirement.object,
474 &super::store_commit::device_self_retirement_semantic_prefix(
475 commit.candidate_family(),
476 &retirement.target.device_id,
477 retirement.retirement_hash,
478 ),
479 )
480 .await
481 .map_err(|error| RegistrationLoadError::Object(StoreObjectError::Storage(error)))?;
482 super::store_commit::StoreDeviceSelfRetirement::parse_at(
483 &bytes,
484 retirement,
485 activating_author,
486 )
487 .map_err(|error| RegistrationLoadError::Invalid(error.to_string()))?;
488 }
489 Ok(registrations)
490}
491
492async fn validate_commit_join_abandonments(
493 storage: &dyn SyncStorage,
494 root: &StoreRootRef,
495 commit: &StoreBatchCommit,
496 activating_author: &StoreDeviceRegistration,
497 predecessor: Option<&RegistrationPredecessorAuthority<'_>>,
498) -> Result<(), RegistrationLoadError> {
499 if commit.device_join_abandonments().is_empty() {
500 return Ok(());
501 }
502 let predecessor = predecessor.ok_or_else(|| {
503 RegistrationLoadError::Invalid(
504 "device join abandonment activation has no exact predecessor authority".to_string(),
505 )
506 })?;
507 if !predecessor.verifies_active_owner(&activating_author.author_pubkey) {
508 return Err(RegistrationLoadError::Invalid(
509 "device join abandonment activation author is not an active Owner".to_string(),
510 ));
511 }
512 for reference in commit.device_join_abandonments() {
513 if commit
514 .device_join_attempts()
515 .iter()
516 .any(|attempt| attempt.attempt_id == reference.attempt_id)
517 {
518 return Err(RegistrationLoadError::Invalid(
519 "device join abandonment and attempt are activated together".to_string(),
520 ));
521 }
522 let context = ProtocolObjectContext::store(
523 root.store_root_hash,
524 ProtocolObjectDomain::DeviceJoinAbandonment,
525 );
526 let bytes = storage
527 .read_protocol_object(
528 &context,
529 &reference.object,
530 &super::store_commit::device_join_abandonment_semantic_prefix(reference.attempt_id),
531 )
532 .await
533 .map_err(|error| RegistrationLoadError::Object(StoreObjectError::Storage(error)))?;
534 let abandonment: super::device_join::DeviceJoinAbandonmentObject =
535 serde_json::from_slice(&bytes)
536 .map_err(|error| RegistrationLoadError::Invalid(error.to_string()))?;
537 if abandonment.store_root_hash != root.store_root_hash
538 || abandonment.owner_registration != commit.author_registration
539 || abandonment.attempt_slot != *reference.object.slot()
540 {
541 return Err(RegistrationLoadError::Invalid(
542 "device join abandonment differs from its activating commit".to_string(),
543 ));
544 }
545 reference
546 .verify(&abandonment, activating_author)
547 .map_err(|error| RegistrationLoadError::Invalid(error.to_string()))?;
548 }
549 Ok(())
550}
551
552async fn validate_commit_join_cleanup_receipts(
553 storage: &dyn SyncStorage,
554 root: &StoreRootRef,
555 commit: &StoreBatchCommit,
556 activating_author: &StoreDeviceRegistration,
557 predecessor: Option<&RegistrationPredecessorAuthority<'_>>,
558) -> Result<(), RegistrationLoadError> {
559 if commit.device_join_cleanup_receipts().is_empty() {
560 return Ok(());
561 }
562 let predecessor = predecessor.ok_or_else(|| {
563 RegistrationLoadError::Invalid(
564 "device join cleanup activation has no exact predecessor authority".to_string(),
565 )
566 })?;
567 if !predecessor.verifies_active_owner(&activating_author.author_pubkey) {
568 return Err(RegistrationLoadError::Invalid(
569 "device join cleanup activation author is not an active Owner".to_string(),
570 ));
571 }
572 for reference in commit.device_join_cleanup_receipts() {
573 let context = ProtocolObjectContext::store(
574 root.store_root_hash,
575 ProtocolObjectDomain::DeviceJoinCleanupReceipt,
576 );
577 let bytes = storage
578 .read_protocol_object(
579 &context,
580 &reference.object,
581 &super::store_commit::device_join_cleanup_receipt_semantic_prefix(
582 reference.attempt_id,
583 ),
584 )
585 .await
586 .map_err(|error| RegistrationLoadError::Object(StoreObjectError::Storage(error)))?;
587 let receipt: super::device_join::DeviceJoinCleanupReceiptObject =
588 serde_json::from_slice(&bytes)
589 .map_err(|error| RegistrationLoadError::Invalid(error.to_string()))?;
590 if receipt.executor != commit.author_registration
591 || receipt.membership != commit.membership_state
592 || !predecessor_contains_join_outcome(
593 storage,
594 root,
595 &commit.order,
596 &receipt.cancellation,
597 )
598 .await?
599 {
600 return Err(RegistrationLoadError::Invalid(
601 "device join cleanup receipt differs from its activating predecessor".to_string(),
602 ));
603 }
604 let attempt_ref = receipt.cancellation.attempt();
605 let attempt_context = ProtocolObjectContext::store(
606 root.store_root_hash,
607 ProtocolObjectDomain::DeviceJoinAttempt,
608 );
609 let attempt_bytes = storage
610 .read_protocol_object(
611 &attempt_context,
612 &attempt_ref.object,
613 &super::store_commit::device_join_attempt_semantic_prefix(attempt_ref.attempt_id),
614 )
615 .await
616 .map_err(|error| RegistrationLoadError::Object(StoreObjectError::Storage(error)))?;
617 let unverified: DeviceJoinAttempt = serde_json::from_slice(&attempt_bytes)
618 .map_err(|error| RegistrationLoadError::Invalid(error.to_string()))?;
619 let owner = load_registration_ref(storage, root, &unverified.owner_registration)
620 .await
621 .map_err(RegistrationLoadError::Object)?
622 .value;
623 let attempt = DeviceJoinAttempt::parse_at(&attempt_bytes, attempt_ref, &owner)
624 .map_err(|error| RegistrationLoadError::Invalid(error.to_string()))?;
625 let expected_administrator = &attempt.provider_approval.request.offer.provider_admin;
626 let protocol_root = load_store_protocol_root(storage, root)
627 .await
628 .map_err(RegistrationLoadError::Object)?
629 .value;
630 if !predecessor.verifies_provider_administrator(
631 &receipt.provider_admin_grant,
632 &receipt.executor,
633 expected_administrator,
634 ) || activating_author.provider != expected_administrator.provider
635 || attempt.provider_approval.request.offer.provider != protocol_root.descriptor.provider
636 {
637 return Err(RegistrationLoadError::Invalid(
638 "device join cleanup executor is not the exact effective provider administrator"
639 .to_string(),
640 ));
641 }
642 reference
643 .verify(&receipt, activating_author)
644 .and_then(|_| receipt.verify(&attempt, activating_author))
645 .map_err(|error| RegistrationLoadError::Invalid(error.to_string()))?;
646 match &receipt.administrator_terminal {
647 super::device_join::ProviderAdminJoinTerminal::Completed(_) => {}
648 super::device_join::ProviderAdminJoinTerminal::Cancelled(closure) => {
649 let administrator =
650 load_registration_ref(storage, root, &closure.administrator_registration)
651 .await
652 .map_err(RegistrationLoadError::Object)?
653 .value;
654 closure
655 .verify(&administrator)
656 .map_err(|error| RegistrationLoadError::Invalid(error.to_string()))?;
657 }
658 super::device_join::ProviderAdminJoinTerminal::WriteRevoked(revocation) => {
659 let executor = load_registration_ref(storage, root, &revocation.executor)
660 .await
661 .map_err(RegistrationLoadError::Object)?
662 .value;
663 revocation
664 .verify(&executor)
665 .map_err(|error| RegistrationLoadError::Invalid(error.to_string()))?;
666 }
667 }
668 match &receipt.joiner_terminal {
669 super::device_join::JoinerJoinTerminal::Ready(_) => {}
670 super::device_join::JoinerJoinTerminal::Cancelled(closure) => closure
671 .verify()
672 .map_err(|error| RegistrationLoadError::Invalid(error.to_string()))?,
673 super::device_join::JoinerJoinTerminal::WriteRevoked(revocation) => {
674 let executor = load_registration_ref(storage, root, &revocation.executor)
675 .await
676 .map_err(RegistrationLoadError::Object)?
677 .value;
678 revocation
679 .verify(&executor)
680 .map_err(|error| RegistrationLoadError::Invalid(error.to_string()))?;
681 }
682 }
683 }
684 Ok(())
685}
686
687async fn validate_commit_join_outcomes(
688 storage: &dyn SyncStorage,
689 root: &StoreRootRef,
690 commit: &StoreBatchCommit,
691 activating_author: &StoreDeviceRegistration,
692 predecessor: Option<&RegistrationPredecessorAuthority<'_>>,
693) -> Result<(), RegistrationLoadError> {
694 if commit.device_join_outcomes().is_empty() {
695 return Ok(());
696 }
697 let predecessor = predecessor.ok_or_else(|| {
698 RegistrationLoadError::Invalid(
699 "device join outcome activation has no exact predecessor authority".to_string(),
700 )
701 })?;
702 if !predecessor.verifies_active_owner(&activating_author.author_pubkey) {
703 return Err(RegistrationLoadError::Invalid(
704 "device join outcome activation author is not an active Owner at its predecessor"
705 .to_string(),
706 ));
707 }
708 for outcome_ref in commit.device_join_outcomes() {
709 if !predecessor_contains_join_attempt(storage, root, &commit.order, outcome_ref.attempt())
710 .await?
711 {
712 return Err(RegistrationLoadError::Invalid(
713 "device join outcome names an attempt absent from its predecessor history"
714 .to_string(),
715 ));
716 }
717 let attempt_context = ProtocolObjectContext::store(
718 root.store_root_hash,
719 ProtocolObjectDomain::DeviceJoinAttempt,
720 );
721 let attempt_bytes = storage
722 .read_protocol_object(
723 &attempt_context,
724 &outcome_ref.attempt().object,
725 &super::store_commit::device_join_attempt_semantic_prefix(
726 outcome_ref.attempt().attempt_id,
727 ),
728 )
729 .await
730 .map_err(|error| RegistrationLoadError::Object(StoreObjectError::Storage(error)))?;
731 let unverified: DeviceJoinAttempt = serde_json::from_slice(&attempt_bytes)
732 .map_err(|error| RegistrationLoadError::Invalid(error.to_string()))?;
733 let owner = load_registration_ref(storage, root, &unverified.owner_registration)
734 .await
735 .map_err(RegistrationLoadError::Object)?
736 .value;
737 let attempt = DeviceJoinAttempt::parse_at(&attempt_bytes, outcome_ref.attempt(), &owner)
738 .map_err(|error| RegistrationLoadError::Invalid(error.to_string()))?;
739 if owner != *activating_author
740 || attempt.owner_registration != commit.author_registration
741 || outcome_ref.slot() != &attempt.outcome_slot
742 {
743 return Err(RegistrationLoadError::Invalid(
744 "device join outcome differs from its exact Owner attempt".to_string(),
745 ));
746 }
747 let outcome = load_device_join_outcome_ref(storage, root, outcome_ref, &owner)
748 .await
749 .map_err(RegistrationLoadError::Object)?
750 .value;
751 if outcome.owner_registration != attempt.owner_registration
752 || outcome.owner_grant != attempt.owner_grant
753 {
754 return Err(RegistrationLoadError::Invalid(
755 "device join outcome signer differs from its attempt".to_string(),
756 ));
757 }
758 let activation = commit.device_registrations().iter().find(|activation| {
759 matches!(
760 &activation.authority,
761 StoreDeviceRegistrationActivationRef::Join { outcome, .. }
762 if outcome == outcome_ref
763 )
764 });
765 if matches!(outcome.body, DeviceJoinOutcomeBody::Activated { .. }) != activation.is_some() {
766 return Err(RegistrationLoadError::Invalid(
767 "device join outcome and registration activation are not one closed operation"
768 .to_string(),
769 ));
770 }
771 }
772 Ok(())
773}
774
775async fn validate_commit_join_attempts(
776 storage: &dyn SyncStorage,
777 root: &StoreRootRef,
778 commit: &StoreBatchCommit,
779 activating_author: &StoreDeviceRegistration,
780 predecessor: Option<&RegistrationPredecessorAuthority<'_>>,
781) -> Result<(), RegistrationLoadError> {
782 if commit.device_join_attempts().is_empty() {
783 return Ok(());
784 }
785 let predecessor = predecessor.ok_or_else(|| {
786 RegistrationLoadError::Invalid(
787 "device join attempt activation has no exact predecessor membership authority"
788 .to_string(),
789 )
790 })?;
791 if !predecessor.verifies_active_owner(&activating_author.author_pubkey) {
792 return Err(RegistrationLoadError::Invalid(
793 "device join attempt activation author is not an active Owner at its predecessor"
794 .to_string(),
795 ));
796 }
797 let bootstrap_cut = commit
798 .order
799 .predecessor_cut()
800 .map_err(|error| RegistrationLoadError::Invalid(error.to_string()))?;
801 for reference in commit.device_join_attempts() {
802 let attempt = Box::pin(load_device_join_attempt_ref(
803 storage,
804 root,
805 reference,
806 activating_author,
807 ))
808 .await
809 .map_err(RegistrationLoadError::Object)?
810 .value;
811 if attempt.owner_registration != commit.author_registration
812 || attempt.membership != commit.membership_state
813 || attempt.bootstrap_cut != bootstrap_cut
814 || !predecessor.verifies_owner(
815 &attempt.membership,
816 &activating_author.author_pubkey,
817 &attempt.owner_grant,
818 )
819 {
820 return Err(RegistrationLoadError::Invalid(
821 "device join attempt differs from its exact activating predecessor authority"
822 .to_string(),
823 ));
824 }
825 }
826 Ok(())
827}
828
829async fn registration_activation(
830 storage: &dyn SyncStorage,
831 root: &StoreRootRef,
832 activated: &ActivatedStoreDeviceRegistrationRef,
833 registration: &StoreDeviceRegistration,
834 activating_author: &StoreDeviceRegistration,
835 activating_order: &super::store_commit::StoreCommitOrder,
836 serial_recovery_activation: Option<&super::store_commit::SerialRecoveryActivation>,
837 predecessor: &RegistrationPredecessorAuthority<'_>,
838) -> Result<StoreDeviceRegistrationActivation, RegistrationLoadError> {
839 if !predecessor.verifies_active_owner(&activating_author.author_pubkey) {
840 return Err(RegistrationLoadError::Invalid(
841 "registration activation commit author is not an active Owner at its predecessor"
842 .to_string(),
843 ));
844 }
845 match (®istration.origin, &activated.authority) {
846 (
847 StoreDeviceRegistrationOrigin::Join {
848 attempt_id: origin_attempt,
849 outcome_slot,
850 ..
851 },
852 StoreDeviceRegistrationActivationRef::Join {
853 attempt_id,
854 outcome,
855 },
856 ) if origin_attempt == attempt_id && outcome_slot == outcome.slot() => {
857 let attempt_context = ProtocolObjectContext::store(
858 root.store_root_hash,
859 ProtocolObjectDomain::DeviceJoinAttempt,
860 );
861 let attempt_prefix =
862 super::store_commit::device_join_attempt_semantic_prefix(*attempt_id);
863 let attempt_bytes = storage
864 .read_protocol_object(&attempt_context, &outcome.attempt().object, &attempt_prefix)
865 .await
866 .map_err(|error| RegistrationLoadError::Object(StoreObjectError::Storage(error)))?;
867 let unverified_attempt: DeviceJoinAttempt = serde_json::from_slice(&attempt_bytes)
868 .map_err(|error| {
869 RegistrationLoadError::Invalid(format!("device join attempt: {error}"))
870 })?;
871 let owner =
872 load_registration_ref(storage, root, &unverified_attempt.owner_registration)
873 .await
874 .map_err(RegistrationLoadError::Object)?
875 .value;
876 let attempt = load_device_join_attempt_ref(storage, root, outcome.attempt(), &owner)
877 .await
878 .map_err(RegistrationLoadError::Object)?
879 .value;
880 if !predecessor_contains_join_attempt(
881 storage,
882 root,
883 activating_order,
884 outcome.attempt(),
885 )
886 .await?
887 || attempt.expected_registration != *registration
888 || attempt.registration_slot != *activated.registration.object.slot()
889 || !predecessor.verifies_owner(
890 &attempt.membership,
891 &owner.author_pubkey,
892 &attempt.owner_grant,
893 )
894 {
895 return Err(RegistrationLoadError::Invalid(
896 "activated registration differs from its exact join attempt".to_string(),
897 ));
898 }
899 let outcome_value = load_device_join_outcome_ref(storage, root, outcome, &owner)
900 .await
901 .map_err(RegistrationLoadError::Object)?
902 .value;
903 if outcome_value.owner_registration != attempt.owner_registration
904 || outcome_value.owner_grant != attempt.owner_grant
905 {
906 return Err(RegistrationLoadError::Invalid(
907 "join outcome signer differs from its exact attempt authority".to_string(),
908 ));
909 }
910 let DeviceJoinOutcomeBody::Activated { readiness } = &outcome_value.body else {
911 return Err(RegistrationLoadError::Invalid(
912 "cancelled device join outcome cannot activate a registration".to_string(),
913 ));
914 };
915 let initial_ack =
916 load_store_ack_ref(storage, root, &readiness.initial_ack, registration)
917 .await
918 .map_err(RegistrationLoadError::Object)?
919 .value;
920 readiness
921 .verify(
922 outcome.attempt(),
923 &attempt,
924 registration,
925 &readiness.initial_ack,
926 &initial_ack,
927 )
928 .map_err(|error| RegistrationLoadError::Invalid(error.to_string()))?;
929 Ok(StoreDeviceRegistrationActivation::Join {
930 attempt_id: *attempt_id,
931 outcome: outcome.clone(),
932 })
933 }
934 (
935 StoreDeviceRegistrationOrigin::Recovery {
936 recovery_id: origin_recovery,
937 recovery_slot,
938 ..
939 },
940 StoreDeviceRegistrationActivationRef::Recovery { recovery_id, node },
941 ) if origin_recovery == recovery_id && recovery_slot == node.slot() => {
942 if matches!(predecessor, RegistrationPredecessorAuthority::Serial { .. })
943 && serial_recovery_activation.is_none_or(|body| &body.registration != activated)
944 {
945 return Err(RegistrationLoadError::Invalid(
946 "Serial recovery activation differs from its closed commit body".to_string(),
947 ));
948 }
949 let node_value = load_owner_recovery_node_ref(storage, root, node)
950 .await
951 .map_err(RegistrationLoadError::Object)?
952 .value;
953 let mut reached_ref = node.clone();
954 let mut reached = node_value.clone();
955 while let Some(predecessor_ref) = reached.predecessor.clone() {
956 let predecessor = load_owner_recovery_node_ref(storage, root, &predecessor_ref)
957 .await
958 .map_err(RegistrationLoadError::Object)?
959 .value;
960 if predecessor.next_slot != *reached_ref.object.slot() {
961 return Err(RegistrationLoadError::Invalid(
962 "recovery node does not occupy its exact predecessor successor slot"
963 .to_string(),
964 ));
965 }
966 if predecessor.recovery_id != node_value.recovery_id {
967 return Err(RegistrationLoadError::Invalid(
968 "recovery predecessor belongs to another recovery operation".to_string(),
969 ));
970 }
971 reached_ref = predecessor_ref;
972 reached = predecessor;
973 }
974 if node_value.recovery_id != *recovery_id
975 || node_value.readiness.registration != activated.registration
976 || node_value.next_slot == *node.object.slot()
977 || registration.author_pubkey != node_value.owner_pubkey
978 || !predecessor.verifies_owner(
979 &node_value.membership,
980 &node_value.owner_pubkey,
981 &node_value.owner_grant,
982 )
983 {
984 return Err(RegistrationLoadError::Invalid(
985 "recovery node differs from its exact registration".to_string(),
986 ));
987 }
988 let initial_ack = load_store_ack_ref(
989 storage,
990 root,
991 &node_value.readiness.initial_ack,
992 registration,
993 )
994 .await
995 .map_err(RegistrationLoadError::Object)?
996 .value;
997 if initial_ack.revision != 1
998 || initial_ack.predecessor.is_some()
999 || initial_ack.author_registration != activated.registration
1000 || initial_ack.store_cut != node_value.readiness.bootstrap_cut
1001 {
1002 return Err(RegistrationLoadError::Invalid(
1003 "recovery readiness differs from its initial acknowledgement".to_string(),
1004 ));
1005 }
1006 Ok(StoreDeviceRegistrationActivation::Recovery {
1007 recovery_id: *recovery_id,
1008 node: node.clone(),
1009 })
1010 }
1011 _ => Err(RegistrationLoadError::Invalid(format!(
1012 "Store registration {} origin differs from its activation authority",
1013 registration.device_id
1014 ))),
1015 }
1016}
1017
1018async fn predecessor_contains_join_attempt(
1019 storage: &dyn SyncStorage,
1020 root: &StoreRootRef,
1021 order: &super::store_commit::StoreCommitOrder,
1022 expected: &super::store_commit::DeviceJoinAttemptRef,
1023) -> Result<bool, RegistrationLoadError> {
1024 let mut pending = match order {
1025 super::store_commit::StoreCommitOrder::MergeConcurrent {
1026 predecessor,
1027 dependencies,
1028 ..
1029 } => predecessor
1030 .iter()
1031 .chain(dependencies.values())
1032 .cloned()
1033 .collect::<Vec<_>>(),
1034 super::store_commit::StoreCommitOrder::Serial {
1035 predecessor: super::store_commit::StoreSerialPredecessor::Commit(predecessor),
1036 ..
1037 } => vec![predecessor.clone()],
1038 super::store_commit::StoreCommitOrder::Serial {
1039 predecessor: super::store_commit::StoreSerialPredecessor::Genesis { .. },
1040 ..
1041 } => Vec::new(),
1042 };
1043 let mut visited = BTreeSet::new();
1044 while let Some(reference) = pending.pop() {
1045 if !visited.insert(reference.clone()) {
1046 continue;
1047 }
1048 let (commit, _) = load_commit_with_author(storage, root, &reference)
1049 .await
1050 .map_err(RegistrationLoadError::Object)?;
1051 if commit
1052 .device_join_attempts()
1053 .binary_search(expected)
1054 .is_ok()
1055 {
1056 return Ok(true);
1057 }
1058 match commit.order {
1059 super::store_commit::StoreCommitOrder::MergeConcurrent {
1060 predecessor,
1061 dependencies,
1062 ..
1063 } => {
1064 pending.extend(predecessor);
1065 pending.extend(dependencies.into_values());
1066 }
1067 super::store_commit::StoreCommitOrder::Serial {
1068 predecessor: super::store_commit::StoreSerialPredecessor::Commit(predecessor),
1069 ..
1070 } => pending.push(predecessor),
1071 super::store_commit::StoreCommitOrder::Serial {
1072 predecessor: super::store_commit::StoreSerialPredecessor::Genesis { .. },
1073 ..
1074 } => {}
1075 }
1076 }
1077 Ok(false)
1078}
1079
1080async fn predecessor_contains_join_outcome(
1081 storage: &dyn SyncStorage,
1082 root: &StoreRootRef,
1083 order: &super::store_commit::StoreCommitOrder,
1084 expected: &super::store_commit::DeviceJoinOutcomeRef,
1085) -> Result<bool, RegistrationLoadError> {
1086 let mut pending = match order {
1087 super::store_commit::StoreCommitOrder::MergeConcurrent {
1088 predecessor,
1089 dependencies,
1090 ..
1091 } => predecessor
1092 .iter()
1093 .chain(dependencies.values())
1094 .cloned()
1095 .collect::<Vec<_>>(),
1096 super::store_commit::StoreCommitOrder::Serial {
1097 predecessor: super::store_commit::StoreSerialPredecessor::Commit(predecessor),
1098 ..
1099 } => vec![predecessor.clone()],
1100 super::store_commit::StoreCommitOrder::Serial {
1101 predecessor: super::store_commit::StoreSerialPredecessor::Genesis { .. },
1102 ..
1103 } => Vec::new(),
1104 };
1105 let mut visited = BTreeSet::new();
1106 while let Some(reference) = pending.pop() {
1107 if !visited.insert(reference.clone()) {
1108 continue;
1109 }
1110 let (commit, _) = load_commit_with_author(storage, root, &reference)
1111 .await
1112 .map_err(RegistrationLoadError::Object)?;
1113 if commit
1114 .device_join_outcomes()
1115 .binary_search(expected)
1116 .is_ok()
1117 {
1118 return Ok(true);
1119 }
1120 match commit.order {
1121 super::store_commit::StoreCommitOrder::MergeConcurrent {
1122 predecessor,
1123 dependencies,
1124 ..
1125 } => {
1126 pending.extend(predecessor);
1127 pending.extend(dependencies.into_values());
1128 }
1129 super::store_commit::StoreCommitOrder::Serial {
1130 predecessor: super::store_commit::StoreSerialPredecessor::Commit(predecessor),
1131 ..
1132 } => pending.push(predecessor),
1133 super::store_commit::StoreCommitOrder::Serial {
1134 predecessor: super::store_commit::StoreSerialPredecessor::Genesis { .. },
1135 ..
1136 } => {}
1137 }
1138 }
1139 Ok(false)
1140}
1141
1142#[doc(hidden)]
1143pub struct SerialResolutionCommit {
1144 pub(crate) commit: StoreBatchCommit,
1145 pub(crate) commit_ref: super::store_commit::StoreBatchCommitRef,
1146 pub(crate) package: Option<Vec<u8>>,
1147 pub(crate) cleanup: Vec<LocalBlobCleanupIntent>,
1148 pub(crate) registrations: Vec<(
1149 StoreDeviceRegistration,
1150 super::store_commit::StoreDeviceRegistrationActivation,
1151 )>,
1152 pub(crate) circle_activations: Vec<super::circle_ops::VerifiedCircleReference>,
1153 pub(crate) authorization_after: SerialAuthorizationState,
1154}
1155
1156#[doc(hidden)]
1157pub struct SerialResolutionPlan {
1158 pub(crate) head: StoreSerialHead,
1159 pub(crate) head_object: super::storage::VersionedObject,
1160 pub(crate) commits: Vec<SerialResolutionCommit>,
1161}
1162
1163enum ApplyOutcome {
1164 Applied(Vec<RowChange>),
1165 Held(HeldStorePositionReason),
1166}
1167
1168#[allow(clippy::too_many_arguments)]
1171pub async fn pull_store_commits(
1172 db: &Database,
1173 tables: &[SyncedTable],
1174 storage: &dyn SyncStorage,
1175 store_root_hash: ObjectHash,
1176 store_dir: &StoreDir,
1177 membership: Option<&MembershipChain>,
1178) -> Result<StorePullResult, StorePullError> {
1179 pull_store_commits_with_identity(
1180 db,
1181 tables,
1182 storage,
1183 None,
1184 store_root_hash,
1185 store_dir,
1186 membership,
1187 None,
1188 )
1189 .await
1190}
1191
1192#[allow(clippy::too_many_arguments)]
1193#[doc(hidden)]
1194pub async fn pull_store_commits_with_coordination(
1195 db: &Database,
1196 tables: &[SyncedTable],
1197 storage: &dyn SyncStorage,
1198 serial_coordination: Option<&dyn CoordinationStorage>,
1199 store_root_hash: ObjectHash,
1200 store_dir: &StoreDir,
1201 membership: Option<&MembershipChain>,
1202) -> Result<StorePullResult, StorePullError> {
1203 pull_store_commits_with_identity(
1204 db,
1205 tables,
1206 storage,
1207 serial_coordination,
1208 store_root_hash,
1209 store_dir,
1210 membership,
1211 None,
1212 )
1213 .await
1214}
1215
1216#[allow(clippy::too_many_arguments)]
1217pub async fn pull_store_commits_with_identity(
1218 db: &Database,
1219 tables: &[SyncedTable],
1220 storage: &dyn SyncStorage,
1221 serial_coordination: Option<&dyn CoordinationStorage>,
1222 store_root_hash: ObjectHash,
1223 store_dir: &StoreDir,
1224 membership: Option<&MembershipChain>,
1225 identity: Option<&crate::keys::UserKeypair>,
1226) -> Result<StorePullResult, StorePullError> {
1227 let root = db
1228 .local_store_root_ref()
1229 .await
1230 .map_err(|error| StorePullError::Database(format!("load exact Store root: {error}")))?
1231 .ok_or_else(|| {
1232 StorePullError::Database("Store root exact reference is absent".to_string())
1233 })?;
1234 if root.store_root_hash != store_root_hash {
1235 return Err(StorePullError::Database(
1236 "requested Store root differs from the durable exact root reference".to_string(),
1237 ));
1238 }
1239 let verified_root = load_store_protocol_root(storage, &root).await?.value;
1240 if db.write_policy() == crate::WritePolicy::Serial {
1241 return pull_serial_store_commits(
1242 db,
1243 tables,
1244 storage,
1245 serial_coordination.ok_or_else(|| {
1246 StorePullError::Serial("coordination capability is absent".to_string())
1247 })?,
1248 &root,
1249 verified_root,
1250 store_dir,
1251 identity,
1252 )
1253 .await;
1254 }
1255 if verified_root.descriptor.write_policy != crate::WritePolicy::MergeConcurrent {
1256 return Err(StorePullError::Database(
1257 "durable write policy differs from the signed Store root".to_string(),
1258 ));
1259 }
1260
1261 let mut active = load_active_merge_registrations(db, storage, &root)
1262 .await
1263 .map_err(|error| {
1264 StorePullError::Database(format!("load active Merge registrations: {error}"))
1265 })?;
1266 if let Some(membership) = membership {
1267 for recovered in
1268 discover_merge_owner_recoveries(storage, &root, &verified_root, membership).await?
1269 {
1270 if active
1271 .iter()
1272 .all(|(reference, _)| reference != &recovered.0)
1273 {
1274 active.push(recovered);
1275 }
1276 }
1277 }
1278 let mut candidates = BTreeMap::new();
1279 let mut visible_heads = Vec::new();
1280 let mut held = Vec::new();
1281 for (registration_ref, registration) in active {
1282 let discovered = discover_merge_stream(storage, &root, ®istration_ref, ®istration)
1283 .await
1284 .map_err(|error| {
1285 StorePullError::Database(format!(
1286 "discover Merge stream for {}: {error}",
1287 registration.device_id
1288 ))
1289 })?;
1290 if let Some(head) = discovered.latest_head {
1291 visible_heads.push(VerifiedStoreDeviceHead {
1292 head,
1293 author: registration.clone(),
1294 });
1295 }
1296 held.extend(discovered.held);
1297 for (commit_ref, commit) in discovered.commits {
1298 if commit_ref.coord.sequence() != commit.seq() {
1299 held.push(held_commit(
1300 &commit_ref,
1301 HeldStorePositionReason::InvalidObject(
1302 "exact commit coordinate differs from signed sequence".to_string(),
1303 ),
1304 ));
1305 continue;
1306 }
1307 let stream_id = commit_stream_id(&commit_ref.coord);
1308 if let Some(materialized) = db
1309 .exact_materialized_ref(&stream_id, commit_ref.coord.sequence())
1310 .await?
1311 {
1312 if materialized == commit_ref {
1313 continue;
1314 }
1315 held.push(held_commit(
1316 &commit_ref,
1317 HeldStorePositionReason::HashMismatch {
1318 referenced_device_id: stream_id,
1319 referenced_commit: commit_ref.clone(),
1320 materialized_hash: materialized.commit_hash,
1321 },
1322 ));
1323 continue;
1324 }
1325 if commit
1326 .store_package()
1327 .as_ref()
1328 .is_some_and(|package| package.schema_version > db.schema_version())
1329 {
1330 let required = commit
1331 .store_package()
1332 .as_ref()
1333 .expect("checked Store package")
1334 .schema_version;
1335 held.push(held_commit(
1336 &commit_ref,
1337 HeldStorePositionReason::NewerSchema {
1338 local: db.schema_version(),
1339 required,
1340 },
1341 ));
1342 continue;
1343 }
1344 let predecessor_membership = if commit.device_join_attempts().is_empty()
1345 && commit.device_join_outcomes().is_empty()
1346 && commit.device_join_abandonments().is_empty()
1347 && commit.device_join_cleanup_receipts().is_empty()
1348 && commit.device_registrations().is_empty()
1349 {
1350 None
1351 } else {
1352 match load_merge_predecessor_membership(storage, &root, &commit.membership_state)
1353 .await
1354 {
1355 Ok(membership) => Some(membership),
1356 Err(RegistrationLoadError::Object(error)) => {
1357 held.push(held_commit(&commit_ref, held_object_error(error)));
1358 continue;
1359 }
1360 Err(RegistrationLoadError::Invalid(error)) => {
1361 held.push(held_commit(
1362 &commit_ref,
1363 HeldStorePositionReason::InvalidObject(error),
1364 ));
1365 continue;
1366 }
1367 }
1368 };
1369 let predecessor_authority = predecessor_membership
1370 .as_ref()
1371 .map(RegistrationPredecessorAuthority::MergeConcurrent);
1372 let registrations = match load_commit_registrations(
1373 storage,
1374 &root,
1375 &commit,
1376 ®istration,
1377 predecessor_authority.as_ref(),
1378 )
1379 .await
1380 {
1381 Ok(registrations) => registrations,
1382 Err(RegistrationLoadError::Object(error)) => {
1383 held.push(held_commit(&commit_ref, held_object_error(error)));
1384 continue;
1385 }
1386 Err(RegistrationLoadError::Invalid(error)) => {
1387 held.push(held_commit(
1388 &commit_ref,
1389 HeldStorePositionReason::InvalidObject(error),
1390 ));
1391 continue;
1392 }
1393 };
1394 if !membership_authorizes(membership, &commit, ®istration) {
1395 held.push(held_commit(
1396 &commit_ref,
1397 HeldStorePositionReason::Unauthorized,
1398 ));
1399 continue;
1400 }
1401 let package = match load_store_package(storage, &commit_ref, &commit).await {
1402 Ok(package) => package.map(|package| package.value),
1403 Err(error) => {
1404 held.push(held_package(&commit_ref, &commit, held_object_error(error)));
1405 continue;
1406 }
1407 };
1408 let circle_activations = match load_pull_circle_activations(
1409 db,
1410 storage,
1411 &root,
1412 &commit_ref,
1413 &commit,
1414 ®istration,
1415 identity,
1416 )
1417 .await
1418 {
1419 Ok(activations) => activations,
1420 Err(PullCircleActivationError::Database(error)) => return Err(error.into()),
1421 Err(PullCircleActivationError::Invalid(error)) => {
1422 held.push(held_commit(
1423 &commit_ref,
1424 HeldStorePositionReason::InvalidObject(error),
1425 ));
1426 continue;
1427 }
1428 };
1429 let key = (
1430 commit_stream_id(&commit_ref.coord),
1431 commit_ref.coord.sequence(),
1432 );
1433 candidates.insert(
1434 key,
1435 Candidate {
1436 commit_ref,
1437 commit,
1438 author: registration.clone(),
1439 package,
1440 registrations,
1441 circle_activations,
1442 },
1443 );
1444 }
1445 }
1446
1447 let schema: Arc<TableSchema> = {
1448 let tables = tables.to_vec();
1449 Arc::new(
1450 db.call(move |conn| TableSchema::from_db(conn, &tables))
1451 .await
1452 .map_err(|error| {
1453 StorePullError::Database(format!("load synced table schema: {error}"))
1454 })?,
1455 )
1456 };
1457 let coverage = db.snapshot_coverage_frontier().await.map_err(|error| {
1458 StorePullError::Database(format!("load snapshot coverage frontier: {error}"))
1459 })?;
1460 let mut frontier = db.materialized_frontier().await.map_err(|error| {
1461 StorePullError::Database(format!("load materialized frontier: {error}"))
1462 })?;
1463 let mut applied_devices = BTreeSet::new();
1464 let mut row_changes = Vec::new();
1465 let mut changesets_applied = 0_u64;
1466 let mut asset_downloads_failed = false;
1467 let mut blocked = BTreeMap::new();
1468
1469 loop {
1470 let mut progressed = false;
1471 let keys = candidates.keys().cloned().collect::<Vec<_>>();
1472 for key in keys {
1473 let candidate = candidates
1474 .get(&key)
1475 .expect("candidate key came from the same map");
1476 match readiness(
1477 db,
1478 storage,
1479 &root,
1480 &coverage,
1481 &frontier,
1482 &candidate.commit_ref,
1483 &candidate.commit,
1484 )
1485 .await
1486 .map_err(|error| {
1487 StorePullError::Database(format!(
1488 "evaluate Store commit readiness for {}/{}: {error}",
1489 key.0, key.1
1490 ))
1491 })? {
1492 Readiness::AlreadyMaterialized => {
1493 candidates.remove(&key);
1494 blocked.remove(&key);
1495 progressed = true;
1496 }
1497 Readiness::Held(held_position) => {
1498 blocked.insert(key, held_position);
1499 }
1500 Readiness::Ready => {
1501 let candidate = candidates
1502 .remove(&key)
1503 .expect("ready candidate remains present");
1504 match apply_candidate(db, storage, store_dir, schema.clone(), &candidate)
1505 .await
1506 .map_err(|error| {
1507 StorePullError::Database(format!(
1508 "materialize Store commit {}/{}: {error}",
1509 key.0, key.1
1510 ))
1511 })? {
1512 ApplyOutcome::Applied(changes) => {
1513 let stream_id = commit_stream_id(&candidate.commit_ref.coord);
1514 frontier.insert(stream_id.clone(), candidate.commit_ref.clone());
1515 applied_devices.insert(stream_id);
1516 row_changes.extend(changes);
1517 changesets_applied =
1518 changesets_applied.checked_add(1).ok_or_else(|| {
1519 StorePullError::Database(
1520 "Store apply count exceeded u64".to_string(),
1521 )
1522 })?;
1523 blocked.remove(&key);
1524 progressed = true;
1525 }
1526 ApplyOutcome::Held(reason) => {
1527 if matches!(reason, HeldStorePositionReason::BlobDownloadFailed) {
1528 asset_downloads_failed = true;
1529 }
1530 let held_position = held_commit(&candidate.commit_ref, reason);
1531 candidates.insert(key.clone(), candidate);
1532 blocked.insert(key, held_position);
1533 }
1534 }
1535 }
1536 }
1537 }
1538 if !progressed {
1539 break;
1540 }
1541 }
1542
1543 held.extend(blocked.into_values());
1544 held.sort_by(|left, right| {
1545 (left.coordinate.device_id(), left.coordinate.seq())
1546 .cmp(&(right.coordinate.device_id(), right.coordinate.seq()))
1547 });
1548 let local_blob_cleanup_pending =
1549 local_cleanup::drain(db, store_dir).await.map_err(|error| {
1550 StorePullError::Database(format!("drain local blob cleanup intents: {error}"))
1551 })?;
1552
1553 Ok(StorePullResult {
1554 changesets_applied,
1555 devices_pulled: u64::try_from(applied_devices.len()).expect("device count fits in u64"),
1556 held_positions: held,
1557 visible_heads,
1558 serial_head: None,
1559 row_changes,
1560 asset_downloads_failed,
1561 local_blob_cleanup_pending,
1562 frontier,
1563 })
1564}
1565
1566struct MergeStreamDiscovery {
1567 latest_head: Option<StoreDeviceHead>,
1568 commits: Vec<(StoreBatchCommitRef, StoreBatchCommit)>,
1569 held: Vec<HeldStorePosition>,
1570}
1571
1572async fn load_active_merge_registrations(
1573 db: &Database,
1574 storage: &dyn SyncStorage,
1575 root: &StoreRootRef,
1576) -> Result<Vec<(StoreDeviceRegistrationRef, StoreDeviceRegistration)>, StorePullError> {
1577 let durable = db.activated_store_device_registration_records().await?;
1578 let mut verified = Vec::with_capacity(durable.len());
1579 for (reference, expected) in durable {
1580 let opened = load_registration_ref(storage, root, &reference).await?;
1581 if opened.value != expected {
1582 return Err(StorePullError::Database(format!(
1583 "activated Store registration {} differs from its exact remote bytes",
1584 reference.device_id
1585 )));
1586 }
1587 if !matches!(
1588 opened.value.store_commits,
1589 StoreCommitAnchor::MergeConcurrent {
1590 announcements: DeviceStreamAnchor::StoreAnnouncements { .. }
1591 }
1592 ) {
1593 return Err(StorePullError::Database(format!(
1594 "activated Store registration {} has no Merge announcement anchor",
1595 reference.device_id
1596 )));
1597 }
1598 verified.push((reference, opened.value));
1599 }
1600 Ok(verified)
1601}
1602
1603async fn discover_merge_owner_recoveries(
1604 storage: &dyn SyncStorage,
1605 root: &StoreRootRef,
1606 protocol: &super::store_commit::StoreProtocolRoot,
1607 membership: &MembershipChain,
1608) -> Result<Vec<(StoreDeviceRegistrationRef, StoreDeviceRegistration)>, StorePullError> {
1609 if membership
1610 .active_owner_grant(&protocol.descriptor.founder_pubkey)
1611 .as_ref()
1612 != Some(&protocol.descriptor.founder_grant)
1613 {
1614 return Ok(Vec::new());
1615 }
1616 let super::store_commit::GrantStreamAnchor::OwnerRecovery { first_slot } =
1617 &protocol.descriptor.founder_recovery
1618 else {
1619 return Err(StorePullError::Database(
1620 "Store founder recovery authority has no recovery stream".into(),
1621 ));
1622 };
1623 let context = ProtocolObjectContext::store(
1624 root.store_root_hash,
1625 ProtocolObjectDomain::OwnerRecoveryNode,
1626 );
1627 let authority = RegistrationPredecessorAuthority::MergeConcurrent(membership);
1628 let mut slot = first_slot.clone();
1629 let mut predecessor: Option<OwnerRecoveryNodeRef> = None;
1630 let mut sequence = 1_u64;
1631 let mut recovered = Vec::new();
1632 loop {
1633 let prefix = super::store_commit::owner_recovery_semantic_prefix(
1634 &protocol.descriptor.founder_pubkey,
1635 protocol.descriptor.founder_grant.clone(),
1636 sequence,
1637 );
1638 let (bytes, object) = match storage.read_protocol_slot(&context, &slot, &prefix).await {
1639 Ok(opened) => opened,
1640 Err(StorageError::NotFound(_)) => break,
1641 Err(error) => return Err(StoreObjectError::Storage(error).into()),
1642 };
1643 let unverified: OwnerRecoveryNode = serde_json::from_slice(&bytes)
1644 .map_err(|error| StorePullError::Database(format!("Owner recovery node: {error}")))?;
1645 let reference = OwnerRecoveryNodeRef {
1646 owner_pubkey: unverified.owner_pubkey.clone(),
1647 owner_grant: unverified.owner_grant.clone(),
1648 sequence: unverified.sequence,
1649 node_hash: unverified.node_hash(),
1650 object,
1651 };
1652 let node = OwnerRecoveryNode::parse_at(&bytes, root, &reference)
1653 .map_err(|error| StorePullError::Database(error.to_string()))?;
1654 if reference.owner_pubkey != protocol.descriptor.founder_pubkey
1655 || reference.owner_grant != protocol.descriptor.founder_grant
1656 || reference.sequence != sequence
1657 || node.predecessor != predecessor
1658 || !authority.verifies_owner(&node.membership, &node.owner_pubkey, &node.owner_grant)
1659 {
1660 return Err(StorePullError::Database(
1661 "Owner recovery stream differs from its root-anchored authority".into(),
1662 ));
1663 }
1664 let registration = load_registration_ref(storage, root, &node.readiness.registration)
1665 .await?
1666 .value;
1667 let initial_ack =
1668 load_store_ack_ref(storage, root, &node.readiness.initial_ack, ®istration)
1669 .await?
1670 .value;
1671 let origin_matches = matches!(
1672 ®istration.origin,
1673 StoreDeviceRegistrationOrigin::Recovery {
1674 recovery_id,
1675 recovery_slot,
1676 owner_grant,
1677 } if *recovery_id == node.recovery_id
1678 && recovery_slot == reference.slot()
1679 && owner_grant == &node.owner_grant
1680 );
1681 if !origin_matches
1682 || registration.author_pubkey != node.owner_pubkey
1683 || initial_ack.revision != 1
1684 || initial_ack.predecessor.is_some()
1685 || initial_ack.store_cut != node.readiness.bootstrap_cut
1686 || initial_ack.author_registration != node.readiness.registration
1687 {
1688 return Err(StorePullError::Database(
1689 "Owner recovery readiness differs from its registration graph".into(),
1690 ));
1691 }
1692 recovered.push((node.readiness.registration.clone(), registration));
1693 slot = node.next_slot.clone();
1694 predecessor = Some(reference);
1695 sequence = sequence
1696 .checked_add(1)
1697 .ok_or_else(|| StorePullError::Database("Owner recovery sequence overflow".into()))?;
1698 }
1699 Ok(recovered)
1700}
1701
1702async fn discover_merge_stream(
1703 storage: &dyn SyncStorage,
1704 root: &StoreRootRef,
1705 registration_ref: &StoreDeviceRegistrationRef,
1706 registration: &StoreDeviceRegistration,
1707) -> Result<MergeStreamDiscovery, StorePullError> {
1708 let StoreCommitAnchor::MergeConcurrent {
1709 announcements: DeviceStreamAnchor::StoreAnnouncements { first_slot },
1710 } = ®istration.store_commits
1711 else {
1712 return Err(StorePullError::Database(format!(
1713 "Store registration {} has no Merge announcement anchor",
1714 registration.device_id
1715 )));
1716 };
1717 let stream_id = super::membership::AuthorStreamId::store_announcements(root, registration_ref);
1718 let activation = StreamActivationId::store_announcements(root, registration_ref);
1719 let context =
1720 ProtocolObjectContext::store(root.store_root_hash, ProtocolObjectDomain::StoreHead);
1721 let mut slot = first_slot.clone();
1722 let mut predecessor = None;
1723 let mut sequence = 1_u64;
1724 let mut latest_head = None;
1725 let mut commits = Vec::new();
1726 let mut held = Vec::new();
1727 let mut visited = BTreeSet::new();
1728
1729 loop {
1730 if !visited.insert(slot.clone()) {
1731 return Err(StorePullError::Database(format!(
1732 "Store announcement stream {stream_id} repeats a reserved slot"
1733 )));
1734 }
1735 let semantic_prefix = head_slot_prefix(®istration.device_id.to_string(), sequence);
1736 let (bytes, object) = match storage
1737 .read_protocol_slot(&context, &slot, &semantic_prefix)
1738 .await
1739 {
1740 Ok(opened) => opened,
1741 Err(StorageError::NotFound(_)) => break,
1742 Err(error) => return Err(StoreObjectError::Storage(error).into()),
1743 };
1744 let unverified: StoreDeviceHead = match serde_json::from_slice(&bytes) {
1745 Ok(head) => head,
1746 Err(error) => {
1747 held.push(HeldStorePosition {
1748 coordinate: HeldStoreCoordinate::Head {
1749 device_id: stream_id.to_string(),
1750 seq: sequence,
1751 head_hash: ObjectHash::digest(&bytes),
1752 },
1753 reason: HeldStorePositionReason::InvalidObject(error.to_string()),
1754 });
1755 break;
1756 }
1757 };
1758 let coord_matches = matches!(
1759 unverified.commit.coord,
1760 StoreCommitCoord::MergeConcurrent {
1761 stream_id: declared,
1762 sequence: declared_sequence,
1763 } if declared == stream_id && declared_sequence == sequence
1764 );
1765 if !coord_matches
1766 || unverified.author_registration != *registration_ref
1767 || unverified.successor.activation != activation
1768 || unverified.successor.predecessor != predecessor
1769 {
1770 held.push(HeldStorePosition {
1771 coordinate: HeldStoreCoordinate::Head {
1772 device_id: stream_id.to_string(),
1773 seq: sequence,
1774 head_hash: unverified.head_hash(),
1775 },
1776 reason: HeldStorePositionReason::WrongSlot(
1777 "Store head differs from its activated successor chain".to_string(),
1778 ),
1779 });
1780 break;
1781 }
1782 let commit = match load_commit_ref(
1783 storage,
1784 root.store_root_hash,
1785 &unverified.commit,
1786 registration,
1787 )
1788 .await
1789 {
1790 Ok(commit) => commit,
1791 Err(error) => {
1792 held.push(held_commit(&unverified.commit, held_object_error(error)));
1793 break;
1794 }
1795 };
1796 let head = match StoreDeviceHead::parse_at(
1797 &bytes,
1798 root.store_root_hash,
1799 registration,
1800 &unverified.commit,
1801 ) {
1802 Ok(head) => head,
1803 Err(error) => {
1804 held.push(HeldStorePosition {
1805 coordinate: HeldStoreCoordinate::Head {
1806 device_id: stream_id.to_string(),
1807 seq: sequence,
1808 head_hash: unverified.head_hash(),
1809 },
1810 reason: held_protocol_error(error),
1811 });
1812 break;
1813 }
1814 };
1815 let next_slot = head.successor.next_slot.clone();
1816 predecessor = Some(object);
1817 sequence = sequence.checked_add(1).ok_or_else(|| {
1818 StorePullError::Database(format!(
1819 "Store announcement stream {stream_id} sequence overflow"
1820 ))
1821 })?;
1822 commits.push((head.commit.clone(), commit.value));
1823 latest_head = Some(head);
1824 slot = next_slot;
1825 }
1826
1827 Ok(MergeStreamDiscovery {
1828 latest_head,
1829 commits,
1830 held,
1831 })
1832}
1833
1834fn held_protocol_error(error: StoreProtocolError) -> HeldStorePositionReason {
1835 match error {
1836 StoreProtocolError::InvalidSignature => HeldStorePositionReason::InvalidSignature,
1837 StoreProtocolError::RelocatedSlot { .. }
1838 | StoreProtocolError::RelocatedPackage { .. }
1839 | StoreProtocolError::StoreRootMismatch { .. }
1840 | StoreProtocolError::StoreMismatch { .. }
1841 | StoreProtocolError::FounderMismatch { .. } => {
1842 HeldStorePositionReason::WrongSlot(error.to_string())
1843 }
1844 error => HeldStorePositionReason::InvalidObject(error.to_string()),
1845 }
1846}
1847pub(crate) async fn load_commit_with_author(
1848 storage: &dyn SyncStorage,
1849 root: &StoreRootRef,
1850 reference: &StoreBatchCommitRef,
1851) -> Result<(StoreBatchCommit, StoreDeviceRegistration), StoreObjectError> {
1852 let semantic_prefix =
1853 super::store_commit::semantic_prefix_from_exact_object(&reference.object, ".json")
1854 .map_err(|source| StoreObjectError::InvalidObject {
1855 semantic_prefix: "Store candidate commit".to_string(),
1856 key: reference.object.slot().logical_key().to_string(),
1857 source: Box::new(source),
1858 })?;
1859 let context =
1860 ProtocolObjectContext::store(root.store_root_hash, ProtocolObjectDomain::StoreCommit);
1861 let bytes = storage
1862 .read_protocol_object(&context, &reference.object, &semantic_prefix)
1863 .await
1864 .map_err(StoreObjectError::Storage)?;
1865 let unverified: StoreBatchCommit =
1866 serde_json::from_slice(&bytes).map_err(|error| StoreObjectError::InvalidObject {
1867 semantic_prefix: semantic_prefix.clone(),
1868 key: reference.object.slot().logical_key().to_string(),
1869 source: Box::new(StoreProtocolError::Malformed(error.to_string())),
1870 })?;
1871 let author = load_registration_ref(storage, root, &unverified.author_registration)
1872 .await?
1873 .value;
1874 let commit =
1875 StoreBatchCommit::parse_at(&bytes, root.store_root_hash, &reference.coord, &author)
1876 .and_then(|commit| {
1877 reference.verify_commit(&commit)?;
1878 Ok(commit)
1879 })
1880 .map_err(|source| StoreObjectError::InvalidObject {
1881 semantic_prefix,
1882 key: reference.object.slot().logical_key().to_string(),
1883 source: Box::new(source),
1884 })?;
1885 Ok((commit, author))
1886}
1887
1888pub(crate) struct DeviceJoinBootstrapCommit {
1889 pub reference: StoreBatchCommitRef,
1890 pub commit: StoreBatchCommit,
1891 pub registrations: Vec<(StoreDeviceRegistration, StoreDeviceRegistrationActivation)>,
1892}
1893
1894pub(crate) struct DeviceJoinBootstrapPlan {
1895 pub founder_reference: StoreDeviceRegistrationRef,
1896 pub founder: StoreDeviceRegistration,
1897 pub founder_bytes: Vec<u8>,
1898 pub genesis: ResolvedStoreDeviceState,
1899 pub coverage: StoreHistoryCut,
1900 pub commits: Vec<DeviceJoinBootstrapCommit>,
1901}
1902
1903fn history_cut_references(cut: &StoreHistoryCut) -> Vec<StoreBatchCommitRef> {
1904 match cut {
1905 StoreHistoryCut::MergeConcurrent(frontier) => frontier.values().cloned().collect(),
1906 StoreHistoryCut::Serial(StoreSerialPredecessor::Commit(reference)) => {
1907 vec![reference.clone()]
1908 }
1909 StoreHistoryCut::Serial(StoreSerialPredecessor::Genesis { .. }) => Vec::new(),
1910 }
1911}
1912
1913fn commit_predecessor_references(commit: &StoreBatchCommit) -> Vec<StoreBatchCommitRef> {
1914 match &commit.order {
1915 super::store_commit::StoreCommitOrder::MergeConcurrent {
1916 predecessor,
1917 dependencies,
1918 ..
1919 } => predecessor
1920 .iter()
1921 .chain(dependencies.values())
1922 .cloned()
1923 .collect(),
1924 super::store_commit::StoreCommitOrder::Serial {
1925 predecessor: StoreSerialPredecessor::Commit(reference),
1926 ..
1927 } => vec![reference.clone()],
1928 super::store_commit::StoreCommitOrder::Serial {
1929 predecessor: StoreSerialPredecessor::Genesis { .. },
1930 ..
1931 } => Vec::new(),
1932 }
1933}
1934
1935pub(crate) async fn prepare_device_join_bootstrap(
1936 storage: &dyn SyncStorage,
1937 root: &StoreRootRef,
1938 coverage: &StoreHistoryCut,
1939 attempt_activation: &StoreBatchCommitRef,
1940 verified_authorization: &DeviceJoinBootstrapAuthorization,
1941) -> Result<DeviceJoinBootstrapPlan, StorePullError> {
1942 if coverage.policy() != attempt_activation.coord.policy() {
1943 return Err(StorePullError::Database(
1944 "device join bootstrap cut and attempt activation use different policies".to_string(),
1945 ));
1946 }
1947 let verified_root = Box::pin(load_store_protocol_root(storage, root))
1948 .await?
1949 .value;
1950 let founder = Box::pin(load_founder_registration(storage, root)).await?;
1951 let founder_reference =
1952 StoreDeviceRegistrationRef::from_registration(&founder.value, founder.object.clone());
1953 let genesis = ResolvedStoreDeviceState::founder(
1954 root,
1955 founder_reference.clone(),
1956 &verified_root.descriptor.founder_pubkey,
1957 verified_root.descriptor.founder_grant.clone(),
1958 &verified_root.descriptor.founder_recovery,
1959 )
1960 .map_err(|error| StorePullError::Database(error.to_string()))?;
1961
1962 let mut pending = history_cut_references(coverage);
1963 pending.push(attempt_activation.clone());
1964 let mut loaded =
1965 BTreeMap::<StoreBatchCommitRef, (StoreBatchCommit, StoreDeviceRegistration)>::new();
1966 while let Some(reference) = pending.pop() {
1967 if loaded.contains_key(&reference) {
1968 continue;
1969 }
1970 let (commit, author) = Box::pin(load_commit_with_author(storage, root, &reference)).await?;
1971 pending.extend(commit_predecessor_references(&commit));
1972 loaded.insert(reference, (commit, author));
1973 }
1974 let activation = loaded.get(attempt_activation).ok_or_else(|| {
1975 StorePullError::Database("device join attempt activation is absent from its graph".into())
1976 })?;
1977 if activation
1978 .0
1979 .order
1980 .predecessor_cut()
1981 .map_err(|error| StorePullError::Database(error.to_string()))?
1982 != *coverage
1983 {
1984 return Err(StorePullError::Database(
1985 "device join attempt activation predecessor differs from its signed bootstrap cut"
1986 .to_string(),
1987 ));
1988 }
1989
1990 let mut states = BTreeMap::<StoreBatchCommitRef, ResolvedStoreDeviceState>::new();
1991 let mut ordered = Vec::with_capacity(loaded.len());
1992 while !loaded.is_empty() {
1993 let next = loaded.iter().find_map(|(reference, (commit, _))| {
1994 commit_predecessor_references(commit)
1995 .iter()
1996 .all(|dependency| states.contains_key(dependency))
1997 .then(|| reference.clone())
1998 });
1999 let Some(reference) = next else {
2000 return Err(StorePullError::Database(
2001 "device join bootstrap history is cyclic or has an unresolved predecessor"
2002 .to_string(),
2003 ));
2004 };
2005 let (commit, author) = loaded
2006 .remove(&reference)
2007 .expect("selected bootstrap commit remains loaded");
2008 let predecessor_state = match &commit.order {
2009 super::store_commit::StoreCommitOrder::MergeConcurrent {
2010 predecessor,
2011 dependencies,
2012 ..
2013 } => {
2014 let mut refs = dependencies.values().collect::<Vec<_>>();
2015 refs.extend(predecessor.iter());
2016 if refs.is_empty() {
2017 genesis.clone()
2018 } else {
2019 ResolvedStoreDeviceState::merge(refs.into_iter().map(|dependency| {
2020 states
2021 .get(dependency)
2022 .expect("topological predecessor state exists")
2023 .clone()
2024 }))
2025 .map_err(|error| StorePullError::Database(error.to_string()))?
2026 }
2027 }
2028 super::store_commit::StoreCommitOrder::Serial {
2029 predecessor: StoreSerialPredecessor::Genesis { .. },
2030 ..
2031 } => genesis.clone(),
2032 super::store_commit::StoreCommitOrder::Serial {
2033 predecessor: StoreSerialPredecessor::Commit(predecessor),
2034 ..
2035 } => states
2036 .get(predecessor)
2037 .expect("topological Serial predecessor state exists")
2038 .clone(),
2039 };
2040 let expected_state = match &commit.order {
2041 super::store_commit::StoreCommitOrder::MergeConcurrent {
2042 predecessor,
2043 dependencies,
2044 ..
2045 } => {
2046 let mut frontier = dependencies.clone();
2047 if let Some(predecessor) = predecessor {
2048 let StoreCommitCoord::MergeConcurrent { stream_id, .. } = predecessor.coord
2049 else {
2050 return Err(StorePullError::Database(
2051 "Merge bootstrap predecessor carries a Serial coordinate".to_string(),
2052 ));
2053 };
2054 if frontier
2055 .insert(stream_id, predecessor.clone())
2056 .is_some_and(|existing| existing != *predecessor)
2057 {
2058 return Err(StorePullError::Database(
2059 "Merge bootstrap predecessor conflicts with its dependency cut"
2060 .to_string(),
2061 ));
2062 }
2063 }
2064 StoreDeviceStateRef::merge_concurrent(
2065 CommitFrontier::MergeConcurrent(frontier),
2066 &predecessor_state,
2067 )
2068 }
2069 super::store_commit::StoreCommitOrder::Serial { predecessor, .. } => {
2070 StoreDeviceStateRef::serial(predecessor.clone(), &predecessor_state)
2071 }
2072 }
2073 .map_err(|error| StorePullError::Database(error.to_string()))?;
2074 if commit.device_state != expected_state
2075 || !predecessor_state
2076 .devices
2077 .get(&commit.author_registration.device_id)
2078 .is_some_and(|record| {
2079 record.registration == commit.author_registration
2080 && matches!(
2081 record.status,
2082 super::store_commit::StoreDeviceStatus::Active
2083 )
2084 })
2085 {
2086 return Err(StorePullError::Database(
2087 "device join bootstrap commit differs from its exact predecessor device state"
2088 .to_string(),
2089 ));
2090 }
2091 let carries_lifecycle = !(commit.device_join_attempts().is_empty()
2092 && commit.device_join_outcomes().is_empty()
2093 && commit.device_join_abandonments().is_empty()
2094 && commit.device_join_cleanup_receipts().is_empty()
2095 && commit.device_registrations().is_empty());
2096 let verified_state = match verified_authorization {
2097 DeviceJoinBootstrapAuthorization::MergeConcurrent { state, .. }
2098 | DeviceJoinBootstrapAuthorization::Serial { state, .. } => state,
2099 };
2100 if carries_lifecycle && verified_state != &commit.membership_state {
2101 return Err(StorePullError::Database(
2102 "device join bootstrap history carries lifecycle authority outside the exact verified membership state"
2103 .to_string(),
2104 ));
2105 }
2106 let authority = match verified_authorization {
2107 DeviceJoinBootstrapAuthorization::MergeConcurrent { chain, .. } => {
2108 RegistrationPredecessorAuthority::MergeConcurrent(chain)
2109 }
2110 DeviceJoinBootstrapAuthorization::Serial {
2111 position,
2112 authorization,
2113 ..
2114 } => RegistrationPredecessorAuthority::Serial {
2115 authorization,
2116 position: position.clone(),
2117 },
2118 };
2119 let registrations = Box::pin(load_commit_registrations(
2120 storage,
2121 root,
2122 &commit,
2123 &author,
2124 carries_lifecycle.then_some(&authority),
2125 ))
2126 .await
2127 .map_err(|error| match error {
2128 RegistrationLoadError::Object(error) => StorePullError::Object(error),
2129 RegistrationLoadError::Invalid(error) => StorePullError::Database(error),
2130 })?;
2131 let mut state = predecessor_state;
2132 for activation in commit.device_registrations() {
2133 state = state
2134 .activate_registration(activation.registration.clone(), None)
2135 .map_err(|error| StorePullError::Database(error.to_string()))?;
2136 }
2137 for retirement in commit.device_retirements() {
2138 state = state
2139 .self_retire(retirement.clone())
2140 .map_err(|error| StorePullError::Database(error.to_string()))?;
2141 }
2142 states.insert(reference.clone(), state);
2143 ordered.push(DeviceJoinBootstrapCommit {
2144 reference,
2145 commit,
2146 registrations,
2147 });
2148 }
2149
2150 Ok(DeviceJoinBootstrapPlan {
2151 founder_reference,
2152 founder: founder.value,
2153 founder_bytes: founder.bytes,
2154 genesis,
2155 coverage: coverage.clone(),
2156 commits: ordered,
2157 })
2158}
2159
2160pub(crate) async fn materialize_device_join_activation(
2161 db: &Database,
2162 storage: &dyn SyncStorage,
2163 root: &StoreRootRef,
2164 reference: &StoreBatchCommitRef,
2165 expected_outcome: &super::store_commit::DeviceJoinOutcomeRef,
2166 authorization: &DeviceJoinBootstrapAuthorization,
2167) -> Result<(), StorePullError> {
2168 let (stream_id, sequence) = match reference.coord {
2169 StoreCommitCoord::MergeConcurrent {
2170 stream_id,
2171 sequence,
2172 } => (stream_id.to_string(), sequence),
2173 StoreCommitCoord::Serial { sequence } => (SERIAL_STREAM_ID.to_string(), sequence),
2174 };
2175 if let Some(materialized) = db.exact_materialized_ref(&stream_id, sequence).await? {
2176 if materialized == *reference {
2177 return Ok(());
2178 }
2179 return Err(StorePullError::Database(format!(
2180 "device join activation coordinate {stream_id}/{sequence} is already occupied by another commit"
2181 )));
2182 }
2183 let (commit, author) = Box::pin(load_commit_with_author(storage, root, reference)).await?;
2184 if commit.device_join_outcomes() != std::slice::from_ref(expected_outcome)
2185 || !commit.device_join_attempts().is_empty()
2186 || !commit.device_join_abandonments().is_empty()
2187 || !commit.device_join_cleanup_receipts().is_empty()
2188 || commit.device_registrations().len() != 1
2189 || !commit.provider_access_grants().is_empty()
2190 || !commit.provider_access_withdrawals().is_empty()
2191 || !commit.device_retirements().is_empty()
2192 || !commit.circle_controls().is_empty()
2193 || !commit.circle_packages().is_empty()
2194 || commit.store_package().is_some()
2195 || commit.membership_authority.is_some()
2196 || commit.control().is_some()
2197 {
2198 return Err(StorePullError::Database(
2199 "device join activation commit carries unrelated operations".to_string(),
2200 ));
2201 }
2202 let authority = match authorization {
2203 DeviceJoinBootstrapAuthorization::MergeConcurrent { chain, .. } => {
2204 RegistrationPredecessorAuthority::MergeConcurrent(chain)
2205 }
2206 DeviceJoinBootstrapAuthorization::Serial {
2207 position,
2208 authorization,
2209 ..
2210 } => RegistrationPredecessorAuthority::Serial {
2211 authorization,
2212 position: position.clone(),
2213 },
2214 };
2215 let registrations = Box::pin(load_commit_registrations(
2216 storage,
2217 root,
2218 &commit,
2219 &author,
2220 Some(&authority),
2221 ))
2222 .await
2223 .map_err(|error| match error {
2224 RegistrationLoadError::Object(error) => StorePullError::Object(error),
2225 RegistrationLoadError::Invalid(error) => StorePullError::Database(error),
2226 })?;
2227 let author_authorized = match authorization {
2228 DeviceJoinBootstrapAuthorization::MergeConcurrent { chain, .. } => {
2229 membership_authorizes(Some(chain), &commit, &author)
2230 }
2231 DeviceJoinBootstrapAuthorization::Serial { authorization, .. } => {
2232 authorization.membership.can_write(&author.author_pubkey)
2233 }
2234 };
2235 if !author_authorized {
2236 return Err(StorePullError::Database(
2237 "device join activation author is not authorized by its exact predecessor membership"
2238 .to_string(),
2239 ));
2240 }
2241 let commit_ref = reference.clone();
2242 let expected_ref = reference.clone();
2243 db.call(move |connection| {
2244 let tx = connection
2245 .unchecked_transaction()
2246 .map_err(DbError::from)?;
2247 if let Some(materialized) =
2248 Database::materialized_commit_ref_on(&tx, &stream_id, sequence)?
2249 {
2250 if materialized != expected_ref {
2251 return Err(DbError::Message(format!(
2252 "device join activation coordinate {stream_id}/{sequence} is already occupied by another commit"
2253 )));
2254 }
2255 tx.commit().map_err(DbError::from)?;
2256 return Ok(());
2257 }
2258 Database::record_activated_store_device_registrations_on(
2259 &tx,
2260 &commit,
2261 ®istrations,
2262 )?;
2263 Database::record_materialized_commit_on(&tx, &commit, &commit_ref)?;
2264 tx.commit().map_err(DbError::from)
2265 })
2266 .await?;
2267 Ok(())
2268}
2269
2270async fn load_authorized_serial_prefix(
2271 storage: &dyn SyncStorage,
2272 root: &StoreRootRef,
2273 tip: Option<StoreBatchCommitRef>,
2274) -> Result<(Vec<AuthorizedSerialCommit>, SerialAuthorizationState), StorePullError> {
2275 let root_value = load_store_protocol_root(storage, root).await?.value;
2276 if root_value.descriptor.write_policy != crate::WritePolicy::Serial {
2277 return Err(StorePullError::Serial(format!(
2278 "Store protocol root uses {:?}, not Serial",
2279 root_value.descriptor.write_policy
2280 )));
2281 }
2282
2283 let mut expected = tip;
2284 let mut reverse = Vec::new();
2285 while let Some(reference) = expected {
2286 if !matches!(reference.coord, StoreCommitCoord::Serial { .. }) {
2287 return Err(StorePullError::Serial(
2288 "global predecessor chain contains a Merge commit reference".to_string(),
2289 ));
2290 }
2291 let (commit, author) = load_commit_with_author(storage, root, &reference).await?;
2292 expected = commit.order.predecessor().cloned();
2293 reverse.push((reference, commit, author));
2294 }
2295 reverse.reverse();
2296
2297 let founder = load_founder_registration(storage, root).await?;
2298 let founder_ref =
2299 StoreDeviceRegistrationRef::from_registration(&founder.value, founder.object.clone());
2300 let mut authorization =
2301 SerialAuthorizationState::from_founder(root, &root_value, &founder_ref, &founder.value)
2302 .map_err(|error| StorePullError::Serial(error.to_string()))?;
2303 let mut active = BTreeMap::new();
2304 let mut predecessor = None;
2305 let mut authorized = Vec::with_capacity(reverse.len());
2306
2307 for (reference, commit, author) in reverse {
2308 match (&predecessor, &commit.order) {
2309 (
2310 None,
2311 super::store_commit::StoreCommitOrder::Serial {
2312 seq: 1,
2313 predecessor:
2314 StoreSerialPredecessor::Genesis {
2315 root: genesis_root,
2316 founder_registration,
2317 },
2318 },
2319 ) if genesis_root == root && founder_registration == &founder_ref => {
2320 let recovery_author =
2321 commit
2322 .serial_recovery_activation()
2323 .as_ref()
2324 .is_some_and(|activation| {
2325 activation.registration.registration == commit.author_registration
2326 });
2327 if founder.value.author_pubkey != root_value.descriptor.founder_pubkey
2328 || (!recovery_author && founder_registration != &commit.author_registration)
2329 {
2330 return Err(StorePullError::Serial(
2331 "Serial genesis registration is not the Store founder".to_string(),
2332 ));
2333 }
2334 active.insert(founder_registration.device_id, founder_registration.clone());
2335 }
2336 (
2337 Some(previous),
2338 super::store_commit::StoreCommitOrder::Serial {
2339 seq,
2340 predecessor: StoreSerialPredecessor::Commit(declared),
2341 },
2342 ) if declared == previous
2343 && *seq
2344 == previous.coord.sequence().checked_add(1).ok_or_else(|| {
2345 StorePullError::Serial("Serial predecessor sequence overflow".to_string())
2346 })? => {}
2347 _ => {
2348 return Err(StorePullError::Serial(format!(
2349 "Serial commit {} does not extend the exact accepted predecessor",
2350 reference.coord.sequence()
2351 )));
2352 }
2353 }
2354
2355 let recovery_author =
2356 commit
2357 .serial_recovery_activation()
2358 .as_ref()
2359 .is_some_and(|activation| {
2360 activation.registration.registration == commit.author_registration
2361 });
2362 if active.get(&commit.author_registration.device_id) != Some(&commit.author_registration)
2363 && !recovery_author
2364 {
2365 return Err(StorePullError::Serial(format!(
2366 "Serial commit {} author registration is not active at its predecessor",
2367 reference.coord.sequence()
2368 )));
2369 }
2370 if author.device_id != commit.author_registration.device_id {
2371 return Err(StorePullError::Serial(
2372 "Serial commit author bytes differ from its exact registration".to_string(),
2373 ));
2374 }
2375 let predecessor_authority = RegistrationPredecessorAuthority::Serial {
2376 authorization: &authorization,
2377 position: match &commit.order {
2378 super::store_commit::StoreCommitOrder::Serial { predecessor, .. } => {
2379 predecessor.clone()
2380 }
2381 super::store_commit::StoreCommitOrder::MergeConcurrent { .. } => {
2382 return Err(StorePullError::Serial(
2383 "Serial chain contains a Merge commit order".to_string(),
2384 ));
2385 }
2386 },
2387 };
2388 let registrations = load_commit_registrations(
2389 storage,
2390 root,
2391 &commit,
2392 &author,
2393 Some(&predecessor_authority),
2394 )
2395 .await
2396 .map_err(|error| match error {
2397 RegistrationLoadError::Object(error) => StorePullError::Object(error),
2398 RegistrationLoadError::Invalid(error) => StorePullError::Serial(error),
2399 })?;
2400 validate_serial_provider_admin_control(storage, root, &root_value, commit.control())
2401 .await?;
2402 authorization = authorization
2403 .authorize_and_apply(&reference, &commit, &author)
2404 .map_err(|error| {
2405 StorePullError::Serial(format!(
2406 "commit {} authorization: {error}",
2407 reference.coord.sequence()
2408 ))
2409 })?;
2410 for activated in commit.device_registrations() {
2411 active.insert(
2412 activated.registration.device_id,
2413 activated.registration.clone(),
2414 );
2415 }
2416 for retirement in commit.device_retirements() {
2417 active.remove(&retirement.target.device_id);
2418 }
2419 predecessor = Some(reference.clone());
2420 authorized.push(AuthorizedSerialCommit {
2421 commit_ref: reference,
2422 commit,
2423 author,
2424 registrations,
2425 authorization_after: authorization.clone(),
2426 });
2427 }
2428 Ok((authorized, authorization))
2429}
2430
2431pub(crate) async fn validate_serial_provider_admin_control(
2432 storage: &dyn SyncStorage,
2433 root: &StoreRootRef,
2434 root_value: &super::store_commit::StoreProtocolRoot,
2435 control: Option<&super::store_commit::StoreControl>,
2436) -> Result<(), StorePullError> {
2437 let Some(super::store_commit::StoreControl::ProviderAdmin {
2438 change:
2439 super::provider::ProviderAdminChange::Set {
2440 administrator,
2441 provider,
2442 capability,
2443 ..
2444 },
2445 }) = control
2446 else {
2447 return Ok(());
2448 };
2449 let registration =
2450 super::store_objects::load_registration_ref(storage, root, administrator).await?;
2451 if registration.value.store_root != *root || registration.value.provider != *provider {
2452 return Err(StorePullError::Serial(
2453 "provider administrator grant does not match its exact device registration".to_string(),
2454 ));
2455 }
2456 capability
2457 .verify(&root_value.descriptor.provider, provider, true)
2458 .map_err(|error| StorePullError::Serial(error.to_string()))
2459}
2460
2461async fn load_authorized_serial_chain(
2462 storage: &dyn SyncStorage,
2463 root: &StoreRootRef,
2464 head: &StoreSerialHead,
2465) -> Result<Vec<AuthorizedSerialCommit>, StorePullError> {
2466 let tip = match &head.state {
2467 StoreSerialHeadState::Genesis {
2468 root: head_root, ..
2469 } => {
2470 if head_root != root {
2471 return Err(StorePullError::Serial(
2472 "Serial genesis head names another exact Store root".to_string(),
2473 ));
2474 }
2475 None
2476 }
2477 StoreSerialHeadState::Commit { commit, .. } => Some(commit.clone()),
2478 };
2479 let (authorized, _) = load_authorized_serial_prefix(storage, root, tip.clone()).await?;
2480 match (&head.state, authorized.last()) {
2481 (StoreSerialHeadState::Genesis { .. }, None) => {}
2482 (
2483 StoreSerialHeadState::Commit {
2484 author_registration,
2485 commit,
2486 },
2487 Some(accepted),
2488 ) if commit == &accepted.commit_ref
2489 && author_registration == &accepted.commit.author_registration => {}
2490 _ => {
2491 return Err(StorePullError::Serial(
2492 "signed global head is not bound to its exact tip commit".to_string(),
2493 ));
2494 }
2495 }
2496 Ok(authorized)
2497}
2498
2499struct VerifiedSerialHead {
2500 head: StoreSerialHead,
2501 object: super::storage::VersionedObject,
2502}
2503
2504async fn read_serial_head(
2505 storage: &dyn SyncStorage,
2506 coordination: &dyn CoordinationStorage,
2507 root: &StoreRootRef,
2508) -> Result<VerifiedSerialHead, StorePullError> {
2509 let object = match coordination.read_head(serial_head_key()).await {
2510 Ok(object) => object,
2511 Err(CoordinationError::NotFound(_)) => {
2512 return Err(StorePullError::Serial("global head is absent".to_string()));
2513 }
2514 Err(error) => return Err(StorePullError::Coordination(error)),
2515 };
2516 let unverified: StoreSerialHead = serde_json::from_slice(&object.bytes)
2517 .map_err(|error| StorePullError::Serial(format!("invalid head: {error}")))?;
2518 let executor_ref = match &unverified.state {
2519 StoreSerialHeadState::Genesis {
2520 founder_registration,
2521 ..
2522 } => founder_registration,
2523 StoreSerialHeadState::Commit {
2524 author_registration,
2525 ..
2526 } => author_registration,
2527 };
2528 let executor = load_registration_ref(storage, root, executor_ref)
2529 .await?
2530 .value;
2531 let head = StoreSerialHead::parse(&object.bytes, root.store_root_hash, &executor)
2532 .map_err(|error| StorePullError::Serial(format!("invalid head: {error}")))?;
2533 Ok(VerifiedSerialHead { head, object })
2534}
2535
2536pub(crate) async fn load_serial_authorization_at_head(
2537 storage: &dyn SyncStorage,
2538 root: &StoreRootRef,
2539 head: &StoreSerialHead,
2540) -> Result<SerialAuthorizationState, StorePullError> {
2541 let authorized = load_authorized_serial_chain(storage, root, head).await?;
2542 match authorized.last() {
2543 Some(tip) => Ok(tip.authorization_after.clone()),
2544 None => load_serial_authorization_at_position(storage, root, None).await,
2545 }
2546}
2547
2548pub(crate) struct SerialCycleAuthorization {
2549 pub authorization: SerialAuthorizationState,
2550 pub head: Option<StoreBatchCommitRef>,
2551 pub visible_activations: Vec<super::wrapped_store_key::WrappedKeyActivation>,
2552}
2553
2554pub(crate) async fn load_serial_cycle_authorization(
2555 storage: &dyn SyncStorage,
2556 coordination: &dyn CoordinationStorage,
2557 root: &StoreRootRef,
2558) -> Result<SerialCycleAuthorization, StorePullError> {
2559 let head = read_serial_head(storage, coordination, root).await?.head;
2560 let authorized = load_authorized_serial_chain(storage, root, &head).await?;
2561 let authorization = match authorized.last() {
2562 Some(tip) => tip.authorization_after.clone(),
2563 None => load_serial_authorization_at_position(storage, root, None).await?,
2564 };
2565 let visible_activations = authorized
2566 .iter()
2567 .map(|commit| {
2568 super::wrapped_store_key::WrappedKeyActivation::Serial(commit.commit_ref.clone())
2569 })
2570 .collect();
2571 let head = match head.state {
2572 StoreSerialHeadState::Genesis { .. } => None,
2573 StoreSerialHeadState::Commit { commit, .. } => Some(commit),
2574 };
2575 Ok(SerialCycleAuthorization {
2576 authorization,
2577 head,
2578 visible_activations,
2579 })
2580}
2581
2582pub async fn load_serial_authorization_at_position(
2583 storage: &dyn SyncStorage,
2584 root: &StoreRootRef,
2585 reference: Option<StoreBatchCommitRef>,
2586) -> Result<SerialAuthorizationState, StorePullError> {
2587 let (_, authorization) = load_authorized_serial_prefix(storage, root, reference).await?;
2588 Ok(authorization)
2589}
2590
2591pub(crate) async fn load_serial_snapshot_authorities_at_position(
2592 storage: &dyn SyncStorage,
2593 root: &StoreRootRef,
2594 reference: Option<StoreBatchCommitRef>,
2595) -> Result<Vec<(StoreDeviceRegistrationRef, StoreDeviceRegistration)>, StorePullError> {
2596 let (authorized, authorization) =
2597 load_authorized_serial_prefix(storage, root, reference).await?;
2598 let founder = load_founder_registration(storage, root).await?;
2599 let founder_ref =
2600 StoreDeviceRegistrationRef::from_registration(&founder.value, founder.object.clone());
2601 let mut active = BTreeMap::from([(founder_ref, founder.value)]);
2602 for accepted in authorized {
2603 for (activated, (registration, _)) in accepted
2604 .commit
2605 .device_registrations()
2606 .iter()
2607 .zip(accepted.registrations)
2608 {
2609 active.insert(activated.registration.clone(), registration);
2610 }
2611 for retirement in accepted.commit.device_retirements() {
2612 active.remove(&retirement.target);
2613 }
2614 }
2615 Ok(active
2616 .into_iter()
2617 .filter(|(_, registration)| {
2618 authorization
2619 .membership
2620 .is_owner(®istration.author_pubkey)
2621 })
2622 .collect())
2623}
2624
2625async fn pull_serial_store_commits(
2626 db: &Database,
2627 tables: &[SyncedTable],
2628 storage: &dyn SyncStorage,
2629 coordination: &dyn CoordinationStorage,
2630 root: &StoreRootRef,
2631 root_value: super::store_commit::StoreProtocolRoot,
2632 store_dir: &StoreDir,
2633 identity: Option<&crate::keys::UserKeypair>,
2634) -> Result<StorePullResult, StorePullError> {
2635 if root_value.descriptor.write_policy != crate::WritePolicy::Serial {
2636 return Err(StorePullError::Serial(
2637 "signed Store root is not Serial".to_string(),
2638 ));
2639 }
2640 let local = db.materialized_frontier().await?.remove(SERIAL_STREAM_ID);
2641 let head = read_serial_head(storage, coordination, root).await?.head;
2642 let authorized_chain = load_authorized_serial_chain(storage, root, &head).await?;
2643 let tip = match &head.state {
2644 StoreSerialHeadState::Genesis { .. } => None,
2645 StoreSerialHeadState::Commit { commit, .. } => Some(commit.clone()),
2646 };
2647 let Some(tip) = tip else {
2648 if local.is_some() {
2649 return Err(StorePullError::Serial(format!(
2650 "global head is genesis but the durable Serial frontier is {local:?}"
2651 )));
2652 }
2653 return empty_serial_pull_result(db, store_dir, Some(head)).await;
2654 };
2655 if local
2656 .as_ref()
2657 .is_some_and(|local| local.coord.sequence() > tip.coord.sequence())
2658 {
2659 return Err(StorePullError::Serial(format!(
2660 "local Serial reference is ahead of the signed head: local={local:?}, head={tip:?}"
2661 )));
2662 }
2663
2664 let first_unmaterialized = match local.as_ref() {
2665 None => 0,
2666 Some(local) => authorized_chain
2667 .iter()
2668 .position(|authorized| &authorized.commit_ref == local)
2669 .map(|index| index + 1)
2670 .ok_or_else(|| {
2671 StorePullError::Serial(format!(
2672 "exact Serial predecessor chain does not reach local reference {local:?}"
2673 ))
2674 })?,
2675 };
2676 if let Some(local) = local.as_ref() {
2677 let authorization = authorized_chain
2678 .get(first_unmaterialized - 1)
2679 .expect("materialized Serial reference was found in the authorized chain")
2680 .authorization_after
2681 .clone();
2682 db.install_serial_authorization_at_position(local.clone(), authorization)
2683 .await?;
2684 }
2685
2686 let mut candidates = Vec::with_capacity(authorized_chain.len() - first_unmaterialized);
2687 for authorized in authorized_chain.into_iter().skip(first_unmaterialized) {
2688 let package =
2689 load_serial_store_package(db, storage, &authorized.commit_ref, &authorized.commit)
2690 .await?;
2691 let circle_activations = match load_pull_circle_activations(
2692 db,
2693 storage,
2694 root,
2695 &authorized.commit_ref,
2696 &authorized.commit,
2697 &authorized.author,
2698 identity,
2699 )
2700 .await
2701 {
2702 Ok(activations) => activations,
2703 Err(PullCircleActivationError::Database(error)) => return Err(error.into()),
2704 Err(PullCircleActivationError::Invalid(error)) => {
2705 return Err(StorePullError::Serial(error));
2706 }
2707 };
2708 candidates.push((
2709 Candidate {
2710 commit_ref: authorized.commit_ref,
2711 commit: authorized.commit,
2712 author: authorized.author,
2713 package,
2714 registrations: authorized.registrations,
2715 circle_activations,
2716 },
2717 authorized.authorization_after,
2718 ));
2719 }
2720
2721 let schema: Arc<TableSchema> = {
2722 let tables = tables.to_vec();
2723 Arc::new(
2724 db.call(move |conn| TableSchema::from_db(conn, &tables))
2725 .await?,
2726 )
2727 };
2728 let mut row_changes = Vec::new();
2729 let mut authors = BTreeSet::new();
2730 let mut applied_candidates = 0_u64;
2731 for (candidate, authorization_after) in &candidates {
2732 let changes = match apply_serial_candidate(
2733 db,
2734 storage,
2735 store_dir,
2736 schema.clone(),
2737 candidate,
2738 authorization_after,
2739 )
2740 .await
2741 {
2742 Ok(changes) => changes,
2743 Err(StorePullError::BlobDownloads(failures)) if !failures.has_transport_failure() => {
2744 tracing::warn!(
2745 stream_id = %commit_stream_id(&candidate.commit_ref.coord),
2746 seq = candidate.commit_ref.coord.sequence(),
2747 %failures,
2748 "holding Serial commit on blob download failure"
2749 );
2750 let frontier = db.materialized_frontier().await?;
2751 let local_blob_cleanup_pending = local_cleanup::drain(db, store_dir).await?;
2752 return Ok(StorePullResult {
2753 changesets_applied: applied_candidates,
2754 devices_pulled: u64::try_from(authors.len()).map_err(|_| {
2755 StorePullError::Serial("author count exceeds u64".to_string())
2756 })?,
2757 held_positions: vec![held_commit(
2758 &candidate.commit_ref,
2759 HeldStorePositionReason::BlobDownloadFailed,
2760 )],
2761 visible_heads: Vec::new(),
2762 serial_head: Some(head),
2763 row_changes,
2764 asset_downloads_failed: true,
2765 local_blob_cleanup_pending,
2766 frontier,
2767 });
2768 }
2769 Err(error) => return Err(error),
2770 };
2771 authors.insert(candidate.author.device_id);
2772 row_changes.extend(changes);
2773 applied_candidates = applied_candidates
2774 .checked_add(1)
2775 .ok_or_else(|| StorePullError::Serial("apply count exceeds u64".to_string()))?;
2776 }
2777 let frontier = db.materialized_frontier().await?;
2778 let local_blob_cleanup_pending = local_cleanup::drain(db, store_dir).await?;
2779 Ok(StorePullResult {
2780 changesets_applied: applied_candidates,
2781 devices_pulled: u64::try_from(authors.len())
2782 .map_err(|_| StorePullError::Serial("author count exceeds u64".to_string()))?,
2783 held_positions: Vec::new(),
2784 visible_heads: Vec::new(),
2785 serial_head: Some(head),
2786 row_changes,
2787 asset_downloads_failed: false,
2788 local_blob_cleanup_pending,
2789 frontier,
2790 })
2791}
2792
2793async fn empty_serial_pull_result(
2794 db: &Database,
2795 store_dir: &StoreDir,
2796 serial_head: Option<StoreSerialHead>,
2797) -> Result<StorePullResult, StorePullError> {
2798 let frontier = db.materialized_frontier().await?;
2799 let local_blob_cleanup_pending = local_cleanup::drain(db, store_dir).await?;
2800 Ok(StorePullResult {
2801 changesets_applied: 0,
2802 devices_pulled: 0,
2803 held_positions: Vec::new(),
2804 visible_heads: Vec::new(),
2805 serial_head,
2806 row_changes: Vec::new(),
2807 asset_downloads_failed: false,
2808 local_blob_cleanup_pending,
2809 frontier,
2810 })
2811}
2812
2813#[doc(hidden)]
2814pub async fn prepare_serial_resolution(
2815 db: &Database,
2816 storage: &dyn SyncStorage,
2817 coordination: &dyn CoordinationStorage,
2818 store_root_hash: ObjectHash,
2819 store_dir: &StoreDir,
2820 branch_base: Option<StoreBatchCommitRef>,
2821 identity: &crate::keys::UserKeypair,
2822) -> Result<SerialResolutionPlan, StorePullError> {
2823 let root = db.local_store_root_ref().await?.ok_or_else(|| {
2824 StorePullError::Serial("Store root exact reference is absent".to_string())
2825 })?;
2826 if root.store_root_hash != store_root_hash {
2827 return Err(StorePullError::Serial(
2828 "Serial resolution root differs from durable exact root".to_string(),
2829 ));
2830 }
2831 let verified_head = read_serial_head(storage, coordination, &root).await?;
2832 let head = verified_head.head;
2833 let authorized_chain = load_authorized_serial_chain(storage, &root, &head).await?;
2834 let first = match branch_base.as_ref() {
2835 None => 0,
2836 Some(base) => authorized_chain
2837 .iter()
2838 .position(|authorized| &authorized.commit_ref == base)
2839 .map(|index| index + 1)
2840 .ok_or_else(|| {
2841 StorePullError::Serial(
2842 "global chain does not descend from the exact conflicting branch base"
2843 .to_string(),
2844 )
2845 })?,
2846 };
2847 let schema: Arc<TableSchema> = {
2848 let tables = db.synced_tables().to_vec();
2849 Arc::new(
2850 db.call(move |conn| TableSchema::from_db(conn, &tables))
2851 .await?,
2852 )
2853 };
2854 let mut commits = Vec::with_capacity(authorized_chain.len() - first);
2855 for authorized in authorized_chain.into_iter().skip(first) {
2856 let package =
2857 load_serial_store_package(db, storage, &authorized.commit_ref, &authorized.commit)
2858 .await?;
2859 let circle_activations = match load_pull_circle_activations(
2860 db,
2861 storage,
2862 &root,
2863 &authorized.commit_ref,
2864 &authorized.commit,
2865 &authorized.author,
2866 Some(identity),
2867 )
2868 .await
2869 {
2870 Ok(activations) => activations,
2871 Err(PullCircleActivationError::Database(error)) => return Err(error.into()),
2872 Err(PullCircleActivationError::Invalid(error)) => {
2873 return Err(StorePullError::Serial(error));
2874 }
2875 };
2876 let candidate = Candidate {
2877 commit_ref: authorized.commit_ref.clone(),
2878 commit: authorized.commit,
2879 author: authorized.author,
2880 package,
2881 registrations: authorized.registrations,
2882 circle_activations,
2883 };
2884 let cleanup = if candidate.package.is_some() {
2885 prepare_serial_candidate(db, storage, store_dir, schema.clone(), &candidate)
2886 .await?
2887 .cleanup
2888 } else {
2889 Vec::new()
2890 };
2891 commits.push(SerialResolutionCommit {
2892 commit: candidate.commit,
2893 commit_ref: candidate.commit_ref,
2894 package: candidate.package,
2895 cleanup,
2896 registrations: candidate.registrations,
2897 circle_activations: candidate.circle_activations,
2898 authorization_after: authorized.authorization_after,
2899 });
2900 }
2901 Ok(SerialResolutionPlan {
2902 head,
2903 head_object: verified_head.object,
2904 commits,
2905 })
2906}
2907
2908#[doc(hidden)]
2909pub async fn cleanup_serial_candidates(
2910 db: &Database,
2911 storage: &dyn SyncStorage,
2912 branch_id: crate::PendingBranchId,
2913 plan: &SerialResolutionPlan,
2914) -> Result<(), StorePullError> {
2915 let targets = db.prepare_serial_candidate_cleanup(branch_id, plan).await?;
2916 for target in targets {
2917 super::store_objects::delete_exact_object(storage, &target.object).await?;
2918 db.mark_candidate_cleanup_absent(target.object).await?;
2919 }
2920 Ok(())
2921}
2922
2923pub async fn cleanup_serial_abandonment_authority(
2924 db: &Database,
2925 storage: &dyn SyncStorage,
2926 plan: &SerialResolutionPlan,
2927) -> Result<(), StorePullError> {
2928 let target = db
2929 .prepare_serial_abandonment_authority_cleanup(plan)
2930 .await?;
2931 if let Some(target) = target {
2932 super::store_objects::delete_exact_object(storage, &target.object).await?;
2933 db.mark_candidate_cleanup_absent(target.object).await?;
2934 }
2935 Ok(())
2936}
2937
2938pub async fn cleanup_merge_candidate(
2939 db: &Database,
2940 storage: &dyn SyncStorage,
2941 write_id: crate::WriteId,
2942) -> Result<(), StorePullError> {
2943 let targets = db.merge_candidate_cleanup_targets(write_id).await?;
2944 for target in targets {
2945 super::store_objects::delete_exact_object(storage, &target.object).await?;
2946 db.mark_candidate_cleanup_absent(target.object).await?;
2947 }
2948 Ok(())
2949}
2950
2951async fn apply_serial_candidate(
2952 db: &Database,
2953 storage: &dyn SyncStorage,
2954 store_dir: &StoreDir,
2955 schema: Arc<TableSchema>,
2956 candidate: &Candidate,
2957 authorization_after: &SerialAuthorizationState,
2958) -> Result<Vec<RowChange>, StorePullError> {
2959 if candidate.package.is_none() {
2960 let commit = candidate.commit.clone();
2961 let commit_ref = candidate.commit_ref.clone();
2962 let registrations = candidate.registrations.clone();
2963 let circle_activations = candidate.circle_activations.clone();
2964 let authorization_after = authorization_after.clone();
2965 db.call(move |conn| {
2966 let tx = conn.unchecked_transaction().map_err(DbError::from)?;
2967 Database::record_activated_store_device_registrations_on(&tx, &commit, ®istrations)?;
2968 Database::record_verified_circle_activations_on(
2969 &tx,
2970 &commit,
2971 &commit_ref,
2972 &circle_activations,
2973 )?;
2974 Database::record_materialized_serial_commit_on(
2975 &tx,
2976 &commit,
2977 &commit_ref,
2978 &authorization_after,
2979 )?;
2980 tx.commit().map_err(DbError::from)
2981 })
2982 .await?;
2983 return Ok(Vec::new());
2984 }
2985
2986 let prepared = prepare_serial_candidate(db, storage, store_dir, schema, candidate).await?;
2987 let PreparedSerialCandidate {
2988 changeset,
2989 changes,
2990 cleanup,
2991 } = prepared;
2992 let commit = candidate.commit.clone();
2993 let commit_ref = candidate.commit_ref.clone();
2994 let registrations = candidate.registrations.clone();
2995 let circle_activations = candidate.circle_activations.clone();
2996 let authorization_after = authorization_after.clone();
2997 let returned_changes = changes.clone();
2998 let blob_decls = db.blob_decls();
2999 let receiver_wall_ms = db.receive_wall_ms();
3000 let mut changeset_max = None;
3001 advance_max_updated_at(
3002 &mut changeset_max,
3003 &changes,
3004 changeset.schema(),
3005 receiver_wall_ms,
3006 );
3007 let hlc = db.hlc();
3008 db.call(move |conn| {
3009 let tx = conn.unchecked_transaction().map_err(DbError::from)?;
3010 apply_changeset_strict_on(&tx, changeset).map_err(|error| {
3011 DbError::Message(format!(
3012 "Serial commit {} did not apply exactly: {error}",
3013 commit_ref.coord.sequence()
3014 ))
3015 })?;
3016 for intent in cleanup {
3017 local_cleanup::record_if_unreferenced_on(&tx, &blob_decls, &intent)?;
3018 }
3019 Database::record_activated_store_device_registrations_on(&tx, &commit, ®istrations)?;
3020 Database::record_verified_circle_activations_on(
3021 &tx,
3022 &commit,
3023 &commit_ref,
3024 &circle_activations,
3025 )?;
3026 Database::record_materialized_serial_commit_on(
3027 &tx,
3028 &commit,
3029 &commit_ref,
3030 &authorization_after,
3031 )?;
3032 tx.commit().map_err(DbError::from)?;
3033 if let Some(max_applied) = changeset_max.as_ref() {
3034 hlc.advance_past(max_applied);
3035 }
3036 Ok(())
3037 })
3038 .await?;
3039 Ok(returned_changes)
3040}
3041
3042struct PreparedSerialCandidate {
3043 changeset: ValidatedChangeset<Vec<u8>>,
3044 changes: Vec<RowChange>,
3045 cleanup: Vec<LocalBlobCleanupIntent>,
3046}
3047
3048async fn prepare_serial_candidate(
3049 db: &Database,
3050 storage: &dyn SyncStorage,
3051 store_dir: &StoreDir,
3052 schema: Arc<TableSchema>,
3053 candidate: &Candidate,
3054) -> Result<PreparedSerialCandidate, StorePullError> {
3055 let package_bytes = candidate.package.as_ref().ok_or_else(|| {
3056 StorePullError::Serial("row preparation requires a Store package".to_string())
3057 })?;
3058 let package =
3059 parse_candidate_store_package(candidate, package_bytes).map_err(StorePullError::Serial)?;
3060 let changeset = ValidatedChangeset::new(package.changeset().to_vec(), schema)
3061 .map_err(|error| StorePullError::Serial(format!("invalid changeset: {error}")))?;
3062 let changes = crate::changeset::walk(changeset.bytes())
3063 .map_err(|error| StorePullError::Serial(format!("invalid changeset: {error}")))?;
3064 let old_changes = crate::changeset::walk_old(changeset.bytes())
3065 .map_err(|error| StorePullError::Serial(format!("invalid changeset: {error}")))?;
3066 let blob_decls = db.blob_decls();
3067 let eager = cache_eager_blobs(&blob_decls, &changes, &package)
3068 .map_err(|error| StorePullError::Serial(format!("invalid blob changes: {error}")))?;
3069 if let Err(failures) = download_blobs(db, eager, storage, store_dir).await {
3070 return Err(StorePullError::BlobDownloads(failures));
3071 }
3072 let cleanup = local_blob_cleanup_intents(&blob_decls, &old_changes, &changes)
3073 .map_err(|error| StorePullError::Serial(format!("invalid blob changes: {error}")))?;
3074 Ok(PreparedSerialCandidate {
3075 changeset,
3076 changes,
3077 cleanup,
3078 })
3079}
3080fn membership_authorizes(
3081 membership: Option<&MembershipChain>,
3082 commit: &StoreBatchCommit,
3083 author: &StoreDeviceRegistration,
3084) -> bool {
3085 let Some(chain) = membership else {
3086 return true;
3087 };
3088 let Some(authority) = commit.membership_authority.as_ref() else {
3089 return chain.contains_member_now(&author.author_pubkey);
3090 };
3091 chain.authorizes_write_authority(authority, &author.author_pubkey)
3092}
3093
3094fn carries_circle_payload(commit: &StoreBatchCommit) -> bool {
3095 !commit.circle_controls().is_empty() || !commit.circle_packages().is_empty()
3096}
3097
3098enum PullCircleActivationError {
3099 Database(DbError),
3100 Invalid(String),
3101}
3102
3103async fn load_pull_circle_activations(
3104 db: &Database,
3105 storage: &dyn SyncStorage,
3106 root: &StoreRootRef,
3107 commit_ref: &StoreBatchCommitRef,
3108 commit: &StoreBatchCommit,
3109 author: &StoreDeviceRegistration,
3110 identity: Option<&crate::keys::UserKeypair>,
3111) -> Result<Vec<super::circle_ops::VerifiedCircleReference>, PullCircleActivationError> {
3112 if !carries_circle_payload(commit) {
3113 return Ok(Vec::new());
3114 }
3115 if !commit.circle_packages().is_empty() {
3116 return Err(PullCircleActivationError::Invalid(
3117 "circle row packages are not implemented".to_string(),
3118 ));
3119 }
3120 let identity = identity.ok_or_else(|| {
3121 PullCircleActivationError::Invalid(format!(
3122 "commit {} carries circle controls but no device identity was supplied",
3123 commit.seq()
3124 ))
3125 })?;
3126 let founder = db
3127 .get_protocol_state(super::membership_ops::OWNER_PUBKEY_STATE_KEY)
3128 .await
3129 .map_err(PullCircleActivationError::Database)?
3130 .ok_or_else(|| {
3131 PullCircleActivationError::Invalid(
3132 "Store founder is absent while loading circle controls".to_string(),
3133 )
3134 })?;
3135 super::circle_ops::load_circle_activations(
3136 storage, root, commit_ref, commit, author, identity, &founder,
3137 )
3138 .await
3139 .map_err(|error| PullCircleActivationError::Invalid(error.to_string()))
3140}
3141
3142async fn load_serial_store_package(
3143 db: &Database,
3144 storage: &dyn SyncStorage,
3145 commit_ref: &StoreBatchCommitRef,
3146 commit: &StoreBatchCommit,
3147) -> Result<Option<Vec<u8>>, StorePullError> {
3148 if let Some(package) = commit.store_package() {
3149 if package.schema_version > db.schema_version() {
3150 return Err(StorePullError::Serial(format!(
3151 "commit {} requires schema {}, local schema is {}",
3152 commit.seq(),
3153 package.schema_version,
3154 db.schema_version()
3155 )));
3156 }
3157 }
3158 match load_store_package(storage, commit_ref, commit).await? {
3159 Some(package) => Ok(Some(package.value)),
3160 None if commit.store_package().is_none() => Ok(None),
3161 None => Err(StorePullError::Serial(format!(
3162 "commit {} Store package is absent",
3163 commit.seq()
3164 ))),
3165 }
3166}
3167
3168enum Readiness {
3169 Ready,
3170 AlreadyMaterialized,
3171 Held(HeldStorePosition),
3172}
3173
3174enum MaterializedCheck {
3175 Yes,
3176 Missing,
3177 Held(HeldStorePositionReason),
3178}
3179
3180fn held_object_error(error: StoreObjectError) -> HeldStorePositionReason {
3181 match error {
3182 StoreObjectError::Storage(source) => HeldStorePositionReason::ObjectUnreadable {
3183 key: "exact Store object".to_string(),
3184 detail: source.to_string(),
3185 },
3186 StoreObjectError::InvalidObject { key, source, .. } => match *source {
3187 StoreProtocolError::InvalidSignature => HeldStorePositionReason::InvalidSignature,
3188 StoreProtocolError::RelocatedSlot { .. }
3189 | StoreProtocolError::RelocatedPackage { .. }
3190 | StoreProtocolError::StoreRootMismatch { .. }
3191 | StoreProtocolError::StoreMismatch { .. }
3192 | StoreProtocolError::FounderMismatch { .. } => {
3193 HeldStorePositionReason::WrongSlot(source.to_string())
3194 }
3195 source => HeldStorePositionReason::ObjectUnreadable {
3196 key,
3197 detail: source.to_string(),
3198 },
3199 },
3200 }
3201}
3202
3203async fn readiness(
3204 db: &Database,
3205 storage: &dyn SyncStorage,
3206 root: &StoreRootRef,
3207 coverage: &super::store_commit::CommitFrontier,
3208 frontier: &BTreeMap<String, StoreBatchCommitRef>,
3209 commit_ref: &StoreBatchCommitRef,
3210 commit: &StoreBatchCommit,
3211) -> Result<Readiness, StorePullError> {
3212 let stream_id = commit_stream_id(&commit_ref.coord);
3213 if let Some(current) = frontier.get(&stream_id) {
3214 if commit_ref.coord.sequence() <= current.coord.sequence() {
3215 match reference_is_materialized(db, storage, root, coverage, &stream_id, commit_ref)
3216 .await?
3217 {
3218 MaterializedCheck::Yes => return Ok(Readiness::AlreadyMaterialized),
3219 MaterializedCheck::Missing => {
3220 return Ok(Readiness::Held(held_commit(
3221 commit_ref,
3222 HeldStorePositionReason::MissingCommit,
3223 )))
3224 }
3225 MaterializedCheck::Held(reason) => {
3226 return Ok(Readiness::Held(held_commit(commit_ref, reason)))
3227 }
3228 }
3229 }
3230 if commit.order.predecessor() != Some(current) {
3231 let reason = match commit.order.predecessor() {
3232 Some(missing) => HeldStorePositionReason::MissingPredecessor(missing.clone()),
3233 None => HeldStorePositionReason::InvalidObject(
3234 "non-genesis Merge commit omits its exact predecessor".to_string(),
3235 ),
3236 };
3237 return Ok(Readiness::Held(held_commit(commit_ref, reason)));
3238 }
3239 if commit_ref.coord.sequence() != current.coord.sequence() + 1 {
3240 return Ok(Readiness::Held(held_commit(
3241 commit_ref,
3242 HeldStorePositionReason::InvalidObject(
3243 "Merge commit sequence does not immediately follow its materialized frontier"
3244 .to_string(),
3245 ),
3246 )));
3247 }
3248 } else if commit_ref.coord.sequence() != 1 || commit.order.predecessor().is_some() {
3249 let reason = match commit.order.predecessor() {
3250 Some(missing) => HeldStorePositionReason::MissingPredecessor(missing.clone()),
3251 None => HeldStorePositionReason::InvalidObject(
3252 "Merge commit beyond genesis omits its exact predecessor".to_string(),
3253 ),
3254 };
3255 return Ok(Readiness::Held(held_commit(commit_ref, reason)));
3256 }
3257
3258 for (required_stream, required_ref) in commit.merge_dependencies().map_err(|error| {
3259 StorePullError::Database(format!("MergeConcurrent commit order: {error}"))
3260 })? {
3261 let required_stream = required_stream.to_string();
3262 match reference_is_materialized(db, storage, root, coverage, &required_stream, required_ref)
3263 .await?
3264 {
3265 MaterializedCheck::Yes => {}
3266 MaterializedCheck::Missing => {
3267 return Ok(Readiness::Held(held_dependency(
3268 commit_ref,
3269 &required_stream,
3270 required_ref,
3271 HeldStorePositionReason::MissingDependency {
3272 device_id: required_stream.clone(),
3273 commit: required_ref.clone(),
3274 },
3275 )))
3276 }
3277 MaterializedCheck::Held(reason) => {
3278 return Ok(Readiness::Held(held_dependency(
3279 commit_ref,
3280 &required_stream,
3281 required_ref,
3282 reason,
3283 )))
3284 }
3285 }
3286 }
3287 Ok(Readiness::Ready)
3288}
3289
3290async fn reference_is_materialized(
3291 db: &Database,
3292 storage: &dyn SyncStorage,
3293 root: &StoreRootRef,
3294 coverage: &super::store_commit::CommitFrontier,
3295 stream_id: &str,
3296 reference: &StoreBatchCommitRef,
3297) -> Result<MaterializedCheck, StorePullError> {
3298 if commit_stream_id(&reference.coord) != stream_id {
3299 return Ok(MaterializedCheck::Held(HeldStorePositionReason::WrongSlot(
3300 format!(
3301 "commit reference stream {} differs from dependency stream {stream_id}",
3302 commit_stream_id(&reference.coord)
3303 ),
3304 )));
3305 }
3306 if let Some(actual) = db
3307 .exact_materialized_ref(stream_id, reference.coord.sequence())
3308 .await?
3309 {
3310 if actual != *reference {
3311 return Ok(MaterializedCheck::Held(
3312 HeldStorePositionReason::HashMismatch {
3313 referenced_device_id: stream_id.to_string(),
3314 referenced_commit: reference.clone(),
3315 materialized_hash: actual.commit_hash,
3316 },
3317 ));
3318 }
3319 return Ok(MaterializedCheck::Yes);
3320 }
3321 let coverage = coverage.clone().into_refs();
3322 let Some(covered) = coverage.get(stream_id) else {
3323 return Ok(MaterializedCheck::Missing);
3324 };
3325 if reference.coord.sequence() > covered.coord.sequence() {
3326 return Ok(MaterializedCheck::Missing);
3327 }
3328 let mut cursor = covered.clone();
3329 loop {
3330 if cursor == *reference {
3331 return Ok(MaterializedCheck::Yes);
3332 }
3333 if cursor.coord.sequence() <= reference.coord.sequence() {
3334 return Ok(MaterializedCheck::Held(
3335 HeldStorePositionReason::HashMismatch {
3336 referenced_device_id: stream_id.to_string(),
3337 referenced_commit: reference.clone(),
3338 materialized_hash: cursor.commit_hash,
3339 },
3340 ));
3341 }
3342 let (commit, _) = match load_commit_with_author(storage, root, &cursor).await {
3343 Ok(commit) => commit,
3344 Err(error) => return Ok(MaterializedCheck::Held(held_object_error(error))),
3345 };
3346 let Some(predecessor) = commit.order.predecessor() else {
3347 return Ok(MaterializedCheck::Missing);
3348 };
3349 cursor = predecessor.clone();
3350 }
3351}
3352
3353async fn apply_candidate(
3354 db: &Database,
3355 storage: &dyn SyncStorage,
3356 store_dir: &StoreDir,
3357 schema: Arc<TableSchema>,
3358 candidate: &Candidate,
3359) -> Result<ApplyOutcome, StorePullError> {
3360 let Some(package_bytes) = candidate.package.as_ref() else {
3361 let commit = candidate.commit.clone();
3362 let commit_ref = candidate.commit_ref.clone();
3363 let registrations = candidate.registrations.clone();
3364 let circle_activations = candidate.circle_activations.clone();
3365 db.call(move |conn| {
3366 let tx = conn.unchecked_transaction().map_err(DbError::from)?;
3367 Database::record_activated_store_device_registrations_on(&tx, &commit, ®istrations)?;
3368 Database::record_verified_circle_activations_on(
3369 &tx,
3370 &commit,
3371 &commit_ref,
3372 &circle_activations,
3373 )?;
3374 Database::record_materialized_commit_on(&tx, &commit, &commit_ref)?;
3375 tx.commit().map_err(DbError::from)
3376 })
3377 .await?;
3378 return Ok(ApplyOutcome::Applied(Vec::new()));
3379 };
3380 let package = match parse_candidate_store_package(candidate, package_bytes) {
3381 Ok(package) => package,
3382 Err(error) => {
3383 return Ok(ApplyOutcome::Held(
3384 HeldStorePositionReason::InvalidChangeset(error),
3385 ));
3386 }
3387 };
3388 let changeset = match ValidatedChangeset::new(package.changeset().to_vec(), schema) {
3389 Ok(changeset) => changeset,
3390 Err(error) => {
3391 return Ok(ApplyOutcome::Held(match error {
3392 super::session::ChangesetIdentityError::Row(error) => {
3393 HeldStorePositionReason::InvalidRowIdentity {
3394 table: error.table().to_string(),
3395 reason: error.to_string(),
3396 }
3397 }
3398 error => HeldStorePositionReason::InvalidChangeset(error.to_string()),
3399 }))
3400 }
3401 };
3402 let changes = match crate::changeset::walk(changeset.bytes()) {
3403 Ok(changes) => changes,
3404 Err(error) => {
3405 return Ok(ApplyOutcome::Held(
3406 HeldStorePositionReason::InvalidChangeset(error.to_string()),
3407 ))
3408 }
3409 };
3410 let old_changes = match crate::changeset::walk_old(changeset.bytes()) {
3411 Ok(changes) => changes,
3412 Err(error) => {
3413 return Ok(ApplyOutcome::Held(
3414 HeldStorePositionReason::InvalidChangeset(error.to_string()),
3415 ))
3416 }
3417 };
3418 let blob_decls = db.blob_decls();
3419 let eager = match cache_eager_blobs(&blob_decls, &changes, &package) {
3420 Ok(eager) => eager,
3421 Err(error) => {
3422 return Ok(ApplyOutcome::Held(
3423 HeldStorePositionReason::InvalidChangeset(error.to_string()),
3424 ))
3425 }
3426 };
3427 if let Err(failures) = download_blobs(db, eager, storage, store_dir).await {
3428 if failures.has_transport_failure() {
3429 return Err(StorePullError::BlobDownloads(failures));
3430 }
3431 return Ok(ApplyOutcome::Held(
3432 HeldStorePositionReason::BlobDownloadFailed,
3433 ));
3434 }
3435 let cleanup = match local_blob_cleanup_intents(&blob_decls, &old_changes, &changes) {
3436 Ok(cleanup) => cleanup,
3437 Err(error) => {
3438 return Ok(ApplyOutcome::Held(
3439 HeldStorePositionReason::InvalidChangeset(error.to_string()),
3440 ))
3441 }
3442 };
3443 let outcome = commit_candidate(db, candidate, package, changes, changeset, cleanup).await?;
3444 #[cfg(any(test, feature = "test-utils"))]
3445 if matches!(outcome, ApplyOutcome::Applied(_)) {
3446 db.reach_test_point(crate::database::DatabaseTestPoint::PullAfterRemoteCommit {
3447 device_id: commit_stream_id(&candidate.commit_ref.coord),
3448 seq: candidate.commit.seq(),
3449 })
3450 .await;
3451 }
3452 Ok(outcome)
3453}
3454
3455async fn commit_candidate(
3456 db: &Database,
3457 candidate: &Candidate,
3458 package: AudiencePackage,
3459 changes: Vec<RowChange>,
3460 changeset: ValidatedChangeset<Vec<u8>>,
3461 cleanup: Vec<LocalBlobCleanupIntent>,
3462) -> Result<ApplyOutcome, StorePullError> {
3463 let commit = candidate.commit.clone();
3464 let commit_ref = candidate.commit_ref.clone();
3465 let registrations = candidate.registrations.clone();
3466 let circle_activations = candidate.circle_activations.clone();
3467 let returned_changes = changes.clone();
3468 let receiver_wall_ms = db.receive_wall_ms();
3469 let blob_decls = db.blob_decls();
3470 let gates = db.gates();
3471 let synced_tables = db.synced_tables().to_vec();
3472 let mut changeset_max = None;
3473 advance_max_updated_at(
3474 &mut changeset_max,
3475 &changes,
3476 changeset.schema(),
3477 receiver_wall_ms,
3478 );
3479 let hlc = db.hlc();
3480 let outcome = db
3481 .call(move |conn| {
3482 let tx = conn.unchecked_transaction().map_err(DbError::from)?;
3483 let apply =
3484 resolve_and_apply_changeset_with_schema_on(&tx, changeset, receiver_wall_ms)?;
3485 if !apply.constraint_conflict_tables.is_empty() {
3486 tx.rollback().map_err(DbError::from)?;
3487 return Ok(ApplyOutcome::Held(
3488 HeldStorePositionReason::ConstraintConflict(apply.constraint_conflict_tables),
3489 ));
3490 }
3491 if apply.had_fk_violations {
3492 tx.rollback().map_err(DbError::from)?;
3493 return Ok(ApplyOutcome::Held(
3494 HeldStorePositionReason::ForeignKeyDependency,
3495 ));
3496 }
3497 let winning_rows =
3498 crate::sync::apply::current_winning_rows(&tx, &synced_tables, package.changeset())?;
3499 Database::install_pulled_blob_activations_on(&tx, &package, &commit_ref)?;
3500 Database::install_winning_blob_bindings_on(
3501 &tx,
3502 &gates,
3503 &synced_tables,
3504 &package,
3505 &BlobActivation {
3506 coord: commit_ref.coord.clone(),
3507 },
3508 &winning_rows,
3509 )?;
3510 for intent in cleanup {
3511 local_cleanup::record_if_unreferenced_on(&tx, &blob_decls, &intent)?;
3512 }
3513 Database::record_activated_store_device_registrations_on(&tx, &commit, ®istrations)?;
3514 Database::record_verified_circle_activations_on(
3515 &tx,
3516 &commit,
3517 &commit_ref,
3518 &circle_activations,
3519 )?;
3520 Database::record_materialized_commit_on(&tx, &commit, &commit_ref)?;
3521 tx.commit().map_err(DbError::from)?;
3522 if let Some(max_applied) = changeset_max.as_ref() {
3523 hlc.advance_past(max_applied);
3524 }
3525 Ok(ApplyOutcome::Applied(returned_changes))
3526 })
3527 .await?;
3528 Ok(outcome)
3529}
3530
3531fn held_commit(
3532 reference: &StoreBatchCommitRef,
3533 reason: HeldStorePositionReason,
3534) -> HeldStorePosition {
3535 HeldStorePosition {
3536 coordinate: HeldStoreCoordinate::Commit {
3537 device_id: commit_stream_id(&reference.coord),
3538 commit: reference.clone(),
3539 },
3540 reason,
3541 }
3542}
3543
3544fn held_package(
3545 reference: &StoreBatchCommitRef,
3546 commit: &StoreBatchCommit,
3547 reason: HeldStorePositionReason,
3548) -> HeldStorePosition {
3549 let package = commit
3550 .store_package()
3551 .expect("held Store package is named by the commit");
3552 HeldStorePosition {
3553 coordinate: HeldStoreCoordinate::Package {
3554 device_id: commit_stream_id(&reference.coord),
3555 seq: commit.seq(),
3556 package_hash: package.content_hash,
3557 },
3558 reason,
3559 }
3560}
3561
3562fn held_dependency(
3563 dependent: &StoreBatchCommitRef,
3564 required_device_id: &str,
3565 required: &StoreBatchCommitRef,
3566 reason: HeldStorePositionReason,
3567) -> HeldStorePosition {
3568 HeldStorePosition {
3569 coordinate: HeldStoreCoordinate::Dependency {
3570 dependent_device_id: commit_stream_id(&dependent.coord),
3571 dependent_commit: dependent.clone(),
3572 required_device_id: required_device_id.to_string(),
3573 required_commit: required.clone(),
3574 },
3575 reason,
3576 }
3577}
3578
3579fn commit_stream_id(coord: &StoreCommitCoord) -> String {
3580 match coord {
3581 StoreCommitCoord::MergeConcurrent { stream_id, .. } => stream_id.to_string(),
3582 StoreCommitCoord::Serial { .. } => SERIAL_STREAM_ID.to_string(),
3583 }
3584}
3585
3586#[cfg(test)]
3587mod tests {
3588 use super::*;
3589
3590 #[tokio::test]
3591 async fn cycle_authorization_rejects_an_absent_serial_coordination_head() {
3592 let db = crate::sync::test_helpers::open_serial_test_db();
3593 let store = crate::sync::test_helpers::TestStore::create(
3594 &db,
3595 "absent-serial-cycle-head",
3596 crate::keys::UserKeypair::generate(),
3597 )
3598 .await
3599 .expect("create Serial Store");
3600 store.home.remove(serial_head_key());
3601
3602 let result = load_serial_cycle_authorization(
3603 &store.storage,
3604 store
3605 .storage
3606 .serial_coordination()
3607 .expect("Serial coordination"),
3608 &store.root,
3609 )
3610 .await;
3611
3612 assert!(matches!(
3613 result,
3614 Err(StorePullError::Serial(reason)) if reason == "global head is absent"
3615 ));
3616 }
3617
3618 #[tokio::test]
3619 async fn merge_gap_reports_the_exact_signed_predecessor() {
3620 let source = crate::sync::test_helpers::open_test_db();
3621 let store = crate::sync::test_helpers::TestStore::create(
3622 &source,
3623 "exact-predecessor-test",
3624 crate::keys::UserKeypair::generate(),
3625 )
3626 .await
3627 .expect("create exact predecessor test Store");
3628 let changeset = crate::sync::test_helpers::capture_bytes(
3629 &crate::sync::test_helpers::open_test_db(),
3630 &[
3631 "INSERT INTO notes (id, title, body, _updated_at, created_at) \
3632 VALUES ('gap-row', 'gap', NULL, '0000000001000-0000-gap', '2026-01-01')",
3633 ],
3634 )
3635 .await;
3636 let first = store
3637 .publish_changeset("founder", 1, &changeset, source.schema_version())
3638 .await
3639 .expect("publish first exact commit");
3640 let second = store
3641 .publish_changeset("founder", 2, &changeset, source.schema_version())
3642 .await
3643 .expect("publish second exact commit");
3644 let third = store
3645 .publish_changeset("founder", 3, &changeset, source.schema_version())
3646 .await
3647 .expect("publish third exact commit");
3648 let (_, founder, _) = store
3649 .founder_device_authority()
3650 .await
3651 .expect("load founder authority");
3652 let commit = super::super::store_objects::load_commit_ref(
3653 &store.storage,
3654 store.root.store_root_hash,
3655 &third,
3656 &founder,
3657 )
3658 .await
3659 .expect("load third exact commit")
3660 .value;
3661 let stream_id = commit_stream_id(&first.coord);
3662 let frontier = BTreeMap::from([(stream_id.clone(), first.clone())]);
3663 let coverage =
3664 CommitFrontier::from_refs(crate::WritePolicy::MergeConcurrent, frontier.clone())
3665 .expect("build exact frontier");
3666 let target = crate::sync::test_helpers::open_test_db();
3667
3668 let readiness = readiness(
3669 &target,
3670 &store.storage,
3671 &store.root,
3672 &coverage,
3673 &frontier,
3674 &third,
3675 &commit,
3676 )
3677 .await
3678 .expect("evaluate exact predecessor gap");
3679
3680 assert!(matches!(
3681 readiness,
3682 Readiness::Held(HeldStorePosition {
3683 reason: HeldStorePositionReason::MissingPredecessor(missing),
3684 ..
3685 }) if missing == second
3686 ));
3687 }
3688}