Skip to content

Commit 9692a2a

Browse files
committed
Merge branch 'master' of https://github.com/bitcoindevkit/bdk into rpc-pruned
2 parents 35f41ee + 063d51f commit 9692a2a

File tree

7 files changed

+143
-10
lines changed

7 files changed

+143
-10
lines changed

.github/workflows/cont_integration.yml

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -12,7 +12,7 @@ jobs:
1212
rust:
1313
- version: 1.60.0 # STABLE
1414
clippy: true
15-
- version: 1.56.0 # MSRV
15+
- version: 1.56.1 # MSRV
1616
features:
1717
- default
1818
- minimal
@@ -146,7 +146,7 @@ jobs:
146146
- run: sudo apt-get update || exit 1
147147
- run: sudo apt-get install -y libclang-common-10-dev clang-10 libc6-dev-i386 || exit 1
148148
- name: Set default toolchain
149-
run: rustup default 1.56.0 # STABLE
149+
run: rustup default 1.56.1 # STABLE
150150
- name: Set profile
151151
run: rustup set profile minimal
152152
- name: Add target wasm32

CHANGELOG.md

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -5,6 +5,8 @@ The format is based on [Keep a Changelog](https://keepachangelog.com/en/1.0.0/),
55
and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0.html).
66

77
## [Unreleased]
8+
- New MSRV set to `1.56.1`
9+
- Fee sniping discouraging through nLockTime - if the user specifies a `current_height`, we use that as a nlocktime, otherwise we use the last sync height (or 0 if we never synced)
810

911
## [v0.19.0] - [v0.18.0]
1012

README.md

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -13,7 +13,7 @@
1313
<a href="https://github.com/bitcoindevkit/bdk/actions?query=workflow%3ACI"><img alt="CI Status" src="https://github.com/bitcoindevkit/bdk/workflows/CI/badge.svg"></a>
1414
<a href="https://codecov.io/gh/bitcoindevkit/bdk"><img src="https://codecov.io/gh/bitcoindevkit/bdk/branch/master/graph/badge.svg"/></a>
1515
<a href="https://docs.rs/bdk"><img alt="API Docs" src="https://img.shields.io/badge/docs.rs-bdk-green"/></a>
16-
<a href="https://blog.rust-lang.org/2020/08/27/Rust-1.56.0.html"><img alt="Rustc Version 1.56+" src="https://img.shields.io/badge/rustc-1.56%2B-lightgrey.svg"/></a>
16+
<a href="https://blog.rust-lang.org/2021/11/01/Rust-1.56.1.html"><img alt="Rustc Version 1.56.1+" src="https://img.shields.io/badge/rustc-1.56.1%2B-lightgrey.svg"/></a>
1717
<a href="https://discord.gg/d7NkDKm"><img alt="Chat on Discord" src="https://img.shields.io/discord/753336465005608961?logo=discord"></a>
1818
</p>
1919

src/descriptor/mod.rs

