use std::collections::HashSet;
use std::sync::Arc;
use freenet_stdlib::prelude::{RelatedContracts, State, StateDelta, UpdateData, ValidateResult};
use super::oracle::{ConformanceOracle, OracleError, OracleErrorKind};
use super::property::{
ConformanceProperty, Inconclusive, OutputDigest, PropertyOutcome, Violation,
};
pub type Bytes = Arc<[u8]>;
pub const MAX_RECONCILIATION_ROUNDS: usize = 16;
pub const DETERMINISM_REPEATS: usize = 3;
pub const MAX_CANONICALIZATION_APPLIES: usize = 3;
#[derive(Debug, Clone)]
pub struct ConformanceCase {
pub property: ConformanceProperty,
pub states: Vec<Bytes>,
pub deltas: Vec<Bytes>,
pub summary: Option<Bytes>,
pub related: RelatedContracts<'static>,
}
impl ConformanceCase {
pub fn new(property: ConformanceProperty, states: Vec<Bytes>) -> Self {
Self {
property,
states,
deltas: Vec::new(),
summary: None,
related: RelatedContracts::default(),
}
}
pub fn with_deltas(mut self, deltas: Vec<Bytes>) -> Self {
self.deltas = deltas;
self
}
pub fn with_summary(mut self, summary: Bytes) -> Self {
self.summary = Some(summary);
self
}
pub fn with_related(mut self, related: RelatedContracts<'static>) -> Self {
self.related = related;
self
}
pub fn input_bytes(&self) -> usize {
self.states.iter().map(|s| s.len()).sum::<usize>()
+ self.deltas.iter().map(|d| d.len()).sum::<usize>()
+ self.summary.as_ref().map_or(0, |s| s.len())
}
fn arity_ok(&self) -> Result<(), Inconclusive> {
let want_states = self.property.state_arity();
if self.states.len() < want_states {
return Err(Inconclusive::MalformedCase(format!(
"{} needs {want_states} states, got {}",
self.property,
self.states.len()
)));
}
let want_deltas = self.property.delta_arity();
if self.deltas.len() < want_deltas {
return Err(Inconclusive::MalformedCase(format!(
"{} needs {want_deltas} deltas, got {}",
self.property,
self.deltas.len()
)));
}
Ok(())
}
}
pub fn verify_case<O: ConformanceOracle + ?Sized>(
oracle: &mut O,
case: &ConformanceCase,
) -> PropertyOutcome {
let first = match run(oracle, case) {
Ok(outcome) => outcome,
Err(reason) => return PropertyOutcome::Inconclusive(reason),
};
if first.is_violation() {
let second = match run(oracle, case) {
Ok(outcome) => outcome,
Err(reason) => return PropertyOutcome::Inconclusive(reason),
};
return match (&first, &second) {
(PropertyOutcome::Violated(a), PropertyOutcome::Violated(b))
if a.property == b.property && reproduced_identically(a, b) =>
{
second
}
_ => escalate_to_determinism(oracle, case),
};
}
first
}
fn escalate_to_determinism<O: ConformanceOracle + ?Sized>(
oracle: &mut O,
case: &ConformanceCase,
) -> PropertyOutcome {
if case.states.len() < ConformanceProperty::UpdateDeterminism.state_arity() {
return PropertyOutcome::Inconclusive(Inconclusive::NotReproducible);
}
let determinism = ConformanceCase {
property: ConformanceProperty::UpdateDeterminism,
states: case.states.clone(),
deltas: Vec::new(),
summary: None,
related: case.related.clone(),
};
match run(oracle, &determinism) {
Ok(outcome @ PropertyOutcome::Violated(_)) => outcome,
_ => PropertyOutcome::Inconclusive(Inconclusive::NotReproducible),
}
}
fn reproduced_identically(first: &Violation, second: &Violation) -> bool {
match first.property {
ConformanceProperty::UpdateDeterminism
| ConformanceProperty::SummaryDeterminism
| ConformanceProperty::DeltaDeterminism => true,
ConformanceProperty::StateIdempotence
| ConformanceProperty::StateCommutativity
| ConformanceProperty::StateAssociativity
| ConformanceProperty::EmittedStateValidity
| ConformanceProperty::DeltaIdempotence
| ConformanceProperty::DeltaPermutationInvariance
| ConformanceProperty::SelfDeltaEmpty
| ConformanceProperty::WholeStateSelfDelta
| ConformanceProperty::ReconciliationCycle
| ConformanceProperty::PathAgreement
| ConformanceProperty::TransitionPathAgreement => {
first.left == second.left && first.right == second.right
}
}
}
fn run<O: ConformanceOracle + ?Sized>(
oracle: &mut O,
case: &ConformanceCase,
) -> Result<PropertyOutcome, Inconclusive> {
case.arity_ok()?;
for state in &case.states {
require_valid(oracle, state, &case.related)?;
}
match case.property {
ConformanceProperty::StateIdempotence => {
let a = &case.states[0];
let mut current = a.to_vec();
for _ in 0..MAX_CANONICALIZATION_APPLIES {
let merged = merge(oracle, ¤t, ¤t)?;
if merged == current {
return Ok(PropertyOutcome::Holds);
}
current = merged;
}
Ok(violation(
case.property,
¤t,
a,
"merge(A, A) never reached a fixpoint: the state changes on every \
re-apply, so redelivery of the same state keeps mutating it",
))
}
ConformanceProperty::StateCommutativity => {
let (a, b) = (&case.states[0], &case.states[1]);
let ab = merge(oracle, a, b)?;
let ba = merge(oracle, b, a)?;
Ok(compare(
case.property,
&ab,
&ba,
"merge(A, B) must equal merge(B, A)",
))
}
ConformanceProperty::StateAssociativity => {
let (a, b, c) = (&case.states[0], &case.states[1], &case.states[2]);
let ab = merge(oracle, a, b)?;
let bc = merge(oracle, b, c)?;
require_valid(oracle, &ab, &case.related)?;
require_valid(oracle, &bc, &case.related)?;
let ab_c = merge(oracle, &ab, c)?;
let a_bc = merge(oracle, a, &bc)?;
Ok(compare(
case.property,
&ab_c,
&a_bc,
"merge(merge(A, B), C) must equal merge(A, merge(B, C))",
))
}
ConformanceProperty::EmittedStateValidity => {
let (a, b) = (&case.states[0], &case.states[1]);
let merged = merge(oracle, a, b)?;
match oracle
.validate_state(&merged, &case.related)
.map_err(inconclusive_from)?
{
ValidateResult::Valid => Ok(PropertyOutcome::Holds),
ValidateResult::RequestRelated(_) => Err(Inconclusive::RelatedRequired),
ValidateResult::Invalid => Ok(PropertyOutcome::Violated(Violation {
property: case.property,
severity: case.property.severity(),
left: OutputDigest::of(&merged),
right: OutputDigest::of(a),
detail: "update_state emitted a state the contract rejects as invalid"
.to_string(),
})),
}
}
ConformanceProperty::UpdateDeterminism => {
let (a, b) = (&case.states[0], &case.states[1]);
let first = merge(oracle, a, b)?;
for _ in 1..DETERMINISM_REPEATS {
oracle.reset_instance();
let again = merge(oracle, a, b)?;
if again != first {
return Ok(PropertyOutcome::Violated(Violation {
property: case.property,
severity: case.property.severity(),
left: OutputDigest::of(&first),
right: OutputDigest::of(&again),
detail: "merge(A, B) returned different bytes on repeated identical calls"
.to_string(),
}));
}
}
Ok(PropertyOutcome::Holds)
}
ConformanceProperty::SummaryDeterminism => {
let a = &case.states[0];
let first = oracle.summarize_state(a).map_err(inconclusive_from)?;
for _ in 1..DETERMINISM_REPEATS {
oracle.reset_instance();
let again = oracle.summarize_state(a).map_err(inconclusive_from)?;
if again != first {
return Ok(PropertyOutcome::Violated(Violation {
property: case.property,
severity: case.property.severity(),
left: OutputDigest::of(&first),
right: OutputDigest::of(&again),
detail: "summarize_state returned different bytes for the same state"
.to_string(),
}));
}
}
Ok(PropertyOutcome::Holds)
}
ConformanceProperty::DeltaDeterminism => {
let a = &case.states[0];
let summary = match &case.summary {
Some(s) => s.to_vec(),
None => oracle.summarize_state(a).map_err(inconclusive_from)?,
};
let first = oracle
.get_state_delta(a, &summary)
.map_err(inconclusive_from)?;
for _ in 1..DETERMINISM_REPEATS {
oracle.reset_instance();
let again = oracle
.get_state_delta(a, &summary)
.map_err(inconclusive_from)?;
if again != first {
return Ok(PropertyOutcome::Violated(Violation {
property: case.property,
severity: case.property.severity(),
left: OutputDigest::of(&first),
right: OutputDigest::of(&again),
detail: "get_state_delta returned different bytes for the same inputs"
.to_string(),
}));
}
}
Ok(PropertyOutcome::Holds)
}
ConformanceProperty::DeltaIdempotence => {
let a = &case.states[0];
let d = &case.deltas[0];
let once = apply_delta(oracle, a, d)?;
let twice = apply_delta(oracle, &once, d)?;
Ok(compare(
case.property,
&once,
&twice,
"applying the same delta twice must equal applying it once",
))
}
ConformanceProperty::DeltaPermutationInvariance => {
let a = &case.states[0];
let (d1, d2) = (&case.deltas[0], &case.deltas[1]);
let a_then_d1 = apply_delta(oracle, a, d1)?;
let forward = apply_delta(oracle, &a_then_d1, d2)?;
let a_then_d2 = apply_delta(oracle, a, d2)?;
let reverse = apply_delta(oracle, &a_then_d2, d1)?;
Ok(compare(
case.property,
&forward,
&reverse,
"applying independent deltas in a different order reached a different state",
))
}
ConformanceProperty::SelfDeltaEmpty => {
let a = &case.states[0];
let summary = oracle.summarize_state(a).map_err(inconclusive_from)?;
let delta = oracle
.get_state_delta(a, &summary)
.map_err(inconclusive_from)?;
if delta.is_empty() {
Ok(PropertyOutcome::Holds)
} else {
Ok(PropertyOutcome::Violated(Violation {
property: case.property,
severity: case.property.severity(),
left: OutputDigest::of(&delta),
right: OutputDigest::of(&[]),
detail: format!(
"delta against an exact summary of the same state is {} bytes, not empty",
delta.len()
),
}))
}
}
ConformanceProperty::WholeStateSelfDelta => {
if case.states[0].is_empty() {
return Ok(PropertyOutcome::Holds);
}
let a = &case.states[0];
let summary = oracle.summarize_state(a).map_err(inconclusive_from)?;
let delta = oracle
.get_state_delta(a, &summary)
.map_err(inconclusive_from)?;
if delta.len() < a.len() {
Ok(PropertyOutcome::Holds)
} else {
Ok(PropertyOutcome::Violated(Violation {
property: case.property,
severity: case.property.severity(),
left: OutputDigest::of(&delta),
right: OutputDigest::of(a),
detail: format!(
"self-delta is {} bytes against a {} byte state: synchronization saves nothing",
delta.len(),
a.len()
),
}))
}
}
ConformanceProperty::ReconciliationCycle => {
reconciliation_cycle(oracle, &case.states[0], &case.states[1], &case.related)
}
ConformanceProperty::PathAgreement => {
let (a, b) = (&case.states[0], &case.states[1]);
let forward = path_agreement(oracle, a, b, &case.related);
if matches!(forward, Ok(PropertyOutcome::Violated(_))) {
return forward;
}
let reverse = path_agreement(oracle, b, a, &case.related);
if matches!(reverse, Ok(PropertyOutcome::Violated(_))) {
return reverse;
}
match (forward, reverse) {
(Ok(outcome), _) => Ok(outcome),
(Err(_), Ok(outcome)) => Ok(outcome),
(Err(reason), Err(_)) => Err(reason),
}
}
ConformanceProperty::TransitionPathAgreement => {
let (base, result) = (&case.states[0], &case.states[1]);
let mut settled = result.to_vec();
let mut reached_fixpoint = false;
for _ in 0..MAX_CANONICALIZATION_APPLIES {
let again = merge(oracle, &settled, &settled)?;
if again == settled {
reached_fixpoint = true;
break;
}
settled = again;
}
if !reached_fixpoint {
return Err(Inconclusive::StateNotSettled);
}
require_valid(oracle, &settled, &case.related)?;
let merged = merge(oracle, base, &settled)?;
require_valid(oracle, &merged, &case.related)?;
Ok(compare(
case.property,
&merged,
&settled,
"merging the settled form of a state a peer actually REACHED back \
into the state it came from did not reproduce that settled form, so \
the merge path cannot reach what the update path reached: every \
peer that receives this state merges it into something else, and \
the two can never agree",
))
}
}
}
fn path_agreement<O: ConformanceOracle + ?Sized>(
oracle: &mut O,
base: &Bytes,
other: &Bytes,
related: &RelatedContracts<'static>,
) -> Result<PropertyOutcome, Inconclusive> {
let base_summary = oracle.summarize_state(base).map_err(inconclusive_from)?;
let delta = oracle
.get_state_delta(other, &base_summary)
.map_err(inconclusive_from)?;
if delta.is_empty() || delta_would_be_refused(&delta, other) {
return Err(Inconclusive::NoDeltaPath);
}
let delta_path = apply_delta_bytes(oracle, base, &delta)?;
let merge_path = merge(oracle, base, other)?;
if delta_path == merge_path {
return Ok(PropertyOutcome::Holds);
}
require_valid(oracle, &delta_path, related)?;
require_valid(oracle, &merge_path, related)?;
let healed = merge(oracle, &delta_path, other)?;
if healed == merge_path {
return Ok(PropertyOutcome::Holds);
}
Ok(compare(
ConformanceProperty::PathAgreement,
&delta_path,
&merge_path,
"the delta path and the merge path disagree: applying the delta the network \
would have sent reached a different state from merging the sender's whole \
state, and re-merging the whole state does not repair the difference, so a \
peer that received this update as a delta can never agree with one that \
received it as a state",
))
}
fn reconciliation_cycle<O: ConformanceOracle + ?Sized>(
oracle: &mut O,
a: &Bytes,
b: &Bytes,
related: &RelatedContracts<'static>,
) -> Result<PropertyOutcome, Inconclusive> {
let mut left = a.to_vec();
let mut right = b.to_vec();
let mut seen: HashSet<([u8; 32], [u8; 32])> = HashSet::new();
seen.insert((digest(&left), digest(&right)));
for _ in 0..MAX_RECONCILIATION_ROUNDS {
if left == right {
return Ok(PropertyOutcome::Holds);
}
let left_summary = oracle.summarize_state(&left).map_err(inconclusive_from)?;
let right_summary = oracle.summarize_state(&right).map_err(inconclusive_from)?;
let to_left = oracle
.get_state_delta(&right, &left_summary)
.map_err(inconclusive_from)?;
let to_right = oracle
.get_state_delta(&left, &right_summary)
.map_err(inconclusive_from)?;
let mut next_left = if delta_would_be_refused(&to_left, &right) {
merge(oracle, &left, &right)?
} else {
apply_delta_bytes(oracle, &left, &to_left)?
};
let mut next_right = if delta_would_be_refused(&to_right, &left) {
merge(oracle, &right, &left)?
} else {
apply_delta_bytes(oracle, &right, &to_right)?
};
if next_left == left && next_right == right {
next_left = merge(oracle, &left, &right)?;
next_right = merge(oracle, &right, &left)?;
}
require_valid(oracle, &next_left, related)?;
require_valid(oracle, &next_right, related)?;
left = next_left;
right = next_right;
if left == right {
return Ok(PropertyOutcome::Holds);
}
if !seen.insert((digest(&left), digest(&right))) {
return Ok(PropertyOutcome::Violated(Violation {
property: ConformanceProperty::ReconciliationCycle,
severity: ConformanceProperty::ReconciliationCycle.severity(),
left: OutputDigest::of(&left),
right: OutputDigest::of(&right),
detail: "two valid states revisited an exact state pair while still divergent: \
reconciliation cannot converge"
.to_string(),
}));
}
}
Err(Inconclusive::RoundLimit)
}
fn digest(bytes: &[u8]) -> [u8; 32] {
*blake3::hash(bytes).as_bytes()
}
fn delta_would_be_refused(delta: &[u8], sender_state: &[u8]) -> bool {
!delta.is_empty()
&& delta.len()
>= sender_state
.len()
.saturating_add(crate::ring::interest::MIN_FULL_STATE_SAVING_BYTES)
}
fn require_valid<O: ConformanceOracle + ?Sized>(
oracle: &mut O,
state: &[u8],
related: &RelatedContracts<'static>,
) -> Result<(), Inconclusive> {
match oracle
.validate_state(state, related)
.map_err(inconclusive_from)?
{
ValidateResult::Valid => Ok(()),
ValidateResult::Invalid => Err(Inconclusive::InputNotValid),
ValidateResult::RequestRelated(_) => Err(Inconclusive::RelatedRequired),
}
}
fn merge<O: ConformanceOracle + ?Sized>(
oracle: &mut O,
base: &[u8],
other: &[u8],
) -> Result<Vec<u8>, Inconclusive> {
let update = UpdateData::State(State::from(other.to_vec()));
apply_updates(oracle, base, &[update])
}
fn apply_delta<O: ConformanceOracle + ?Sized>(
oracle: &mut O,
base: &[u8],
delta: &[u8],
) -> Result<Vec<u8>, Inconclusive> {
apply_delta_bytes(oracle, base, delta)
}
fn apply_delta_bytes<O: ConformanceOracle + ?Sized>(
oracle: &mut O,
base: &[u8],
delta: &[u8],
) -> Result<Vec<u8>, Inconclusive> {
if delta.is_empty() {
return Ok(base.to_vec());
}
let update = UpdateData::Delta(StateDelta::from(delta.to_vec()));
apply_updates(oracle, base, &[update])
}
fn apply_updates<O: ConformanceOracle + ?Sized>(
oracle: &mut O,
base: &[u8],
updates: &[UpdateData<'_>],
) -> Result<Vec<u8>, Inconclusive> {
let modification = oracle
.update_state(base, updates)
.map_err(inconclusive_from)?;
match modification.new_state {
Some(state) => Ok(state.into_bytes()),
None if modification.requires_dependencies() => Err(Inconclusive::RelatedRequired),
None => Err(Inconclusive::NoOutputState),
}
}
fn inconclusive_from(err: OracleError) -> Inconclusive {
match err.kind {
OracleErrorKind::Resource => Inconclusive::ResourceLimit(err.message),
OracleErrorKind::Contract | OracleErrorKind::Runtime => {
Inconclusive::ContractError(err.message)
}
}
}
fn compare(
property: ConformanceProperty,
left: &[u8],
right: &[u8],
detail: &str,
) -> PropertyOutcome {
if left == right {
return PropertyOutcome::Holds;
}
let detail = if is_reordering(left, right) {
&format!(
"{detail}. NOTE: both results hold exactly the same bytes in a different \
order, so the merge agreed on content and the ENCODING is not canonical \
(e.g. a HashMap serialized in iteration order). Peers compare state by \
hash, so this still prevents convergence, but the fix is a deterministic \
encoding rather than a change to the merge"
)
} else {
detail
};
violation(property, left, right, detail)
}
fn is_reordering(left: &[u8], right: &[u8]) -> bool {
if left.len() != right.len() {
return false;
}
let mut a = left.to_vec();
let mut b = right.to_vec();
a.sort_unstable();
b.sort_unstable();
a == b
}
fn violation(
property: ConformanceProperty,
left: &[u8],
right: &[u8],
detail: &str,
) -> PropertyOutcome {
PropertyOutcome::Violated(Violation {
property,
severity: property.severity(),
left: OutputDigest::of(left),
right: OutputDigest::of(right),
detail: detail.to_string(),
})
}