Skip to content

Commit 4e51d18

Browse files
LLFournevanlinjin
authored andcommitted
Move bdk_coin_select in from old PR
* Introduce new coin selection implementation * Add some tooling to make bdk_coin_select work on 1.48.0 * Update `example_cli` to use new `bdk_coin_select`
1 parent feafaac commit 4e51d18

18 files changed

+2129
-1190
lines changed

Cargo.1.48.0.toml

Lines changed: 4 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,4 @@
1+
[workspace]
2+
members = [
3+
"nursery/coin_select"
4+
]

build-msrv-crates.sh

Lines changed: 16 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,16 @@
1+
#!/usr/bin/env sh
2+
trap '
3+
signal=$?;
4+
cleanup
5+
exit $signal;
6+
' INT
7+
8+
cleanup() {
9+
mv Cargo.tmp.toml Cargo.toml 2>/dev/null
10+
}
11+
12+
cp Cargo.toml Cargo.tmp.toml
13+
cp Cargo.1.48.0.toml Cargo.toml
14+
cat Cargo.toml
15+
cargo build --release
16+
cleanup

example-crates/example_cli/src/lib.rs

Lines changed: 103 additions & 79 deletions
Original file line numberDiff line numberDiff line change
@@ -1,9 +1,9 @@
11
pub use anyhow;
22
use anyhow::Context;
3-
use bdk_coin_select::{coin_select_bnb, CoinSelector, CoinSelectorOpt, WeightedValue};
3+
use bdk_coin_select::{Candidate, CoinSelector};
44
use bdk_file_store::Store;
55
use serde::{de::DeserializeOwned, Serialize};
6-
use std::{cmp::Reverse, collections::HashMap, path::PathBuf, sync::Mutex, time::Duration};
6+
use std::{cmp::Reverse, collections::HashMap, path::PathBuf, sync::Mutex};
77

88
use bdk_chain::{
99
bitcoin::{
@@ -16,7 +16,7 @@ use bdk_chain::{
1616
descriptor::{DescriptorSecretKey, KeyMap},
1717
Descriptor, DescriptorPublicKey,
1818
},
19-
Anchor, Append, ChainOracle, DescriptorExt, FullTxOut, Persist, PersistBackend,
19+
Anchor, Append, ChainOracle, FullTxOut, Persist, PersistBackend,
2020
};
2121
pub use bdk_file_store;
2222
pub use clap;
@@ -412,39 +412,18 @@ where
412412
};
413413

