use polyc_state::{
context::CallContext, error::StateError, id::PartitionId, journal::PartitionJournal,
};
#[derive(Debug, Clone, PartialEq, Eq)]
pub struct VerifyPartitionReplay {
partition: PartitionId,
}
impl VerifyPartitionReplay {
#[must_use]
pub const fn new(partition: PartitionId) -> Self {
Self { partition }
}
#[must_use]
pub const fn partition(&self) -> &PartitionId {
&self.partition
}
}
#[derive(Debug, Clone, Copy, PartialEq, Eq)]
pub enum ViolationCategory {
TruncatedReplay,
MalformedRoot,
SignatureInvalid,
RootMismatch,
MerkleLog,
Unreadable,
RepairInterrupted,
Unclassified,
}
impl ViolationCategory {
#[must_use]
pub const fn as_str(self) -> &'static str {
match self {
Self::TruncatedReplay => "truncated_replay",
Self::MalformedRoot => "malformed_root_marker",
Self::SignatureInvalid => "signature_invalid",
Self::RootMismatch => "root_mismatch",
Self::MerkleLog => "mmr_error",
Self::Unreadable => "unreadable_replay",
Self::RepairInterrupted => "repair_interrupted",
Self::Unclassified => "integrity_check_failed",
}
}
#[must_use]
pub fn from_identifier(identifier: &str) -> Self {
match identifier {
"truncated_replay" => Self::TruncatedReplay,
"malformed_root_marker" => Self::MalformedRoot,
"signature_invalid" => Self::SignatureInvalid,
"root_mismatch" => Self::RootMismatch,
"mmr_error" => Self::MerkleLog,
"unreadable_replay" => Self::Unreadable,
"repair_interrupted" => Self::RepairInterrupted,
_ => Self::Unclassified,
}
}
}
impl std::fmt::Display for ViolationCategory {
fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
f.write_str(self.as_str())
}
}
#[derive(Debug, Clone, PartialEq, Eq)]
pub enum PartitionVerification {
Verified {
event_count: u64,
signed_root_count: u64,
},
Violation {
category: ViolationCategory,
reason: String,
},
}
impl PartitionVerification {
#[must_use]
pub const fn verified(event_count: u64, signed_root_count: u64) -> Self {
Self::Verified {
event_count,
signed_root_count,
}
}
#[must_use]
pub fn violation(category: ViolationCategory, reason: impl Into<String>) -> Self {
Self::Violation {
category,
reason: reason.into(),
}
}
#[must_use]
pub const fn is_verified(&self) -> bool {
matches!(self, Self::Verified { .. })
}
#[must_use]
pub const fn event_count(&self) -> Option<u64> {
match self {
Self::Verified { event_count, .. } => Some(*event_count),
Self::Violation { .. } => None,
}
}
#[must_use]
pub const fn signed_root_count(&self) -> Option<u64> {
match self {
Self::Verified {
signed_root_count, ..
} => Some(*signed_root_count),
Self::Violation { .. } => None,
}
}
#[must_use]
pub const fn category(&self) -> Option<ViolationCategory> {
match self {
Self::Verified { .. } => None,
Self::Violation { category, .. } => Some(*category),
}
}
#[must_use]
pub fn reason(&self) -> Option<&str> {
match self {
Self::Verified { .. } => None,
Self::Violation { reason, .. } => Some(reason),
}
}
}
pub trait VerifiedReplay: Send + Sync {
fn verify_replay(
&self,
request: VerifyPartitionReplay,
context: &CallContext,
) -> Result<PartitionVerification, StateError>;
}
pub trait JournalAuthority:
PartitionJournal + polyc_state::journal::JournalDestructionWorkflow + VerifiedReplay
{
}
impl<T> JournalAuthority for T where
T: PartitionJournal + polyc_state::journal::JournalDestructionWorkflow + VerifiedReplay
{
}
#[cfg(test)]
mod tests {
#![allow(clippy::pedantic, clippy::nursery, missing_docs, clippy::unwrap_used)]
use super::*;
#[test]
fn a_verdict_reports_exactly_one_of_a_count_and_a_reason() {
let verified = PartitionVerification::verified(7, 2);
assert!(verified.is_verified());
assert_eq!(verified.event_count(), Some(7));
assert_eq!(verified.signed_root_count(), Some(2));
assert_eq!(verified.category(), None);
assert_eq!(verified.reason(), None);
let violation = PartitionVerification::violation(
ViolationCategory::RootMismatch,
"a root does not hold",
);
assert!(!violation.is_verified());
assert_eq!(violation.event_count(), None);
assert_eq!(violation.signed_root_count(), None);
assert_eq!(violation.category(), Some(ViolationCategory::RootMismatch));
assert_eq!(violation.reason(), Some("a root does not hold"));
}
#[test]
fn every_category_round_trips_through_its_stable_identifier() {
for category in [
ViolationCategory::TruncatedReplay,
ViolationCategory::MalformedRoot,
ViolationCategory::SignatureInvalid,
ViolationCategory::RootMismatch,
ViolationCategory::MerkleLog,
ViolationCategory::Unreadable,
ViolationCategory::RepairInterrupted,
ViolationCategory::Unclassified,
] {
assert_eq!(
ViolationCategory::from_identifier(category.as_str()),
category
);
}
assert_eq!(
ViolationCategory::from_identifier("a_category_from_the_future"),
ViolationCategory::Unclassified
);
assert_eq!(
ViolationCategory::from_identifier(""),
ViolationCategory::Unclassified
);
}
#[test]
fn an_empty_partition_verifies_over_zero_events() {
let verdict = PartitionVerification::verified(0, 0);
assert!(verdict.is_verified());
assert_eq!(verdict.event_count(), Some(0));
assert_eq!(verdict.signed_root_count(), Some(0));
}
#[test]
fn a_request_carries_the_partition_it_asks_about() {
let request = VerifyPartitionReplay::new(PartitionId::new("conv-1"));
assert_eq!(request.partition(), &PartitionId::new("conv-1"));
}
}