use brk_types::{OutputType, Sats};
use crate::scale::{sats_to_bin, HistogramRaw};
const MIN_SATS: u64 = 1000;
const EXCLUDED_OUTPUT_TYPES: &[OutputType] = &[OutputType::P2TR];
const EXCLUDED_MASK: u16 = {
let mut mask = 0u16;
let mut i = 0;
while i < EXCLUDED_OUTPUT_TYPES.len() {
mask |= 1u16 << EXCLUDED_OUTPUT_TYPES[i] as u8;
i += 1;
}
mask
};
#[derive(Clone, Copy, Debug, Eq, PartialEq)]
pub struct PaymentFilter {
tx_output_fanout_cap: usize,
}
impl PaymentFilter {
pub const PRE_MODERN_TX_OUTPUT_FANOUT_CAP: usize = 100;
pub const MODERN_TX_OUTPUT_FANOUT_CAP: usize = 250;
pub const MODERN_TX_OUTPUT_FANOUT_CAP_START_HEIGHT: usize = 630_000;
pub const MODERN: Self = Self::with_fanout_cap(Self::MODERN_TX_OUTPUT_FANOUT_CAP);
const fn with_fanout_cap(tx_output_fanout_cap: usize) -> Self {
Self {
tx_output_fanout_cap,
}
}
pub const fn for_height(height: usize) -> Self {
if height < Self::MODERN_TX_OUTPUT_FANOUT_CAP_START_HEIGHT {
Self::with_fanout_cap(Self::PRE_MODERN_TX_OUTPUT_FANOUT_CAP)
} else {
Self::MODERN
}
}
#[inline(always)]
pub fn eligible_bin(sats: Sats, output_type: OutputType) -> Option<u16> {
if EXCLUDED_MASK & (1u16 << output_type as u8) != 0 {
return None;
}
if *sats < MIN_SATS || sats.is_common_round_value() {
return None;
}
sats_to_bin(sats).map(|b| b as u16)
}
#[inline]
pub fn for_each_bin(
self,
outputs: impl ExactSizeIterator<Item = (Sats, OutputType)> + Clone,
mut emit: impl FnMut(u16),
) {
if outputs.len() > self.tx_output_fanout_cap {
return;
}
if outputs.clone().any(|(_, ty)| ty == OutputType::OpReturn) {
return;
}
for (sats, ty) in outputs {
if let Some(bin) = Self::eligible_bin(sats, ty) {
emit(bin);
}
}
}
#[inline]
pub fn histogram<Outputs>(self, txs: impl IntoIterator<Item = Outputs>) -> HistogramRaw
where
Outputs: ExactSizeIterator<Item = (Sats, OutputType)> + Clone,
{
let mut hist = HistogramRaw::zeros();
for outputs in txs {
self.for_each_bin(outputs, |bin| hist.increment(bin as usize));
}
hist
}
}
#[cfg(test)]
mod tests {
use super::*;
fn payment_outputs(len: usize) -> impl ExactSizeIterator<Item = (Sats, OutputType)> + Clone {
std::iter::repeat_n((Sats::new(12_345), OutputType::P2WPKH), len)
}
fn emitted_count(height: usize, len: usize) -> usize {
let mut count = 0;
PaymentFilter::for_height(height).for_each_bin(payment_outputs(len), |_| count += 1);
count
}
#[test]
fn early_fanout_cap_is_strict() {
assert_eq!(
emitted_count(
PaymentFilter::MODERN_TX_OUTPUT_FANOUT_CAP_START_HEIGHT - 1,
PaymentFilter::PRE_MODERN_TX_OUTPUT_FANOUT_CAP,
),
PaymentFilter::PRE_MODERN_TX_OUTPUT_FANOUT_CAP
);
assert_eq!(
emitted_count(
PaymentFilter::MODERN_TX_OUTPUT_FANOUT_CAP_START_HEIGHT - 1,
PaymentFilter::PRE_MODERN_TX_OUTPUT_FANOUT_CAP + 1,
),
0
);
}
#[test]
fn modern_fanout_cap_is_relaxed_but_not_lifted() {
assert_eq!(
emitted_count(
PaymentFilter::MODERN_TX_OUTPUT_FANOUT_CAP_START_HEIGHT,
PaymentFilter::MODERN_TX_OUTPUT_FANOUT_CAP,
),
PaymentFilter::MODERN_TX_OUTPUT_FANOUT_CAP
);
assert_eq!(
emitted_count(
PaymentFilter::MODERN_TX_OUTPUT_FANOUT_CAP_START_HEIGHT,
PaymentFilter::MODERN_TX_OUTPUT_FANOUT_CAP + 1,
),
0
);
}
fn emitted_count_modern(len: usize) -> usize {
let mut count = 0;
PaymentFilter::MODERN.for_each_bin(payment_outputs(len), |_| count += 1);
count
}
#[test]
fn modern_helper_uses_modern_fanout_cap() {
assert_eq!(
emitted_count_modern(PaymentFilter::MODERN_TX_OUTPUT_FANOUT_CAP),
PaymentFilter::MODERN_TX_OUTPUT_FANOUT_CAP
);
assert_eq!(
emitted_count_modern(PaymentFilter::MODERN_TX_OUTPUT_FANOUT_CAP + 1),
0
);
}
#[test]
fn payment_histogram_drops_op_return_transaction() {
let sats = Sats::new(12_345);
let txs = vec![
vec![(sats, OutputType::P2WPKH), (sats, OutputType::P2PKH)],
vec![
(Sats::new(54_321), OutputType::OpReturn),
(sats, OutputType::P2WPKH),
],
];
let hist = PaymentFilter::MODERN.histogram(txs.into_iter().map(|tx| tx.into_iter()));
let bin = PaymentFilter::eligible_bin(sats, OutputType::P2WPKH).unwrap() as usize;
assert_eq!(hist[bin], 2);
}
#[test]
fn builds_fresh_payment_histogram() {
let sats = Sats::new(12_345);
let txs = vec![vec![
(sats, OutputType::P2WPKH),
(Sats::new(100_000_000), OutputType::P2WPKH),
]];
let hist = PaymentFilter::MODERN.histogram(txs.into_iter().map(|tx| tx.into_iter()));
let bin = PaymentFilter::eligible_bin(sats, OutputType::P2WPKH).unwrap() as usize;
assert_eq!(hist[bin], 1);
}
}