use ozzy_proto::{Envelope, Opcode};
use ozzy_proto::{LinkSessionId, NodeId, RequestId};
use super::{
COMMON_BYTES, ControlEncoding, PeerBinding, Reader, WireError, WireLimits, Writer,
validate_prefix, validate_scope,
};
use crate::recovery::{CheckpointAnchor, RecoveryLog, RecoveryResponse};
use crate::{JournalGeneration, Scope};
const REQUEST_BYTES: usize = COMMON_BYTES + 16;
const BACKUP_BYTES: usize = REQUEST_BYTES + 1;
const PRIMARY_BYTES: usize = BACKUP_BYTES + 16 + 40 + 40 + 1;
const ANCHOR_BYTES: usize = 40 + 40 + 32 + 32 + 8 + 4;
#[derive(Debug, Clone, Copy, PartialEq, Eq)]
pub struct RecoveryRequest {
pub scope: Scope,
pub request_id: RequestId,
pub nonce: RequestId,
}
impl RecoveryRequest {
fn validate(self) -> Result<(), WireError> {
validate_scope(self.scope)?;
if self.request_id.as_bytes() == &[0; 16] || self.nonce.as_bytes() == &[0; 16] {
return Err(WireError::Correlation);
}
Ok(())
}
}
#[derive(Debug, Clone, Copy, PartialEq, Eq)]
pub struct RecoveryState {
pub request_id: RequestId,
pub response: RecoveryResponse,
}
impl RecoveryState {
pub fn validate_response(self, request: RecoveryRequest) -> Result<(), WireError> {
self.validate()?;
request.validate()?;
if self.request_id != request.request_id || self.response.nonce != request.nonce {
return Err(WireError::Correlation);
}
if self.response.scope
!= (Scope {
view: self.response.scope.view,
..request.scope
})
{
return Err(WireError::Scope);
}
Ok(())
}
fn validate(self) -> Result<(), WireError> {
RecoveryRequest {
scope: self.response.scope,
request_id: self.request_id,
nonce: self.response.nonce,
}
.validate()?;
if let Some(log) = self.response.primary {
if let Some(anchor) = log.checkpoint {
anchor
.validate(log.committed)
.map_err(|_| WireError::History)?;
}
validate_prefix(log.accepted)?;
validate_prefix(log.committed)?;
if log.generation.0 == 0
|| log.committed.op > log.accepted.op
|| (log.committed.op == log.accepted.op && log.committed != log.accepted)
{
return Err(WireError::History);
}
}
Ok(())
}
}
#[derive(Debug, Clone, Copy, PartialEq, Eq)]
#[expect(
clippy::large_enum_variant,
reason = "bounded sans-I/O decoding returns inline evidence without allocation"
)]
pub enum RecoveryMessage {
Request(RecoveryRequest),
State(RecoveryState),
}
pub fn encode_recovery_state(
sender: NodeId,
session: LinkSessionId,
state: RecoveryState,
output: &mut [u8],
limits: WireLimits,
) -> Result<ControlEncoding, WireError> {
state.validate()?;
let metadata_bytes = if state.response.primary.is_some() {
PRIMARY_BYTES
+ usize::from(
state
.response
.primary
.is_some_and(|log| log.checkpoint.is_some()),
) * ANCHOR_BYTES
} else {
BACKUP_BYTES
};
let header = Envelope {
opcode: Opcode::RecoveryState,
response: true,
request_id: Some(state.request_id),
sender,
session: Some(session),
}
.encode_header(metadata_bytes, 0, limits.envelope)?;
if output.len() < metadata_bytes {
return Err(WireError::Capacity);
}
let mut writer = Writer::new(&mut output[..metadata_bytes]);
writer.scope(state.response.scope, sender);
writer.bytes(state.response.nonce.as_bytes());
writer.bytes(&[u8::from(state.response.primary.is_some())]);
if let Some(log) = state.response.primary {
writer.bytes(&log.generation.0.to_be_bytes());
writer.prefix(log.accepted);
writer.prefix(log.committed);
writer.bytes(&[u8::from(log.checkpoint.is_some())]);
if let Some(anchor) = log.checkpoint {
encode_anchor(&mut writer, anchor);
}
}
Ok(ControlEncoding {
header,
metadata_bytes,
})
}
pub fn encode_recovery(
sender: NodeId,
session: LinkSessionId,
request: RecoveryRequest,
output: &mut [u8],
limits: WireLimits,
) -> Result<ControlEncoding, WireError> {
request.validate()?;
let header = Envelope {
opcode: Opcode::Recovery,
response: false,
request_id: Some(request.request_id),
sender,
session: Some(session),
}
.encode_header(REQUEST_BYTES, 0, limits.envelope)?;
if output.len() < REQUEST_BYTES {
return Err(WireError::Capacity);
}
let mut writer = Writer::new(&mut output[..REQUEST_BYTES]);
writer.scope(request.scope, sender);
writer.bytes(request.nonce.as_bytes());
Ok(ControlEncoding {
header,
metadata_bytes: REQUEST_BYTES,
})
}
pub(super) fn decode_request(
scope: Scope,
envelope: Envelope,
mut reader: Reader<'_>,
payload: &[u8],
) -> Result<RecoveryRequest, WireError> {
if !payload.is_empty() {
return Err(WireError::Payload);
}
if envelope.response {
return Err(WireError::Correlation);
}
let request = RecoveryRequest {
scope,
request_id: envelope.request_id.ok_or(WireError::Correlation)?,
nonce: RequestId::from_bytes(reader.bytes()?),
};
reader.finish()?;
request.validate()?;
Ok(request)
}
pub(super) fn decode_state(
scope: Scope,
envelope: Envelope,
mut reader: Reader<'_>,
payload: &[u8],
binding: PeerBinding,
) -> Result<RecoveryState, WireError> {
if !payload.is_empty() {
return Err(WireError::Payload);
}
if !envelope.response {
return Err(WireError::Correlation);
}
let request_id = envelope.request_id.ok_or(WireError::Correlation)?;
let nonce = RequestId::from_bytes(reader.bytes()?);
let primary = match reader.bytes::<1>()? {
[0] => None,
[1] => {
let generation = JournalGeneration(u128::from_be_bytes(reader.bytes()?));
let accepted = reader.prefix()?;
let committed = reader.prefix()?;
let checkpoint = match reader.bytes::<1>()? {
[0] => None,
[1] => Some(decode_anchor(&mut reader)?),
_ => return Err(WireError::History),
};
Some(RecoveryLog {
generation,
accepted,
committed,
checkpoint,
})
}
_ => return Err(WireError::History),
};
reader.finish()?;
if primary.is_some() != (binding.peer == binding.configuration.primary(scope.view)) {
return Err(WireError::History);
}
let state = RecoveryState {
request_id,
response: RecoveryResponse {
scope,
nonce,
primary,
},
};
state.validate()?;
Ok(state)
}
fn encode_anchor(writer: &mut Writer<'_>, anchor: CheckpointAnchor) {
writer.prefix(anchor.predecessor);
writer.prefix(anchor.position);
writer.bytes(anchor.schema.as_bytes());
writer.bytes(anchor.state_digest.as_bytes());
writer.bytes(&anchor.state_bytes.to_be_bytes());
writer.bytes(&anchor.chunk_bytes.to_be_bytes());
}
fn decode_anchor(reader: &mut Reader<'_>) -> Result<CheckpointAnchor, WireError> {
Ok(CheckpointAnchor {
predecessor: reader.prefix()?,
position: reader.prefix()?,
schema: crate::Digest::from_bytes(reader.bytes()?),
state_digest: crate::Digest::from_bytes(reader.bytes()?),
state_bytes: u64::from_be_bytes(reader.bytes()?),
chunk_bytes: u32::from_be_bytes(reader.bytes()?),
})
}