Skip to content

Commit bcb018f

Browse files
ValuedMammalnymius
authored andcommitted
feat(selector)!: allow multiple change sources
Changed `change_descriptor` to `change_script: ScriptSource` in both Selector and SelectorParams. Added `change_weights` as a field of SelectorParams and removed `to_cs_change_weights`. An alternative is to require the ChangePolicy directly, or perhaps make ChangePolicyType more flexible. Implement `From<(ScriptSource, Amount)>` for Output. example: Add helper on example Wallet to return the drain weights which can then be passed to `SelectorParams::new`.
1 parent 551e684 commit bcb018f

File tree

4 files changed

+73
-89
lines changed

4 files changed

+73
-89
lines changed

examples/common.rs

Lines changed: 26 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -4,9 +4,10 @@ use bdk_bitcoind_rpc::{Emitter, NO_EXPECTED_MEMPOOL_TXIDS};
44
use bdk_chain::{
55
bdk_core, Anchor, Balance, CanonicalizationParams, ChainPosition, ConfirmationBlockTime,
66
};
7+
use bdk_coin_select::DrainWeights;
78
use bdk_testenv::{bitcoincore_rpc::RpcApi, TestEnv};
89
use bdk_tx::{CanonicalUnspents, Input, InputCandidates, RbfParams, TxStatus, TxWithStatus};
9-
use bitcoin::{absolute, Address, BlockHash, OutPoint, Transaction, Txid};
10+
use bitcoin::{absolute, Address, Amount, BlockHash, OutPoint, Transaction, TxOut, Txid};
1011
use miniscript::{
1112
plan::{Assets, Plan},
1213
Descriptor, DescriptorPublicKey, ForEachKey,
@@ -134,6 +135,30 @@ impl Wallet {
134135
.map(|c_tx| (c_tx.tx_node.tx, status_from_position(c_tx.chain_position)))
135136
}
136137

138+
/// Computes an upper bound on the weight of a change output plus the future weight to spend it.
139+
pub fn change_weight(&self) -> DrainWeights {
140+
let desc = self
141+
.graph
142+
.index
143+
.get_descriptor(INTERNAL)
144+
.unwrap()
145+
.at_derivation_index(0)
146+
.unwrap();
147+
let output_weight = TxOut {
148+
script_pubkey: desc.script_pubkey(),
149+
value: Amount::ZERO,
150+
}
151+
.weight()
152+
.to_wu();
153+
let spend_weight = desc.max_weight_to_satisfy().unwrap().to_wu();
154+
155+
DrainWeights {
156+
output_weight,
157+
spend_weight,
158+
n_outputs: 1,
159+
}
160+
}
161+
137162
pub fn all_candidates(&self) -> bdk_tx::InputCandidates {
138163
let index = &self.graph.index;
139164
let assets = self.assets();

examples/synopsis.rs

Lines changed: 6 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -1,7 +1,7 @@
11
use bdk_testenv::{bitcoincore_rpc::RpcApi, TestEnv};
22
use bdk_tx::{
33
filter_unspendable_now, group_by_spk, selection_algorithm_lowest_fee_bnb, ChangePolicyType,
4-
Output, PsbtParams, SelectorParams, Signer,
4+
Output, PsbtParams, ScriptSource, SelectorParams, Signer,
55
};
66
use bitcoin::{key::Secp256k1, Amount, FeeRate, Sequence};
77
use miniscript::Descriptor;
@@ -60,8 +60,9 @@ fn main() -> anyhow::Result<()> {
6060
recipient_addr.script_pubkey(),
6161
Amount::from_sat(21_000_000),
6262
)],
63-
bdk_tx::ScriptSource::Descriptor(Box::new(internal.at_derivation_index(0)?)),
64-
bdk_tx::ChangePolicyType::NoDustAndLeastWaste { longterm_feerate },
63+
ScriptSource::Descriptor(Box::new(internal.at_derivation_index(0)?)),
64+
ChangePolicyType::NoDustAndLeastWaste { longterm_feerate },
65+
wallet.change_weight(),
6566
),
6667
)?;
6768

