use core::hash::{Hash, Hasher};
use crate::{
CheckpointGeneration, CommitSeq, ElementId, IncidenceId, IndexId, LabelId, ProjectionId,
PropertyFamily, PropertyKeyId, PropertySubject, PropertyType, PropertyValue, RelationId,
RelationTypeId, RoleId, TransactionId,
projection::{ProjectionElementId, ProjectionIncidenceId, ProjectionRelationId},
wire,
};
#[derive(Default)]
struct ProofHasher {
state: u64,
}
impl Hasher for ProofHasher {
fn finish(&self) -> u64 {
self.state
}
fn write(&mut self, bytes: &[u8]) {
for byte in bytes {
self.state = self.state.wrapping_mul(257).wrapping_add(u64::from(*byte));
}
}
}
fn proof_hash<T: Hash>(value: T) -> u64 {
let mut hasher = ProofHasher::default();
value.hash(&mut hasher);
hasher.finish()
}
macro_rules! prove_id_newtype {
($type:ty, $checked_next:ident, $ord_eq:ident, $hash_eq:ident) => {
#[kani::proof]
fn $checked_next() {
let raw: u64 = kani::any();
kani::assume(raw < u64::MAX);
let current = <$type>::new(raw);
let Some(next) = current.checked_next() else {
assert!(false);
return;
};
assert!(next > current);
assert_eq!(next.get(), raw + 1);
}
#[kani::proof]
fn $ord_eq() {
let left_raw: u64 = kani::any();
let right_raw: u64 = kani::any();
let left = <$type>::new(left_raw);
let right = <$type>::new(right_raw);
assert_eq!(left == right, left.cmp(&right).is_eq());
}
#[kani::proof]
fn $hash_eq() {
let raw: u64 = kani::any();
let left = <$type>::new(raw);
let right = <$type>::new(raw);
assert_eq!(proof_hash(left), proof_hash(right));
}
};
}
macro_rules! prove_projection_id {
($type:ty, $ord_eq:ident, $hash_eq:ident) => {
#[kani::proof]
fn $ord_eq() {
let left_raw: u32 = kani::any();
let right_raw: u32 = kani::any();
let left = <$type>::new(left_raw);
let right = <$type>::new(right_raw);
assert_eq!(left == right, left.cmp(&right).is_eq());
}
#[kani::proof]
fn $hash_eq() {
let raw: u32 = kani::any();
let left = <$type>::new(raw);
let right = <$type>::new(raw);
assert_eq!(proof_hash(left), proof_hash(right));
}
};
}
prove_id_newtype!(
ElementId,
element_id_checked_next_is_monotonic,
element_id_ordering_equality_matches_value_equality,
element_id_hash_matches_equality
);
prove_id_newtype!(
RelationId,
relation_id_checked_next_is_monotonic,
relation_id_ordering_equality_matches_value_equality,
relation_id_hash_matches_equality
);
prove_id_newtype!(
IncidenceId,
incidence_id_checked_next_is_monotonic,
incidence_id_ordering_equality_matches_value_equality,
incidence_id_hash_matches_equality
);
prove_id_newtype!(
RoleId,
role_id_checked_next_is_monotonic,
role_id_ordering_equality_matches_value_equality,
role_id_hash_matches_equality
);
prove_id_newtype!(
LabelId,
label_id_checked_next_is_monotonic,
label_id_ordering_equality_matches_value_equality,
label_id_hash_matches_equality
);
prove_id_newtype!(
RelationTypeId,
relation_type_id_checked_next_is_monotonic,
relation_type_id_ordering_equality_matches_value_equality,
relation_type_id_hash_matches_equality
);
prove_id_newtype!(
PropertyKeyId,
property_key_id_checked_next_is_monotonic,
property_key_id_ordering_equality_matches_value_equality,
property_key_id_hash_matches_equality
);
prove_id_newtype!(
ProjectionId,
projection_id_checked_next_is_monotonic,
projection_id_ordering_equality_matches_value_equality,
projection_id_hash_matches_equality
);
prove_id_newtype!(
IndexId,
index_id_checked_next_is_monotonic,
index_id_ordering_equality_matches_value_equality,
index_id_hash_matches_equality
);
prove_id_newtype!(
CommitSeq,
commit_sequence_checked_next_is_monotonic,
commit_sequence_ordering_equality_matches_value_equality,
commit_sequence_hash_matches_equality
);
prove_id_newtype!(
TransactionId,
transaction_id_checked_next_is_monotonic,
transaction_id_ordering_equality_matches_value_equality,
transaction_id_hash_matches_equality
);
prove_id_newtype!(
CheckpointGeneration,
checkpoint_generation_checked_next_is_monotonic,
checkpoint_generation_ordering_equality_matches_value_equality,
checkpoint_generation_hash_matches_equality
);
prove_projection_id!(
ProjectionElementId,
projection_element_id_ordering_equality_matches_value_equality,
projection_element_id_hash_matches_equality
);
prove_projection_id!(
ProjectionRelationId,
projection_relation_id_ordering_equality_matches_value_equality,
projection_relation_id_hash_matches_equality
);
prove_projection_id!(
ProjectionIncidenceId,
projection_incidence_id_ordering_equality_matches_value_equality,
projection_incidence_id_hash_matches_equality
);
#[kani::proof]
fn wire_relation_type_roundtrip() {
let raw: u64 = kani::any();
kani::assume(raw >= 1);
let id = RelationTypeId::new(raw);
assert_eq!(
wire::decode_relation_type(wire::encode_relation_type(Some(id))),
Some(id)
);
assert_eq!(wire::encode_relation_type(None), wire::RELATION_TYPE_NONE);
assert_eq!(wire::decode_relation_type(wire::RELATION_TYPE_NONE), None);
}
#[kani::proof]
fn wire_property_family_tag_roundtrip() {
for family in [
PropertyFamily::Element,
PropertyFamily::Relation,
PropertyFamily::Incidence,
] {
assert_eq!(
wire::property_family_from_tag(wire::property_family_tag(family)),
Some(family)
);
}
let tag: u32 = kani::any();
kani::assume(tag > 2);
assert_eq!(wire::property_family_from_tag(tag), None);
}
#[kani::proof]
fn wire_property_type_tag_roundtrip() {
for value_type in [
PropertyType::Boolean,
PropertyType::Integer,
PropertyType::Text,
] {
assert_eq!(
wire::property_type_from_tag(wire::property_type_tag(value_type)),
Some(value_type)
);
}
let tag: u32 = kani::any();
kani::assume(tag > 2);
assert_eq!(wire::property_type_from_tag(tag), None);
}
#[kani::proof]
fn wire_subject_roundtrip() {
let raw: u64 = kani::any();
let subjects = [
PropertySubject::Element(ElementId::new(raw)),
PropertySubject::Relation(RelationId::new(raw)),
PropertySubject::Incidence(IncidenceId::new(raw)),
];
for subject in subjects {
let (kind, id) = wire::encode_subject(subject);
assert_eq!(wire::decode_subject(kind, id), Some(subject));
}
let kind: u32 = kani::any();
kani::assume(kind > 2);
assert_eq!(wire::decode_subject(kind, raw), None);
}
#[kani::proof]
fn wire_flags_pack_roundtrip() {
let low: u32 = kani::any();
let high: u32 = kani::any();
kani::assume(low <= 0xFFFF);
kani::assume(high <= 0xFFFF);
assert_eq!(wire::unpack_flags(wire::pack_flags(low, high)), (low, high));
}
fn any_op(op_kind: u32) -> wire::MutationOp {
let flags: u32 = kani::any();
let mut payload = [zerocopy::byteorder::U64::<zerocopy::byteorder::LE>::new(0);
wire::MUTATION_OP_PAYLOAD_WORDS];
let mut index = 0;
while index < wire::MUTATION_OP_PAYLOAD_WORDS {
let word: u64 = kani::any();
payload[index] = zerocopy::byteorder::U64::new(word);
index += 1;
}
wire::MutationOp {
op_kind: op_kind.into(),
flags: flags.into(),
payload,
}
}
macro_rules! prove_op_roundtrip {
($name:ident, $op_kind:path) => {
#[kani::proof]
#[kani::unwind(72)]
fn $name() {
use zerocopy::{FromBytes, IntoBytes};
let op = any_op($op_kind);
let bytes = op.as_bytes();
let Ok(decoded) = wire::MutationOp::read_from_bytes(bytes) else {
assert!(false);
return;
};
assert!(op == decoded);
assert_eq!(decoded.op_kind.get(), $op_kind);
}
};
}
prove_op_roundtrip!(op_create_element_roundtrips, wire::OP_CREATE_ELEMENT);
prove_op_roundtrip!(op_tombstone_element_roundtrips, wire::OP_TOMBSTONE_ELEMENT);
prove_op_roundtrip!(op_create_relation_roundtrips, wire::OP_CREATE_RELATION);
prove_op_roundtrip!(
op_tombstone_relation_roundtrips,
wire::OP_TOMBSTONE_RELATION
);
prove_op_roundtrip!(op_create_incidence_roundtrips, wire::OP_CREATE_INCIDENCE);
prove_op_roundtrip!(
op_tombstone_incidence_roundtrips,
wire::OP_TOMBSTONE_INCIDENCE
);
prove_op_roundtrip!(op_set_relation_type_roundtrips, wire::OP_SET_RELATION_TYPE);
prove_op_roundtrip!(op_add_element_label_roundtrips, wire::OP_ADD_ELEMENT_LABEL);
prove_op_roundtrip!(
op_add_relation_label_roundtrips,
wire::OP_ADD_RELATION_LABEL
);
prove_op_roundtrip!(op_set_property_roundtrips, wire::OP_SET_PROPERTY);
prove_op_roundtrip!(op_remove_property_roundtrips, wire::OP_REMOVE_PROPERTY);
prove_op_roundtrip!(op_register_role_roundtrips, wire::OP_CATALOG_REGISTER_ROLE);
prove_op_roundtrip!(
op_register_label_roundtrips,
wire::OP_CATALOG_REGISTER_LABEL
);
prove_op_roundtrip!(
op_register_relation_type_roundtrips,
wire::OP_CATALOG_REGISTER_RELATION_TYPE
);
prove_op_roundtrip!(
op_register_property_key_roundtrips,
wire::OP_CATALOG_REGISTER_PROPERTY_KEY
);
prove_op_roundtrip!(
op_register_projection_roundtrips,
wire::OP_CATALOG_REGISTER_PROJECTION
);
prove_op_roundtrip!(
op_register_index_roundtrips,
wire::OP_CATALOG_REGISTER_INDEX
);
prove_op_roundtrip!(op_next_id_watermark_roundtrips, wire::OP_NEXT_ID_WATERMARK);
#[kani::proof]
#[kani::unwind(80)]
fn single_record_framing_consistent() {
use zerocopy::{FromBytes, IntoBytes};
let lsn: u64 = kani::any();
let txn: u64 = kani::any();
let generation: u64 = kani::any();
let op = any_op(wire::OP_CREATE_ELEMENT);
let byte: u8 = kani::any();
let header_len = size_of::<wire::LogRecordHeader>();
let op_len = size_of::<wire::MutationOp>();
let total = header_len + op_len + 1;
let header = wire::LogRecordHeader {
base_generation: generation.into(),
lsn: lsn.into(),
txn_id: txn.into(),
magic: wire::OXGLOGR.into(),
len: (total as u32).into(),
op_count: 1u32.into(),
crc32c: 0u32.into(),
};
let mut record = Vec::with_capacity(total);
record.extend_from_slice(header.as_bytes());
record.extend_from_slice(op.as_bytes());
record.push(byte);
assert_eq!(record.len(), total);
let Ok(parsed) = wire::LogRecordHeader::read_from_bytes(&record[..header_len]) else {
assert!(false);
return;
};
assert_eq!(parsed.len.get() as usize, record.len());
assert_eq!(parsed.magic.get(), wire::OXGLOGR);
assert_eq!(parsed.lsn.get(), lsn);
assert_eq!(parsed.txn_id.get(), txn);
assert_eq!(parsed.base_generation.get(), generation);
assert_eq!(parsed.op_count.get(), 1);
let Ok(decoded_op) =
wire::MutationOp::read_from_bytes(&record[header_len..header_len + op_len])
else {
assert!(false);
return;
};
assert!(decoded_op == op);
assert_eq!(record[header_len + op_len], byte);
}
#[cfg(feature = "kani-heavy")]
#[kani::proof]
#[kani::unwind(8)]
fn overlay_merge_is_total_and_duplicate_free() {
use crate::overlay::{BaseRecords, MergedState, Overlay, StateView};
let base_has: [bool; 3] = [kani::any(), kani::any(), kani::any()];
let overlay_kind: [u8; 3] = [kani::any(), kani::any(), kani::any()];
for kind in overlay_kind {
kani::assume(kind <= 2);
}
let ids = [ElementId::new(1), ElementId::new(2), ElementId::new(3)];
let base_ids: Vec<ElementId> = ids
.iter()
.copied()
.enumerate()
.filter_map(|(index, id)| base_has[index].then_some(id))
.collect();
let base = BaseRecords::proof_elements(&base_ids);
let overlay_entries: Vec<(ElementId, bool)> = ids
.iter()
.copied()
.enumerate()
.filter_map(|(index, id)| match overlay_kind[index] {
1 => Some((id, true)),
2 => Some((id, false)),
_no_opinion => None,
})
.collect();
let overlay = Overlay::proof_element_entries(&overlay_entries);
let view = MergedState::new(&base, &overlay);
let merged: Vec<ElementId> = view.elements().map(|record| record.id).collect();
let mut index = 1;
while index < merged.len() {
assert!(merged[index - 1] < merged[index]);
index += 1;
}
let mut id_index = 0;
while id_index < ids.len() {
let id = ids[id_index];
let overlay_set = overlay_kind[id_index] == 1;
let overlay_tombstone = overlay_kind[id_index] == 2;
let base_present = base_has[id_index];
let expected_visible = overlay_set || (base_present && !overlay_tombstone);
let in_merged = merged.iter().any(|candidate| *candidate == id);
assert_eq!(in_merged, expected_visible);
let point = view.element(id);
assert_eq!(point.is_some(), expected_visible);
assert_eq!(
point.map(|record| record.id),
expected_visible.then_some(id)
);
if overlay_tombstone {
assert!(overlay.proof_is_element_tombstoned(id));
}
id_index += 1;
}
}
#[cfg(feature = "kani-heavy")]
#[kani::proof]
#[kani::unwind(6)]
fn overlay_tombstone_is_idempotent() {
use crate::overlay::{BaseRecords, MergedState, Overlay, StateView, WriteOverlay};
let raw: u64 = kani::any();
kani::assume(raw >= 1);
let id = ElementId::new(raw);
let base_present: bool = kani::any();
let base_ids: Vec<ElementId> = if base_present { vec![id] } else { Vec::new() };
let base = BaseRecords::proof_elements(&base_ids);
let next = Overlay::proof_element_entries(&[]).next_ids();
let mut once = WriteOverlay::new(next, crate::Catalog::empty());
once.tombstone_element(&base, id);
let once = once.freeze();
let mut twice = WriteOverlay::new(next, crate::Catalog::empty());
twice.tombstone_element(&base, id);
twice.tombstone_element(&base, id);
let twice = twice.freeze();
assert!(MergedState::new(&base, &once).element(id).is_none());
assert!(MergedState::new(&base, &twice).element(id).is_none());
assert!(once.proof_is_element_tombstoned(id));
assert!(twice.proof_is_element_tombstoned(id));
if !base_present {
assert_eq!(MergedState::new(&base, &once).element_count(), 0);
}
}
#[cfg(feature = "kani-heavy")]
#[kani::proof]
#[kani::unwind(4)]
fn overlay_watermark_monotonic_under_apply() {
use crate::overlay::{Overlay, OverlayLayer, WriteOverlay};
let raw: u64 = kani::any();
kani::assume(raw >= 1 && raw < u64::MAX - 1);
let parent = Overlay::proof_with_next_element(raw);
let mut child_write = WriteOverlay::new(parent.next_ids(), parent.catalog().clone());
let created = child_write.create_element().expect("create");
assert_eq!(created.get(), raw);
let child = parent.with_applied(&child_write);
assert!(child.next_ids().element > parent.next_ids().element);
assert_eq!(child.next_ids().element.get(), raw + 1);
assert_eq!(child.next_ids().relation, parent.next_ids().relation);
assert_eq!(child.next_ids().role, parent.next_ids().role);
}
#[kani::proof]
fn property_value_try_from_u64_matches_checked() {
let raw: u64 = kani::any();
match PropertyValue::try_from(raw) {
Ok(PropertyValue::Integer(got)) => assert_eq!(Some(got), i64::try_from(raw).ok()),
Ok(_) => assert!(false, "u64 conversion must yield Integer or an error"),
Err(_) => assert!(i64::try_from(raw).is_err()),
}
}
#[kani::proof]
fn property_value_as_count_matches_checked() {
let raw: i64 = kani::any();
assert_eq!(
PropertyValue::Integer(raw).as_count(),
usize::try_from(raw).ok()
);
}