Skip to content

Commit

Permalink
draft: Add arb_tests
Browse files Browse the repository at this point in the history
  • Loading branch information
yancyribbens committed Oct 17, 2024
1 parent 31453f4 commit c344712
Show file tree
Hide file tree
Showing 5 changed files with 404 additions and 33 deletions.
15 changes: 14 additions & 1 deletion Cargo.toml
Original file line number Diff line number Diff line change
Expand Up @@ -14,14 +14,27 @@ keywords = ["bitcoin", "coin-selection", "coin", "coinselection", "utxo"]
readme = "README.md"

[dependencies]
bitcoin = "0.32.3"
bitcoin = { git = "https://github.com/yancyribbens/rust-bitcoin.git", rev = "a0c58a4a8b4244d7c541906c61d1343dd6acdccd" }
rand = {version = "0.8.5", default-features = false, optional = true}

[dev-dependencies]
bitcoin = { git = "https://github.com/yancyribbens/rust-bitcoin.git", rev = "a0c58a4a8b4244d7c541906c61d1343dd6acdccd", features = ["arbitrary"] }
criterion = "0.3"
bitcoin-coin-selection = {path = ".", features = ["rand"]}
rand = "0.8.5"
arbitrary = { version = "1", features = ["derive"] }
arbtest = "0.3.1"
exhaustigen = "0.1.0"

[[bench]]
name = "coin_selection"
harness = false