@@ -134,10 +135,11 @@ fn main() -> anyhow::Result<()> {
134135
// be less wasteful to have no output, however that will not be a valid tx).
135136
// If you only want to fee bump, put the original txs' recipients here.
136137
target_outputs: vec![],
137-
change_source: bdk_tx::ScriptSource::Descriptor(Box::new(
138+
change_script: ScriptSource::Descriptor(Box::new(
138139
internal.at_derivation_index(1)?,
139140
)),
140141
change_policy: ChangePolicyType::NoDustAndLeastWaste { longterm_feerate },
142+
change_weight: wallet.change_weight(),
141143
// This ensures that we satisfy mempool-replacement policy rules 4 and 6.
142144
replace: Some(rbf_params),
143145
},

src/output.rs

Lines changed: 17 additions & 16 deletions
Original file line numberDiff line numberDiff line change
@@ -1,26 +1,21 @@
11
use alloc::boxed::Box;
2-
use bitcoin::{Amount, ScriptBuf, TxOut, Weight};
2+
use bitcoin::{Amount, ScriptBuf, TxOut};
33
use miniscript::bitcoin;
44

55
use crate::DefiniteDescriptor;
66

77
/// Source of the output script pubkey
88
#[derive(Debug, Clone)]
99
pub enum ScriptSource {
10-
/// a bitcoin script pubkey optionally with its max satisfaction weight
11-
Script {
12-
/// the script
13-
script_pubkey: ScriptBuf,
14-
/// the max weight at which the script can be satisfied
15-
max_weight_to_satisfy: Option<Weight>,
16-
},
10+
/// bitcoin script
11+
Script(ScriptBuf),
1712
/// definite descriptor
1813
Descriptor(Box<DefiniteDescriptor>),
1914
}
2015

2116
impl From<ScriptBuf> for ScriptSource {
2217
fn from(script: ScriptBuf) -> Self {
23-
Self::from_script(script, None)
18+
Self::from_script(script)
2419
}
2520
}
2621

@@ -32,11 +27,8 @@ impl From<DefiniteDescriptor> for ScriptSource {
3227

3328
impl ScriptSource {
3429
/// From script
35-
pub fn from_script(script_pubkey: ScriptBuf, max_weight_to_satisfy: Option<u64>) -> Self {
36-
Self::Script {
37-
script_pubkey,
38-
max_weight_to_satisfy: max_weight_to_satisfy.map(Weight::from_wu),
39-
}
30+
pub fn from_script(script: ScriptBuf) -> Self {
31+
Self::Script(script)
4032
}
4133

4234
/// From descriptor
@@ -47,15 +39,15 @@ impl ScriptSource {
4739
/// To ScriptBuf
4840
pub fn script(&self) -> ScriptBuf {
4941
match self {
50-
ScriptSource::Script { script_pubkey, .. } => script_pubkey.clone(),
42+
ScriptSource::Script(spk) => spk.clone(),
5143
ScriptSource::Descriptor(descriptor) => descriptor.script_pubkey(),
5244
}
5345
}
5446

5547
/// Get descriptor (if any).
5648
pub fn descriptor(&self) -> Option<&DefiniteDescriptor> {
5749
match self {
58-
ScriptSource::Script { .. } => None,
50+
ScriptSource::Script(_) => None,
5951
ScriptSource::Descriptor(descriptor) => Some(descriptor),
6052
}
6153
}
@@ -82,6 +74,15 @@ impl From<(DefiniteDescriptor, Amount)> for Output {
8274
}
8375
}
8476

77+
impl From<(ScriptSource, Amount)> for Output {
78+
fn from((src, value): (ScriptSource, Amount)) -> Self {
79+
match src {
80+
ScriptSource::Descriptor(desc) => Self::with_descriptor(*desc, value),
81+
ScriptSource::Script(s) => Self::with_script(s, value),
82+
}
83+
}
84+
}
85+
8586
impl Output {
8687
/// From script
8788
pub fn with_script(script: ScriptBuf, value: Amount) -> Self {

src/selector.rs

Lines changed: 24 additions & 68 deletions
Original file line numberDiff line numberDiff line change
@@ -1,7 +1,7 @@
11
use bdk_coin_select::{
22
ChangePolicy, DrainWeights, InsufficientFunds, Replace, Target, TargetFee, TargetOutputs,
33
};
4-
use bitcoin::{Amount, FeeRate, Transaction, TxOut, Weight};
4+
use bitcoin::{Amount, FeeRate, Transaction, Weight};
55
use miniscript::bitcoin;
66

77
use crate::{cs_feerate, InputCandidates, InputGroup, Output, ScriptSource, Selection};
@@ -15,7 +15,7 @@ pub struct Selector<'c> {
1515
target_outputs: Vec<Output>,
1616
target: Target,
1717
change_policy: bdk_coin_select::ChangePolicy,
18-
change_source: ScriptSource,
18+
change_script: ScriptSource,
1919
inner: bdk_coin_select::CoinSelector<'c>,
2020
}
2121

@@ -42,11 +42,14 @@ pub struct SelectorParams {
4242
/// To derive change output.
4343
///
4444
/// Will error if this is unsatisfiable descriptor.
45-
pub change_source: ScriptSource,
45+
pub change_script: ScriptSource,
4646

4747
/// The policy to determine whether we create a change output.
4848
pub change_policy: ChangePolicyType,
4949

50+
/// Weight of the change output plus the future weight to spend the change
51+
pub change_weight: DrainWeights,
52+
5053
/// Params for replacing tx(s).
5154
pub replace: Option<RbfParams>,
5255
}
@@ -140,14 +143,16 @@ impl SelectorParams {
140143
pub fn new(
141144
target_feerate: bitcoin::FeeRate,
142145
target_outputs: Vec<Output>,
143-
change_source: ScriptSource,
146+
change_script: ScriptSource,
144147
change_policy: ChangePolicyType,
148+
change_weight: DrainWeights,
145149
) -> Self {
146150
Self {
147-
change_source,
148-
change_policy,
149151
target_feerate,
150152
target_outputs,
153+
change_script,
154+
change_policy,
155+
change_weight,
151156
replace: None,
152157
}
153158
}
@@ -171,53 +176,14 @@ impl SelectorParams {
171176
}
172177
}
173178

174-
/// To change output weights.
175-
///
176-
/// # Error
177-
///
178-
/// Fails if `change_source` cannot be satisfied.
179-
pub fn to_cs_change_weights(&self) -> Result<bdk_coin_select::DrainWeights, SelectorError> {
180-
let (script_pubkey, spend_weight) = match &self.change_source {
181-
ScriptSource::Script {
182-
script_pubkey,
183-
max_weight_to_satisfy,
184-
} => {
185-
if let Some(weight) = max_weight_to_satisfy {
186-
(script_pubkey.clone(), weight.to_wu())
187-
} else {
188-
return Err(SelectorError::MissingChangeSatisfactionWeight);
189-
}
190-
}
191-
ScriptSource::Descriptor(definite_descriptor) => {
192-
let script_pubkey = definite_descriptor.script_pubkey();
193-
let weight = definite_descriptor
194-
.max_weight_to_satisfy()
195-
.map(|weight| weight.to_wu())
196-
.map_err(SelectorError::Miniscript)?;
197-
(script_pubkey, weight)
198-
}
199-
};
200-
let drain_weight = DrainWeights {
201-
output_weight: (TxOut {
202-
script_pubkey,
203-
value: Amount::ZERO,
204-
})
205-
.weight()
206-
.to_wu(),
207-
spend_weight,
208-
n_outputs: 1,
209-
};
210-
Ok(drain_weight)
211-
}
212-
213179
/// To change policy.
214180
///
215181
/// # Error
216182
///
217-
/// Fails if `change_source` cannot be satisfied.
218-
pub fn to_cs_change_policy(&self) -> Result<bdk_coin_select::ChangePolicy, SelectorError> {
219-
let change_weights = self.to_cs_change_weights()?;
220-
let dust_value = self.change_source.script().minimal_non_dust().to_sat();
183+
/// Fails if `change_descriptor` cannot be satisfied.
184+
pub fn to_cs_change_policy(&self) -> Result<bdk_coin_select::ChangePolicy, miniscript::Error> {
185+
let change_weights = self.change_weight;
186+
let dust_value = self.change_script.script().minimal_non_dust().to_sat();
221187
Ok(match self.change_policy {
222188
ChangePolicyType::NoDust => ChangePolicy::min_value(change_weights, dust_value),
223189
ChangePolicyType::NoDustAndLeastWaste { longterm_feerate } => {
@@ -255,19 +221,13 @@ pub enum SelectorError {
255221
Miniscript(miniscript::Error),
256222
/// meeting the target is not possible
257223
CannotMeetTarget(CannotMeetTarget),
258-
/// missing weight to satisfy change output
259-
MissingChangeSatisfactionWeight,
260224
}
261225

262226
impl fmt::Display for SelectorError {
263227
fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
264228
match self {
265229
Self::Miniscript(err) => write!(f, "{err}"),
266230
Self::CannotMeetTarget(err) => write!(f, "{err}"),
267-
Self::MissingChangeSatisfactionWeight => write!(
268-
f,
269-
"Cannot determinate change policy without satisfaction weight"
270-
),
271231
}
272232
}
273233
}
@@ -287,9 +247,11 @@ impl<'c> Selector<'c> {
287247
params: SelectorParams,
288248
) -> Result<Self, SelectorError> {
289249
let target = params.to_cs_target();
290-
let change_policy = params.to_cs_change_policy()?;
250+
let change_policy = params
251+
.to_cs_change_policy()
252+
.map_err(SelectorError::Miniscript)?;
291253
let target_outputs = params.target_outputs;
292-
let change_source = params.change_source;
254+
let change_script = params.change_script;
293255
if target.value() > candidates.groups().map(|grp| grp.value().to_sat()).sum() {
294256
return Err(SelectorError::CannotMeetTarget(CannotMeetTarget));
295257
}
@@ -302,7 +264,7 @@ impl<'c> Selector<'c> {
302264
target,
303265
target_outputs,
304266
change_policy,
305-
change_source,
267+
change_script,
306268
inner,
307269
})
308270
}
@@ -379,16 +341,10 @@ impl<'c> Selector<'c> {
379341
outputs: {
380342
let mut outputs = self.target_outputs.clone();
381343
if maybe_change.is_some() {
382-
let change_amount = Amount::from_sat(maybe_change.value);
383-
let change_output = match &self.change_source {
384-
ScriptSource::Descriptor(definite_descriptor) => {
385-
Output::with_descriptor(*definite_descriptor.clone(), change_amount)
386-
}
387-
ScriptSource::Script { script_pubkey, .. } => {
388-
Output::with_script(script_pubkey.clone(), change_amount)
389-
}
390-
};
391-
outputs.push(change_output);
344+
outputs.push(Output::from((
345+
self.change_script.clone(),
346+
Amount::from_sat(maybe_change.value),
347+
)));
392348
}
393349
outputs
394350
},

0 commit comments

Comments
 (0)