Skip to content

Commit 030018a

Browse files
committed
WIP: More functional
1 parent 0f4b6c9 commit 030018a

File tree

4 files changed

+199
-80
lines changed

4 files changed

+199
-80
lines changed

src/coin_control.rs

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -6,7 +6,7 @@ use crate::{
66
};
77
use alloc::vec::Vec;
88
use bdk_chain::{BlockId, ChainOracle, ConfirmationBlockTime, TxGraph};
9-
use bitcoin::{absolute, OutPoint, Transaction, Txid};
9+
use bitcoin::{absolute, OutPoint, Txid};
1010
use miniscript::{bitcoin, plan::Plan};
1111

1212
/// Coin control.

src/input_candidates.rs

Lines changed: 130 additions & 8 deletions
Original file line numberDiff line numberDiff line change
@@ -1,7 +1,12 @@
1-
use crate::collections::HashSet;
1+
use core::{
2+
fmt::{Debug, Display},
3+
ops::Deref,
4+
};
5+
6+
use crate::{collections::HashSet, cs_feerate, Selection, Selector, SelectorParams};
27
use alloc::vec::Vec;
3-
use bdk_coin_select::Candidate;
4-
use bitcoin::OutPoint;
8+
use bdk_coin_select::{metrics::LowestFee, Candidate, NoBnbSolution};
9+
use bitcoin::{FeeRate, OutPoint};
510
use miniscript::bitcoin;
611

712
use crate::InputGroup;
@@ -15,6 +20,101 @@ pub struct InputCandidates {
1520
cs_candidates: Vec<Candidate>,
1621
}
1722

