laminar_core/cluster/control/topology/
target_preparation.rs1use serde::{Deserialize, Serialize};
4
5use super::{TopologyAdmissionPhase, TopologyAdmissionStatus, TopologyError};
6use crate::checkpoint::{CheckpointParticipant, MAX_CHECKPOINT_PARTICIPANTS};
7
8pub const TOPOLOGY_TARGET_PREPARATION_PROTOCOL_VERSION: u16 = 3;
10
11#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize)]
15#[serde(deny_unknown_fields)]
16pub struct TopologyTargetPreparationReceipt {
17 pub participant: CheckpointParticipant,
19 pub process_term: u64,
21 pub protocol_version: u16,
23 pub authority_sequence: u64,
25}
26
27impl TopologyAdmissionStatus {
28 #[must_use]
32 pub fn target_preparation_complete(&self) -> bool {
33 self.phase == TopologyAdmissionPhase::CutPrepared
34 && self.target_preparation_roster_complete()
35 }
36
37 pub(crate) fn target_preparation_roster_complete(&self) -> bool {
38 self.migration_root.is_some()
39 && self.cut.as_ref().is_some_and(|cut| {
40 cut.inventory
41 .assignment_fence
42 .as_ref()
43 .is_some_and(|fence| {
44 self.target_preparations.len() == fence.participants.len()
45 && self
46 .target_preparations
47 .iter()
48 .zip(&fence.participants)
49 .all(|(receipt, participant)| receipt.participant == *participant)
50 })
51 })
52 }
53
54 pub(crate) fn same_restore_binding(&self, other: &Self) -> bool {
57 (self.phase == other.phase || (self.has_target_commit() && other.has_target_commit()))
58 && self.commit == other.commit
59 && self.same_migration_binding(other)
60 }
61
62 pub(crate) fn same_migration_binding(&self, other: &Self) -> bool {
64 let Self {
65 operation_id,
66 plan,
67 admitted_by,
68 admitted_sequence,
69 status_sequence: _,
70 phase: _,
71 cut,
72 preparation,
73 migration_root,
74 target_preparations: _,
75 commit: _,
76 activation: _,
77 } = self;
78 *operation_id == other.operation_id
79 && *plan == other.plan
80 && *admitted_by == other.admitted_by
81 && *admitted_sequence == other.admitted_sequence
82 && *cut == other.cut
83 && *preparation == other.preparation
84 && *migration_root == other.migration_root
85 }
86
87 pub(crate) fn validate_target_preparations(&self, head: u64) -> Result<(), TopologyError> {
88 if self.target_preparations.is_empty() {
89 return Ok(());
90 }
91 let root = self.migration_root.as_ref().ok_or_else(|| {
92 TopologyError::Invalid("target preparation requires a published root".into())
93 })?;
94 let preparation = self.preparation.as_ref().ok_or_else(|| {
95 TopologyError::Invalid("target preparation requires candidate certificates".into())
96 })?;
97 if !matches!(
98 self.phase,
99 TopologyAdmissionPhase::CutPrepared
100 | TopologyAdmissionPhase::Committed
101 | TopologyAdmissionPhase::Activating
102 | TopologyAdmissionPhase::Active
103 | TopologyAdmissionPhase::Aborted { .. }
104 ) || self.target_preparations.len() > MAX_CHECKPOINT_PARTICIPANTS
105 || !self
106 .target_preparations
107 .windows(2)
108 .all(|pair| pair[0].participant.node_id < pair[1].participant.node_id)
109 || self.target_preparations.iter().any(|receipt| {
110 !matches!(
111 receipt.protocol_version,
112 TOPOLOGY_TARGET_PREPARATION_PROTOCOL_VERSION
113 | super::TOPOLOGY_COMMIT_PROTOCOL_VERSION
114 ) || receipt.authority_sequence <= root.authority_sequence
115 || receipt.authority_sequence > self.status_sequence
116 || receipt.authority_sequence > head
117 || !preparation.certificates.iter().any(|certificate| {
118 certificate.participant == receipt.participant
119 && certificate.process_term == receipt.process_term
120 })
121 })
122 {
123 return Err(TopologyError::Invalid(
124 "invalid target preparation observations".into(),
125 ));
126 }
127 Ok(())
128 }
129
130 pub(crate) fn validate_target_preparation_successor(
131 &self,
132 after: &Self,
133 sequence: u64,
134 ) -> Result<(), TopologyError> {
135 if self.target_preparations == after.target_preparations {
136 return Ok(());
137 }
138 after.validate_target_preparations(sequence)?;
139 if self.phase != TopologyAdmissionPhase::CutPrepared
140 || self.migration_root.is_none()
141 || !self.same_restore_binding(after)
142 || after.target_preparations.len() != self.target_preparations.len() + 1
143 || self
144 .target_preparations
145 .iter()
146 .any(|receipt| !after.target_preparations.contains(receipt))
147 || after
148 .target_preparations
149 .iter()
150 .filter(|receipt| !self.target_preparations.contains(receipt))
151 .any(|receipt| receipt.authority_sequence != sequence)
152 {
153 return Err(TopologyError::Invalid(
154 "authority cannot rewrite, forget or append stale target preparation observations"
155 .into(),
156 ));
157 }
158 Ok(())
159 }
160}