-
Notifications
You must be signed in to change notification settings - Fork 0
Commit
This commit does not belong to any branch on this repository, and may belong to a fork outside of the repository.
- Loading branch information
1 parent
7cedf9a
commit f5bdea9
Showing
4 changed files
with
272 additions
and
0 deletions.
There are no files selected for viewing
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -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 |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,6 @@ | ||
# Aiken compilation artifacts | ||
artifacts/ | ||
# Aiken's project working directory | ||
build/ | ||
# Aiken's default documentation export | ||
docs/ |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,36 @@ | ||
<div align="center"> | ||
<hr /> | ||
<h2 align="center" style="border-bottom: none"><img style="position: relative; top: 0.25rem;" src="https://raw.githubusercontent.com/aiken-lang/branding/main/assets/icon.png" alt="Aiken" height="30" /> aiken/bench</h2> | ||
|
||
[![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) | ||
<hr/> | ||
</div> | ||
|
||
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 |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -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<a> { | ||
fn(_n) { fn(prng) { Some((prng, x)) } } | ||
} | ||
|
||
/// Create a sampler that generates integers with configurable growth | ||
pub fn int(growth: Growth) -> Sampler<Int> { | ||
fn(n) -> Fuzzer<Int> { | ||
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<ByteArray> { | ||
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<a>, i: Int, scaled_length: Int, fuzzer: Fuzzer<a>, prng: PRNG) -> Option<(PRNG, List<a>)> { | ||
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<a>, growth: Growth) -> Sampler<List<a>> { | ||
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<a>, second_sampler: Sampler<b>) -> Sampler<Pair<a, b>> { | ||
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<a>, second_sampler: Sampler<b>, 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<ByteArray>, value_sampler: Sampler<a>, 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<a>, f: fn(a) -> b) -> Sampler<b> { | ||
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<a>, sampler2: Sampler<b>, f: fn(a, b) -> c) -> Sampler<c> { | ||
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 | ||
} | ||
} | ||
} | ||
} |