diff --git a/src/effects/mod.rs b/src/effects/mod.rs index 5ba210e..091bd3c 100644 --- a/src/effects/mod.rs +++ b/src/effects/mod.rs @@ -7,3 +7,6 @@ pub use resource::{ResPut, ResWith}; mod event; pub use event::EventSend; + +#[cfg(test)] +mod one_way_fn; diff --git a/src/effects/one_way_fn.rs b/src/effects/one_way_fn.rs new file mode 100644 index 0000000..5496c45 --- /dev/null +++ b/src/effects/one_way_fn.rs @@ -0,0 +1,57 @@ +//! A test utility for generating one-way-functions with proptest. + +use blake2::digest::consts::U16; +use blake2::{Blake2b, Digest}; +use proptest::prelude::*; +use proptest_derive::Arbitrary; + +const MAX_COMPLEXITY: u8 = 32; + +/// A pseudo one-way-function that can be used as `proptest` input. +/// +/// Since `proptest` requires that input types implement `Debug`, it's impossible to have +/// `proptest` use a closure as input. +/// So, instead, we can have `proptest` generate this fairly normal type, which determines the +/// behavior of [`OneWayFn::call`] - the actual "generated" function. +/// +/// When `complexity` is 0, `call` is the identity function. +/// In any other case, it involves blake2 hashing, so it is cryptographically a one-way-function. +/// This can be useful for essentially proving that some higher order function actually called its +/// input function - it's very unlikely for it to have "guessed" the correct output. +/// While 0 being the identity provides a reasonable "trivial case" for `proptest` shrinking. +/// +/// For now this only provides a function like `Fn(u128) -> u128`. +#[derive(Copy, Clone, Debug, PartialEq, Eq, Arbitrary)] +pub struct OneWayFn { + #[proptest(strategy = "0..MAX_COMPLEXITY")] + complexity: u8, +} + +fn hash_once(input: [u8; 16]) -> [u8; 16] { + let mut hasher = Blake2b::::new(); + hasher.update(input); + hasher.finalize().into() +} + +impl OneWayFn { + /// Performs the function on the input. + pub fn call(&self, input: u128) -> u128 { + u128::from_be_bytes((0..self.complexity).fold(input.to_be_bytes(), |acc, _| hash_once(acc))) + } +} + +proptest! { + #[test] + fn one_way_fn_of_0_complexity_is_identity(input: u128) { + let output = OneWayFn { complexity: 0 }.call(input); + prop_assert_eq!(input, output); + } + + #[test] + fn one_way_fn_complexity_changes_output_inductively(input: u128, hypothesis_complexity in 0..MAX_COMPLEXITY - 1) { + let hypothesis_one_way_fn = OneWayFn { complexity: hypothesis_complexity }; + let step_one_way_fn = OneWayFn { complexity: hypothesis_complexity + 1 }; + + prop_assert_ne!(hypothesis_one_way_fn.call(input), step_one_way_fn.call(input)); + } +} diff --git a/src/effects/resource.rs b/src/effects/resource.rs index 65dea1b..6372450 100644 --- a/src/effects/resource.rs +++ b/src/effects/resource.rs @@ -61,34 +61,16 @@ where #[cfg(test)] mod tests { - use blake2::digest::consts::U16; - use blake2::{Blake2b, Digest}; use proptest::prelude::*; use proptest_derive::Arbitrary; use super::*; + use crate::effects::one_way_fn::OneWayFn; use crate::system_combinators::affect; #[derive(Copy, Clone, Debug, PartialEq, Eq, Resource, Arbitrary)] struct NumberResource(u128); - fn number_resource_one_way_fn_fn( - salt: Vec, - ) -> impl FnOnce(NumberResource) -> NumberResource + Clone { - move |NumberResource(input)| { - let data = salt - .into_iter() - .chain(input.to_be_bytes()) - .collect::>(); - - let mut hasher = Blake2b::::new(); - hasher.update(data); - let res = hasher.finalize(); - - NumberResource(u128::from_be_bytes(res.into())) - } - } - proptest! { #[test] fn res_put_overwrites_initial_state(initial: NumberResource, put: NumberResource) { @@ -107,15 +89,15 @@ mod tests { } #[test] - fn res_with_correctly_executes_one_way_function(initial: NumberResource, salt: Vec) { - let f = number_resource_one_way_fn_fn(salt); - - let expected = f.clone()(initial); + fn res_with_correctly_executes_one_way_function(initial: NumberResource, f: OneWayFn) { + let expected = NumberResource(f.call(initial.0)); let mut app = App::new(); - app.insert_resource(initial.clone()) - .add_systems(Update, (move || ResWith::new(f.clone())).pipe(affect)); + app.insert_resource(initial.clone()).add_systems( + Update, + (move || ResWith::new(move |NumberResource(n)| NumberResource(f.call(n)))).pipe(affect), + ); prop_assert_eq!(app.world().resource::(), &initial);