use crate::evidence::{content_hash, sort_findings, sorted_findings};
use serde::{Deserialize, Serialize};
use std::collections::BTreeMap;
pub const RESERVATION_LEDGER_REPORT_SCHEMA_VERSION: u32 = 1;
pub const RESERVATION_RECONCILIATION_REPORT_SCHEMA_VERSION: u32 = 1;
pub const RESERVATION_TRANSITION_SCHEMA_VERSION: u32 = 1;
pub const RESERVATION_STATE_RESERVED: u32 = 0;
pub const RESERVATION_STATE_COMMITTED: u32 = 1;
pub const RESERVATION_STATE_REFUNDED: u32 = 2;
pub const RESERVATION_STATE_EXPIRED: u32 = 3;
pub const RESERVATION_STATE_ORPHANED: u32 = 4;
pub const RESERVATION_OP_RESERVE: u32 = 0;
pub const RESERVATION_OP_COMMIT: u32 = 1;
pub const RESERVATION_OP_REFUND: u32 = 2;
pub const RESERVATION_OP_EXPIRE: u32 = 3;
pub const RESERVATION_OP_ORPHAN: u32 = 4;
pub const RESERVATION_REASON_DOUBLE_COMMIT: u32 = 1;
pub const RESERVATION_REASON_COMMIT_WITHOUT_RESERVE: u32 = 2;
pub const RESERVATION_REASON_REFUND_INVALID_STATE: u32 = 3;
pub const RESERVATION_REASON_REFUND_AFTER_COMMIT: u32 = 4;
pub const RESERVATION_REASON_EXPIRE_INVALID_STATE: u32 = 5;
pub const RESERVATION_REASON_ORPHAN_INVALID_STATE: u32 = 6;
pub const RESERVATION_REASON_DUPLICATE_RESERVE: u32 = 7;
pub const RESERVATION_REASON_RESERVE_INVALID_SUBJECT_OR_UNITS: u32 = 8;
pub const RESERVATION_REASON_TRANSITION_ON_TERMINAL: u32 = 9;
pub type ReservationState = u32;
#[derive(Clone, Copy, Debug, PartialEq, Eq, Hash, PartialOrd, Ord, Serialize, Deserialize)]
pub struct ReservationId(pub [u8; 32]);
#[derive(Clone, Debug, PartialEq, Eq, Serialize, Deserialize)]
pub struct ReservationSubjectRef {
pub namespace: u32,
pub key_bytes: Vec<u8>,
}
impl PartialOrd for ReservationSubjectRef {
fn partial_cmp(&self, other: &Self) -> Option<std::cmp::Ordering> {
Some(self.cmp(other))
}
}
impl Ord for ReservationSubjectRef {
fn cmp(&self, other: &Self) -> std::cmp::Ordering {
self.namespace
.cmp(&other.namespace)
.then_with(|| self.key_bytes.cmp(&other.key_bytes))
}
}
#[derive(Clone, Copy, Debug, PartialEq, Eq, PartialOrd, Ord, Serialize, Deserialize)]
pub struct ReservationQuantity {
pub units: u64,
}
#[derive(Clone, Debug, PartialEq, Eq, PartialOrd, Ord, Serialize, Deserialize)]
pub struct ReservationCauseRef {
pub lane: u32,
pub opaque_key: Vec<u8>,
}
#[derive(Clone, Debug, PartialEq, Eq, Serialize, Deserialize)]
pub struct ReservationTransition {
pub schema_version: u32,
pub sequence: u64,
pub reservation_id: ReservationId,
pub op: u32,
pub quantity_units: u64,
pub subject: Option<ReservationSubjectRef>,
pub cause_refs: Vec<ReservationCauseRef>,
}
#[derive(Clone, Debug, PartialEq, Eq, Serialize, Deserialize)]
pub struct ReservationEntry {
pub reservation_id: ReservationId,
pub subject_ref: ReservationSubjectRef,
pub quantity: ReservationQuantity,
pub state: ReservationState,
pub opened_at_sequence: u64,
}
#[derive(Clone, Debug, PartialEq, Eq, PartialOrd, Ord, Serialize, Deserialize)]
pub enum ReservationFinding {
InvalidTransition {
reservation_id: ReservationId,
from_state: u32,
attempted_op: u32,
reason_code: u32,
},
UnsupportedTransitionSchemaVersion {
reservation_id: ReservationId,
observed: u32,
expected: u32,
},
}
#[derive(Clone, Debug, PartialEq, Eq, Serialize, Deserialize)]
pub struct ReservationLedgerReportBody {
pub schema_version: u32,
pub transition_log_digest: [u8; 32],
pub entries_sorted: Vec<ReservationEntry>,
pub findings_sorted: Vec<ReservationFinding>,
}
#[derive(Clone, Debug, PartialEq, Eq, Serialize, Deserialize)]
pub struct ReservationReconciliationReportBody {
pub schema_version: u32,
pub reserved_open_ids: Vec<ReservationId>,
pub expired_ids: Vec<ReservationId>,
pub orphaned_ids: Vec<ReservationId>,
pub committed_ids: Vec<ReservationId>,
pub refunded_ids: Vec<ReservationId>,
}
pub type ReservationDigest = [u8; 32];
#[must_use]
pub fn normalize_reservation_subject_ref(subject: &ReservationSubjectRef) -> ReservationSubjectRef {
subject.clone()
}
#[must_use]
pub fn normalize_reservation_transition(t: &ReservationTransition) -> ReservationTransition {
let mut cause_refs = t.cause_refs.clone();
cause_refs.sort();
ReservationTransition {
cause_refs,
..t.clone()
}
}
pub fn reservation_transition_bytes(
t: &ReservationTransition,
) -> Result<Vec<u8>, rmp_serde::encode::Error> {
let n = normalize_reservation_transition(t);
crate::encoding::to_bytes(&n)
}
#[must_use]
pub fn normalize_reservation_transition_list(
transitions: &[ReservationTransition],
) -> Vec<ReservationTransition> {
let mut out: Vec<ReservationTransition> = transitions
.iter()
.map(normalize_reservation_transition)
.collect();
out.sort_by(|a, b| {
a.sequence
.cmp(&b.sequence)
.then_with(|| a.reservation_id.cmp(&b.reservation_id))
});
out
}
pub fn reservation_transition_log_digest(
transitions_sorted: &[ReservationTransition],
) -> Result<ReservationDigest, rmp_serde::encode::Error> {
let mut buf = Vec::new();
for t in transitions_sorted {
buf.extend_from_slice(&reservation_transition_bytes(t)?);
}
Ok(content_hash(&buf))
}
fn push_invalid(
out: &mut Vec<ReservationFinding>,
id: ReservationId,
from: u32,
op: u32,
reason: u32,
) {
out.push(ReservationFinding::InvalidTransition {
reservation_id: id,
from_state: from,
attempted_op: op,
reason_code: reason,
});
}
pub fn simulate_reservation_ledger(
transitions: &[ReservationTransition],
) -> Result<ReservationLedgerReportBody, rmp_serde::encode::Error> {
let sorted = normalize_reservation_transition_list(transitions);
let digest = reservation_transition_log_digest(&sorted)?;
let mut findings = Vec::new();
let mut ledger: BTreeMap<ReservationId, ReservationEntry> = BTreeMap::new();
for t in &sorted {
if t.schema_version != RESERVATION_TRANSITION_SCHEMA_VERSION {
findings.push(ReservationFinding::UnsupportedTransitionSchemaVersion {
reservation_id: t.reservation_id,
observed: t.schema_version,
expected: RESERVATION_TRANSITION_SCHEMA_VERSION,
});
continue;
}
let id = t.reservation_id;
match t.op {
RESERVATION_OP_RESERVE => {
if let Some(existing) = ledger.get(&id) {
push_invalid(
&mut findings,
id,
existing.state,
t.op,
RESERVATION_REASON_DUPLICATE_RESERVE,
);
continue;
}
let Some(subject) = t.subject.as_ref() else {
push_invalid(
&mut findings,
id,
u32::MAX,
t.op,
RESERVATION_REASON_RESERVE_INVALID_SUBJECT_OR_UNITS,
);
continue;
};
if t.quantity_units == 0 {
push_invalid(
&mut findings,
id,
u32::MAX,
t.op,
RESERVATION_REASON_RESERVE_INVALID_SUBJECT_OR_UNITS,
);
continue;
}
let subject_ref = normalize_reservation_subject_ref(subject);
ledger.insert(
id,
ReservationEntry {
reservation_id: id,
subject_ref,
quantity: ReservationQuantity {
units: t.quantity_units,
},
state: RESERVATION_STATE_RESERVED,
opened_at_sequence: t.sequence,
},
);
}
RESERVATION_OP_COMMIT => {
let Some(entry) = ledger.get_mut(&id) else {
push_invalid(
&mut findings,
id,
u32::MAX,
t.op,
RESERVATION_REASON_COMMIT_WITHOUT_RESERVE,
);
continue;
};
match entry.state {
RESERVATION_STATE_RESERVED => entry.state = RESERVATION_STATE_COMMITTED,
RESERVATION_STATE_COMMITTED => {
push_invalid(
&mut findings,
id,
entry.state,
t.op,
RESERVATION_REASON_DOUBLE_COMMIT,
);
}
_ => {
push_invalid(
&mut findings,
id,
entry.state,
t.op,
RESERVATION_REASON_TRANSITION_ON_TERMINAL,
);
}
}
}
RESERVATION_OP_REFUND => {
let Some(entry) = ledger.get_mut(&id) else {
push_invalid(
&mut findings,
id,
u32::MAX,
t.op,
RESERVATION_REASON_REFUND_INVALID_STATE,
);
continue;
};
match entry.state {
RESERVATION_STATE_RESERVED => entry.state = RESERVATION_STATE_REFUNDED,
RESERVATION_STATE_COMMITTED => {
push_invalid(
&mut findings,
id,
entry.state,
t.op,
RESERVATION_REASON_REFUND_AFTER_COMMIT,
);
}
_ => {
push_invalid(
&mut findings,
id,
entry.state,
t.op,
RESERVATION_REASON_REFUND_INVALID_STATE,
);
}
}
}
RESERVATION_OP_EXPIRE => {
let Some(entry) = ledger.get_mut(&id) else {
push_invalid(
&mut findings,
id,
u32::MAX,
t.op,
RESERVATION_REASON_EXPIRE_INVALID_STATE,
);
continue;
};
if entry.state == RESERVATION_STATE_RESERVED {
entry.state = RESERVATION_STATE_EXPIRED;
} else {
push_invalid(
&mut findings,
id,
entry.state,
t.op,
RESERVATION_REASON_EXPIRE_INVALID_STATE,
);
}
}
RESERVATION_OP_ORPHAN => {
let Some(entry) = ledger.get_mut(&id) else {
push_invalid(
&mut findings,
id,
u32::MAX,
t.op,
RESERVATION_REASON_ORPHAN_INVALID_STATE,
);
continue;
};
if entry.state == RESERVATION_STATE_RESERVED {
entry.state = RESERVATION_STATE_ORPHANED;
} else {
push_invalid(
&mut findings,
id,
entry.state,
t.op,
RESERVATION_REASON_ORPHAN_INVALID_STATE,
);
}
}
_ => {
push_invalid(
&mut findings,
id,
u32::MAX,
t.op,
RESERVATION_REASON_TRANSITION_ON_TERMINAL,
);
}
}
}
sort_findings(&mut findings);
let mut entries_sorted: Vec<ReservationEntry> = ledger.into_values().collect();
entries_sorted.sort_by(|a, b| a.reservation_id.cmp(&b.reservation_id));
Ok(ReservationLedgerReportBody {
schema_version: RESERVATION_LEDGER_REPORT_SCHEMA_VERSION,
transition_log_digest: digest,
entries_sorted,
findings_sorted: findings,
})
}
#[must_use]
pub fn reservation_reconciliation_report(
entries: &[ReservationEntry],
) -> ReservationReconciliationReportBody {
let mut reserved_open_ids = Vec::new();
let mut expired_ids = Vec::new();
let mut orphaned_ids = Vec::new();
let mut committed_ids = Vec::new();
let mut refunded_ids = Vec::new();
for e in entries {
match e.state {
RESERVATION_STATE_RESERVED => reserved_open_ids.push(e.reservation_id),
RESERVATION_STATE_EXPIRED => expired_ids.push(e.reservation_id),
RESERVATION_STATE_ORPHANED => orphaned_ids.push(e.reservation_id),
RESERVATION_STATE_COMMITTED => committed_ids.push(e.reservation_id),
RESERVATION_STATE_REFUNDED => refunded_ids.push(e.reservation_id),
_ => {}
}
}
reserved_open_ids.sort();
expired_ids.sort();
orphaned_ids.sort();
committed_ids.sort();
refunded_ids.sort();
ReservationReconciliationReportBody {
schema_version: RESERVATION_RECONCILIATION_REPORT_SCHEMA_VERSION,
reserved_open_ids,
expired_ids,
orphaned_ids,
committed_ids,
refunded_ids,
}
}
pub fn reservation_ledger_report_body_hash(
body: &ReservationLedgerReportBody,
) -> Result<ReservationDigest, rmp_serde::encode::Error> {
let findings_sorted = sorted_findings(&body.findings_sorted);
let mut entries_sorted = body.entries_sorted.clone();
entries_sorted.sort_by(|a, b| a.reservation_id.cmp(&b.reservation_id));
let normalized = ReservationLedgerReportBody {
findings_sorted,
entries_sorted,
..body.clone()
};
let bytes = crate::encoding::to_bytes(&normalized)?;
Ok(content_hash(&bytes))
}
pub fn reservation_reconciliation_report_body_hash(
body: &ReservationReconciliationReportBody,
) -> Result<ReservationDigest, rmp_serde::encode::Error> {
let bytes = crate::encoding::to_bytes(body)?;
Ok(content_hash(&bytes))
}