Lines changed: 4 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -134,13 +134,13 @@ impl IntoWalletDescriptor for (ExtendedDescriptor, KeyMap) {
134134

135135
let check_key = |pk: &DescriptorPublicKey| {
136136
let (pk, _, networks) = if self.0.is_witness() {
137-
let desciptor_key: DescriptorKey<miniscript::Segwitv0> =
137+
let descriptor_key: DescriptorKey<miniscript::Segwitv0> =
138138
pk.clone().into_descriptor_key()?;
139-
desciptor_key.extract(secp)?
139+
descriptor_key.extract(secp)?
140140
} else {
141-
let desciptor_key: DescriptorKey<miniscript::Legacy> =
141+
let descriptor_key: DescriptorKey<miniscript::Legacy> =
142142
pk.clone().into_descriptor_key()?;
143-
desciptor_key.extract(secp)?
143+
descriptor_key.extract(secp)?
144144
};
145145

146146
if networks.contains(&network) {

src/types.rs

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -202,8 +202,10 @@ pub struct TransactionDetails {
202202
pub txid: Txid,
203203

204204
/// Received value (sats)
205+
/// Sum of owned outputs of this transaction.
205206
pub received: u64,
206207
/// Sent value (sats)
208+
/// Sum of owned inputs of this transaction.
207209
pub sent: u64,
208210
/// Fee value (sats) if available.
209211
/// The availability of the fee depends on the backend. It's never `None` with an Electrum

src/wallet/mod.rs

Lines changed: 116 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -619,9 +619,27 @@ where
619619
_ => 1,
620620
};
621621

622+
// We use a match here instead of a map_or_else as it's way more readable :)
623+
let current_height = match params.current_height {
624+
// If they didn't tell us the current height, we assume it's the latest sync height.
625+
None => self
626+
.database()
627+
.get_sync_time()?
628+
.map(|sync_time| sync_time.block_time.height),
629+
h => h,
630+
};
631+
622632
let lock_time = match params.locktime {
623-
// No nLockTime, default to 0
624-
None => requirements.timelock.unwrap_or(0),
633+
// When no nLockTime is specified, we try to prevent fee sniping, if possible
634+
None => {
635+
// Fee sniping can be partially prevented by setting the timelock
636+
// to current_height. If we don't know the current_height,
637+
// we default to 0.
638+
let fee_sniping_height = current_height.unwrap_or(0);
639+
// We choose the biggest between the required nlocktime and the fee sniping
640+
// height
641+
std::cmp::max(requirements.timelock.unwrap_or(0), fee_sniping_height)
642+
}
625643
// Specific nLockTime required and we have no constraints, so just set to that value
626644
Some(x) if requirements.timelock.is_none() => x,
627645
// Specific nLockTime required and it's compatible with the constraints
@@ -791,7 +809,14 @@ where
791809
let drain_val = (coin_selection.selected_amount() - outgoing).saturating_sub(fee_amount);
792810

793811
if tx.output.is_empty() {
794-
if params.drain_to.is_some() {
812+
// Uh oh, our transaction has no outputs.
813+
// We allow this when:
814+
// - We have a drain_to address and the utxos we must spend (this happens,
815+
// for example, when we RBF)
816+
// - We have a drain_to address and drain_wallet set
817+
// Otherwise, we don't know who we should send the funds to, and how much
818+
// we should send!
819+
if params.drain_to.is_some() && (params.drain_wallet || !params.utxos.is_empty()) {
795820
if drain_val.is_dust(&drain_output.script_pubkey) {
796821
return Err(Error::InsufficientFunds {
797822
needed: drain_output.script_pubkey.dust_value().as_sat(),
@@ -1980,9 +2005,59 @@ pub(crate) mod test {
19802005
builder.add_recipient(addr.script_pubkey(), 25_000);
19812006
let (psbt, _) = builder.finish().unwrap();
19822007

2008+
// Since we never synced the wallet we don't have a last_sync_height
2009+
// we could use to try to prevent fee sniping. We default to 0.
19832010
assert_eq!(psbt.unsigned_tx.lock_time, 0);
19842011
}
19852012

2013+
#[test]
2014+
fn test_create_tx_fee_sniping_locktime_provided_height() {
2015+
let (wallet, _, _) = get_funded_wallet(get_test_wpkh());
2016+
let addr = wallet.get_address(New).unwrap();
2017+
let mut builder = wallet.build_tx();
2018+
builder.add_recipient(addr.script_pubkey(), 25_000);
2019+
let sync_time = SyncTime {
2020+
block_time: BlockTime {
2021+
height: 24,
2022+
timestamp: 0,
2023+
},
2024+
};
2025+
wallet
2026+
.database
2027+
.borrow_mut()
2028+
.set_sync_time(sync_time)
2029+
.unwrap();
2030+
let current_height = 25;
2031+
builder.set_current_height(current_height);
2032+
let (psbt, _) = builder.finish().unwrap();
2033+
2034+
// current_height will override the last sync height
2035+
assert_eq!(psbt.unsigned_tx.lock_time, current_height);
2036+
}
2037+
2038+
#[test]
2039+
fn test_create_tx_fee_sniping_locktime_last_sync() {
2040+
let (wallet, _, _) = get_funded_wallet(get_test_wpkh());
2041+
let addr = wallet.get_address(New).unwrap();
2042+
let mut builder = wallet.build_tx();
2043+
builder.add_recipient(addr.script_pubkey(), 25_000);
2044+
let sync_time = SyncTime {
2045+
block_time: BlockTime {
2046+
height: 25,
2047+
timestamp: 0,
2048+
},
2049+
};
2050+
wallet
2051+
.database
2052+
.borrow_mut()
2053+
.set_sync_time(sync_time.clone())
2054+
.unwrap();
2055+
let (psbt, _) = builder.finish().unwrap();
2056+
2057+
// If there's no current_height we're left with using the last sync height
2058+
assert_eq!(psbt.unsigned_tx.lock_time, sync_time.block_time.height);
2059+
}
2060+
19862061
#[test]
19872062
fn test_create_tx_default_locktime_cltv() {
19882063
let (wallet, _, _) = get_funded_wallet(get_test_single_sig_cltv());
@@ -2001,9 +2076,13 @@ pub(crate) mod test {
20012076
let mut builder = wallet.build_tx();
20022077
builder
20032078
.add_recipient(addr.script_pubkey(), 25_000)
2079+
.set_current_height(630_001)
20042080
.nlocktime(630_000);
20052081
let (psbt, _) = builder.finish().unwrap();
20062082

2083+
// When we explicitly specify a nlocktime
2084+
// we don't try any fee sniping prevention trick
2085+
// (we ignore the current_height)
20072086
assert_eq!(psbt.unsigned_tx.lock_time, 630_000);
20082087
}
20092088

@@ -2176,6 +2255,40 @@ pub(crate) mod test {
21762255
assert_eq!(drain_output.value, 30_000 - details.fee.unwrap_or(0));
21772256
}
21782257

2258+
#[test]
2259+
fn test_create_tx_drain_to_and_utxos() {
2260+
let (wallet, _, _) = get_funded_wallet(get_test_wpkh());
2261+
let addr = wallet.get_address(New).unwrap();
2262+
let utxos: Vec<_> = wallet
2263+
.get_available_utxos()
2264+
.unwrap()
2265+
.into_iter()
2266+
.map(|(u, _)| u.outpoint)
2267+
.collect();
2268+
let mut builder = wallet.build_tx();
2269+
builder
2270+
.drain_to(addr.script_pubkey())
2271+
.add_utxos(&utxos)
2272+
.unwrap();
2273+
let (psbt, details) = builder.finish().unwrap();
2274+
2275+
assert_eq!(psbt.unsigned_tx.output.len(), 1);
2276+
assert_eq!(
2277+
psbt.unsigned_tx.output[0].value,
2278+
50_000 - details.fee.unwrap_or(0)
2279+
);
2280+
}
2281+
2282+
#[test]
2283+
#[should_panic(expected = "NoRecipients")]
2284+
fn test_create_tx_drain_to_no_drain_wallet_no_utxos() {
2285+
let (wallet, _, _) = get_funded_wallet(get_test_wpkh());
2286+
let drain_addr = wallet.get_address(New).unwrap();
2287+
let mut builder = wallet.build_tx();
2288+
builder.drain_to(drain_addr.script_pubkey());
2289+
builder.finish().unwrap();
2290+
}
2291+
21792292
#[test]
21802293
fn test_create_tx_default_fee_rate() {
21812294
let (wallet, _, _) = get_funded_wallet(get_test_wpkh());

src/wallet/tx_builder.rs

Lines changed: 16 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -147,6 +147,7 @@ pub(crate) struct TxParams {
147147
pub(crate) add_global_xpubs: bool,
148148
pub(crate) include_output_redeem_witness_script: bool,
149149
pub(crate) bumping_fee: Option<PreviousFee>,
150+
pub(crate) current_height: Option<u32>,
150151
}
151152

152153
#[derive(Clone, Copy, Debug)]
@@ -543,6 +544,17 @@ impl<'a, D: BatchDatabase, Cs: CoinSelectionAlgorithm<D>, Ctx: TxBuilderContext>
543544
self.params.rbf = Some(RbfValue::Value(nsequence));
544545
self
545546
}
547+
548+
/// Set the current blockchain height.
549+
///
550+
/// This will be used to set the nLockTime for preventing fee sniping. If the current height is
551+
/// not provided, the last sync height will be used instead.
552+
///
553+
/// **Note**: This will be ignored if you manually specify a nlocktime using [`TxBuilder::nlocktime`].
554+
pub fn set_current_height(&mut self, height: u32) -> &mut Self {
555+
self.params.current_height = Some(height);
556+
self
557+
}
546558
}
547559

548560
impl<'a, D: BatchDatabase, Cs: CoinSelectionAlgorithm<D>> TxBuilder<'a, D, Cs, CreateTx> {
@@ -574,6 +586,9 @@ impl<'a, D: BatchDatabase, Cs: CoinSelectionAlgorithm<D>> TxBuilder<'a, D, Cs, C
574586
/// difference is that it is valid to use `drain_to` without setting any ordinary recipients
575587
/// with [`add_recipient`] (but it is perfectly fine to add recipients as well).
576588
///
589+
/// If you choose not to set any recipients, you should either provide the utxos that the
590+
/// transaction should spend via [`add_utxos`], or set [`drain_wallet`] to spend all of them.
591+
///
577592
/// When bumping the fees of a transaction made with this option, you probably want to
578593
/// use [`allow_shrinking`] to allow this output to be reduced to pay for the extra fees.
579594
///
@@ -604,6 +619,7 @@ impl<'a, D: BatchDatabase, Cs: CoinSelectionAlgorithm<D>> TxBuilder<'a, D, Cs, C
604619
///
605620
/// [`allow_shrinking`]: Self::allow_shrinking
606621
/// [`add_recipient`]: Self::add_recipient
622+
/// [`add_utxos`]: Self::add_utxos
607623
/// [`drain_wallet`]: Self::drain_wallet
608624
pub fn drain_to(&mut self, script_pubkey: Script) -> &mut Self {
609625
self.params.drain_to = Some(script_pubkey);

0 commit comments

Comments
 (0)