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