Skip to content

Commit 16400f3

Browse files
committed
Merge #5: feat: enable random anti-fee sniping
179860e feat!: implement anti-fee-sniping protection (Abiodun) c95e8ed refactor: remove unnecessary clippy allow attributes (Abiodun) Pull request description: This PR implements Anti-Fee-Sniping with randomization as discussed in issue #4 ## Notes to the reviewers The implementation adds randomization to anti-fee-sniping behavior: 1. Uses a 50/50 chance to choose between nLockTime and nSequence (when possible) 2. Adds a 10% chance to set either value further back in time (by a random value between 0-99) 3. Detects taproot inputs and their confirmation status ## Changelog notice ### Changed - `CreatePsbtError::MissingFullTxForLegacyInput` and `CreatePsbtError::MissingFullTxForSegwitV0Input` now wrap `Input` in `Box` (breaking change) ### Added - `PsbtParams::enable_anti_fee_sniping` field for BIP326 anti-fee-sniping protection - `CreatePsbtError::InvalidLockTime` and `CreatePsbtError::UnsupportedVersion` error variants - `Selection::create_psbt_with_rng` method for custom RNG * [x] I've signed all my commits * [x] I ran `cargo fmt` and `cargo clippy` before committing * [x] I've added docs for the new feature Closes #4 ACKs for top commit: ValuedMammal: ACK 179860e nymius: ACK 179860e Tree-SHA512: 52a9190560e2714f99afae519f7facd304013afc0cf4167204be92b084feed166496250f041378e0c676ac383006a7d90ebd37d4cd486bb7c0664f834ca188e4
2 parents b901a9e + 179860e commit 16400f3

5 files changed

Lines changed: 658 additions & 21 deletions

File tree

Cargo.toml

Lines changed: 8 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -11,24 +11,29 @@ license = "MIT OR Apache-2.0"
1111
readme = "README.md"
1212

1313
[dependencies]
14-
miniscript = { version = "12", default-features = false }
14+
miniscript = { version = "12.3.5", default-features = false }
1515
bdk_coin_select = "0.4.0"
16+
rand_core = { version = "0.6.4", default-features = false }
17+
rand = { version = "0.8", optional = true }
1618

1719
[dev-dependencies]
1820
anyhow = "1"
1921
bdk_tx = { path = "." }
20-
bitcoin = { version = "0.32", features = ["rand-std"] }
22+
bitcoin = { version = "0.32", default-features = false, features = ["rand-std"] }
2123
bdk_testenv = "0.13.0"
2224
bdk_bitcoind_rpc = "0.20.0"
2325
bdk_chain = { version = "0.23.0" }
2426

2527
[features]
2628
default = ["std"]
27-
std = ["miniscript/std"]
29+
std = ["miniscript/std", "rand/std"]
2830

2931
[[example]]
3032
name = "synopsis"
3133

3234
[[example]]
3335
name = "common"
3436
crate-type = ["lib"]
37+
38+
[[example]]
39+
name = "anti_fee_sniping"

examples/anti_fee_sniping.rs

