1use ozzy_proto::{Envelope, Opcode};
4use ozzy_proto::{LinkSessionId, NodeId, RequestId};
5
6use super::{
7 COMMON_BYTES, ControlEncoding, Incoming, PeerBinding, ReadFields, Reader, WireError,
8 WireLimits, WriteFields, Writer, validate_prefix, validate_scope,
9};
10use crate::recovery::{CheckpointAnchor, RecoveryLog, RecoveryResponse};
11use crate::{JournalGeneration, Scope};
12
13const NONCE_BYTES: usize = COMMON_BYTES + 16;
14const REQUEST_BYTES: usize = NONCE_BYTES + 1;
15const BACKUP_BYTES: usize = NONCE_BYTES + 1;
16const PRIMARY_BYTES: usize = BACKUP_BYTES + 16 + 40 + 40 + 1;
17const ANCHOR_BYTES: usize = 40 + 40 + 32 + 32 + 8 + 4;
18
19#[derive(Debug, Clone, Copy, PartialEq, Eq)]
21pub struct RecoveryRequest {
22 pub scope: Scope,
24 pub request_id: RequestId,
26 pub nonce: RequestId,
28 pub nonvoting: bool,
31}
32
33impl RecoveryRequest {
34 fn validate(self) -> Result<(), WireError> {
35 validate_exchange(self.scope, self.request_id, self.nonce)
36 }
37}
38
39fn validate_exchange(
40 scope: Scope,
41 request_id: RequestId,
42 nonce: RequestId,
43) -> Result<(), WireError> {
44 validate_scope(scope)?;
45 if request_id.as_bytes() == &[0; 16] || nonce.as_bytes() == &[0; 16] {
46 return Err(WireError::Correlation);
47 }
48 Ok(())
49}
50
51#[derive(Debug, Clone, Copy, PartialEq, Eq)]
53pub struct RecoveryState {
54 pub request_id: RequestId,
56 pub response: RecoveryResponse,
58}
59
60impl RecoveryState {
61 pub fn validate_response(self, request: RecoveryRequest) -> Result<(), WireError> {
67 self.validate()?;
68 request.validate()?;
69 if self.request_id != request.request_id || self.response.nonce != request.nonce {
70 return Err(WireError::Correlation);
71 }
72 if self.response.scope
73 != (Scope {
74 view: self.response.scope.view,
75 ..request.scope
76 })
77 {
78 return Err(WireError::Scope);
79 }
80 Ok(())
81 }
82
83 fn validate(self) -> Result<(), WireError> {
84 validate_exchange(self.response.scope, self.request_id, self.response.nonce)?;
85 if let Some(log) = self.response.primary {
86 if let Some(anchor) = log.checkpoint {
87 anchor
88 .validate(log.committed)
89 .map_err(|_| WireError::History)?;
90 }
91 validate_prefix(log.accepted)?;
92 validate_prefix(log.committed)?;
93 if log.generation.0 == 0
94 || log.committed.op > log.accepted.op
95 || (log.committed.op == log.accepted.op && log.committed != log.accepted)
96 {
97 return Err(WireError::History);
98 }
99 }
100 Ok(())
101 }
102}
103
104#[derive(Debug, Clone, Copy, PartialEq, Eq)]
106#[expect(
107 clippy::large_enum_variant,
108 reason = "bounded sans-I/O decoding returns inline evidence without allocation"
109)]
110pub enum RecoveryMessage {
111 Request(RecoveryRequest),
113 State(RecoveryState),
115}
116
117pub fn encode_recovery_state(
122 sender: NodeId,
123 session: LinkSessionId,
124 state: RecoveryState,
125 output: &mut [u8],
126 limits: WireLimits,
127) -> Result<ControlEncoding, WireError> {
128 state.validate()?;
129 let metadata_bytes = match state.response.primary {
130 Some(log) if log.checkpoint.is_some() => PRIMARY_BYTES + ANCHOR_BYTES,
131 Some(_) => PRIMARY_BYTES,
132 None => BACKUP_BYTES,
133 };
134 let header = Envelope {
135 opcode: Opcode::RecoveryState,
136 response: true,
137 request_id: Some(state.request_id),
138 sender,
139 session: Some(session),
140 }
141 .encode_header(metadata_bytes, 0, limits.envelope)?;
142 if output.len() < metadata_bytes {
143 return Err(WireError::Capacity);
144 }
145 let mut writer = Writer::with_len(output, metadata_bytes)?;
146 writer.scope(state.response.scope, sender)?;
147 writer.bytes(state.response.nonce.as_bytes())?;
148 writer.bytes(&[u8::from(state.response.primary.is_some())])?;
149 if let Some(log) = state.response.primary {
150 writer.bytes(&log.generation.0.to_be_bytes())?;
151 writer.prefix(log.accepted)?;
152 writer.prefix(log.committed)?;
153 writer.bytes(&[u8::from(log.checkpoint.is_some())])?;
154 if let Some(anchor) = log.checkpoint {
155 encode_anchor(&mut writer, anchor)?;
156 }
157 }
158 Ok(ControlEncoding {
159 header,
160 metadata_bytes,
161 })
162}
163
164pub fn encode_recovery(
166 sender: NodeId,
167 session: LinkSessionId,
168 request: RecoveryRequest,
169 output: &mut [u8],
170 limits: WireLimits,
171) -> Result<ControlEncoding, WireError> {
172 request.validate()?;
173 let header = Envelope {
174 opcode: Opcode::Recovery,
175 response: false,
176 request_id: Some(request.request_id),
177 sender,
178 session: Some(session),
179 }
180 .encode_header(REQUEST_BYTES, 0, limits.envelope)?;
181 if output.len() < REQUEST_BYTES {
182 return Err(WireError::Capacity);
183 }
184 let mut writer = Writer::with_len(output, REQUEST_BYTES)?;
185 writer.scope(request.scope, sender)?;
186 writer.bytes(request.nonce.as_bytes())?;
187 writer.bytes(&[u8::from(request.nonvoting)])?;
188 Ok(ControlEncoding {
189 header,
190 metadata_bytes: REQUEST_BYTES,
191 })
192}
193
194pub(super) fn decode_request(
195 Incoming {
196 scope,
197 envelope,
198 mut reader,
199 payload,
200 }: Incoming<'_>,
201) -> Result<RecoveryRequest, WireError> {
202 if !payload.is_empty() {
203 return Err(WireError::Payload);
204 }
205 if envelope.response {
206 return Err(WireError::Correlation);
207 }
208 let request = RecoveryRequest {
209 scope,
210 request_id: envelope.request_id.ok_or(WireError::Correlation)?,
211 nonce: RequestId::from_bytes(reader.array()?),
212 nonvoting: match reader.array::<1>()? {
213 [0] => false,
214 [1] => true,
215 _ => return Err(WireError::History),
216 },
217 };
218 reader.finish()?;
219 request.validate()?;
220 Ok(request)
221}
222
223pub(super) fn decode_state(
224 Incoming {
225 scope,
226 envelope,
227 mut reader,
228 payload,
229 }: Incoming<'_>,
230 binding: PeerBinding,
231) -> Result<RecoveryState, WireError> {
232 if !payload.is_empty() {
233 return Err(WireError::Payload);
234 }
235 if !envelope.response {
236 return Err(WireError::Correlation);
237 }
238 let request_id = envelope.request_id.ok_or(WireError::Correlation)?;
239 let nonce = RequestId::from_bytes(reader.array()?);
240 let primary = match reader.array::<1>()? {
241 [0] => None,
242 [1] => {
243 let generation = JournalGeneration(u128::from_be_bytes(reader.array()?));
244 let accepted = reader.prefix()?;
245 let committed = reader.prefix()?;
246 let checkpoint = match reader.array::<1>()? {
247 [0] => None,
248 [1] => Some(decode_anchor(&mut reader)?),
249 _ => return Err(WireError::History),
250 };
251 Some(RecoveryLog {
252 generation,
253 accepted,
254 committed,
255 checkpoint,
256 })
257 }
258 _ => return Err(WireError::History),
259 };
260 reader.finish()?;
261 if primary.is_some() != (binding.peer == binding.configuration.primary(scope.view)) {
262 return Err(WireError::History);
263 }
264 let state = RecoveryState {
265 request_id,
266 response: RecoveryResponse {
267 scope,
268 nonce,
269 primary,
270 },
271 };
272 state.validate()?;
273 Ok(state)
274}
275
276fn encode_anchor(writer: &mut Writer<'_>, anchor: CheckpointAnchor) -> Result<(), WireError> {
277 writer.prefix(anchor.predecessor)?;
278 writer.prefix(anchor.position)?;
279 writer.bytes(anchor.schema.as_bytes())?;
280 writer.bytes(anchor.state_digest.as_bytes())?;
281 writer.bytes(&anchor.state_bytes.to_be_bytes())?;
282 writer.bytes(&anchor.chunk_bytes.to_be_bytes())?;
283 Ok(())
284}
285
286fn decode_anchor(reader: &mut Reader<'_>) -> Result<CheckpointAnchor, WireError> {
287 Ok(CheckpointAnchor {
288 predecessor: reader.prefix()?,
289 position: reader.prefix()?,
290 schema: crate::Digest::from_bytes(reader.array()?),
291 state_digest: crate::Digest::from_bytes(reader.array()?),
292 state_bytes: u64::from_be_bytes(reader.array()?),
293 chunk_bytes: u32::from_be_bytes(reader.array()?),
294 })
295}