1use ozzy_proto::{Envelope, Opcode};
4use ozzy_proto::{LinkSessionId, NodeId, RequestId};
5
6use super::{
7 COMMON_BYTES, ControlEncoding, PeerBinding, Reader, WireError, WireLimits, Writer,
8 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 = if state.response.primary.is_some() {
123 PRIMARY_BYTES
124 + usize::from(
125 state
126 .response
127 .primary
128 .is_some_and(|log| log.checkpoint.is_some()),
129 ) * ANCHOR_BYTES
130 } else {
131 BACKUP_BYTES
132 };
133 let header = Envelope {
134 opcode: Opcode::RecoveryState,
135 response: true,
136 request_id: Some(state.request_id),
137 sender,
138 session: Some(session),
139 }
140 .encode_header(metadata_bytes, 0, limits.envelope)?;
141 if output.len() < metadata_bytes {
142 return Err(WireError::Capacity);
143 }
144 let mut writer = Writer::new(&mut output[..metadata_bytes]);
145 writer.scope(state.response.scope, sender);
146 writer.bytes(state.response.nonce.as_bytes());
147 writer.bytes(&[u8::from(state.response.primary.is_some())]);
148 if let Some(log) = state.response.primary {
149 writer.bytes(&log.generation.0.to_be_bytes());
150 writer.prefix(log.accepted);
151 writer.prefix(log.committed);
152 writer.bytes(&[u8::from(log.checkpoint.is_some())]);
153 if let Some(anchor) = log.checkpoint {
154 encode_anchor(&mut writer, anchor);
155 }
156 }
157 Ok(ControlEncoding {
158 header,
159 metadata_bytes,
160 })
161}
162
163pub fn encode_recovery(
165 sender: NodeId,
166 session: LinkSessionId,
167 request: RecoveryRequest,
168 output: &mut [u8],
169 limits: WireLimits,
170) -> Result<ControlEncoding, WireError> {
171 request.validate()?;
172 let header = Envelope {
173 opcode: Opcode::Recovery,
174 response: false,
175 request_id: Some(request.request_id),
176 sender,
177 session: Some(session),
178 }
179 .encode_header(REQUEST_BYTES, 0, limits.envelope)?;
180 if output.len() < REQUEST_BYTES {
181 return Err(WireError::Capacity);
182 }
183 let mut writer = Writer::new(&mut output[..REQUEST_BYTES]);
184 writer.scope(request.scope, sender);
185 writer.bytes(request.nonce.as_bytes());
186 Ok(ControlEncoding {
187 header,
188 metadata_bytes: REQUEST_BYTES,
189 })
190}
191
192pub(super) fn decode_request(
193 scope: Scope,
194 envelope: Envelope,
195 mut reader: Reader<'_>,
196 payload: &[u8],
197) -> Result<RecoveryRequest, WireError> {
198 if !payload.is_empty() {
199 return Err(WireError::Payload);
200 }
201 if envelope.response {
202 return Err(WireError::Correlation);
203 }
204 let request = RecoveryRequest {
205 scope,
206 request_id: envelope.request_id.ok_or(WireError::Correlation)?,
207 nonce: RequestId::from_bytes(reader.bytes()?),
208 };
209 reader.finish()?;
210 request.validate()?;
211 Ok(request)
212}
213
214pub(super) fn decode_state(
215 scope: Scope,
216 envelope: Envelope,
217 mut reader: Reader<'_>,
218 payload: &[u8],
219 binding: PeerBinding,
220) -> Result<RecoveryState, WireError> {
221 if !payload.is_empty() {
222 return Err(WireError::Payload);
223 }
224 if !envelope.response {
225 return Err(WireError::Correlation);
226 }
227 let request_id = envelope.request_id.ok_or(WireError::Correlation)?;
228 let nonce = RequestId::from_bytes(reader.bytes()?);
229 let primary = match reader.bytes::<1>()? {
230 [0] => None,
231 [1] => {
232 let generation = JournalGeneration(u128::from_be_bytes(reader.bytes()?));
233 let accepted = reader.prefix()?;
234 let committed = reader.prefix()?;
235 let checkpoint = match reader.bytes::<1>()? {
236 [0] => None,
237 [1] => Some(decode_anchor(&mut reader)?),
238 _ => return Err(WireError::History),
239 };
240 Some(RecoveryLog {
241 generation,
242 accepted,
243 committed,
244 checkpoint,
245 })
246 }
247 _ => return Err(WireError::History),
248 };
249 reader.finish()?;
250 if primary.is_some() != (binding.peer == binding.configuration.primary(scope.view)) {
251 return Err(WireError::History);
252 }
253 let state = RecoveryState {
254 request_id,
255 response: RecoveryResponse {
256 scope,
257 nonce,
258 primary,
259 },
260 };
261 state.validate()?;
262 Ok(state)
263}
264
265fn encode_anchor(writer: &mut Writer<'_>, anchor: CheckpointAnchor) {
266 writer.prefix(anchor.predecessor);
267 writer.prefix(anchor.position);
268 writer.bytes(anchor.schema.as_bytes());
269 writer.bytes(anchor.state_digest.as_bytes());
270 writer.bytes(&anchor.state_bytes.to_be_bytes());
271 writer.bytes(&anchor.chunk_bytes.to_be_bytes());
272}
273
274fn decode_anchor(reader: &mut Reader<'_>) -> Result<CheckpointAnchor, WireError> {
275 Ok(CheckpointAnchor {
276 predecessor: reader.prefix()?,
277 position: reader.prefix()?,
278 schema: crate::Digest::from_bytes(reader.bytes()?),
279 state_digest: crate::Digest::from_bytes(reader.bytes()?),
280 state_bytes: u64::from_be_bytes(reader.bytes()?),
281 chunk_bytes: u32::from_be_bytes(reader.bytes()?),
282 })
283}