use crate::base::canonical::{canonical_json, strict_set};
use crate::base::hash::{hex_encode, sha256_canonical, sha256_concat_ids, Hash256};
use crate::base::schema::SPEC_VERSION;
use crate::base::time::Time;
use crate::record::kind::{ReasonCode, VerdictResult};
use crate::record::record::Record;
use crate::record::refs::RecordId;
use crate::verify::rules::VerifierRules;
use crate::verify::verifier::verify_log;
use serde::{Deserialize, Serialize};
use std::collections::BTreeSet;
#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize)]
#[serde(deny_unknown_fields)]
pub struct Receipt {
pub spec_version: String,
pub rules: VerifierRules,
pub records: Vec<Record>,
}
impl Receipt {
pub fn new(records: &[Record], rules: &VerifierRules) -> Self {
Self {
spec_version: SPEC_VERSION.to_string(),
rules: rules.clone(),
records: records.to_vec(),
}
}
pub fn to_bytes(&self) -> Result<Vec<u8>, serde_json::Error> {
canonical_json(self)
}
pub fn from_bytes(bytes: &[u8]) -> Result<Self, serde_json::Error> {
serde_json::from_slice(bytes)
}
}
#[derive(Debug, Clone, Copy, PartialEq, Eq, Serialize, Deserialize)]
pub enum ValidationStatus {
Clean,
Tainted,
Invalid,
}
#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize)]
#[serde(deny_unknown_fields)]
pub struct Report {
pub status: ValidationStatus,
pub reason: Option<ReasonCode>,
pub problem: Option<String>,
pub spec_version: String,
pub record_count: u64,
pub checked_records: u64,
pub last_time: Time,
pub head_hash: Hash256,
pub rules_hash: Hash256,
#[serde(with = "strict_set")]
pub retracted_records: BTreeSet<RecordId>,
#[serde(with = "strict_set")]
pub tainted_records: BTreeSet<RecordId>,
}
impl Report {
fn structural_failure(problem: String, spec_version: String) -> Self {
Report {
status: ValidationStatus::Invalid,
reason: None,
problem: Some(problem),
spec_version,
record_count: 0,
checked_records: 0,
last_time: 0,
head_hash: [0u8; 32],
rules_hash: [0u8; 32],
retracted_records: BTreeSet::new(),
tainted_records: BTreeSet::new(),
}
}
}
#[derive(Debug, Clone, Copy, PartialEq, Eq)]
pub struct ValidationLimits {
pub max_bytes: usize,
pub max_records: usize,
pub max_payload_bytes: usize,
pub max_refs_per_record: usize,
}
impl Default for ValidationLimits {
fn default() -> Self {
Self {
max_bytes: 64 << 20,
max_records: 1_000_000,
max_payload_bytes: 16 << 20, max_refs_per_record: 4_096,
}
}
}
impl ValidationLimits {
pub fn unlimited() -> Self {
Self {
max_bytes: usize::MAX,
max_records: usize::MAX,
max_payload_bytes: usize::MAX,
max_refs_per_record: usize::MAX,
}
}
}
pub fn validate(bytes: &[u8]) -> Report {
validate_with_limits(bytes, &ValidationLimits::default())
}
pub fn validate_with_limits(bytes: &[u8], limits: &ValidationLimits) -> Report {
if bytes.len() > limits.max_bytes {
return Report::structural_failure(
format!(
"receipt exceeds size limit ({} > {} bytes)",
bytes.len(),
limits.max_bytes
),
String::new(),
);
}
let receipt = match Receipt::from_bytes(bytes) {
Ok(r) => r,
Err(e) => {
return Report::structural_failure(format!("unparseable receipt: {e}"), String::new())
}
};
if receipt.records.len() > limits.max_records {
return Report::structural_failure(
format!(
"receipt exceeds record limit ({} > {})",
receipt.records.len(),
limits.max_records
),
receipt.spec_version,
);
}
for (idx, record) in receipt.records.iter().enumerate() {
if record.data.len() > limits.max_payload_bytes {
return Report::structural_failure(
format!("record {idx} exceeds payload size limit"),
receipt.spec_version,
);
}
if record.refs.len() > limits.max_refs_per_record {
return Report::structural_failure(
format!("record {idx} exceeds ref-count limit"),
receipt.spec_version,
);
}
}
if receipt.spec_version != SPEC_VERSION {
return Report::structural_failure(
format!(
"unsupported spec version {:?} (this validator implements {:?})",
receipt.spec_version, SPEC_VERSION
),
receipt.spec_version,
);
}
let rules_hash = match sha256_canonical(&receipt.rules) {
Ok(h) => h,
Err(e) => {
return Report::structural_failure(
format!("rules do not canonicalize: {e}"),
receipt.spec_version,
)
}
};
let ids: Vec<Hash256> = receipt.records.iter().map(|r| r.id).collect();
let head_hash = sha256_concat_ids(&ids);
let verdict = verify_log(&receipt.records, &receipt.rules, None);
let status = match verdict.result {
VerdictResult::Reject => ValidationStatus::Invalid,
VerdictResult::Accept => {
if verdict.retracted_records.is_empty() && verdict.tainted_records.is_empty() {
ValidationStatus::Clean
} else {
ValidationStatus::Tainted
}
}
};
Report {
status,
reason: verdict.reason,
problem: None,
spec_version: receipt.spec_version,
record_count: receipt.records.len() as u64,
checked_records: verdict.checked_records,
last_time: verdict.last_time,
head_hash,
rules_hash,
retracted_records: verdict.retracted_records,
tainted_records: verdict.tainted_records,
}
}
impl std::fmt::Display for Report {
fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
let status = match self.status {
ValidationStatus::Clean => "CLEAN",
ValidationStatus::Tainted => "TAINTED",
ValidationStatus::Invalid => "INVALID",
};
writeln!(f, "status: {status}")?;
if let Some(problem) = &self.problem {
writeln!(f, "problem: {problem}")?;
}
if let Some(reason) = &self.reason {
writeln!(f, "reject reason: {reason:?}")?;
}
writeln!(f, "spec version: {}", self.spec_version)?;
writeln!(f, "records: {}", self.record_count)?;
writeln!(f, "checked: {}", self.checked_records)?;
writeln!(f, "last time: {}", self.last_time)?;
writeln!(f, "head hash: {}", hex_encode(&self.head_hash))?;
writeln!(f, "rules hash: {}", hex_encode(&self.rules_hash))?;
if !self.retracted_records.is_empty() {
writeln!(f, "retracted ({}):", self.retracted_records.len())?;
for id in &self.retracted_records {
writeln!(f, " {}", hex_encode(id))?;
}
}
if !self.tainted_records.is_empty() {
writeln!(f, "tainted ({}):", self.tainted_records.len())?;
for id in &self.tainted_records {
writeln!(f, " {}", hex_encode(id))?;
}
}
Ok(())
}
}