use crate::{
no_std_rand::{random_probability, random_range},
Input,
};
use alloc::vec::Vec;
use miniscript::bitcoin::{
absolute::{self, LockTime},
transaction::Version,
Sequence, Transaction,
};
use rand_core::RngCore;
#[derive(Debug, Clone, PartialEq)]
pub enum AntiFeeSnipingError {
UnsupportedVersion(Version),
UnsupportedLockTime(absolute::LockTime),
}
impl core::fmt::Display for AntiFeeSnipingError {
fn fmt(&self, f: &mut core::fmt::Formatter<'_>) -> core::fmt::Result {
match self {
Self::UnsupportedVersion(version) => write!(
f,
"anti-fee-sniping requires tx.version >= 2 (got {version})"
),
Self::UnsupportedLockTime(locktime) => write!(
f,
"anti-fee-sniping requires a height-based tx locktime (got time-based {locktime}); \
check `min_locktime` and any input CLTV requirements"
),
}
}
}
#[cfg(feature = "std")]
impl std::error::Error for AntiFeeSnipingError {}
pub(crate) fn apply_anti_fee_sniping(
tx: &mut Transaction,
inputs: &[Input],
tip_height: absolute::Height,
rng: &mut impl RngCore,
) -> Result<(), AntiFeeSnipingError> {
const MAX_RELATIVE_HEIGHT: u32 = 65_535;
const FIFTY_PERCENT_PROBABILITY_RANGE: u32 = 2;
const MIN_SEQUENCE_VALUE: u32 = 1;
const TEN_PERCENT_PROBABILITY_RANGE: u32 = 10;
const MAX_RANDOM_OFFSET: u32 = 100;
if tx.version < Version::TWO {
return Err(AntiFeeSnipingError::UnsupportedVersion(tx.version));
}
if !tx.lock_time.is_block_height() {
return Err(AntiFeeSnipingError::UnsupportedLockTime(tx.lock_time));
}
let rbf_enabled = tx.is_explicitly_rbf();
let taproot_inputs: Vec<(usize, &Input)> = tx
.input
.iter()
.enumerate()
.filter_map(|(vin, txin)| {
let input = inputs
.iter()
.find(|input| input.prev_outpoint() == txin.previous_output)?;
if input.prev_txout().script_pubkey.is_p2tr() && input.relative_timelock().is_none() {
Some((vin, input))
} else {
None
}
})
.collect();
let must_use_locktime = taproot_inputs.is_empty()
|| inputs.iter().any(|input| {
let confirmation = input.confirmations(tip_height);
confirmation == 0 || confirmation > MAX_RELATIVE_HEIGHT
});
let use_locktime = !rbf_enabled
|| must_use_locktime
|| taproot_inputs.is_empty()
|| random_probability(rng, FIFTY_PERCENT_PROBABILITY_RANGE);
if use_locktime {
let mut afs_height = tip_height.to_consensus_u32();
if random_probability(rng, TEN_PERCENT_PROBABILITY_RANGE) {
let random_offset = random_range(rng, MAX_RANDOM_OFFSET);
afs_height = afs_height.saturating_sub(random_offset);
}
let afs_locktime = LockTime::from_height(afs_height).expect("must be valid Height");
if tx.lock_time.is_implied_by(afs_locktime) {
tx.lock_time = afs_locktime;
}
} else {
let random_index = random_range(rng, taproot_inputs.len() as u32);
let (input_index, input) = taproot_inputs[random_index as usize];
let confirmation = input.confirmations(tip_height);
let mut sequence_value = confirmation;
if random_probability(rng, TEN_PERCENT_PROBABILITY_RANGE) {
let random_offset = random_range(rng, MAX_RANDOM_OFFSET);
sequence_value = sequence_value
.saturating_sub(random_offset)
.max(MIN_SEQUENCE_VALUE);
}
tx.input[input_index].sequence = Sequence(sequence_value);
}
Ok(())
}