Skip to content

Commit 1098efc

Browse files
committed
feat: bip326 anti-fee sniping made compatible with new current library
1 parent ed2503a commit 1098efc

File tree

4 files changed

+159
-5
lines changed

4 files changed

+159
-5
lines changed

Cargo.toml

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -13,11 +13,11 @@ miniscript = { version = "12", default-features = false }
1313
bdk_coin_select = "0.4.0"
1414
rand_core = { version = "0.6.0", features = ["getrandom"] }
1515
bdk_chain = { version = "0.21" }
16+
bitcoin = { version = "0.32", features = ["rand-std"] }
1617

1718
[dev-dependencies]
1819
anyhow = "1"
1920
bdk_tx = { path = "." }
20-
bitcoin = { version = "0.32", features = ["rand-std"] }
2121
bdk_testenv = "0.11.1"
2222
bdk_bitcoind_rpc = "0.18.0"
2323

src/lib.rs

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -18,6 +18,7 @@ mod rbf;
1818
mod selection;
1919
mod selector;
2020
mod signer;
21+
mod utils;
2122

2223
pub use canonical_unspents::*;
2324
pub use finalizer::*;
@@ -31,6 +32,7 @@ pub use rbf::*;
3132
pub use selection::*;
3233
pub use selector::*;
3334
pub use signer::*;
35+
use utils::*;
3436

3537
pub(crate) mod collections {
3638
#![allow(unused)]

src/selection.rs

Lines changed: 33 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -6,7 +6,7 @@ use bitcoin::{absolute, transaction, Sequence};
66
use miniscript::bitcoin;
77
use miniscript::psbt::PsbtExt;
88

9-
use crate::{Finalizer, Input, Output};
9+
use crate::{apply_anti_fee_sniping, Finalizer, Input, Output};
1010

1111
const FALLBACK_SEQUENCE: bitcoin::Sequence = bitcoin::Sequence::ENABLE_LOCKTIME_NO_RBF;
1212

@@ -44,6 +44,9 @@ pub struct PsbtParams {
4444
///
4545
/// [`non_witness_utxo`]: bitcoin::psbt::Input::non_witness_utxo
4646
pub mandate_full_tx_for_segwit_v0: bool,
47+
48+
/// Whether to use BIP326 anti-fee-sniping
49+
pub enable_anti_fee_sniping: bool,
4750
}
4851

4952
impl Default for PsbtParams {
@@ -53,6 +56,7 @@ impl Default for PsbtParams {
5356
fallback_locktime: absolute::LockTime::ZERO,
5457
fallback_sequence: FALLBACK_SEQUENCE,
5558
mandate_full_tx_for_segwit_v0: true,
59+
enable_anti_fee_sniping: true,
5660
}
5761
}
5862
}
@@ -70,6 +74,10 @@ pub enum CreatePsbtError {
7074
Psbt(bitcoin::psbt::Error),
7175
/// Update psbt output with descriptor error.
7276
OutputUpdate(miniscript::psbt::OutputUpdateError),
77+
/// Invalid locktime
78+
InvalidLockTime(absolute::LockTime),
79+
/// Invalid height
80+
InvalidHeight(u32),
7381
}
7482

