diff --git a/.github/workflows/continuous-integration.yml b/.github/workflows/continuous-integration.yml new file mode 100644 index 0000000..ebec289 --- /dev/null +++ b/.github/workflows/continuous-integration.yml @@ -0,0 +1,18 @@ +name: Continuous Integration + +on: + push: + branches: ["main"] + pull_request: + +jobs: + build: + runs-on: ubuntu-latest + steps: + - uses: actions/checkout@v3 + - uses: aiken-lang/setup-aiken@v1 + with: + version: v1.1.9 + - run: aiken fmt --check + - run: aiken check -D + - run: aiken build diff --git a/.gitignore b/.gitignore new file mode 100644 index 0000000..ff7811b --- /dev/null +++ b/.gitignore @@ -0,0 +1,6 @@ +# Aiken compilation artifacts +artifacts/ +# Aiken's project working directory +build/ +# Aiken's default documentation export +docs/ diff --git a/README.md b/README.md new file mode 100644 index 0000000..589721d --- /dev/null +++ b/README.md @@ -0,0 +1,36 @@ +
+
+

Aiken aiken/bench

+ +[![Licence](https://img.shields.io/github/license/aiken-lang/bench?style=for-the-badge)](https://github.com/aiken-lang/bench/blob/main/LICENSE) +[![Continuous Integration](https://img.shields.io/github/actions/workflow/status/aiken-lang/bench/continuous-integration.yml?style=for-the-badge)](https://github.com/aiken-lang/bench/actions/workflows/continuous-integration.yml) +
+
+ +The official library for writing _samplers_ (a.k.a Scaled Fuzzers) for the [Aiken](https://aiken-lang.org) Cardano smart-contract language. + +> ### ⚠️ WARNING +> +> **IMPORTANT:** This is a work in progress and the API is not stable yet; additionally Samplers are not yet supported in the current version of Aiken (v1.1.9). + +## Installation + +``` +aiken add aiken-lang/bench --version v0.0.0 +``` + +## Getting started + +First, make sure you have the [Aiken's user manual about tests](https://aiken-lang.org/language-tour/tests#property-based-test); in particular the section about benchmarking functions. + +In many situations, you can use primitives from this library out-of-the-box, composing them inline when necessary. For example, if you need a growing non-empty list of values, you can simply write: + +``` +use aiken/bench + +bench my_bench(xs via bench.list(bench.int(Linear(1)), Linear(1))) { + // some function +} +``` + +You can also write your own more complex sampler. Note that writing good samplers can be complicated, so here are a few guiding principles you should follow.. TODO \ No newline at end of file diff --git a/lib/aiken/sample.ak b/lib/aiken/sample.ak new file mode 100644 index 0000000..92ea18e --- /dev/null +++ b/lib/aiken/sample.ak @@ -0,0 +1,212 @@ +use aiken/builtin +use cardano/assets.{Value} +use aiken/interval.{Interval} +use cardano/script_context.{ScriptContext} +use cardano/transaction.{Input, Output, ScriptPurpose, Transaction} +use aiken/crypto.{Blake2b_224, Hash} +use cardano/address.{Address, Credential, Inline, VerificationKey} +use aiken/collection/dict + +/// A growth pattern determines how complexity scales with input size +pub type Growth { + /// Constant growth: f(n) = c + Constant + /// Linear growth: f(n) = n + Linear(Int) + /// Exponential growth: f(n) = 2^n + Exponential(Int) + /// Logarithmic growth: f(n) = log(n) + Logarithmic(Int) +} + +/// Core random number generator that produces a byte (0-255) +fn rand(prng: PRNG) -> Option<(PRNG, Int)> { + when prng is { + Seeded { seed, choices } -> { + let choice = builtin.index_bytearray(seed, 0) + Some(( + Seeded { + seed: crypto.blake2b_256(seed), + choices: builtin.cons_bytearray(choice, choices), + }, + choice, + )) + } + Replayed { cursor, choices } -> { + if cursor >= 1 { + let cursor = cursor - 1 + Some(( + Replayed { cursor, choices }, + builtin.index_bytearray(choices, cursor), + )) + } else { + None + } + } + } +} + +/// Apply a growth pattern to scale an input size +pub fn apply_growth(pattern: Growth, n: Int) -> Int { + when pattern is { + Constant -> 1 + Linear(base) -> base * n + Exponential(base) -> + if n <= 0 { + 0 + } else { + exp_int_helper(base, n, 1) + } + Logarithmic(base) -> + if n <= 1 { + 0 + } else { + count_divisions(base, n, 0) + } + } +} + +// TODO: Probably need to use a builtin in the future. +fn exp_int_helper(base: Int, n: Int, acc: Int) -> Int { + if base <= 0 { + acc + } else { + exp_int_helper(base - 1, n, acc * n) + } +} + +// TODO: Probably need to use a builtin in the future. +fn count_divisions(base: Int, n: Int, acc: Int) -> Int { + if n <= 1 { + acc + } else { + count_divisions(base, n / base, acc + 1) + } +} + +// SAMPLERS: + +/// Create a constant sampler that always returns the same value +pub fn constant(x: a) -> Sampler { + fn(_n) { fn(prng) { Some((prng, x)) } } +} + +/// Create a sampler that generates integers with configurable growth +pub fn int(growth: Growth) -> Sampler { + fn(n) -> Fuzzer { + let scaled_multiplier = apply_growth(growth, n) + fn(prng) { + when rand(prng) is { + Some((new_prng, value)) -> Some((new_prng, value * scaled_multiplier)) + None -> None + } + } + } +} + +/// Generate bytestrings with length scaling according to the growth pattern +pub fn bytestring(growth: Growth) -> Sampler { + fn(n) { + let length_fuzzer = int(growth)(n) + fn(prng) { + when length_fuzzer(prng) is { + Some((prng2, len)) -> { + todo + } + None -> None + } + } + } +} + +fn list_handler(items_so_far: List, i: Int, scaled_length: Int, fuzzer: Fuzzer, prng: PRNG) -> Option<(PRNG, List)> { + if i > scaled_length { + Some((prng, items_so_far)) + } else { + when fuzzer(prng) is { + Some((new_prng, item)) -> + list_handler([item, ..items_so_far], i + 1, scaled_length, fuzzer, new_prng) + None -> None + } + } +} + +/// Create a sampler that generates lists with configurable growth +pub fn list(element_sampler: Sampler, growth: Growth) -> Sampler> { + fn(n) { + let scaled_length = apply_growth(growth, n) + let element_fuzzer = element_sampler(n) + fn(prng) { + list_handler([], 0, scaled_length, element_fuzzer, prng) + } + } +} + +pub fn pair(first_sampler: Sampler, second_sampler: Sampler) -> Sampler> { + fn(n) { + let first_fuzzer = first_sampler(n) + let second_fuzzer = second_sampler(n) + fn(prng) { + when first_fuzzer(prng) is { + Some((prng2, first_val)) -> { + when second_fuzzer(prng2) is { + Some((prng3, second_val)) -> + Some((prng3, Pair(first_val, second_val))) + None -> None + } + } + None -> None + } + } + } +} + +pub fn pairs(first_sampler: Sampler, second_sampler: Sampler, growth: Growth) { + fn (n) { + let scaled_length = apply_growth(growth, n) + let pair_sampler = pair(first_sampler, second_sampler)(n) + + fn(prng) { + list_handler([], 0, scaled_length, pair_sampler, prng) + } + } +} + +pub fn dict(key_sampler: Sampler, value_sampler: Sampler, growth: Growth) { + fn (n) { + let scaled_length = apply_growth(growth, n) + map(pairs(key_sampler, value_sampler, growth), fn(pairs) { + dict.from_pairs(pairs) + }) + } +} + +pub fn map(sampler: Sampler, f: fn(a) -> b) -> Sampler { + fn(n) { + let fuzzer = sampler(n) + fn(prng) { + when fuzzer(prng) is { + Some((prng2, a)) -> Some((prng2, f(a))) + None -> None + } + } + } +} + +pub fn map2(sampler: Sampler, sampler2: Sampler, f: fn(a, b) -> c) -> Sampler { + fn(n) { + let fuzzer1 = sampler(n) + let fuzzer2 = sampler2(n) + fn(prng) { + when fuzzer1(prng) is { + Some((prng2, a)) -> { + when fuzzer2(prng2) is { + Some((prng3, b)) -> Some((prng3, f(a, b))) + None -> None + } + } + None -> None + } + } + } +} \ No newline at end of file