Lines changed: 150 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,150 @@
1+
#![allow(dead_code)]
2+
use bdk_testenv::{bitcoincore_rpc::RpcApi, TestEnv};
3+
use bdk_tx::{
4+
filter_unspendable_now, group_by_spk, selection_algorithm_lowest_fee_bnb, FeeStrategy, Output,
5+
PsbtParams, ScriptSource, SelectorParams,
6+
};
7+
use bitcoin::{absolute::LockTime, key::Secp256k1, Amount, FeeRate, Sequence};
8+
use miniscript::Descriptor;
9+
10+
mod common;
11+
12+
use common::Wallet;
13+
14+
fn main() -> anyhow::Result<()> {
15+
let secp = Secp256k1::new();
16+
let (external, _) = Descriptor::parse_descriptor(&secp, bdk_testenv::utils::DESCRIPTORS[0])?;
17+
let (internal, _) = Descriptor::parse_descriptor(&secp, bdk_testenv::utils::DESCRIPTORS[1])?;
18+
19+
let env = TestEnv::new()?;
20+
let genesis_hash = env.genesis_hash()?;
21+
env.mine_blocks(101, None)?;
22+
23+
let mut wallet = Wallet::new(genesis_hash, external, internal.clone())?;
24+
wallet.sync(&env)?;
25+
26+
let addr = wallet.next_address().expect("must derive address");
27+
28+
let txid1 = env.send(&addr, Amount::ONE_BTC)?;
29+
env.mine_blocks(1, None)?;
30+
wallet.sync(&env)?;
31+
println!("Received confirmed input: {}", txid1);
32+
33+
let txid2 = env.send(&addr, Amount::ONE_BTC)?;
34+
env.mine_blocks(1, None)?;
35+
wallet.sync(&env)?;
36+
println!("Received confirmed input: {}", txid2);
37+
38+
println!("Balance (confirmed): {}", wallet.balance());
39+
40+
let (tip_height, tip_time) = wallet.tip_info(env.rpc_client())?;
41+
println!("Current height: {}", tip_height);
42+
let longterm_feerate = FeeRate::from_sat_per_vb_unchecked(1);
43+
44+
let recipient_addr = env
45+
.rpc_client()
46+
.get_new_address(None, None)?
47+
.assume_checked();
48+
49+
// When anti-fee-sniping is enabled, the transaction will either use nLockTime or nSequence.
50+
//
51+
// Locktime approach is used when:
52+
// - RBF is disabled, OR
53+
// - Any input requires locktime (non-taproot, unconfirmed, or >65535 confirmations), OR
54+
// - There are no taproot inputs, OR
55+
// - Random 50/50 coin flip chose locktime
56+
//
57+
// Sequence approach is used otherwise:
58+
// - Sets tx.lock_time to ZERO
59+
// - Modifies one randomly selected taproot input's sequence
60+
//
61+
// Once the approach is selected, to reduce transaction fingerprinting,
62+
// - For nLockTime: With 10% probability, subtract a random 0-99 block offset from current height
63+
// - For nSequence: With 10% probability, subtract a random 0-99 block offset (minimum value of 1)
64+
//
65+
// Note: When locktime is used, all sequence values remain unchanged.
66+
67+
let mut locktime_count = 0;
68+
let mut sequence_count = 0;
69+
70+
for _ in 0..10 {
71+
let selection = wallet
72+
.all_candidates()
73+
.regroup(group_by_spk())
74+
.filter(filter_unspendable_now(tip_height, tip_time))
75+
.into_selection(
76+
selection_algorithm_lowest_fee_bnb(longterm_feerate, 100_000),
77+
SelectorParams::new(
78+
FeeStrategy::FeeRate(FeeRate::from_sat_per_vb_unchecked(10)),
79+
vec![Output::with_script(
80+
recipient_addr.script_pubkey(),
81+
Amount::from_sat(50_000_000),
82+
)],
83+
ScriptSource::Descriptor(Box::new(internal.at_derivation_index(0)?)),
84+
wallet.change_policy(),
85+
),
86+
)?;
87+
88+
let fallback_locktime: LockTime = LockTime::from_consensus(tip_height.to_consensus_u32());
89+
90+
let selection_inputs = selection.inputs.clone();
91+
92+
let psbt = selection.create_psbt(PsbtParams {
93+
enable_anti_fee_sniping: true,
94+
fallback_locktime,
95+
fallback_sequence: Sequence::ENABLE_RBF_NO_LOCKTIME,
96+
..Default::default()
97+
})?;
98+
99+
let tx = psbt.unsigned_tx;
100+
101+
if tx.lock_time != LockTime::ZERO {
102+
locktime_count += 1;
103+
let locktime_value = tx.lock_time.to_consensus_u32();
104+
let current_height = tip_height.to_consensus_u32();
105+
106+
let offset = current_height.saturating_sub(locktime_value);
107+
if offset > 0 {
108+
println!(
109+
"nLockTime = {} (tip height: {}, offset: -{})",
110+
locktime_value, current_height, offset
111+
);
112+
} else {
113+
println!(
114+
"nLockTime = {} (tip height: {}, no offset)",
115+
locktime_value, current_height
116+
);
117+
}
118+
} else {
119+
sequence_count += 1;
120+
121+
for (i, inp) in tx.input.iter().enumerate() {
122+
let sequence_value = inp.sequence.to_consensus_u32();
123+
124+
if (1..0xFFFFFFFD).contains(&sequence_value) {
125+
let input_confirmations = selection_inputs[i].confirmations(tip_height);
126+
let offset = input_confirmations.saturating_sub(sequence_value);
127+
128+
if offset > 0 {
129+
println!(
130+
"nSequence[{}] = {} (confirmations: {}, offset: -{})",
131+
i, sequence_value, input_confirmations, offset
132+
);
133+
} else {
134+
println!(
135+
"nSequence[{}] = {} (confirmations: {}, no offset)",
136+
i, sequence_value, input_confirmations
137+
);
138+
}
139+
140+
break;
141+
}
142+
}
143+
}
144+
}
145+
146+
println!("nLockTime approach used: {} times", locktime_count);
147+
println!("nSequence approach used: {} times", sequence_count);
148+
149+
Ok(())
150+
}

src/lib.rs

Lines changed: 2 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -1,8 +1,4 @@
11
//! `bdk_tx`
2-
3-
// FIXME: try to remove clippy "allows"
4-
#![allow(clippy::large_enum_variant)]
5-
#![allow(clippy::result_large_err)]
62
#![warn(missing_docs)]
73
#![no_std]
84

@@ -21,6 +17,7 @@ mod rbf;
2117
mod selection;
2218
mod selector;
2319
mod signer;
20+
mod utils;
2421

2522
pub use canonical_unspents::*;
2623
pub use finalizer::*;
@@ -34,6 +31,7 @@ pub use rbf::*;
3431
pub use selection::*;
3532
pub use selector::*;
3633
pub use signer::*;
34+
use utils::*;
3735

3836
#[cfg(feature = "std")]
3937
pub(crate) mod collections {

0 commit comments

Comments
 (0)