Skip to content

WIP: bdk_core::coin_select integration #773

New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Closed
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
658 changes: 658 additions & 0 deletions src/bdk_core/coin_select/bnb.rs

Large diffs are not rendered by default.

654 changes: 654 additions & 0 deletions src/bdk_core/coin_select/coin_selector.rs

Large diffs are not rendered by default.

246 changes: 246 additions & 0 deletions src/bdk_core/coin_select/mod.rs
Original file line number Diff line number Diff line change
@@ -0,0 +1,246 @@
mod bnb;
pub use bnb::*;

mod coin_selector;
pub use coin_selector::*;

/// Txin "base" fields include `outpoint` (32+4) and `nSequence` (4). This does not include
/// `scriptSigLen` or `scriptSig`.
pub const TXIN_BASE_WEIGHT: u32 = (32 + 4 + 4) * 4;

/// Helper to calculate varint size. `v` is the value the varint represents.
pub fn varint_size(v: usize) -> u32 {
if v <= 0xfc {
return 1;
}
if v <= 0xffff {
return 3;
}
if v <= 0xffff_ffff {
return 5;
}

9
}

pub mod evaluate_cs {
//! Coin Select Evaluation
//!
//! Module to evaluate coin selection.

use super::{CoinSelector, ExcessStrategyKind, Selection};

/// Evaluates a coin selection algorithm.
pub fn evaluate<F>(
initial_selector: CoinSelector,
mut select: F,
) -> Result<Evaluation, EvaluationFailure>
where
F: FnMut(&mut CoinSelector) -> bool,
{
let mut selector = initial_selector.clone();
let start_time = std::time::SystemTime::now();
let has_solution = select(&mut selector);
let elapsed = start_time.elapsed().expect("system time error");

if has_solution {
let solution = selector.finish().expect("failed to finish what we started");

let elapsed_per_candidate = elapsed / selector.candidates.len() as _;

let waste_vec = solution
.excess_strategies
.iter()
.map(|(_, s)| s.waste)
.collect::<Vec<_>>();

let waste_mean = waste_vec.iter().sum::<i64>() as f32 / waste_vec.len() as f32;
let waste_median = if waste_vec.len() % 2 != 0 {
waste_vec[waste_vec.len() / 2] as f32
} else {
(waste_vec[(waste_vec.len() - 1) / 2] + waste_vec[waste_vec.len() / 2]) as f32 / 2.0
};

Ok(Evaluation {
initial_selector,
solution,
elapsed,
elapsed_per_candidate,
waste_median,
waste_mean,
})
} else {
Err(EvaluationFailure {
initial: initial_selector,
elapsed,
})
}
}

/// Evaluation result of a coin selection.
#[derive(Debug, Clone)]
pub struct Evaluation<'a> {
/// Initial [`CoinSelector`].
pub initial_selector: CoinSelector<'a>,
/// Final solution.
pub solution: Selection,

/// Elapsed duration of coin selection.
pub elapsed: std::time::Duration,
/// Elapsed duration per candidate.
pub elapsed_per_candidate: std::time::Duration,

/// Median waste.
pub waste_median: f32,
/// Mean waste.
pub waste_mean: f32,
}

impl<'a> Evaluation<'a> {
/// Obtain waste of specified excess strategy kind.
pub fn waste(&self, strategy_kind: ExcessStrategyKind) -> i64 {
self.solution.excess_strategies[&strategy_kind].waste
}

/// Obtain feerate offset of specified excess strategy kind.
pub fn feerate_offset(&self, strategy_kind: ExcessStrategyKind) -> f32 {
let target_rate = self.initial_selector.opts.target_feerate;
let actual_rate = self.solution.excess_strategies[&strategy_kind].feerate();
actual_rate - target_rate
}
}

impl<'a> core::fmt::Display for Evaluation<'a> {
fn fmt(&self, f: &mut core::fmt::Formatter<'_>) -> core::fmt::Result {
writeln!(f, "Evaluation:")?;
writeln!(
f,
"\t* Candidates: {}",
self.initial_selector.candidates.len()
)?;
writeln!(
f,
"\t* Initial selection: {}",
self.initial_selector.selected_count()
)?;
writeln!(f, "\t* Final selection: {}", self.solution.selected.len())?;
writeln!(f, "\t* Elapsed: {:?}", self.elapsed)?;
writeln!(
f,
"\t* Elapsed per candidate: {:?}",
self.elapsed_per_candidate
)?;
Ok(())
}
}

/// Evaluation failure.
#[derive(Debug, Clone)]
pub struct EvaluationFailure<'a> {
initial: CoinSelector<'a>,
elapsed: std::time::Duration,
}

