use std::collections::{BTreeMap, BTreeSet, VecDeque};
use prikk_error::{PrikkError, Result};
use prikk_object::{BlockKind, BlockPayload, MerkleRoot, ObjectId, ObjectType};
use crate::lifecycle_cache::replay::{
LifecycleReplayError, TextCache, apply_candidate_patches, apply_one_block_with_text_cache,
};
use crate::node_lifecycle::NodeLifecycleState;
use crate::object_store::ObjectReader;
use crate::state_root::{compute_state_root, entries_from_state};
pub fn validate_block_v2_shape(payload: &BlockPayload) -> Result<()> {
match (payload.kind, payload.parent_block_ids.as_slice()) {
(BlockKind::Root, []) | (BlockKind::Normal, [_]) => validate_non_merge_shape(payload),
(BlockKind::Merge, [_, _]) => validate_merge_shape(payload),
(BlockKind::Root, _) => Err(PrikkError::Integrity(
"format-2 Root Block must have zero parents".to_string(),
)),
(BlockKind::Normal, _) => Err(PrikkError::Integrity(
"format-2 Normal Block must have exactly one parent".to_string(),
)),
(BlockKind::Merge, _) => Err(PrikkError::Integrity(
"format-2 Merge Block must have exactly two parents".to_string(),
)),
(BlockKind::Repair | BlockKind::Import, _) => Err(PrikkError::Integrity(
"format-2 Block kind is not authorized".to_string(),
)),
}
}
fn validate_non_merge_shape(payload: &BlockPayload) -> Result<()> {
if payload.mainline_parent_id.is_some() || payload.merge_baseline_block_id.is_some() {
return Err(PrikkError::Integrity(format!(
"format-2 {:?} Block must not carry a mainline parent or merge baseline",
payload.kind
)));
}
Ok(())
}
fn validate_merge_shape(payload: &BlockPayload) -> Result<()> {
let Some(mainline) = payload.mainline_parent_id else {
return Err(PrikkError::Integrity(
"format-2 Merge Block must name a mainline parent".to_string(),
));
};
if !payload.parent_block_ids.contains(&mainline) {
return Err(PrikkError::Integrity(
"format-2 Merge Block mainline parent must be one of its own parents".to_string(),
));
}
if payload.merge_baseline_block_id.is_none() {
return Err(PrikkError::Integrity(
"format-2 Merge Block must record the baseline confluence was proven against"
.to_string(),
));
}
Ok(())
}
fn state_derivation_parent(payload: &BlockPayload) -> Option<ObjectId> {
if payload.kind == BlockKind::Merge {
payload.mainline_parent_id
} else {
payload.parent_block_ids.first().copied()
}
}
#[derive(Debug, Default)]
pub(crate) struct LineageStateMemo {
verified: BTreeMap<ObjectId, (NodeLifecycleState, TextCache)>,
}
impl LineageStateMemo {
pub(crate) fn new() -> Self {
Self::default()
}
pub(crate) fn len(&self) -> usize {
self.verified.len()
}
fn evict(&mut self, block_id: &ObjectId) {
self.verified.remove(block_id);
}
}
pub fn derive_next_state_root(
reader: &impl ObjectReader,
parent: Option<ObjectId>,
patch_ids: &[ObjectId],
) -> Result<MerkleRoot> {
derive_next_state_root_with_memo(reader, parent, patch_ids, &mut LineageStateMemo::new())
}
pub(crate) fn derive_next_state_root_with_memo(
reader: &impl ObjectReader,
parent: Option<ObjectId>,
patch_ids: &[ObjectId],
memo: &mut LineageStateMemo,
) -> Result<MerkleRoot> {
let (mut state, mut text_cache) = resolved_parent_state(reader, parent, memo)?;
apply_candidate_patches(reader, &mut state, &mut text_cache, patch_ids)?;
compute_state_root(&entries_from_state(&state)?)
}
#[derive(Debug)]
pub(crate) enum CandidateStateDerivationError {
Lineage(PrikkError),
Patch(LifecycleReplayError),
}
pub(crate) fn derive_next_state_root_for_candidate(
reader: &impl ObjectReader,
parent: Option<ObjectId>,
patch_ids: &[ObjectId],
) -> std::result::Result<MerkleRoot, CandidateStateDerivationError> {
let (mut state, mut text_cache) =
resolved_parent_state(reader, parent, &mut LineageStateMemo::new())
.map_err(CandidateStateDerivationError::Lineage)?;
apply_candidate_patches(reader, &mut state, &mut text_cache, patch_ids)
.map_err(CandidateStateDerivationError::Patch)?;
compute_state_root(&entries_from_state(&state).map_err(CandidateStateDerivationError::Lineage)?)
.map_err(CandidateStateDerivationError::Lineage)
}
fn resolved_parent_state(
reader: &impl ObjectReader,
parent: Option<ObjectId>,
memo: &mut LineageStateMemo,
) -> Result<(NodeLifecycleState, TextCache)> {
match parent {
Some(parent_id) => {
let lineage = validate_v2_lineage(reader, parent_id, memo)?;
verify_v2_lineage_roots(reader, &lineage, memo)?;
memo.verified.get(&parent_id).cloned().ok_or_else(|| {
PrikkError::Integrity(format!(
"format-2 parent Block {parent_id} was not verified before state derivation"
))
})
}
None => Ok((NodeLifecycleState::new(), TextCache::new())),
}
}
pub(crate) fn verify_block_v2_state(
reader: &impl ObjectReader,
block_id: ObjectId,
payload: &BlockPayload,
memo: &mut LineageStateMemo,
) -> Result<()> {
validate_block_v2_shape(payload)?;
let parent = state_derivation_parent(payload);
let (mut state, mut text_cache) = resolved_parent_state(reader, parent, memo)?;
apply_candidate_patches(reader, &mut state, &mut text_cache, &payload.patch_ids)?;
let computed = compute_state_root(&entries_from_state(&state)?)?;
if computed != payload.state_merkle_root {
return Err(PrikkError::Integrity(format!(
"format-2 Block {block_id} state root does not match authoritative replay"
)));
}
memo.verified.insert(block_id, (state, text_cache));
Ok(())
}
#[derive(Debug, Clone, PartialEq, Eq)]
pub enum BlockStateStatus {
Verified,
Failed {
message: String,
},
NotEvaluated {
blocked_by: ObjectId,
},
}
#[derive(Debug, Clone, PartialEq, Eq)]
pub struct BlockStateOutcome {
pub block_id: ObjectId,
pub status: BlockStateStatus,
}
#[derive(Debug, Clone)]
pub(crate) struct TopologicalVerification {
pub(crate) outcomes: Vec<BlockStateOutcome>,
#[allow(dead_code)]
pub(crate) peak_memo_entries: usize,
}
pub(crate) fn verify_blocks_topological(
reader: &impl ObjectReader,
blocks: &[(ObjectId, BlockPayload)],
memo: &mut LineageStateMemo,
) -> Result<TopologicalVerification> {
let by_id: BTreeMap<ObjectId, &BlockPayload> =
blocks.iter().map(|(id, payload)| (*id, payload)).collect();
let mut children: BTreeMap<ObjectId, Vec<ObjectId>> = BTreeMap::new();
let mut pending_parent: BTreeMap<ObjectId, bool> = BTreeMap::new();
for (id, payload) in blocks {
let has_in_batch_parent = match state_derivation_parent(payload) {
Some(parent_id) if by_id.contains_key(&parent_id) => {
children.entry(parent_id).or_default().push(*id);
true
}
_ => false,
};
pending_parent.insert(*id, has_in_batch_parent);
}
let mut remaining_children: BTreeMap<ObjectId, usize> = blocks
.iter()
.map(|(id, _)| (*id, children.get(id).map_or(0, Vec::len)))
.collect();
let mut ready: Vec<ObjectId> = pending_parent
.iter()
.filter(|&(_, has_parent)| !has_parent)
.map(|(id, _)| *id)
.collect();
ready.sort();
let mut queue: VecDeque<ObjectId> = ready.into();
let mut peak = memo.len();
let mut processed = BTreeSet::new();
let mut resolved: BTreeMap<ObjectId, BlockStateStatus> = BTreeMap::new();
let mut outcomes: Vec<BlockStateOutcome> = Vec::with_capacity(blocks.len());
while let Some(id) = queue.pop_front() {
let payload = by_id.get(&id).ok_or_else(|| {
PrikkError::Integrity("format-2 topological pass lost a block".into())
})?;
let in_batch_parent =
state_derivation_parent(payload).filter(|parent_id| by_id.contains_key(parent_id));
let blocking_parent =
in_batch_parent.and_then(|parent_id| match resolved.get(&parent_id) {
Some(BlockStateStatus::Verified) | None => None,
Some(BlockStateStatus::Failed { .. } | BlockStateStatus::NotEvaluated { .. }) => {
Some(parent_id)
}
});
let status = if let Some(blocked_by) = blocking_parent {
BlockStateStatus::NotEvaluated { blocked_by }
} else {
match verify_block_v2_state(reader, id, payload, memo) {
Ok(()) => BlockStateStatus::Verified,
Err(err) => BlockStateStatus::Failed {
message: err.to_string(),
},
}
};
peak = peak.max(memo.len());
resolved.insert(id, status.clone());
outcomes.push(BlockStateOutcome {
block_id: id,
status,
});
processed.insert(id);
if let Some(parent_id) = state_derivation_parent(payload) {
if let Some(count) = remaining_children.get_mut(&parent_id) {
*count = count.saturating_sub(1);
if *count == 0 {
memo.evict(&parent_id);
}
}
}
if remaining_children.get(&id).copied() == Some(0) {
memo.evict(&id);
}
for child in children.get(&id).into_iter().flatten() {
let entry = pending_parent.get_mut(child).ok_or_else(|| {
PrikkError::Integrity("format-2 topological pass lost a tracked child".into())
})?;
*entry = false;
queue.push_back(*child);
}
}
if processed.len() != blocks.len() {
return Err(
match blocks
.iter()
.map(|(id, _)| *id)
.find(|id| !processed.contains(id))
{
Some(stuck) => {
PrikkError::Integrity(format!("format-2 Block lineage cycle at {stuck}"))
}
None => PrikkError::Integrity(
"format-2 topological pass detected an inconsistent cycle count".to_string(),
),
},
);
}
Ok(TopologicalVerification {
outcomes,
peak_memo_entries: peak,
})
}
fn validate_v2_lineage(
reader: &impl ObjectReader,
tip: ObjectId,
memo: &LineageStateMemo,
) -> Result<Vec<(ObjectId, BlockPayload)>> {
let mut visited = BTreeSet::new();
let mut lineage = Vec::new();
let mut current = Some(tip);
while let Some(block_id) = current {
if memo.verified.contains_key(&block_id) {
break;
}
if !visited.insert(block_id) {
return Err(PrikkError::Integrity(format!(
"format-2 Block lineage cycle at {block_id}"
)));
}
let envelope = reader.read_object(block_id)?.ok_or_else(|| {
PrikkError::Integrity(format!("format-2 parent Block {block_id} is missing"))
})?;
if envelope.object_type != ObjectType::Block {
return Err(PrikkError::ObjectTypeMismatch {
expected: ObjectType::Block.to_string(),
actual: envelope.object_type.to_string(),
});
}
if envelope.schema_version != 2 {
return Err(PrikkError::Integrity(format!(
"format-2 lineage contains Block {block_id} with schema {}",
envelope.schema_version
)));
}
let payload = BlockPayload::decode_canonical(&envelope.canonical_payload)?;
validate_block_v2_shape(&payload)?;
current = state_derivation_parent(&payload);
lineage.push((block_id, payload));
}
Ok(lineage)
}
fn verify_v2_lineage_roots(
reader: &impl ObjectReader,
lineage_from_tip: &[(ObjectId, BlockPayload)],
memo: &mut LineageStateMemo,
) -> Result<()> {
let Some((_, deepest)) = lineage_from_tip.last() else {
return Ok(());
};
let (mut state, mut text_cache) = match state_derivation_parent(deepest) {
Some(parent_id) => memo.verified.get(&parent_id).cloned().ok_or_else(|| {
PrikkError::Integrity(format!(
"format-2 parent Block {parent_id} was not verified before state derivation"
))
})?,
None => (NodeLifecycleState::new(), TextCache::new()),
};
for (block_id, payload) in lineage_from_tip.iter().rev() {
apply_one_block_with_text_cache(reader, payload, &mut state, &mut text_cache)?;
let computed = compute_state_root(&entries_from_state(&state)?)?;
if computed != payload.state_merkle_root {
return Err(PrikkError::Integrity(format!(
"format-2 parent Block {block_id} state root does not match authoritative replay"
)));
}
memo.verified
.insert(*block_id, (state.clone(), text_cache.clone()));
}
Ok(())
}
#[cfg(test)]
mod tests;