[patch.crates-io]
bitcoin_hashes = { git = "https://github.com/yancyribbens/rust-bitcoin.git", rev = "a0c58a4a8b4244d7c541906c61d1343dd6acdccd" }
base58ck = { git = "https://github.com/yancyribbens/rust-bitcoin.git", rev = "a0c58a4a8b4244d7c541906c61d1343dd6acdccd" }
bitcoin-internals = { git = "https://github.com/yancyribbens/rust-bitcoin.git", rev = "a0c58a4a8b4244d7c541906c61d1343dd6acdccd" }
bitcoin-io = { git = "https://github.com/yancyribbens/rust-bitcoin.git", rev = "a0c58a4a8b4244d7c541906c61d1343dd6acdccd" }
bitcoin-primitives = { git = "https://github.com/yancyribbens/rust-bitcoin.git", rev = "a0c58a4a8b4244d7c541906c61d1343dd6acdccd" }
bitcoin-addresses = { git = "https://github.com/yancyribbens/rust-bitcoin.git", rev = "a0c58a4a8b4244d7c541906c61d1343dd6acdccd" }
bitcoin-units = { git = "https://github.com/yancyribbens/rust-bitcoin.git", rev = "a0c58a4a8b4244d7c541906c61d1343dd6acdccd" }
10 changes: 10 additions & 0 deletions run_tests.sh
Original file line number Diff line number Diff line change
@@ -0,0 +1,10 @@
#!/bin/bash
while :
do
if cargo test proptest ; then
echo "success"
else
echo "fail"
break
fi
done
217 changes: 189 additions & 28 deletions src/branch_and_bound.rs
Original file line number Diff line number Diff line change
Expand Up @@ -320,12 +320,17 @@ mod tests {
use core::str::FromStr;
use std::iter::{once, zip};

use bitcoin::{Amount, ScriptBuf, TxOut, Weight};
use arbitrary::{Arbitrary, Unstructured};
use arbtest::arbtest;
use bitcoin::transaction::effective_value;
use bitcoin::{Amount, Weight};

use super::*;
use crate::tests::Utxo;
use crate::tests::{build_utxo, Utxo, UtxoPool};
use crate::WeightedUtxo;

const TX_IN_BASE_WEIGHT: u64 = 160;

#[derive(Debug)]
pub struct ParamsStr<'a> {
target: &'a str,
Expand All @@ -335,11 +340,6 @@ mod tests {
weighted_utxos: Vec<&'a str>,
}

fn build_utxo(amt: Amount, satisfaction_weight: Weight) -> Utxo {
let output = TxOut { value: amt, script_pubkey: ScriptBuf::new() };
Utxo { output, satisfaction_weight }
}

fn build_pool(fee: Amount) -> Vec<Utxo> {
let amts = [
Amount::from_str("1 cBTC").unwrap() + fee,
Expand Down Expand Up @@ -380,24 +380,13 @@ mod tests {
assert_eq!(input_str_list, expected_str_list);
}

// This is a temporary patch and can be removed when a new relesae of rust-bitcoin is
// published. See: https://github.com/rust-bitcoin/rust-bitcoin/pull/3346
fn amount_from_str_patch(amount: &str) -> Amount {
let a = Amount::from_str(amount);

match a {
Ok(a) => a,
Err(_) => Amount::ZERO,
}
}

fn assert_coin_select_params(p: &ParamsStr, expected_inputs: Option<&[&str]>) {
let fee_rate = p.fee_rate.parse::<u64>().unwrap(); // would be nice if FeeRate had
// from_str like Amount::from_str()
let lt_fee_rate = p.lt_fee_rate.parse::<u64>().unwrap();

let target = amount_from_str_patch(p.target);
let cost_of_change = amount_from_str_patch(p.cost_of_change);
let target = Amount::from_str(p.target).unwrap();
let cost_of_change = Amount::from_str(p.cost_of_change).unwrap();
let fee_rate = FeeRate::from_sat_per_kwu(fee_rate);
let lt_fee_rate = FeeRate::from_sat_per_kwu(lt_fee_rate);

Expand All @@ -424,6 +413,25 @@ mod tests {
}
}

// Use in place of arbitrary_in_range()
// see: https://github.com/rust-fuzz/arbitrary/pull/192
fn arb_amount_in_range(u: &mut Unstructured, r: std::ops::RangeInclusive<u64>) -> Amount {
let u = u.int_in_range::<u64>(r).unwrap();
Amount::from_sat(u)
}

// Use in place of arbitrary_in_range()
// see: https://github.com/rust-fuzz/arbitrary/pull/192
fn arb_fee_rate_in_range(u: &mut Unstructured, r: std::ops::RangeInclusive<u64>) -> FeeRate {
let u = u.int_in_range::<u64>(r).unwrap();
FeeRate::from_sat_per_kwu(u)
}

fn calculate_max_fee_rate(amount: Amount, weight: Weight) -> Option<FeeRate> {
let weight = weight + Weight::from_wu(TX_IN_BASE_WEIGHT);
amount.checked_div_by_weight(weight)
}

#[test]
fn select_coins_bnb_one() { assert_coin_select("1 cBTC", &["1 cBTC"]); }

Expand Down Expand Up @@ -456,7 +464,7 @@ mod tests {
assert_coin_select("10 cBTC", &["4 cBTC", "3 cBTC", "2 cBTC", "1 cBTC"]);
}

#[test]
#[test]
fn select_coins_bnb_zero() {
let params = ParamsStr {
target: "0",
Expand Down Expand Up @@ -651,17 +659,16 @@ mod tests {
});

let amts: Vec<_> = zip(alpha, beta)
// flatten requires iterable types.
// use once() to make tuple iterable.
// flatten requires iterable types.
// use once() to make tuple iterable.
.flat_map(|tup| once(tup.0).chain(once(tup.1)))
.map(|a| Amount::from_sat(a as u64))
.collect();

let pool: Vec<_> = amts.into_iter().map(|a| build_utxo(a, Weight::ZERO)).collect();

let list = select_coins_bnb(target, Amount::ONE_SAT, FeeRate::ZERO, FeeRate::ZERO, &pool);

assert!(list.is_none());
let result = select_coins_bnb(target, Amount::ONE_SAT, FeeRate::ZERO, FeeRate::ZERO, &pool);
assert!(result.is_none());
}

#[test]
Expand All @@ -678,15 +685,15 @@ mod tests {
let amts: Vec<_> = vals.map(Amount::from_sat).collect();
let pool: Vec<_> = amts.into_iter().map(|a| build_utxo(a, Weight::ZERO)).collect();

let list = select_coins_bnb(
let result = select_coins_bnb(
Amount::from_sat(target),
Amount::ONE_SAT,
FeeRate::ZERO,
FeeRate::ZERO,
&pool,
);

assert!(list.is_none());
assert!(result.is_none());
}

#[test]
Expand Down Expand Up @@ -719,4 +726,158 @@ mod tests {
assert_eq!(list.len(), 1);
assert_eq!(list.next().unwrap().value(), Amount::from_sat(target));
}

#[test]
fn select_one_of_one_idealized_proptest() {
let minimal_non_dust: u64 = 1;
let effective_value_max: u64 = SignedAmount::MAX.to_sat() as u64;

arbtest(|u| {
let amount = arb_amount_in_range(u, minimal_non_dust..=effective_value_max);
let utxo = build_utxo(amount, Weight::ZERO);
let pool: Vec<Utxo> = vec![utxo.clone()];

let coins: Vec<Utxo> =
select_coins_bnb(utxo.value(), Amount::ZERO, FeeRate::ZERO, FeeRate::ZERO, &pool)
.unwrap()
.cloned()
.collect();

assert_eq!(coins, pool);

Ok(())
});
}

#[test]
fn select_one_of_many_proptest() {
arbtest(|u| {
let pool = UtxoPool::arbitrary(u)?;
let utxos = pool.utxos.clone();

let utxo = u.choose(&utxos)?;

let max_fee_rate = calculate_max_fee_rate(utxo.value(), utxo.satisfaction_weight());
if let Some(f) = max_fee_rate {
let fee_rate = arb_fee_rate_in_range(u, 1..=f.to_sat_per_kwu());

let target_effective_value =
effective_value(fee_rate, utxo.satisfaction_weight(), utxo.value()).unwrap();

if let Ok(target) = target_effective_value.to_unsigned() {
let result = select_coins_bnb(target, Amount::ZERO, fee_rate, fee_rate, &utxos);

if let Some(r) = result {
let sum: SignedAmount = r
.map(|u| {
effective_value(fee_rate, u.satisfaction_weight(), u.value())
.unwrap()
})
.sum();
let amount_sum = sum.to_unsigned().unwrap();
assert_eq!(amount_sum, target);
} else {
// if result was none, then assert that fail happened because overflow when
// ssumming pool. In the future, assert specific error when added.
let available_value = utxos.into_iter().map(|u| u.value()).checked_sum();
assert!(available_value.is_none());
}
}
}

Ok(())
});
}

#[test]
fn select_many_of_many_proptest() {
arbtest(|u| {
let pool = UtxoPool::arbitrary(u)?;
let utxos = pool.utxos.clone();

// generate all the possible utxos subsets
let mut gen = exhaustigen::Gen::new();
let mut subsets: Vec<Vec<&Utxo>> = Vec::new();
while !gen.done() {
let s = gen.gen_subset(&pool.utxos).collect::<Vec<_>>();
subsets.push(s);
}

// choose a set at random to be the target
let target_selection: &Vec<&Utxo> = u.choose(&subsets).unwrap();

// find the minmum fee_rate that will result in all utxos having a posiive
// effective_value
let mut fee_rates: Vec<FeeRate> = target_selection
.iter()
.map(|u| {
calculate_max_fee_rate(u.value(), u.satisfaction_weight())
.unwrap_or(FeeRate::ZERO)
})
.collect();
fee_rates.sort();

let min_fee_rate = fee_rates.first().unwrap_or(&FeeRate::ZERO).to_sat_per_kwu();
let fee_rate = arb_fee_rate_in_range(u, 0..=min_fee_rate);

let effective_values: Vec<SignedAmount> = target_selection
.iter()
.map(|u| {
let e = effective_value(fee_rate, u.satisfaction_weight(), u.value());

e.unwrap_or(SignedAmount::ZERO)
})
.collect();

let eff_values_sum = effective_values.into_iter().checked_sum();

// if None, then this random subset is an invalid target (skip)
if let Some(s) = eff_values_sum {
if let Ok(target) = s.to_unsigned() {
let result = select_coins_bnb(target, Amount::ZERO, fee_rate, fee_rate, &utxos);

if let Some(r) = result {
let effective_value_sum: Amount = r
.map(|u| {
effective_value(fee_rate, u.satisfaction_weight(), u.value())
.unwrap()
.to_unsigned()
.unwrap()
})
.sum();
assert_eq!(effective_value_sum, target);
} else {
let available_value = utxos.into_iter().map(|u| u.value()).checked_sum();
assert!(
available_value.is_none()
|| target_selection.is_empty()
|| target == Amount::ZERO
);
}
}
}

Ok(())
});
}

use crate::tests::assert_proptest_bnb;
#[test]
fn select_bnb_proptest() {
arbtest(|u| {
let pool = UtxoPool::arbitrary(u)?;
let target = Amount::arbitrary(u)?;
let cost_of_change = Amount::arbitrary(u)?;
let fee_rate = FeeRate::arbitrary(u)?;
let lt_fee_rate = FeeRate::arbitrary(u)?;

let utxos = pool.utxos.clone();

let result = select_coins_bnb(target, cost_of_change, fee_rate, lt_fee_rate, &utxos);

assert_proptest_bnb(target, cost_of_change, fee_rate, pool, result);

Ok(())
});
}
}
Loading

0 comments on commit c344712

Please sign in to comment.