7583
impl core::fmt::Display for CreatePsbtError {
@@ -90,6 +98,12 @@ impl core::fmt::Display for CreatePsbtError {
9098
CreatePsbtError::OutputUpdate(output_update_error) => {
9199
Display::fmt(&output_update_error, f)
92100
}
101+
CreatePsbtError::InvalidLockTime(locktime) => {
102+
write!(f, "The locktime - {}, is invalid", locktime)
103+
}
104+
CreatePsbtError::InvalidHeight(height) => {
105+
write!(f, "The height - {}, is invalid", height)
106+
}
93107
}
94108
}
95109
}
@@ -127,7 +141,7 @@ impl Selection {
127141

128142
/// Create psbt.
129143
pub fn create_psbt(&self, params: PsbtParams) -> Result<bitcoin::Psbt, CreatePsbtError> {
130-
let mut psbt = bitcoin::Psbt::from_unsigned_tx(bitcoin::Transaction {
144+
let mut tx = bitcoin::Transaction {
131145
version: params.version,
132146
lock_time: Self::_accumulate_max_locktime(
133147
self.inputs
@@ -146,8 +160,23 @@ impl Selection {
146160
})
147161
.collect(),
148162
output: self.outputs.iter().map(|output| output.txout()).collect(),
149-
})
150-
.map_err(CreatePsbtError::Psbt)?;
163+
};
164+
165+
if params.enable_anti_fee_sniping {
166+
let rbf_enabled = params.fallback_sequence.is_rbf();
167+
let height = params
168+
.fallback_locktime
169+
.is_block_height()
170+
.then(|| params.fallback_locktime.to_consensus_u32())
171+
.ok_or(CreatePsbtError::InvalidLockTime(params.fallback_locktime))?;
172+
173+
let current_height = bitcoin::absolute::Height::from_consensus(height)
174+
.map_err(|_conversion_error| CreatePsbtError::InvalidHeight(height))?;
175+
176+
apply_anti_fee_sniping(&mut tx, &self.inputs, current_height, rbf_enabled);
177+
};
178+
179+
let mut psbt = bitcoin::Psbt::from_unsigned_tx(tx).map_err(CreatePsbtError::Psbt)?;
151180

152181
for (plan_input, psbt_input) in self.inputs.iter().zip(psbt.inputs.iter_mut()) {
153182
if let Some(finalized_psbt_input) = plan_input.psbt_input() {

src/utils.rs

Lines changed: 123 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,123 @@
1+
use crate::Input;
2+
use bitcoin::{
3+
absolute::{self, LockTime},
4+
transaction::Version,
5+
Sequence, Transaction, WitnessVersion,
6+
};
7+
use std::vec::Vec;
8+
9+
use rand_core::{OsRng, RngCore};
10+
11+
/// Applies BIP326 anti‐fee‐sniping
12+
pub fn apply_anti_fee_sniping(
13+
tx: &mut Transaction,
14+
inputs: &[Input],
15+
current_height: absolute::Height,
16+
rbf_enabled: bool,
17+
) {
18+
const MAX_SEQUENCE_VALUE: u32 = 65_535;
19+
const USE_NLOCKTIME_PROBABILITY: f64 = 0.5;
20+
const MIN_SEQUENCE_VALUE: u32 = 1;
21+
const FURTHER_BACK_PROBABILITY: f64 = 0.1;
22+
const MAX_RANDOM_OFFSET: u32 = 99;
23+
24+
tx.version = Version::TWO;
25+
26+
let taproot_inputs: Vec<_> = inputs
27+
.iter()
28+
.enumerate()
29+
.filter(|(_, input)| {
30+
matches!(
31+
input.plan().and_then(|plan| plan.witness_version()),
32+
Some(WitnessVersion::V1)
33+
)
34+
})
35+
.collect();
36+
37+
// Initialize all nsequence to indicate the requested RBF state
38+
for input in &mut tx.input {
39+
input.sequence = if rbf_enabled {
40+
Sequence(0xFFFFFFFF - 2) // 2^32 - 3
41+
} else {
42+
Sequence(0xFFFFFFFF - 1) // 2^32 - 2
43+
}
44+
}
45+
// Check always‐locktime conditions
46+
let must_use_locktime = inputs.iter().any(|input| {
47+
let confirmation = input.confirmations(current_height);
48+
confirmation == 0
49+
|| confirmation > MAX_SEQUENCE_VALUE
50+
|| !matches!(
51+
input.plan().and_then(|plan| plan.witness_version()),
52+
Some(WitnessVersion::V1)
53+
)
54+
});
55+
56+
let use_locktime = !rbf_enabled
57+
|| must_use_locktime
58+
|| taproot_inputs.is_empty()
59+
|| random_probability(USE_NLOCKTIME_PROBABILITY);
60+
61+
if use_locktime {
62+
// Use nLockTime
63+
let mut locktime = current_height.to_consensus_u32();
64+
65+
if random_probability(FURTHER_BACK_PROBABILITY) {
66+
let random_offset = random_range(0, MAX_RANDOM_OFFSET);
67+
locktime = locktime.saturating_sub(random_offset);
68+
}
69+
70+
tx.lock_time = LockTime::from_height(locktime).unwrap();
71+
} else {
72+
// Use Sequence
73+
tx.lock_time = LockTime::ZERO;
74+
75+
let input_index = random_range(0, taproot_inputs.len() as u32) as usize;
76+
77+
let (idx, input) = &taproot_inputs[input_index];
78+
79+
let confirmation = input.confirmations(current_height);
80+
81+
let mut sequence_value = confirmation;
82+
83+
if random_probability(FURTHER_BACK_PROBABILITY) {
84+
let random_offset = random_range(0, MAX_RANDOM_OFFSET);
85+
sequence_value = sequence_value
86+
.saturating_sub(random_offset)
87+
.max(MIN_SEQUENCE_VALUE);
88+
}
89+
90+
tx.input[*idx].sequence = Sequence(sequence_value);
91+
}
92+
}
93+
94+
fn random_probability(probability: f64) -> bool {
95+
debug_assert!(
96+
(0.0..=1.0).contains(&probability),
97+
"Probability must be between 0.0 and 1.0"
98+
);
99+
100+
let mut rng = OsRng;
101+
let rand_val = rng.next_u32() as f64;
102+
let max_u32 = u32::MAX as f64;
103+
(rand_val / max_u32) < probability
104+
}
105+
106+
fn random_range(min: u32, max: u32) -> u32 {
107+
if min >= max {
108+
return min;
109+
}
110+
let mut rng = OsRng;
111+
let range = max.saturating_sub(min);
112+
let threshold = u32::MAX.saturating_sub(u32::MAX % range);
113+
let min_val = min + (rng.next_u32() % (max - min));
114+
let mut r;
115+
116+
loop {
117+
r = rng.next_u32();
118+
if r < threshold {
119+
break;
120+
}
121+
}
122+
min_val.saturating_add(r % range)
123+
}

0 commit comments

Comments
 (0)