use core::fmt;
use core::iter::Peekable;
use super::check::{self, ChangeInput, Op, execute};
use super::master::AccountSpec;
use super::state::{MemState, StateView};
use super::writeset::{IndexRow, MemoPlain, Outcome};
use crate::error::Rejection;
use crate::log::TreeHead;
use crate::model::{
AccountAction, AccountChange, AccountHandle, AccountRef, ClearingAction, Currency, DecodeError,
Fingerprint, Layer, LedgerId, MemoCommitment, PeriodId, Record, Seal, SealOrderError,
};
use crate::store::{ProjectionPart, ProjectionRow, StoreProjection};
#[derive(Debug, Clone, Copy, PartialEq, Eq)]
pub struct ReplayLeaf<'a> {
pub position: u64,
pub bytes: &'a [u8],
pub memo: Option<&'a MemoPlain>,
pub fingerprint: Option<Fingerprint>,
}
#[derive(Debug, Clone, Copy, PartialEq, Eq)]
#[non_exhaustive]
pub struct AuditReport {
pub size: u64,
pub entries: u64,
pub seals: u64,
pub accounts: u64,
pub head: TreeHead,
}
#[derive(Debug, Clone, PartialEq, Eq, thiserror::Error)]
#[error("audit failed{}: {kind}", position_suffix(.position))]
#[non_exhaustive]
pub struct AuditFailure {
pub position: Option<u64>,
pub kind: AuditFailureKind,
}
fn position_suffix(position: &Option<u64>) -> String {
position.map_or_else(String::new, |position| format!(" at position {position}"))
}
#[derive(Debug, Clone, PartialEq, Eq, thiserror::Error)]
#[non_exhaustive]
pub enum AuditFailureKind {
#[error("the leaf does not decode: {0}")]
Undecodable(DecodeError),
#[error("the leaf is not in canonical form")]
NonCanonical,
#[error("the log does not start with a genesis record")]
GenesisMissing,
#[error("the genesis names another ledger or scale")]
GenesisMismatch,
#[error("expected the leaf at position {expected}")]
PositionMismatch {
expected: u64,
},
#[error("the rule core refuses the record: {0}")]
WouldReject(Rejection),
#[error("the rule core would not have written the record")]
NotProduced,
#[error("the seal of {period} does not match the replay")]
SealMismatch {
period: PeriodId,
},
#[error("the registration has handle {found}, expected {expected}")]
HandleMismatch {
expected: u32,
found: AccountHandle,
},
#[error("the memo does not match its commitment")]
MemoMismatch,
#[error("the {currency} {layer:?} totals decrease from one seal to the next")]
SealTotalsDecrease {
currency: Currency,
layer: Layer,
},
#[error("the stored draft fingerprint does not match the replay")]
FingerprintMismatch,
#[error("the claimed tree head does not match the replay")]
HeadMismatch,
#[error(
"the {part} table does not match the replay: expected {}, found {}",
show(.expected),
show(.found)
)]
ProjectionMismatch {
part: ProjectionPart,
expected: Option<Box<ProjectionRow>>,
found: Option<Box<ProjectionRow>>,
},
}
fn show(row: &Option<Box<ProjectionRow>>) -> String {
row.as_ref()
.map_or_else(|| String::from("no row"), ToString::to_string)
}
#[derive(Debug, Clone)]
pub struct Replayer<const P: u8> {
state: MemState<P>,
seals: u64,
last_seal: Option<Seal<P>>,
}
impl<const P: u8> Replayer<P> {
#[must_use]
pub fn new(ledger: &LedgerId) -> Self {
Self {
state: MemState::new(ledger.clone()),
seals: 0,
last_seal: None,
}
}
#[must_use]
pub fn head(&self) -> TreeHead {
self.state.head()
}
pub fn feed(&mut self, leaf: ReplayLeaf<'_>) -> Result<(), AuditFailure> {
let position = self.state.header().size;
let fail = |kind| AuditFailure {
position: Some(position),
kind,
};
if leaf.position != position {
return Err(fail(AuditFailureKind::PositionMismatch {
expected: position,
}));
}
let record = match Record::<P>::decode(leaf.bytes) {
Ok(record) => record,
Err(DecodeError::PrecisionMismatch { .. }) if position == 0 => {
return Err(fail(AuditFailureKind::GenesisMismatch));
}
Err(error) => return Err(fail(AuditFailureKind::Undecodable(error))),
};
if record.to_leaf_bytes() != leaf.bytes {
return Err(fail(AuditFailureKind::NonCanonical));
}
let outcome = match &record {
Record::Genesis(genesis) if position == 0 => {
if genesis.ledger != *self.state.ledger() {
return Err(fail(AuditFailureKind::GenesisMismatch));
}
execute(&self.state, &Op::Genesis)
}
_ if position == 0 => return Err(fail(AuditFailureKind::GenesisMissing)),
Record::Entry(entry) => {
let memo = leaf.memo.cloned();
if let Some(memo) = &memo
&& entry.parts().memo != Some(MemoCommitment::compute(&memo.salt, &memo.text))
{
return Err(fail(AuditFailureKind::MemoMismatch));
}
check::replay(&self.state, entry.clone(), memo)
}
Record::Account(event) => {
if let AccountAction::Register(registration) = &event.action {
let expected = self.state.header().accounts;
if registration.handle.get() != expected {
return Err(fail(AuditFailureKind::HandleMismatch {
expected,
found: registration.handle,
}));
}
}
execute(&self.state, &operation(&record))
}
_ => execute(&self.state, &operation(&record)),
};
let write = match outcome {
Ok(Outcome::Append(write)) => *write,
Ok(Outcome::Unchanged(_) | Outcome::Batch { .. }) => {
return Err(fail(AuditFailureKind::NotProduced));
}
Err(rejection) => return Err(fail(AuditFailureKind::WouldReject(rejection))),
};
if let Some(claimed) = leaf.fingerprint {
let replayed = write.index.iter().find_map(|row| match row {
IndexRow::EntryKey { fingerprint, .. } => Some(*fingerprint),
_ => None,
});
if replayed != Some(Some(claimed)) {
return Err(fail(AuditFailureKind::FingerprintMismatch));
}
}
if write.leaf != leaf.bytes {
return Err(fail(match &record {
Record::Seal(seal) => AuditFailureKind::SealMismatch {
period: seal.parts().period.clone(),
},
_ => AuditFailureKind::NotProduced,
}));
}
if let Record::Seal(seal) = &record {
if let Some(earlier) = &self.last_seal {
seal.check_follows(earlier).map_err(|error| {
fail(match error {
SealOrderError::TreeMissing { currency, layer }
| SealOrderError::TotalDecreased { currency, layer } => {
AuditFailureKind::SealTotalsDecrease { currency, layer }
}
_ => AuditFailureKind::SealMismatch {
period: seal.parts().period.clone(),
},
})
})?;
}
self.seals = self.seals.saturating_add(1);
self.last_seal = Some(seal.clone());
}
self.state
.apply(write)
.map_err(|_| fail(AuditFailureKind::NotProduced))
}
#[must_use]
pub fn report(&self) -> AuditReport {
let header = self.state.header();
AuditReport {
size: header.size,
entries: header.entries,
seals: self.seals,
accounts: u64::from(header.accounts),
head: self.state.head(),
}
}
#[must_use]
pub fn projection(&self) -> StoreProjection {
let mut projection = StoreProjection::new(self.state.head());
for part in ProjectionPart::ALL {
*projection.rows_mut(part) = self.state.rows(part).collect();
}
projection
}
pub fn finish(&self, claimed: &StoreProjection) -> Result<AuditReport, AuditFailure> {
if claimed.head() != self.state.head() {
return Err(AuditFailure {
position: None,
kind: AuditFailureKind::HeadMismatch,
});
}
for part in ProjectionPart::ALL {
let mut check = self.check_part(part);
for row in claimed.rows(part) {
check.feed(row.clone())?;
}
check.finish()?;
}
Ok(self.report())
}
pub(crate) fn finish_state(&self, state: &MemState<P>) -> Result<AuditReport, AuditFailure> {
if state.head() != self.state.head() {
return Err(AuditFailure {
position: None,
kind: AuditFailureKind::HeadMismatch,
});
}
for part in ProjectionPart::ALL {
let mut check = self.check_part(part);
for row in state.rows(part) {
check.feed(row)?;
}
check.finish()?;
}
Ok(self.report())
}
pub(crate) fn check_part(&self, part: ProjectionPart) -> PartCheck<'_> {
PartCheck {
part,
expected: self.state.rows(part).peekable(),
}
}
pub(crate) fn into_state(self) -> MemState<P> {
self.state
}
}
pub(crate) struct PartCheck<'a> {
part: ProjectionPart,
expected: Peekable<Box<dyn Iterator<Item = ProjectionRow> + Send + 'a>>,
}
impl PartCheck<'_> {
fn mismatch(
&self,
expected: Option<ProjectionRow>,
found: Option<ProjectionRow>,
) -> AuditFailure {
AuditFailure {
position: None,
kind: AuditFailureKind::ProjectionMismatch {
part: self.part,
expected: expected.map(Box::new),
found: found.map(Box::new),
},
}
}
pub(crate) fn feed(&mut self, found: ProjectionRow) -> Result<(), AuditFailure> {
match self.expected.next() {
Some(expected) if expected.to_bytes() == found.to_bytes() => Ok(()),
expected => Err(self.mismatch(expected, Some(found))),
}
}
pub(crate) fn finish(mut self) -> Result<(), AuditFailure> {
match self.expected.next() {
None => Ok(()),
Some(expected) => Err(self.mismatch(Some(expected), None)),
}
}
}
fn operation<const P: u8>(record: &Record<P>) -> Op<P> {
match record {
Record::Account(event) => match &event.action {
AccountAction::Register(registration) => {
let mut spec = AccountSpec::new(
registration.path.to_string(),
registration.kind,
registration.open_from,
)
.limit(registration.limit);
if let Some(class) = ®istration.class {
spec = spec.class(class);
}
if let Some(open_to) = registration.open_to {
spec = spec.open_to(open_to);
}
Op::Register {
spec,
actor: event.actor.to_string(),
}
}
AccountAction::Change { handle, change } => Op::Change {
account: AccountRef::Handle(*handle),
change: match change {
AccountChange::OpenTo(open_to) => ChangeInput::OpenTo(*open_to),
AccountChange::Limit(limit) => ChangeInput::Limit(*limit),
AccountChange::Class(class) => {
ChangeInput::Class(class.as_ref().map(ToString::to_string))
}
},
effective: event.effective,
actor: event.actor.to_string(),
},
},
Record::Currency(declaration) => Op::Currency {
code: declaration.code,
exponent: declaration.exponent,
effective: declaration.effective,
actor: declaration.actor.to_string(),
},
Record::Policy(event) => Op::Policy {
policy: event.policy.clone(),
effective: event.effective,
actor: event.actor.to_string(),
},
Record::Period(period) => Op::Period {
id: period.id().to_string(),
start: period.start(),
end: period.end(),
},
Record::Clearing(event) => match &event.action {
ClearingAction::Apply {
key,
debit,
credit,
amount,
} => Op::Clear {
key: key.to_string(),
debit: *debit,
credit: *credit,
amount: *amount,
date: event.date,
},
ClearingAction::Reset { key, clearing } => Op::ResetClearing {
key: key.to_string(),
clearing: *clearing,
date: event.date,
},
},
Record::Seal(seal) => Op::Seal {
period: seal.parts().period.to_string(),
},
Record::Genesis(_) | Record::Entry(_) => Op::Genesis,
}
}
impl fmt::Display for AuditReport {
fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
let counts = [
("records", self.size),
("entries", self.entries),
("seals", self.seals),
("accounts", self.accounts),
];
let width = counts
.iter()
.map(|(_, count)| count.to_string().len())
.max()
.unwrap_or(1);
writeln!(f, "audit passed")?;
for (label, count) in counts {
writeln!(f, " {label:<9}{count:>width$}")?;
}
write!(f, " {:<9}{}", "root", self.head.root)
}
}