impl<'a> core::fmt::Display for EvaluationFailure<'a> {
fn fmt(&self, f: &mut core::fmt::Formatter<'_>) -> core::fmt::Result {
write!(
f,
"cs algorithm failed to find a solution: elapsed={}s target_feerate={}sats/wu",
self.elapsed.as_secs(),
self.initial.opts.target_feerate
)
}
}

impl<'a> std::error::Error for EvaluationFailure<'a> {}
}

#[cfg(test)]
pub mod tester {
use super::*;
use bitcoin::{
secp256k1::{All, Secp256k1},
TxOut,
};
use miniscript::{
// plan::{Assets, Plan},
Descriptor,
DescriptorPublicKey,
DescriptorTrait,
};

#[derive(Debug, Clone)]
pub struct TestCandidate {
pub txo: TxOut,
// pub plan: Plan<DescriptorPublicKey>,
}

impl From<TestCandidate> for WeightedValue {
fn from(test_candidate: TestCandidate) -> Self {
Self {
value: test_candidate.txo.value,
// weight: TXIN_BASE_WEIGHT + test_candidate.plan.expected_weight() as u32,
weight: TXIN_BASE_WEIGHT + 65, // TODO: Is this correct for schnorr?
input_count: 1,
// is_segwit: test_candidate.plan.witness_version().is_some(),
is_segwit: true,
}
}
}

pub struct Tester {
descriptor: Descriptor<DescriptorPublicKey>,
// assets: Assets<DescriptorPublicKey>,
}

impl Tester {
pub fn new(secp: &Secp256k1<All>, desc_str: &str) -> Self {
// let desc_str = "tr(xprv9uBuvtdjghkz8D1qzsSXS9Vs64mqrUnXqzNccj2xcvnCHPpXKYE1U2Gbh9CDHk8UPyF2VuXpVkDA7fk5ZP4Hd9KnhUmTscKmhee9Dp5sBMK)";
let (descriptor, _seckeys) =
Descriptor::<DescriptorPublicKey>::parse_descriptor(secp, desc_str).unwrap();

// let assets = Assets {
// keys: seckeys.keys().cloned().collect(),
// ..Default::default()
// };

// Self { descriptor, assets }
Self { descriptor }
}

pub fn gen_candidate(&self, derivation_index: u32, value: u64) -> TestCandidate {
let secp = Secp256k1::new();
// let descriptor = self.descriptor.at_derivation_index(derivation_index);
let descriptor = self
.descriptor
.derived_descriptor(&secp, derivation_index)
.expect("woops");
// let plan = descriptor.plan_satisfaction(&self.assets).unwrap();
let txo = TxOut {
value,
script_pubkey: descriptor.script_pubkey(),
};
// TestCandidate { txo, plan }
TestCandidate { txo }
}

pub fn gen_weighted_value(&self, value: u64) -> WeightedValue {
self.gen_candidate(0, value).into()
}

pub fn gen_weighted_values(&self, out: &mut Vec<WeightedValue>, count: usize, value: u64) {
(0..count).for_each(|_| out.push(self.gen_candidate(0, value).into()))
}

pub fn gen_opts(&self, recipient_value: u64) -> CoinSelectorOpt {
let recipient = self.gen_candidate(0, recipient_value);
let drain = self.gen_candidate(0, 0);
CoinSelectorOpt::fund_outputs(
&[recipient.txo],
&drain.txo,
65,
// drain.plan.expected_weight() as u32,
)
}
}
}
17 changes: 17 additions & 0 deletions src/bdk_core/mod.rs
Original file line number Diff line number Diff line change
@@ -0,0 +1,17 @@
// Bitcoin Dev Kit
// Written in 2020 by Alekos Filini <[email protected]>
//
// Copyright (c) 2020-2021 Bitcoin Dev Kit Developers
//
// This file is licensed under the Apache License, Version 2.0 <LICENSE-APACHE
// or http://www.apache.org/licenses/LICENSE-2.0> or the MIT license
// <LICENSE-MIT or http://opensource.org/licenses/MIT>, at your option.
// You may not use this file except in accordance with one or both of these
// licenses.

//! BDK Core
//!
//! This is the core modules of BDK.

mod coin_select;
pub use coin_select::*;
22 changes: 19 additions & 3 deletions src/descriptor/checksum.rs
Original file line number Diff line number Diff line change
Expand Up @@ -41,12 +41,21 @@ fn poly_mod(mut c: u64, val: u64) -> u64 {
c
}

