Skip to content
This repository has been archived by the owner on Mar 14, 2023. It is now read-only.

Simplify coin selector and branch and bound #46

Draft
wants to merge 26 commits into
base: master
Choose a base branch
from
Draft

Simplify coin selector and branch and bound #46

wants to merge 26 commits into from

Conversation

LLFourn
Copy link
Owner

@LLFourn LLFourn commented Oct 18, 2022

  1. CoinSelector now only takes a base_weight parameter. It doesn't store your target_value, feerate etc. You can apply policies on these outside.
  2. You can sort the candidates inside CoinSelector so that for calls to unselected they will appear in that order without changing the original order.
  3. You can ban candidates in the coin selector so that they won't appear in unselected anymore.
  4. "branch and bound" is implemented generically 🎉 -- you can pass in your own scoring function. The scoring function is passed the current selection and a bool to indicate whether it should return the score of the current selection or the lower bound score for any child selection of the current one (i.e. the best it could do by adding more inputs). Note textbook branch and bound would have two separate functions but because of difficulties of rust closures they had to be one.
  5. I show how to minimize waste metric on top of branch and bound implementation.

This is a WIP but I think that it's good enough for me to explore how (1) affects the API in BDK.

Early impressions appreciated.

@LLFourn LLFourn marked this pull request as draft October 18, 2022 09:27
Copy link
Collaborator

@rajarshimaitra rajarshimaitra left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I just made a first pass over the new bnb code, and I won't pretend I understood it fully, but it does look structurally very different from the existing bnb or any other bnb codes I have seen so far..

I "kinda" get what its trying to do inside the BnbIter.. But I am happy that I am now finally motivated to go through Murch's thesis to weigh on this code.. 😅

One generic question I had.. Does a "generic branch and bound" means we can use the same algorithm to optimize for different "policies"?? If so, what kind of different policies are we talking about?? Is it (theoretically) possible to combine various existing CS algos into this generic model??

Will make another thorough pass after this..

Below are some more questions and comments..

}

fn consider_adding_to_queue(&mut self, cs: &CoinSelector<'a>, already_scored: bool) {
if let Some(heuristic) = (self.score_fn)(cs, true) {
Copy link
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

maybe heuristic_score as the name??


fn consider_adding_to_queue(&mut self, cs: &CoinSelector<'a>, already_scored: bool) {
if let Some(heuristic) = (self.score_fn)(cs, true) {
if self.best.is_none() || self.best.as_ref().unwrap() > &heuristic {
Copy link
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I think we can also expect here or a if let maybe??

score_fn,
};

iter.consider_adding_to_queue(selector, false);
Copy link
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Why don't we just score it every time we add a branch?


const TXOUT_BASE_WEIGHT: u32 = 4 * size_of::<u64>() as u32; // just the value

pub trait TxOutExt {
Copy link
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

TFW a wild trait appears!!! 😂😂

self.queue.push(Branch {
heuristic_score: heuristic,
selector: cs.clone(),
already_scored,
Copy link
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

For some time I was stuck on this thinking if its about "already calculated the score".. Instead its like "if this branch is already considered for best score".. Is that correct thinking??

.take(1634)
.filter_map(|(i, sol)| Some((i, sol?)))
.last()
.expect("it found a solution");
Copy link
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I am getting a

running 1 test
thread 'coin_select::bnb2::test::finds_a_solution_in_n_iter' panicked at 'it found a solution', bdk_core/src/coin_select/bnb2.rs:198:14

error here.. Not sure if that's what it's suppose to do..


let (i, (best, _score)) = solutions
.enumerate()
.take(1634)
Copy link
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Is this just a big number, or has deeper meaning?

Comment on lines 103 to 105
pub fn select(&mut self, index: usize) -> bool {
assert!(index < self.candidates.len());
self.selected.to_mut().insert(index)
Copy link
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I don't know if I am reading wrong, but it seems the index here is the index of the item in the candidate list?? Where as the value of the stuff in that index is the actual index of the actual utxo (or whatever we are selecting)?? If so, Why keep so many indexes? Cant we just directly select the utxo index in the selected list?

To put the individual coin selection metrics in. I'm not sure this
actually makes sense after I've been through it.
@LLFourn
Copy link
Owner Author

LLFourn commented Oct 19, 2022

Update: I've moved the coin selection to its own crate that only depends on rust-bitcoin as requested in wizardsardine/liana#51 (comment). It's also easier like this to start putting it into bdk.

I've tried to design branch and bound algorithms so they can be composed e.g. if you want to find a "changeless" solution but then tiebreak by the waste metric i hoped you could easily create a scoring function that returns (has_change, waste_metric(..)). Since this is Ord it should "just work". However, this is subtly very tricky. That works for the score of a particular selection but not the heuristic (or the "bound"). How can we get the lower bound of the boolean indicating whether change was used? You could always return false which would work but it would be worse than if we created a combined heuristic targeted to do the job properly.

@rajarshimaitra thanks for the initial review. I'll try and respond to comments tomorrow.

@codecov-commenter
Copy link

Codecov Report

❗ No coverage uploaded for pull request base (master@6bcec82). Click here to learn what that means.
The diff coverage is n/a.

@@            Coverage Diff            @@
##             master      #46   +/-   ##
=========================================
  Coverage          ?   71.06%           
=========================================
  Files             ?        3           
  Lines             ?      318           
  Branches          ?        0           
=========================================
  Hits              ?      226           
  Misses            ?       92           
  Partials          ?        0           

Help us with your feedback. Take ten seconds to tell us how you rate us. Have a feature suggestion? Share it here.

Sign up for free to subscribe to this conversation on GitHub. Already have an account? Sign in.
Labels
None yet
Projects
None yet
Development

Successfully merging this pull request may close these issues.

None yet

3 participants