use crate::evidence::{content_hash, sort_findings, sorted_findings};
use serde::{Deserialize, Serialize};
pub const STATE_TRANSITION_EVENT_SCHEMA_VERSION: u32 = 1;
pub const STATE_TRANSITION_REPORT_SCHEMA_VERSION: u32 = 1;
pub const TRANSITION_INVALID_DISALLOWED_EDGE: u32 = 1;
#[derive(Clone, Copy, Debug, PartialEq, Eq, Hash, PartialOrd, Ord, Serialize, Deserialize)]
pub struct TransitionMachineId(pub [u8; 32]);
#[derive(Clone, Copy, Debug, PartialEq, Eq, Hash, PartialOrd, Ord, Serialize, Deserialize)]
pub struct TransitionSubjectId(pub [u8; 32]);
#[derive(Clone, Copy, Debug, PartialEq, Eq, Hash, PartialOrd, Ord, Serialize, Deserialize)]
pub struct TransitionId(pub [u8; 32]);
#[derive(Clone, Debug, PartialEq, Eq, PartialOrd, Ord, Serialize, Deserialize)]
pub struct TransitionCauseRef {
pub lane: u32,
pub opaque_key: Vec<u8>,
}
#[derive(Clone, Debug, PartialEq, Eq, Serialize, Deserialize)]
pub struct StateTransitionEvent {
pub schema_version: u32,
pub machine_id: TransitionMachineId,
pub subject_id: TransitionSubjectId,
pub previous_state: u64,
pub next_state: u64,
pub transition_id: TransitionId,
pub causes: Vec<TransitionCauseRef>,
pub ordering_sequence: Option<u64>,
pub frontier_digest: Option<[u8; 32]>,
}
#[derive(Clone, Debug, PartialEq, Eq, PartialOrd, Ord, Serialize, Deserialize)]
pub enum StateTransitionFinding {
UnsupportedEventSchemaVersion {
observed: u32,
expected: u32,
},
InvalidTransition {
machine_id: TransitionMachineId,
subject_id: TransitionSubjectId,
from_state: u64,
to_state: u64,
reason_code: u32,
},
UnsortedCausesInSourceEvent,
UnsortedAllowedTransitionEdges,
}
#[derive(Clone, Debug, PartialEq, Eq, Serialize, Deserialize)]
pub struct StateTransitionReportBody {
pub schema_version: u32,
pub transition_event_digest: [u8; 32],
pub machine_id: TransitionMachineId,
pub subject_id: TransitionSubjectId,
pub previous_state: u64,
pub next_state: u64,
pub transition_id: TransitionId,
pub causes_sorted: Vec<TransitionCauseRef>,
pub ordering_sequence: Option<u64>,
pub frontier_digest: Option<[u8; 32]>,
pub findings: Vec<StateTransitionFinding>,
}
pub type TransitionEvidenceDigest = [u8; 32];
#[must_use]
pub fn normalize_state_transition_event(event: &StateTransitionEvent) -> StateTransitionEvent {
let mut causes = event.causes.clone();
causes.sort();
StateTransitionEvent {
causes,
..event.clone()
}
}
pub fn state_transition_event_bytes(
event: &StateTransitionEvent,
) -> Result<Vec<u8>, rmp_serde::encode::Error> {
let normalized = normalize_state_transition_event(event);
crate::encoding::to_bytes(&normalized)
}
pub fn state_transition_event_digest(
event: &StateTransitionEvent,
) -> Result<TransitionEvidenceDigest, rmp_serde::encode::Error> {
let bytes = state_transition_event_bytes(event)?;
Ok(content_hash(&bytes))
}
#[must_use]
pub fn transition_causes_are_sorted(causes: &[TransitionCauseRef]) -> bool {
causes.windows(2).all(|w| w[0] <= w[1])
}
#[must_use]
pub fn allowed_transition_edges_are_sorted(allowed_edges: &[(u64, u64)]) -> bool {
allowed_edges.windows(2).all(|w| w[0] <= w[1])
}
fn edge_allowed(allowed_edges_sorted: &[(u64, u64)], from: u64, to: u64) -> bool {
allowed_edges_sorted
.binary_search_by(|e| (e.0, e.1).cmp(&(from, to)))
.is_ok()
}
pub fn build_state_transition_report(
event: &StateTransitionEvent,
allowed_edges: &[(u64, u64)],
) -> Result<StateTransitionReportBody, rmp_serde::encode::Error> {
let mut findings = Vec::new();
if event.schema_version != STATE_TRANSITION_EVENT_SCHEMA_VERSION {
findings.push(StateTransitionFinding::UnsupportedEventSchemaVersion {
observed: event.schema_version,
expected: STATE_TRANSITION_EVENT_SCHEMA_VERSION,
});
}
if !transition_causes_are_sorted(&event.causes) {
findings.push(StateTransitionFinding::UnsortedCausesInSourceEvent);
}
let mut edges_sorted: Vec<(u64, u64)> = allowed_edges.to_vec();
edges_sorted.sort();
if edges_sorted.as_slice() != allowed_edges {
findings.push(StateTransitionFinding::UnsortedAllowedTransitionEdges);
}
let allowed = edges_sorted.as_slice();
if !edge_allowed(allowed, event.previous_state, event.next_state) {
findings.push(StateTransitionFinding::InvalidTransition {
machine_id: event.machine_id,
subject_id: event.subject_id,
from_state: event.previous_state,
to_state: event.next_state,
reason_code: TRANSITION_INVALID_DISALLOWED_EDGE,
});
}
let normalized = normalize_state_transition_event(event);
let transition_event_digest = state_transition_event_digest(event)?;
sort_findings(&mut findings);
Ok(StateTransitionReportBody {
schema_version: STATE_TRANSITION_REPORT_SCHEMA_VERSION,
transition_event_digest,
machine_id: normalized.machine_id,
subject_id: normalized.subject_id,
previous_state: normalized.previous_state,
next_state: normalized.next_state,
transition_id: normalized.transition_id,
causes_sorted: normalized.causes,
ordering_sequence: normalized.ordering_sequence,
frontier_digest: normalized.frontier_digest,
findings,
})
}
pub fn state_transition_report_body_hash(
report: &StateTransitionReportBody,
) -> Result<TransitionEvidenceDigest, rmp_serde::encode::Error> {
let findings = sorted_findings(&report.findings);
let normalized = StateTransitionReportBody {
findings,
..report.clone()
};
let bytes = crate::encoding::to_bytes(&normalized)?;
Ok(content_hash(&bytes))
}