/// Computes the checksum bytes of a descriptor
pub fn get_checksum_bytes(desc: &str) -> Result<[u8; 8], DescriptorError> {
/// Computes the checksum bytes of a descriptor.
/// `exclude_hash = true` ignores all data after the first '#' (inclusive).
pub fn get_checksum_bytes(mut desc: &str, exclude_hash: bool) -> Result<[u8; 8], DescriptorError> {
let mut c = 1;
let mut cls = 0;
let mut clscount = 0;

let mut original_checksum = None;
if exclude_hash {
if let Some(split) = desc.split_once('#') {
desc = split.0;
original_checksum = Some(split.1);
}
}

for ch in desc.as_bytes() {
let pos = INPUT_CHARSET
.iter()
Expand All @@ -72,13 +81,20 @@ pub fn get_checksum_bytes(desc: &str) -> Result<[u8; 8], DescriptorError> {
checksum[j] = CHECKSUM_CHARSET[((c >> (5 * (7 - j))) & 31) as usize];
}

// if input data already had a checksum, check calculated checksum against original checksum
if let Some(original_checksum) = original_checksum {
if original_checksum.as_bytes() != checksum {
return Err(DescriptorError::InvalidDescriptorChecksum);
}
}

Ok(checksum)
}

/// Compute the checksum of a descriptor
pub fn get_checksum(desc: &str) -> Result<String, DescriptorError> {
// unsafe is okay here as the checksum only uses bytes in `CHECKSUM_CHARSET`
get_checksum_bytes(desc).map(|b| unsafe { String::from_utf8_unchecked(b.to_vec()) })
get_checksum_bytes(desc, true).map(|b| unsafe { String::from_utf8_unchecked(b.to_vec()) })
}

#[cfg(test)]
Expand Down
21 changes: 9 additions & 12 deletions src/descriptor/mod.rs
Original file line number Diff line number Diff line change
Expand Up @@ -40,6 +40,7 @@ pub mod policy;
pub mod template;

pub use self::checksum::get_checksum;
use self::checksum::get_checksum_bytes;
pub use self::derived::{AsDerived, DerivedDescriptorKey};
pub use self::error::Error as DescriptorError;
pub use self::policy::Policy;
Expand Down Expand Up @@ -84,19 +85,15 @@ impl IntoWalletDescriptor for &str {
secp: &SecpCtx,
network: Network,
) -> Result<(ExtendedDescriptor, KeyMap), DescriptorError> {
let descriptor = if self.contains('#') {
let parts: Vec<&str> = self.splitn(2, '#').collect();
if !get_checksum(parts[0])
.ok()
.map(|computed| computed == parts[1])
.unwrap_or(false)
{
return Err(DescriptorError::InvalidDescriptorChecksum);
let descriptor = match self.split_once('#') {
Some((desc, original_checksum)) => {
let checksum = get_checksum_bytes(desc, false)?;
if original_checksum.as_bytes() != checksum {
return Err(DescriptorError::InvalidDescriptorChecksum);
}
desc
}

parts[0]
} else {
self
None => self,
};

ExtendedDescriptor::parse_descriptor(secp, descriptor)?
Expand Down
6 changes: 6 additions & 0 deletions src/error.rs
Original file line number Diff line number Diff line change
Expand Up @@ -240,3 +240,9 @@ impl From<crate::blockchain::esplora::EsploraError> for Error {
Error::Esplora(Box::new(other))
}
}

impl From<crate::bdk_core::SelectionFailure> for Error {
fn from(f: crate::bdk_core::SelectionFailure) -> Self {
Error::Generic(format!("bdk_core: {}", f))
}
}
1 change: 1 addition & 0 deletions src/lib.rs
Original file line number Diff line number Diff line change
Expand Up @@ -257,6 +257,7 @@ pub mod testutils;
#[allow(unused_imports)]
#[macro_use]
pub(crate) mod error;
pub mod bdk_core;
pub mod blockchain;
pub mod database;
pub mod descriptor;
Expand Down
6 changes: 5 additions & 1 deletion src/psbt/mod.rs
Original file line number Diff line number Diff line change
Expand Up @@ -167,6 +167,10 @@ mod test {
let fee_amount = psbt.fee_amount();
assert!(fee_amount.is_some());

let weight = psbt.clone().extract_tx().weight();
println!("psbt weight: {}", weight);
println!("inputs: {}", psbt.clone().extract_tx().input.len());

let unfinalized_fee_rate = psbt.fee_rate().unwrap();

let finalized = wallet.sign(&mut psbt, Default::default()).unwrap();
Expand All @@ -191,8 +195,8 @@ mod test {
let (mut psbt, _) = builder.finish().unwrap();
let fee_amount = psbt.fee_amount();
assert!(fee_amount.is_some());
let unfinalized_fee_rate = psbt.fee_rate().unwrap();

let unfinalized_fee_rate = psbt.fee_rate().unwrap();
let finalized = wallet.sign(&mut psbt, Default::default()).unwrap();
assert!(finalized);

Expand Down
Loading