Skip to content

Commit

Permalink
Add Single Random Draw algo
Browse files Browse the repository at this point in the history
  • Loading branch information
yancyribbens committed Jan 26, 2023
1 parent a113d8a commit 63b9fbf
Show file tree
Hide file tree
Showing 3 changed files with 199 additions and 0 deletions.
30 changes: 30 additions & 0 deletions src/lib.rs
Original file line number Diff line number Diff line change
Expand Up @@ -17,11 +17,23 @@
#[cfg(bench)]
extern crate test;

mod single_random_draw;
mod spend;

#[cfg(any(test, feature = "rand"))]
pub use crate::spend::Spend;

#[cfg(any(test, feature = "rand"))]
pub use crate::single_random_draw::select_coins_srd;

/// Trait that a UTXO struct must implement to be used as part of the coin selection
/// algorithm.
pub trait Utxo: Clone {
/// Return the value of the UTXO.
fn get_value(&self) -> u64;

/// Return the weight of the UTXO.
fn get_weight(&self) -> u64;
}

/// Select coins first using BnB algorithm similar to what is done in bitcoin
Expand All @@ -32,10 +44,28 @@ pub trait Utxo: Clone {
#[cfg(any(test, feature = "rand"))]
#[cfg_attr(docsrs, doc(cfg(feature = "rand")))]
pub fn select_coins<T: Utxo>(
target: u64,
cost_of_change: u64,
fee_rate: u64,
utxo_pool: &mut [T],
) -> Option<Vec<T>> {
let spend = Spend::new(fee_rate);

match select_coins_bnb(target, cost_of_change, utxo_pool) {
Some(_res) => None,
None => select_coins_srd(target, utxo_pool, spend)
}
}

/// Select coins using BnB algorithm similar to what is done in bitcoin
/// core see: <https://github.com/bitcoin/bitcoin/blob/f3bc1a72825fe2b51f4bc20e004cef464f05b965/src/wallet/coinselection.cpp>
/// Returns None if BnB doesn't find a solution.
pub fn select_coins_bnb<T: Utxo>(
_target: u64,
_cost_of_change: u64,
_utxo_pool: &mut [T],
) -> Option<Vec<T>> {
// TODO
None
}

Expand Down
104 changes: 104 additions & 0 deletions src/single_random_draw.rs
Original file line number Diff line number Diff line change
@@ -0,0 +1,104 @@
//! This library provides efficient algorithms to compose a set of unspent transaction outputs
//! (UTXOs).

#[cfg(any(test, feature = "rand"))]
use crate::Utxo;

#[cfg(any(test, feature = "rand"))]
use crate::spend::Spend;

#[cfg(any(test, feature = "rand"))]
use rand::{seq::SliceRandom, thread_rng};

/// Randomly select coins for the given target by shuffling the utxo pool and
/// taking UTXOs until the given target is reached, or returns None if the target
/// cannot be reached with the given utxo pool.
/// Requires compilation with the "rand" feature.
#[cfg(any(test, feature = "rand"))]
#[cfg_attr(docsrs, doc(cfg(feature = "rand")))]
pub fn select_coins_srd<T: Utxo>(target: u64, utxo_pool: &mut [T], spend: Spend) -> Option<Vec<T>> {
utxo_pool.shuffle(&mut thread_rng());

let mut sum = 0;

let res = utxo_pool
.iter()
.take_while(|x| {
if sum >= target {
return false;
}
sum += spend.get_effective_value(*x);
true
})
.cloned()
.collect::<Vec<T>>();

if sum >= target {
return Some(res);
}

None
}

#[cfg(test)]
mod tests {
use crate::single_random_draw::select_coins_srd;
pub use crate::spend::Spend;
use crate::Utxo;

// https://github.com/bitcoin/bitcoin/blob/b264410e012a61b103e1a03c43df4e17b9b75452/src/test/util/setup_common.h#L80
const CENT:u64 = 1_000_000;
const WEIGHT:u64 = 1_000;
const FEE_RATE:u64 = 10;

const UTXO_POOL: [MinimalUtxo; 4] = [
MinimalUtxo { value: CENT, weight: WEIGHT },
MinimalUtxo { value: 2 * CENT, weight: WEIGHT },
MinimalUtxo { value: 3 * CENT, weight: WEIGHT },
MinimalUtxo { value: 4 * CENT, weight: WEIGHT },
];

#[derive(Clone, Debug, Eq, PartialEq)]
struct MinimalUtxo {
value: u64,
weight: u64
}

impl Utxo for MinimalUtxo {
fn get_value(&self) -> u64 {
self.value
}

fn get_weight(&self) -> u64 {
self.weight
}
}

#[test]
fn select_coins_srd_no_solution() {
let mut pool = UTXO_POOL.clone();
let spend = Spend::new(FEE_RATE);

let utxo_match = select_coins_srd(11 * CENT, &mut pool, spend);
assert!(utxo_match.is_none());
}

#[test]
fn select_coins_srd_effective_value_too_small() {
let mut pool = vec![UTXO_POOL[0].clone()];
let spend = Spend::new(FEE_RATE);

// This test will fail if SRD is using value instead of effective_value
let utxo_match = select_coins_srd(CENT, &mut pool, spend);
assert!(utxo_match.is_none());
}

#[test]
fn select_coins_srd_with_solution() {
let mut pool = UTXO_POOL.clone();
let spend = Spend::new(FEE_RATE);

let utxo_match = select_coins_srd(CENT, &mut pool, spend);
utxo_match.expect("Did not properly randomly select coins");
}
}
65 changes: 65 additions & 0 deletions src/spend.rs
Original file line number Diff line number Diff line change
@@ -0,0 +1,65 @@
/// This module provides functionality needed to spend a UTXO.

#[cfg(any(test, feature = "rand"))]
use crate::Utxo;

/// Dynamic values required at the time a UTXO is spent.
#[derive(Debug, PartialEq)]
pub struct Spend {
/// The fee_rate the participant chooses to spend.
fee_rate: u64,
}

// Currently only single_random_draw uses Spend, however
// in the future when other algos use Spend, then the rand
// feature flag can be removed.
#[cfg(any(test, feature = "rand"))]
impl Spend {
/// Create a new Spend type.
pub fn new(fee_rate: u64) -> Spend {
Spend {
fee_rate
}
}

/// the value that's left after subtracting the fee to spend the output.
pub fn get_effective_value(&self, utxo: &impl Utxo) -> u64 {
let fee = utxo.get_weight() * self.fee_rate;
utxo.get_value() - fee
}
}

#[cfg(test)]
mod tests {
use crate::spend::*;

// https://github.com/bitcoin/bitcoin/blob/b264410e012a61b103e1a03c43df4e17b9b75452/src/test/util/setup_common.h#L80
const CENT:u64 = 1_000_000;

#[derive(Clone, Debug, Eq, PartialEq)]
struct MinimalUtxo {
value: u64,
weight: u64
}

impl Utxo for MinimalUtxo {
fn get_value(&self) -> u64 {
self.value
}

fn get_weight(&self) -> u64 {
self.weight
}
}

#[test]
fn get_effective_value() {
let utxo = MinimalUtxo { value: CENT, weight: 1_000 };
let spend = Spend::new(10);
let effective_value = spend.get_effective_value(&utxo);

// value - (weight * fee_rate)
// 1_000_000 - (1_000 * 10)
assert_eq!(990_000, effective_value);
}
}

0 comments on commit 63b9fbf

Please sign in to comment.