414414
// TODO use planning module
415-
let mut candidates = planned_utxos(graph, chain, &assets)?;
416-
417-
// apply coin selection algorithm
418-
match cs_algorithm {
419-
CoinSelectionAlgo::LargestFirst => {
420-
candidates.sort_by_key(|(_, utxo)| Reverse(utxo.txout.value))
421-
}
422-
CoinSelectionAlgo::SmallestFirst => candidates.sort_by_key(|(_, utxo)| utxo.txout.value),
423-
CoinSelectionAlgo::OldestFirst => {
424-
candidates.sort_by_key(|(_, utxo)| utxo.chain_position.clone())
425-
}
426-
CoinSelectionAlgo::NewestFirst => {
427-
candidates.sort_by_key(|(_, utxo)| Reverse(utxo.chain_position.clone()))
428-
}
429-
CoinSelectionAlgo::BranchAndBound => {}
430-
}
431-
415+
let raw_candidates = planned_utxos(graph, chain, &assets)?;
432416
// turn the txos we chose into weight and value
433-
let wv_candidates = candidates
417+
let candidates = raw_candidates
434418
.iter()
435419
.map(|(plan, utxo)| {
436-
WeightedValue::new(
420+
Candidate::new(
437421
utxo.txout.value,
438422
plan.expected_weight() as _,
439423
plan.witness_version().is_some(),
440424
)
441425
})
442-
.collect();
443-
444-
let mut outputs = vec![TxOut {
445-
value,
446-
script_pubkey: address.script_pubkey(),
447-
}];
426+
.collect::<Vec<_>>();
448427

449428
let internal_keychain = if graph.index.keychains().get(&Keychain::Internal).is_some() {
450429
Keychain::Internal
@@ -457,7 +436,7 @@ where
457436
additions.append(change_additions);
458437

459438
// Clone to drop the immutable reference.
460-
let change_script = change_script.into();
439+
let change_script = change_script.to_owned();
461440

462441
let change_plan = bdk_tmp_plan::plan_satisfaction(
463442
&graph
@@ -471,68 +450,113 @@ where
471450
)
472451
.expect("failed to obtain change plan");
473452

474-
let mut change_output = TxOut {
475-
value: 0,
476-
script_pubkey: change_script,
453+
let mut transaction = Transaction {
454+
version: 0x02,
455+
// because the temporary planning module does not support timelocks, we can use the chain
456+
// tip as the `lock_time` for anti-fee-sniping purposes
457+
lock_time: chain
458+
.get_chain_tip()?
459+
.and_then(|block_id| absolute::LockTime::from_height(block_id.height).ok())
460+
.unwrap_or(absolute::LockTime::ZERO),
461+
input: vec![],
462+
output: vec![TxOut {
463+
value,
464+
script_pubkey: address.script_pubkey(),
465+
}],
477466
};
478467

479-
let cs_opts = CoinSelectorOpt {
480-
target_feerate: 0.5,
481-
min_drain_value: graph
482-
.index
483-
.keychains()
484-
.get(&internal_keychain)
485-
.expect("must exist")
486-
.dust_value(),
487-
..CoinSelectorOpt::fund_outputs(
488-
&outputs,
489-
&change_output,
490-
change_plan.expected_weight() as u32,
491-
)
468+
let target = bdk_coin_select::Target {
469+
feerate: bdk_coin_select::FeeRate::from_sat_per_vb(1.0),
470+
min_fee: 0,
471+
value: transaction.output.iter().map(|txo| txo.value).sum(),
492472
};
493473

494-
// TODO: How can we make it easy to shuffle in order of inputs and outputs here?
495-
// apply coin selection by saying we need to fund these outputs
496-
let mut coin_selector = CoinSelector::new(&wv_candidates, &cs_opts);
474+
let drain = bdk_coin_select::Drain {
475+
weight: {
476+
// we calculate the weight difference of including the drain output in the base tx
477+
// this method will detect varint size changes of txout count
478+
let tx_weight = transaction.weight();
479+
let tx_weight_with_drain = {
480+
let mut tx = transaction.clone();
481+
tx.output.push(TxOut {
482+
script_pubkey: change_script.clone(),
483+
..Default::default()
484+
});
485+
tx.weight()
486+
};
487+
(tx_weight_with_drain - tx_weight).to_wu() as u32 - 1
488+
},
489+
value: 0,
490+
spend_weight: change_plan.expected_weight() as u32,
491+
};
492+
let long_term_feerate = bdk_coin_select::FeeRate::from_sat_per_wu(0.25);
493+
let drain_policy = bdk_coin_select::change_policy::min_waste(drain, long_term_feerate);
497494

498-
// just select coins in the order provided until we have enough
499-
// only use the first result (least waste)
500-
let selection = match cs_algorithm {
495+
let mut selector = CoinSelector::new(&candidates, transaction.weight().to_wu() as u32);
496+
match cs_algorithm {
501497
CoinSelectionAlgo::BranchAndBound => {
502-
coin_select_bnb(Duration::from_secs(10), coin_selector.clone())
503-
.map_or_else(|| coin_selector.select_until_finished(), |cs| cs.finish())?
498+
let metric = bdk_coin_select::metrics::Waste {
499+
target,
500+
long_term_feerate,
501+
change_policy: &drain_policy,
502+
};
503+
let (final_selection, _score) = selector
504+
.branch_and_bound(metric)
505+
.take(50_000)
506+
// we only process viable solutions
507+
.flatten()
508+
.reduce(|(best_sol, best_score), (curr_sol, curr_score)| {
509+
// we are reducing waste
510+
if curr_score < best_score {
511+
(curr_sol, curr_score)
512+
} else {
513+
(best_sol, best_score)
514+
}
515+
})
516+
.ok_or(anyhow::format_err!("no bnb solution found"))?;
517+
selector = final_selection;
518+
}
519+
cs_algorithm => {
520+
match cs_algorithm {
521+
CoinSelectionAlgo::LargestFirst => {
522+
selector.sort_candidates_by_key(|(_, c)| Reverse(c.value))
523+
}
524+
CoinSelectionAlgo::SmallestFirst => {
525+
selector.sort_candidates_by_key(|(_, c)| c.value)
526+
}
527+
CoinSelectionAlgo::OldestFirst => selector
528+
.sort_candidates_by_key(|(i, _)| raw_candidates[i].1.chain_position.clone()),
529+
CoinSelectionAlgo::NewestFirst => selector.sort_candidates_by_key(|(i, _)| {
530+
Reverse(raw_candidates[i].1.chain_position.clone())
531+
}),
532+
CoinSelectionAlgo::BranchAndBound => unreachable!("bnb variant is matched already"),
533+
}
534+
selector.select_until_target_met(target, drain)?
504535
}
505-
_ => coin_selector.select_until_finished()?,
506536
};
507-
let (_, selection_meta) = selection.best_strategy();
508537

509538
// get the selected utxos
510-
let selected_txos = selection.apply_selection(&candidates).collect::<Vec<_>>();
539+
let selected_txos = selector
540+
.apply_selection(&raw_candidates)
541+
.collect::<Vec<_>>();
511542

512-
if let Some(drain_value) = selection_meta.drain_value {
513-
change_output.value = drain_value;
514-
// if the selection tells us to use change and the change value is sufficient, we add it as an output
515-
outputs.push(change_output)
543+
let drain = drain_policy(&selector, target);
544+
if drain.is_some() {
545+
transaction.output.push(TxOut {
546+
value: drain.value,
547+
script_pubkey: change_script,
548+
});
516549
}
517550

518-
let mut transaction = Transaction {
519-
version: 0x02,
520-
// because the temporary planning module does not support timelocks, we can use the chain
521-
// tip as the `lock_time` for anti-fee-sniping purposes
522-
lock_time: chain
523-
.get_chain_tip()?
524-
.and_then(|block_id| absolute::LockTime::from_height(block_id.height).ok())
525-
.unwrap_or(absolute::LockTime::ZERO),
526-
input: selected_txos
527-
.iter()
528-
.map(|(_, utxo)| TxIn {
529-
previous_output: utxo.outpoint,
530-
sequence: Sequence::ENABLE_RBF_NO_LOCKTIME,
531-
..Default::default()
532-
})
533-
.collect(),
534-
output: outputs,
535-
};
551+
// fill transaction inputs
552+
transaction.input = selected_txos
553+
.iter()
554+
.map(|(_, utxo)| TxIn {
555+
previous_output: utxo.outpoint,
556+
sequence: Sequence::ENABLE_RBF_NO_LOCKTIME,
557+
..Default::default()
558+
})
559+
.collect();
536560

537561
let prevouts = selected_txos
538562
.iter()
@@ -593,7 +617,7 @@ where
593617
}
594618
}
595619

596-
let change_info = if selection_meta.drain_value.is_some() {
620+
let change_info = if drain.is_some() {
597621
Some((additions, (internal_keychain, change_index)))
598622
} else {
599623
None

nursery/coin_select/Cargo.toml

Lines changed: 9 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -1,10 +1,16 @@
11
[package]
22
name = "bdk_coin_select"
3-
version = "0.0.1"
4-
authors = [ "LLFourn <[email protected]>" ]
3+
version = "0.1.0"
4+
edition = "2018"
5+
license = "MIT OR Apache-2.0"
56

67
[dependencies]
7-
bdk_chain = { path = "../../crates/chain" }
8+
# No dependencies! Don't add any please!
9+
10+
[dev-dependencies]
11+
rand = "0.8"
12+
proptest = "1"
13+
bitcoin = "0.30"
814

915
[features]
1016
default = ["std"]

nursery/coin_select/README.md

Lines changed: 59 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,59 @@
1+
# BDK Coin Selection
2+
3+
`bdk_coin_select` is a tool to help you select inputs for making Bitcoin (ticker: BTC) transactions. It's got zero dependencies so you can pasta it into your project without concern.
4+
5+
6+
## Synopsis
7+
8+
```rust
9+
use bdk_coin_select::{CoinSelector, Candidate, TXIN_BASE_WEIGHT};
10+
use bitcoin::{ Transaction, TxIn };
11+
12+
// You should use miniscript to figure out the satisfaction weight for your coins!
13+
const tr_satisfaction_weight: u32 = 66;
14+
const tr_input_weight: u32 = txin_base_weight + tr_satisfaction_weight;
15+
16+
17+
let candidates = vec![
18+
Candidate {
19+
// How many inputs does this candidate represent. Needed so we can figure out the weight
20+
// of the varint that encodes the number of inputs.
21+
input_count: 1,
22+
// the value of the input
23+
value: 1_000_000,
24+
// the total weight of the input(s). This doesn't include
25+
weight: TR_INPUT_WEIGHT,
26+
// wether it's a segwit input. Needed so we know whether to include the segwit header
27+
// in total weight calculations.
28+
is_segwit: true
29+
},
30+
Candidate {
31+
// A candidate can represent multiple inputs in the case where you always want some inputs
32+
// to be spent together.
33+
input_count: 2,
34+
weight: 2*tr_input_weight,
35+
value: 3_000_000,
36+
is_segwit: true
37+
},
38+
Candidate {
39+
input_count: 1,
40+
weight: TR_INPUT_WEIGHT,
41+
value: 5_000_000,
42+
is_segwit: true,
43+
}
44+
];
45+
46+
let base_weight = Transaction {
47+
input: vec![],
48+
output: vec![],
49+
lock_time: bitcoin::absolute::LockTime::from_height(0).unwrap(),
50+
version: 1,
51+
}.weight().to_wu() as u32;
52+
53+
panic!("{}", base_weight);
54+
55+
let mut coin_selector = CoinSelector::new(&candidates,base_weight);
56+
57+
58+
```
59+

0 commit comments

Comments
 (0)