23+
/// Occurs when we cannot find a solution for selection.
24+
#[derive(Debug)]
25+
pub enum IntoSelectionError<E> {
26+
/// Parameters provided created an invalid change policy.
27+
InvalidChangePolicy(miniscript::Error),
28+
/// Selection algorithm failed.
29+
SelectionAlgorithm(E),
30+
/// Cannot meet target.
31+
CannotMeetTarget,
32+
}
33+
34+
impl<E: Display> Display for IntoSelectionError<E> {
35+
fn fmt(&self, f: &mut core::fmt::Formatter<'_>) -> core::fmt::Result {
36+
match self {
37+
IntoSelectionError::InvalidChangePolicy(error) => {
38+
write!(f, "invalid change policy: {}", error)
39+
}
40+
IntoSelectionError::SelectionAlgorithm(error) => {
41+
write!(f, "selection algorithm failed: {}", error)
42+
}
43+
IntoSelectionError::CannotMeetTarget => write!(f, "cannot meet target"),
44+
}
45+
}
46+
}
47+
48+
#[cfg(feature = "std")]
49+
impl<E: Debug + Display> std::error::Error for IntoSelectionError<E> {}
50+
51+
/// Occurs when we are missing ouputs.
52+
#[derive(Debug)]
53+
pub struct MissingOutputs(HashSet<OutPoint>);
54+
55+
impl Deref for MissingOutputs {
56+
type Target = HashSet<OutPoint>;
57+
58+
fn deref(&self) -> &Self::Target {
59+
&self.0
60+
}
61+
}
62+
63+
impl Display for MissingOutputs {
64+
fn fmt(&self, f: &mut core::fmt::Formatter<'_>) -> core::fmt::Result {
65+
write!(f, "missing outputs: {:?}", self.0)
66+
}
67+
}
68+
69+
#[cfg(feature = "std")]
70+
impl std::error::Error for MissingOutputs {}
71+
72+
/// Occurs when a must-select policy cannot be fulfilled.
73+
#[derive(Debug)]
74+
pub enum PolicyFailure<PF> {
75+
/// Missing outputs.
76+
MissingOutputs(MissingOutputs),
77+
/// Policy failure.
78+
PolicyFailure(PF),
79+
}
80+
81+
impl<PF: Debug> Display for PolicyFailure<PF> {
82+
fn fmt(&self, f: &mut core::fmt::Formatter<'_>) -> core::fmt::Result {
83+
match self {
84+
PolicyFailure::MissingOutputs(missing_outputs) => Debug::fmt(missing_outputs, f),
85+
PolicyFailure::PolicyFailure(failure) => {
86+
write!(f, "policy failure: {:?}", failure)
87+
}
88+
}
89+
}
90+
}
91+
92+
#[cfg(feature = "std")]
93+
impl<PF: Debug> std::error::Error for PolicyFailure<PF> {}
94+
95+
/// Select for lowest fee with bnb
96+
pub fn selection_algorithm_lowest_fee_bnb(
97+
longterm_feerate: FeeRate,
98+
max_rounds: usize,
99+
) -> impl FnMut(&InputCandidates, &mut Selector) -> Result<(), NoBnbSolution> {
100+
let long_term_feerate = cs_feerate(longterm_feerate);
101+
move |_, selector| {
102+
let target = selector.target();
103+
let change_policy = selector.change_policy();
104+
selector
105+
.inner_mut()
106+
.run_bnb(
107+
LowestFee {
108+
target,
109+
long_term_feerate,
110+
change_policy,
111+
},
112+
max_rounds,
113+
)
114+
.map(|_| ())
115+
}
116+
}
117+
18118
impl InputCandidates {
19119
/// Create
20120
///
@@ -51,7 +151,7 @@ impl InputCandidates {
51151
/// # Error
52152
///
53153
/// Returns the original [`InputCandidates`] if any outpoint is not found.
54-
pub fn with_must_select(self, outpoints: HashSet<OutPoint>) -> Result<Self, Self> {
154+
pub fn with_must_select(self, outpoints: HashSet<OutPoint>) -> Result<Self, MissingOutputs> {
55155
let (must_select, can_select) = self.groups.iter().partition::<Vec<_>, _>(|group| {
56156
group.any(|input| outpoints.contains(&input.prev_outpoint()))
57157
});
@@ -62,7 +162,9 @@ impl InputCandidates {
62162
.flat_map(|group| group.inputs().iter().map(|input| input.prev_outpoint()))
63163
.collect::<HashSet<OutPoint>>();
64164
if !must_select_map.is_superset(&outpoints) {
65-
return Err(self);
165+
return Err(MissingOutputs(
166+
outpoints.difference(&must_select_map).copied().collect(),
167+
));
66168
}
67169

68170
let must_select_count = must_select.len();
@@ -88,12 +190,32 @@ impl InputCandidates {
88190
}
89191

90192
/// Like [`InputCandidates::with_must_select`], but with a policy closure.
91-
pub fn with_must_select_policy<P>(self, mut policy: P) -> Result<Self, Self>
193+
pub fn with_must_select_policy<P, PF>(self, mut policy: P) -> Result<Self, PolicyFailure<PF>>
92194
where
93-
P: FnMut(&Self) -> HashSet<OutPoint>,
195+
P: FnMut(&Self) -> Result<HashSet<OutPoint>, PF>,
94196
{
95-
let outpoints = policy(&self);
197+
let outpoints = policy(&self).map_err(PolicyFailure::PolicyFailure)?;
96198
self.with_must_select(outpoints)
199+
.map_err(PolicyFailure::MissingOutputs)
200+
}
201+
202+
/// Into selection.
203+
pub fn into_selection<A, E>(
204+
self,
205+
mut selection_algorithm: A,
206+
params: SelectorParams,
207+
) -> Result<Selection, IntoSelectionError<E>>
208+
where
209+
A: FnMut(&Self, &mut Selector) -> Result<(), E>,
210+
{
211+
let mut selector =
212+
Selector::new(&self, params).map_err(IntoSelectionError::InvalidChangePolicy)?;
213+
selection_algorithm(&self, &mut selector)
214+
.map_err(IntoSelectionError::SelectionAlgorithm)?;
215+
let selection = selector
216+
.try_finalize()
217+
.ok_or(IntoSelectionError::CannotMeetTarget)?;
218+
Ok(selection)
97219
}
98220

99221
/// Whether the outpoint is contained in our candidates.

src/rbf.rs

Lines changed: 29 additions & 11 deletions
Original file line numberDiff line numberDiff line change
@@ -1,3 +1,5 @@
1+
use core::fmt::Display;
2+
13
use alloc::vec::Vec;
24
use bitcoin::{absolute, Amount, OutPoint, Transaction, TxOut, Txid};
35
use miniscript::bitcoin;
@@ -11,6 +13,26 @@ pub struct RbfSet<'t> {
1113
prev_txouts: HashMap<OutPoint, TxOut>,
1214
}
1315

16+
/// Occurs when the given original tx has no input spend that is still available for spending.
17+
#[derive(Debug)]
18+
pub struct OriginalTxHasNoInputsAvailable {
19+
/// Original txid.
20+
pub txid: Txid,
21+
}
22+
23+
impl Display for OriginalTxHasNoInputsAvailable {
24+
fn fmt(&self, f: &mut core::fmt::Formatter<'_>) -> core::fmt::Result {
25+
write!(
26+
f,
27+
"original tx {} has no input spend that is still available",
28+
self.txid
29+
)
30+
}
31+
}
32+
33+
#[cfg(feature = "std")]
34+
impl std::error::Error for OriginalTxHasNoInputsAvailable {}
35+
1436
impl<'t> RbfSet<'t> {
1537
/// Create.
1638
///
@@ -78,13 +100,13 @@ impl<'t> RbfSet<'t> {
78100
/// This guarantees that the txs are replaced.
79101
pub fn must_select_largest_input_per_tx(
80102
&self,
81-
) -> impl FnMut(&InputCandidates) -> HashSet<OutPoint> + '_ {
103+
) -> impl FnMut(&InputCandidates) -> Result<HashSet<OutPoint>, OriginalTxHasNoInputsAvailable> + '_
104+
{
82105
|input_candidates| {
83106
let mut must_select = HashSet::new();
84107

85108
for original_tx in self.txs.values() {
86109
let mut largest_value = Amount::ZERO;
87-
let mut largest_value_not_canonical = Amount::ZERO;
88110
let mut largest_spend = Option::<OutPoint>::None;
89111
let original_tx_spends = original_tx.input.iter().map(|txin| txin.previous_output);
90112
for spend in original_tx_spends {
@@ -95,14 +117,8 @@ impl<'t> RbfSet<'t> {
95117
}
96118
let txout = self.prev_txouts.get(&spend).expect("must have prev txout");
97119

98-
// not canonical
120+
// not available
99121
if !input_candidates.contains(spend) {
100-
if largest_value == Amount::ZERO
101-
&& txout.value > largest_value_not_canonical
102-
{
103-
largest_value_not_canonical = txout.value;
104-
largest_spend = Some(spend);
105-
}
106122
continue;
107123
}
108124

@@ -111,11 +127,13 @@ impl<'t> RbfSet<'t> {
111127
largest_spend = Some(spend);
112128
}
113129
}
114-
let largest_spend = largest_spend.expect("tx must have atleast one input");
130+
let largest_spend = largest_spend.ok_or(OriginalTxHasNoInputsAvailable {
131+
txid: original_tx.compute_txid(),
132+
})?;
115133
must_select.insert(largest_spend);
116134
}
117135

118-
must_select
136+
Ok(must_select)
119137
}
120138
}
121139

tests/synopsis.rs

Lines changed: 39 additions & 60 deletions
Original file line numberDiff line numberDiff line change
@@ -2,8 +2,8 @@ use bdk_bitcoind_rpc::Emitter;
22
use bdk_chain::{bdk_core, local_chain::LocalChain, Balance};
33
use bdk_testenv::{bitcoincore_rpc::RpcApi, TestEnv};
44
use bdk_tx::{
5-
filter_unspendable_now, group_by_spk, no_grouping, ChangePolicyType, CoinControl, Output,
6-
PsbtParams, RbfSet, Selector, SelectorParams, Signer,
5+
filter_unspendable_now, group_by_spk, no_grouping, selection_algorithm_lowest_fee_bnb,
6+
ChangePolicyType, CoinControl, Output, PsbtParams, RbfSet, Selector, SelectorParams, Signer,
77
};
88
use bitcoin::{absolute, key::Secp256k1, Address, Amount, BlockHash, FeeRate, Sequence, Txid};
99
use miniscript::{plan::Assets, Descriptor, DescriptorPublicKey, ForEachKey};
@@ -138,42 +138,29 @@ fn synopsis() -> anyhow::Result<()> {
138138
println!("balance: {}", wallet.balance());
139139

140140
let (tip_height, tip_time) = wallet.tip_info(env.rpc_client())?;
141+
let longterm_feerate = FeeRate::from_sat_per_vb_unchecked(1);
141142

142143
let recipient_addr = env
143144
.rpc_client()
144145
.get_new_address(None, None)?
145146
.assume_checked();
146147

147148
// okay now create tx.
148-
let coin_control = wallet.coin_control(None)?;
149-
println!("excluded: {:?}", coin_control.excluded());
150-
let input_candidates =
151-
coin_control.into_candidates(group_by_spk(), filter_unspendable_now(tip_height, tip_time));
152-
println!(
153-
":: input candidate groups: {}",
154-
input_candidates.groups().len()
155-
);
156-
157-
let longterm_feerate = FeeRate::from_sat_per_vb_unchecked(1);
158-
let mut selector = Selector::new(
159-
&input_candidates,
160-
SelectorParams::new(
161-
FeeRate::from_sat_per_vb_unchecked(10),
162-
vec![Output::with_script(
163-
recipient_addr.script_pubkey(),
164-
Amount::from_sat(21_000_000),
165-
)],
166-
internal.at_derivation_index(0)?,
167-
bdk_tx::ChangePolicyType::NoDustAndLeastWaste { longterm_feerate },
168-
),
169-
)?;
170-
println!(
171-
"bnb score: {}",
172-
selector.select_for_lowest_fee(longterm_feerate, 100_000)?
173-
);
174-
let selection = selector
175-
.try_finalize()
176-
.ok_or(anyhow::anyhow!("failed to find solution"))?;
149+
let selection = wallet
150+
.coin_control(None)?
151+
.into_candidates(group_by_spk(), filter_unspendable_now(tip_height, tip_time))
152+
.into_selection(
153+
selection_algorithm_lowest_fee_bnb(longterm_feerate, 100_000),
154+
SelectorParams::new(
155+
FeeRate::from_sat_per_vb_unchecked(10),
156+
vec![Output::with_script(
157+
recipient_addr.script_pubkey(),
158+
Amount::from_sat(21_000_000),
159+
)],
160+
internal.at_derivation_index(0)?,
161+
bdk_tx::ChangePolicyType::NoDustAndLeastWaste { longterm_feerate },
162+
),
163+
)?;
177164

178165
let mut psbt = selection.create_psbt(PsbtParams {
179166
fallback_sequence: Sequence::ENABLE_RBF_NO_LOCKTIME,
@@ -227,7 +214,7 @@ fn synopsis() -> anyhow::Result<()> {
227214
.expect("must have no missing prevouts");
228215

229216
// Input candidates.
230-
let candidates = wallet
217+
let selection = wallet
231218
// We canonicalize first.
232219
// This ensures all input candidates are of a consistent UTXO set.
233220
// The canonicalization is modified by excluding the original txs and their
@@ -242,34 +229,26 @@ fn synopsis() -> anyhow::Result<()> {
242229
// need to guarantee atleast one prevout of each original tx is picked, otherwise we
243230
// may not actually replace the original txs.
244231
// The policy used here is to choose the largest value prevout of each original tx.
245-
.with_must_select_policy(rbf_set.must_select_largest_input_per_tx())
246-
.expect("this cannot fail here");
247-
248-
// Now we actually do coin selection.
249-
let mut selector = Selector::new(
250-
&candidates,
251-
SelectorParams {
252-
// This is just a lower-bound feerate. The actual result will be much higher to
253-
// satisfy mempool-replacement policy.
254-
target_feerate: FeeRate::from_sat_per_vb_unchecked(1),
255-
// We cancel the tx by specifying no target outputs. This way, all excess returns
256-
// to our change output (unless if the prevouts picked are so small that it will
257-
// be less wasteful to have no output, however that will not be a valid tx).
258-
// If you only want to fee bump, put the original txs' recipients here.
259-
target_outputs: vec![],
260-
change_descriptor: internal.at_derivation_index(1)?,
261-
change_policy: ChangePolicyType::NoDustAndLeastWaste { longterm_feerate },
262-
// This ensures that we satisfy mempool-replacement policy rules 4 and 6.
263-
replace: Some(rbf_set.selector_rbf_params()),
264-
},
265-
)?;
266-
println!(
267-
"replacement tx score: {}",
268-
selector.select_for_lowest_fee(longterm_feerate, 100_000)?
269-
);
270-
let selection = selector
271-
.try_finalize()
272-
.expect("failed to finalize replacement tx");
232+
.with_must_select_policy(rbf_set.must_select_largest_input_per_tx())?
233+
// Do coin selection.
234+
.into_selection(
235+
// Coin selection algorithm.
236+
selection_algorithm_lowest_fee_bnb(longterm_feerate, 100_000),
237+
SelectorParams {
238+
// This is just a lower-bound feerate. The actual result will be much higher to
239+
// satisfy mempool-replacement policy.
240+
target_feerate: FeeRate::from_sat_per_vb_unchecked(1),
241+
// We cancel the tx by specifying no target outputs. This way, all excess returns
242+
// to our change output (unless if the prevouts picked are so small that it will
243+
// be less wasteful to have no output, however that will not be a valid tx).
244+
// If you only want to fee bump, put the original txs' recipients here.
245+
target_outputs: vec![],
246+
change_descriptor: internal.at_derivation_index(1)?,
247+
change_policy: ChangePolicyType::NoDustAndLeastWaste { longterm_feerate },
248+
// This ensures that we satisfy mempool-replacement policy rules 4 and 6.
249+
replace: Some(rbf_set.selector_rbf_params()),
250+
},
251+
)?;
273252

274253
let mut psbt = selection.create_psbt(PsbtParams {
275254
// Not strictly necessary, but it may help us replace this replacement faster.

0 commit comments

Comments
 (0)