use super::oracle::ConformanceOracle;
use super::property::{ConformanceProperty, PropertyOutcome};
use super::verifier::{Bytes, ConformanceCase, verify_case};
pub const DEFAULT_MAX_SHRINK_ATTEMPTS: usize = 32;
#[derive(Debug, Clone)]
pub struct MinimizeConfig {
pub max_attempts: usize,
}
impl Default for MinimizeConfig {
fn default() -> Self {
Self {
max_attempts: DEFAULT_MAX_SHRINK_ATTEMPTS,
}
}
}
#[derive(Debug, Clone, Copy, PartialEq, Eq)]
pub struct MinimizeReport {
pub original_bytes: usize,
pub final_bytes: usize,
pub attempts: usize,
}
pub fn minimize<O: ConformanceOracle + ?Sized>(
oracle: &mut O,
case: &ConformanceCase,
state_candidates: &[Bytes],
delta_candidates: &[Bytes],
config: &MinimizeConfig,
) -> (ConformanceCase, MinimizeReport) {
let original_bytes = case.input_bytes();
let mut report = MinimizeReport {
original_bytes,
final_bytes: original_bytes,
attempts: 0,
};
let property = match violated_property(oracle, case) {
Some(property) => property,
None => return (case.clone(), report),
};
report.attempts += 1;
let mut ordered_states: Vec<Bytes> = state_candidates.to_vec();
ordered_states.sort_by_key(|c| c.len());
let mut ordered_deltas: Vec<Bytes> = delta_candidates.to_vec();
ordered_deltas.sort_by_key(|c| c.len());
let mut current = case.clone();
for slot in 0..current.states.len() {
if !shrink_slot(
oracle,
&mut current,
property,
&ordered_states,
config,
&mut report,
|case, i| &mut case.states[i],
slot,
) {
break;
}
}
for slot in 0..current.deltas.len() {
if !shrink_slot(
oracle,
&mut current,
property,
&ordered_deltas,
config,
&mut report,
|case, i| &mut case.deltas[i],
slot,
) {
break;
}
}
report.final_bytes = current.input_bytes();
(current, report)
}
#[allow(clippy::too_many_arguments)]
fn shrink_slot<O: ConformanceOracle + ?Sized>(
oracle: &mut O,
current: &mut ConformanceCase,
property: ConformanceProperty,
ordered: &[Bytes],
config: &MinimizeConfig,
report: &mut MinimizeReport,
slot_of: impl Fn(&mut ConformanceCase, usize) -> &mut Bytes,
slot: usize,
) -> bool {
for candidate in ordered {
if report.attempts >= config.max_attempts {
return false;
}
{
let existing = slot_of(current, slot);
if candidate.len() >= existing.len() {
break;
}
}
let mut trial = current.clone();
*slot_of(&mut trial, slot) = candidate.clone();
report.attempts += 1;
if violated_property(oracle, &trial) == Some(property) {
*current = trial;
break;
}
}
true
}
fn violated_property<O: ConformanceOracle + ?Sized>(
oracle: &mut O,
case: &ConformanceCase,
) -> Option<ConformanceProperty> {
match verify_case(oracle, case) {
PropertyOutcome::Violated(violation) => Some(violation.property),
PropertyOutcome::Holds | PropertyOutcome::Inconclusive(_) => None,
}
}
#[cfg(test)]
mod tests {
use std::sync::Arc;
use freenet_stdlib::prelude::{
RelatedContracts, State, UpdateData, UpdateModification, ValidateResult,
};
use super::*;
use crate::conformance::oracle::OracleError;
struct LastWriteWins;
impl ConformanceOracle for LastWriteWins {
fn validate_state(
&mut self,
_state: &[u8],
_related: &RelatedContracts<'_>,
) -> Result<ValidateResult, OracleError> {
Ok(ValidateResult::Valid)
}
fn update_state(
&mut self,
_state: &[u8],
updates: &[UpdateData<'_>],
) -> Result<UpdateModification<'static>, OracleError> {
match updates.first() {
Some(UpdateData::State(incoming)) => Ok(UpdateModification::valid(State::from(
incoming.as_ref().to_vec(),
))),
_ => Err(OracleError::contract("unsupported update")),
}
}
fn summarize_state(&mut self, state: &[u8]) -> Result<Vec<u8>, OracleError> {
Ok(state.to_vec())
}
fn get_state_delta(
&mut self,
state: &[u8],
_summary: &[u8],
) -> Result<Vec<u8>, OracleError> {
Ok(state.to_vec())
}
}
struct AlwaysConforming;
impl ConformanceOracle for AlwaysConforming {
fn validate_state(
&mut self,
_state: &[u8],
_related: &RelatedContracts<'_>,
) -> Result<ValidateResult, OracleError> {
Ok(ValidateResult::Valid)
}
fn update_state(
&mut self,
state: &[u8],
updates: &[UpdateData<'_>],
) -> Result<UpdateModification<'static>, OracleError> {
let mut merged = state.to_vec();
for update in updates {
if let UpdateData::State(incoming) = update {
let incoming = incoming.as_ref();
if incoming > merged.as_slice() {
merged = incoming.to_vec();
}
}
}
Ok(UpdateModification::valid(State::from(merged)))
}
fn summarize_state(&mut self, state: &[u8]) -> Result<Vec<u8>, OracleError> {
Ok(state.to_vec())
}
fn get_state_delta(
&mut self,
_state: &[u8],
_summary: &[u8],
) -> Result<Vec<u8>, OracleError> {
Ok(Vec::new())
}
}
fn bytes(len: usize, fill: u8) -> Bytes {
Arc::from(vec![fill; len].as_slice())
}
fn big_case() -> ConformanceCase {
ConformanceCase::new(
ConformanceProperty::StateCommutativity,
vec![bytes(4096, 1), bytes(4096, 2)],
)
}
#[test]
fn a_large_witness_shrinks_to_a_small_one() {
let candidates = vec![bytes(2048, 3), bytes(4, 4), bytes(8, 5)];
let (minimized, report) = minimize(
&mut LastWriteWins,
&big_case(),
&candidates,
&candidates,
&MinimizeConfig::default(),
);
assert!(
report.final_bytes < report.original_bytes,
"shrinking achieved nothing: {report:?}"
);
assert!(
report.final_bytes <= 16,
"expected the smallest available witness, got {} bytes",
report.final_bytes
);
assert_eq!(
violated_property(&mut LastWriteWins, &minimized),
Some(ConformanceProperty::StateCommutativity)
);
}
#[test]
fn shrinking_never_loses_the_violation() {
let candidates: Vec<Bytes> = (1u8..20).map(|i| bytes(i as usize, i)).collect();
let (minimized, _) = minimize(
&mut LastWriteWins,
&big_case(),
&candidates,
&candidates,
&MinimizeConfig::default(),
);
assert!(verify_case(&mut LastWriteWins, &minimized).is_violation());
}
#[test]
fn a_case_that_does_not_fail_is_returned_untouched() {
let original = big_case();
let candidates = vec![bytes(4, 9)];
let (minimized, report) = minimize(
&mut AlwaysConforming,
&original,
&candidates,
&candidates,
&MinimizeConfig::default(),
);
assert_eq!(minimized.states, original.states);
assert_eq!(report.final_bytes, report.original_bytes);
}
#[test]
fn a_large_delta_witness_shrinks_too() {
struct Concat;
impl ConformanceOracle for Concat {
fn validate_state(
&mut self,
_state: &[u8],
_related: &RelatedContracts<'_>,
) -> Result<ValidateResult, OracleError> {
Ok(ValidateResult::Valid)
}
fn update_state(
&mut self,
state: &[u8],
updates: &[UpdateData<'_>],
) -> Result<UpdateModification<'static>, OracleError> {
let mut out = state.to_vec();
for update in updates {
if let UpdateData::Delta(d) = update {
out.extend_from_slice(d.as_ref());
}
}
Ok(UpdateModification::valid(State::from(out)))
}
fn summarize_state(&mut self, state: &[u8]) -> Result<Vec<u8>, OracleError> {
Ok(state.to_vec())
}
fn get_state_delta(
&mut self,
state: &[u8],
_summary: &[u8],
) -> Result<Vec<u8>, OracleError> {
Ok(state.to_vec())
}
}
let case = ConformanceCase::new(
ConformanceProperty::DeltaPermutationInvariance,
vec![bytes(4, 1)],
)
.with_deltas(vec![bytes(4096, 2), bytes(4096, 3)]);
let candidates = vec![bytes(2, 7), bytes(2, 8)];
let (minimized, report) = minimize(
&mut Concat,
&case,
&candidates,
&candidates,
&MinimizeConfig::default(),
);
assert!(
report.final_bytes < report.original_bytes / 2,
"the delta witness did not shrink: {report:?}"
);
assert!(verify_case(&mut Concat, &minimized).is_violation());
}
#[test]
fn shrinking_never_puts_a_state_into_a_delta_slot() {
struct Append;
impl ConformanceOracle for Append {
fn validate_state(
&mut self,
_state: &[u8],
_related: &RelatedContracts<'_>,
) -> Result<ValidateResult, OracleError> {
Ok(ValidateResult::Valid)
}
fn update_state(
&mut self,
state: &[u8],
updates: &[UpdateData<'_>],
) -> Result<UpdateModification<'static>, OracleError> {
let mut out = state.to_vec();
for update in updates {
if let UpdateData::Delta(d) = update {
out.extend_from_slice(d.as_ref());
}
}
Ok(UpdateModification::valid(State::from(out)))
}
fn summarize_state(&mut self, state: &[u8]) -> Result<Vec<u8>, OracleError> {
Ok(state.to_vec())
}
fn get_state_delta(
&mut self,
state: &[u8],
_summary: &[u8],
) -> Result<Vec<u8>, OracleError> {
Ok(state.to_vec())
}
}
let state_pool = vec![bytes(4, 0xAA), bytes(8, 0xAB)];
let delta_pool = vec![bytes(4, 0xDD), bytes(8, 0xDE)];
let case = ConformanceCase::new(
ConformanceProperty::DeltaPermutationInvariance,
vec![bytes(4096, 1)],
)
.with_deltas(vec![bytes(4096, 2), bytes(4096, 3)]);
let (minimized, _) = minimize(
&mut Append,
&case,
&state_pool,
&delta_pool,
&MinimizeConfig::default(),
);
for (i, delta) in minimized.deltas.iter().enumerate() {
assert!(
!delta.iter().any(|b| *b == 0xAA || *b == 0xAB),
"delta slot {i} contains bytes from the STATE pool, i.e. an input the \
contract never produced as a delta"
);
}
for (i, state) in minimized.states.iter().enumerate() {
assert!(
!state.iter().any(|b| *b == 0xDD || *b == 0xDE),
"state slot {i} contains bytes from the DELTA pool"
);
}
}
#[test]
fn shrinking_respects_its_attempt_budget() {
let candidates: Vec<Bytes> = (1u8..100).map(|i| bytes(i as usize, i)).collect();
let config = MinimizeConfig { max_attempts: 3 };
let (_, report) = minimize(
&mut LastWriteWins,
&big_case(),
&candidates,
&candidates,
&config,
);
assert!(
report.attempts <= config.max_attempts,
"shrinking ran {} verifications against a budget of {}",
report.attempts,
config.max_attempts
);
}
#[test]
fn no_smaller_candidate_leaves_the_case_alone() {
let original = ConformanceCase::new(
ConformanceProperty::StateCommutativity,
vec![bytes(4, 1), bytes(4, 2)],
);
let candidates = vec![bytes(64, 3), bytes(128, 4)];
let (minimized, report) = minimize(
&mut LastWriteWins,
&original,
&candidates,
&candidates,
&MinimizeConfig::default(),
);
assert_eq!(minimized.states, original.states);
assert_eq!(report.final_bytes, report.original_bytes);
}
}