use std::collections::HashMap;
use std::sync::Arc;
use freenet_stdlib::prelude::{ContractInstanceId, RelatedContracts, State};
use serde::{Deserialize, Serialize};
use super::property::{ConformanceProperty, PremiseSource, Violation};
use super::verifier::ConformanceCase;
pub const EVIDENCE_SCHEMA_VERSION: u16 = 1;
pub const MAX_EVIDENCE_INPUT_BYTES: usize = 512 * 1024;
pub const MAX_EVIDENCE_RELATED: usize = 8;
#[derive(Debug, Clone, Copy, PartialEq, Eq, Hash, PartialOrd, Ord, Serialize, Deserialize)]
pub struct EvidenceId([u8; 32]);
impl EvidenceId {
pub fn as_bytes(&self) -> &[u8; 32] {
&self.0
}
}
impl std::fmt::Display for EvidenceId {
fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
f.write_str(&hex::encode(&self.0[..8]))
}
}
#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize)]
pub struct RuntimeIdentity {
pub core_version: String,
pub evidence_schema: u16,
}
impl RuntimeIdentity {
pub fn current() -> Self {
Self {
core_version: env!("CARGO_PKG_VERSION").to_string(),
evidence_schema: EVIDENCE_SCHEMA_VERSION,
}
}
}
#[derive(Debug, Clone, PartialEq, Eq, thiserror::Error)]
pub enum EvidenceRejected {
#[error("evidence schema {found} is not supported (this peer speaks {supported})")]
UnsupportedSchema { found: u16, supported: u16 },
#[error("evidence carries {found} input bytes, limit is {limit}")]
TooLarge { found: usize, limit: usize },
#[error("evidence carries {found} related contracts, limit is {limit}")]
TooManyRelated { found: usize, limit: usize },
#[error(
"{property} rests on provenance the evidence bytes cannot carry, so it is \
local-only and never shippable as evidence"
)]
NotSelfVerifying { property: ConformanceProperty },
#[error(
"{property} needs {want} states and {want_deltas} deltas, evidence has {got} and {got_deltas}"
)]
Arity {
property: ConformanceProperty,
want: usize,
got: usize,
want_deltas: usize,
got_deltas: usize,
},
}
#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize)]
pub struct ConformanceEvidence {
pub schema_version: u16,
pub contract: ContractInstanceId,
pub parameters: Vec<u8>,
pub property: ConformanceProperty,
pub states: Vec<Vec<u8>>,
pub deltas: Vec<Vec<u8>>,
pub summary: Option<Vec<u8>>,
pub related: Vec<(ContractInstanceId, Vec<u8>)>,
pub observed: Option<Violation>,
pub runtime: RuntimeIdentity,
}
impl ConformanceEvidence {
pub fn new(
contract: ContractInstanceId,
parameters: Vec<u8>,
case: &ConformanceCase,
observed: Option<Violation>,
) -> Self {
Self {
schema_version: EVIDENCE_SCHEMA_VERSION,
contract,
parameters,
property: case.property,
states: case.states.iter().map(|s| s.to_vec()).collect(),
deltas: case.deltas.iter().map(|d| d.to_vec()).collect(),
summary: case.summary.as_ref().map(|s| s.to_vec()),
related: related_to_pairs(&case.related),
observed,
runtime: RuntimeIdentity::current(),
}
}
pub fn input_bytes(&self) -> usize {
self.states.iter().map(Vec::len).sum::<usize>()
+ self.deltas.iter().map(Vec::len).sum::<usize>()
+ self.summary.as_ref().map_or(0, Vec::len)
+ self.related.iter().map(|(_, s)| s.len()).sum::<usize>()
+ self.parameters.len()
}
pub fn check_bounds(&self) -> Result<(), EvidenceRejected> {
if self.schema_version != EVIDENCE_SCHEMA_VERSION {
return Err(EvidenceRejected::UnsupportedSchema {
found: self.schema_version,
supported: EVIDENCE_SCHEMA_VERSION,
});
}
if !self.property.is_self_verifying() {
debug_assert_eq!(
self.property.premise_source(),
PremiseSource::LocalProvenance
);
return Err(EvidenceRejected::NotSelfVerifying {
property: self.property,
});
}
let bytes = self.input_bytes();
if bytes > MAX_EVIDENCE_INPUT_BYTES {
return Err(EvidenceRejected::TooLarge {
found: bytes,
limit: MAX_EVIDENCE_INPUT_BYTES,
});
}
if self.related.len() > MAX_EVIDENCE_RELATED {
return Err(EvidenceRejected::TooManyRelated {
found: self.related.len(),
limit: MAX_EVIDENCE_RELATED,
});
}
let want = self.property.state_arity();
let want_deltas = self.property.delta_arity();
if self.states.len() != want || self.deltas.len() != want_deltas {
return Err(EvidenceRejected::Arity {
property: self.property,
want,
got: self.states.len(),
want_deltas,
got_deltas: self.deltas.len(),
});
}
Ok(())
}
pub fn id(&self) -> EvidenceId {
let mut hasher = blake3::Hasher::new();
hasher.update(b"freenet-conformance-evidence-v1");
hasher.update(&self.schema_version.to_le_bytes());
hasher.update(self.contract.as_bytes());
hash_blob(&mut hasher, &self.parameters);
hasher.update(self.property.as_str().as_bytes());
hasher.update(&(self.states.len() as u64).to_le_bytes());
for state in &self.states {
hash_blob(&mut hasher, state);
}
hasher.update(&(self.deltas.len() as u64).to_le_bytes());
for delta in &self.deltas {
hash_blob(&mut hasher, delta);
}
match &self.summary {
Some(summary) => {
hasher.update(&[1u8]);
hash_blob(&mut hasher, summary);
}
None => {
hasher.update(&[0u8]);
}
}
let mut related = self.related.clone();
related.sort_by(|(a, _), (b, _)| a.as_bytes().cmp(b.as_bytes()));
hasher.update(&(related.len() as u64).to_le_bytes());
for (id, state) in &related {
hasher.update(id.as_bytes());
hash_blob(&mut hasher, state);
}
EvidenceId(*hasher.finalize().as_bytes())
}
pub fn to_case(&self) -> Result<ConformanceCase, EvidenceRejected> {
self.check_bounds()?;
let related: HashMap<ContractInstanceId, Option<State<'static>>> = self
.related
.iter()
.map(|(id, state)| (*id, Some(State::from(state.clone()))))
.collect();
let related = RelatedContracts::from(related);
Ok(ConformanceCase {
property: self.property,
states: self
.states
.iter()
.map(|s| Arc::from(s.as_slice()))
.collect(),
deltas: self
.deltas
.iter()
.map(|d| Arc::from(d.as_slice()))
.collect(),
summary: self.summary.as_ref().map(|s| Arc::from(s.as_slice())),
related,
})
}
}
fn hash_blob(hasher: &mut blake3::Hasher, blob: &[u8]) {
hasher.update(&(blob.len() as u64).to_le_bytes());
hasher.update(blob);
}
fn related_to_pairs(related: &RelatedContracts<'static>) -> Vec<(ContractInstanceId, Vec<u8>)> {
let mut pairs: Vec<_> = related
.states()
.filter_map(|(id, state)| state.as_ref().map(|s| (*id, s.as_ref().to_vec())))
.collect();
pairs.sort_by(|(a, _), (b, _)| a.as_bytes().cmp(b.as_bytes()));
pairs
}