Skip to content

Commit aadeca5

Browse files
committed
feat(selector)!: allow multiple change sources
Previously, the `change_descriptor` field only accepted a definite descriptor to derive the change output script pubkey. This worked for most cases but failed for scenarios - such as silent payments - where a definite descriptor is not available. Now `change_descriptor` is `change_script` and can take a ScriptSource, which can be a descriptor or the script itself. Also, `change_weights` field is added as a field of SelectorParams, to externalize the calculus of the satisfaction weight from the API. This allowed the remove of the `to_cs_change_weigths` method. `From<(ScriptSource, Amount)>` for Output is added for conveniency.
1 parent 4f8a213 commit aadeca5

File tree

4 files changed

+62
-41
lines changed

4 files changed

+62
-41
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: 8 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-
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,8 +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_descriptor: internal.at_derivation_index(1)?,
138+
change_script: ScriptSource::Descriptor(Box::new(
139+
internal.at_derivation_index(1)?,
140+
)),
138141
change_policy: ChangePolicyType::NoDustAndLeastWaste { longterm_feerate },
142+
change_weight: wallet.change_weight(),
139143
// This ensures that we satisfy mempool-replacement policy rules 4 and 6.
140144
replace: Some(rbf_params),
141145
},

src/output.rs

Lines changed: 9 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -74,6 +74,15 @@ impl From<(DefiniteDescriptor, Amount)> for Output {
7474
}
7575
}
7676

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+
7786
impl Output {
7887
/// From script
7988
pub fn with_script(script: ScriptBuf, value: Amount) -> Self {

src/selector.rs

Lines changed: 19 additions & 36 deletions
Original file line numberDiff line numberDiff line change
@@ -1,10 +1,10 @@
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

7-
use crate::{cs_feerate, DefiniteDescriptor, InputCandidates, InputGroup, Output, Selection};
7+
use crate::{cs_feerate, InputCandidates, InputGroup, Output, ScriptSource, Selection};
88
use alloc::vec::Vec;
99
use core::fmt;
1010

@@ -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_descriptor: DefiniteDescriptor,
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_descriptor: DefiniteDescriptor,
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_descriptor: DefiniteDescriptor,
146+
change_script: ScriptSource,
144147
change_policy: ChangePolicyType,
148+
change_weight: DrainWeights,
145149
) -> Self {
146150
Self {
147-
change_descriptor,
148-
change_policy,
149151
target_feerate,
150152
target_outputs,
153+
change_script,
154+
change_policy,
155+
change_weight,
151156
replace: None,
152157
}
153158
}
@@ -171,36 +176,14 @@ impl SelectorParams {
171176
}
172177
}
173178

174-
/// To change output weights.
175-
///
176-
/// # Error
177-
///
178-
/// Fails if `change_descriptor` cannot be satisfied.
179-
pub fn to_cs_change_weights(&self) -> Result<bdk_coin_select::DrainWeights, miniscript::Error> {
180-
Ok(DrainWeights {
181-
output_weight: (TxOut {
182-
script_pubkey: self.change_descriptor.script_pubkey(),
183-
value: Amount::ZERO,
184-
})
185-
.weight()
186-
.to_wu(),
187-
spend_weight: self.change_descriptor.max_weight_to_satisfy()?.to_wu(),
188-
n_outputs: 1,
189-
})
190-
}
191-
192179
/// To change policy.
193180
///
194181
/// # Error
195182
///
196183
/// Fails if `change_descriptor` cannot be satisfied.
197184
pub fn to_cs_change_policy(&self) -> Result<bdk_coin_select::ChangePolicy, miniscript::Error> {
198-
let change_weights = self.to_cs_change_weights()?;
199-
let dust_value = self
200-
.change_descriptor
201-
.script_pubkey()
202-
.minimal_non_dust()
203-
.to_sat();
185+
let change_weights = self.change_weight;
186+
let dust_value = self.change_script.script().minimal_non_dust().to_sat();
204187
Ok(match self.change_policy {
205188
ChangePolicyType::NoDust => ChangePolicy::min_value(change_weights, dust_value),
206189
ChangePolicyType::NoDustAndLeastWaste { longterm_feerate } => {
@@ -268,7 +251,7 @@ impl<'c> Selector<'c> {
268251
.to_cs_change_policy()
269252
.map_err(SelectorError::Miniscript)?;
270253
let target_outputs = params.target_outputs;
271-
let change_descriptor = params.change_descriptor;
254+
let change_script = params.change_script;
272255
if target.value() > candidates.groups().map(|grp| grp.value().to_sat()).sum() {
273256
return Err(SelectorError::CannotMeetTarget(CannotMeetTarget));
274257
}
@@ -281,7 +264,7 @@ impl<'c> Selector<'c> {
281264
target,
282265
target_outputs,
283266
change_policy,
284-
change_descriptor,
267+
change_script,
285268
inner,
286269
})
287270
}
@@ -358,10 +341,10 @@ impl<'c> Selector<'c> {
358341
outputs: {
359342
let mut outputs = self.target_outputs.clone();
360343
if maybe_change.is_some() {
361-
outputs.push(Output::with_descriptor(
362-
self.change_descriptor.clone(),
344+
outputs.push(Output::from((
345+
self.change_script.clone(),
363346
Amount::from_sat(maybe_change.value),
364-
));
347+
)));
365348
}
366349
outputs
367350
},

0 commit comments

Comments
 (0)