mod script_analyzer;
pub use script_analyzer::{ScriptType, detect_input_script_type};
use blvm_consensus::opcodes::*;
use blvm_consensus::segwit::Witness;
use blvm_consensus::types::{ByteString, Transaction, UtxoSet};
use script_analyzer::TransactionType;
use serde::{Deserialize, Serialize};
pub const DEFAULT_DUST_THRESHOLD: i64 = 546;
pub const DEFAULT_MIN_FEE_RATE: u64 = 1;
pub const DEFAULT_MAX_WITNESS_SIZE: usize = 1000;
pub const DEFAULT_MAX_SIZE_VALUE_RATIO: f64 = 1000.0;
#[derive(Debug, Clone, Copy, PartialEq, Eq)]
pub enum SpamFilterPreset {
Disabled,
Conservative,
Moderate,
Aggressive,
StrictInscriptions,
}
impl SpamFilterPreset {
pub fn to_config(&self) -> SpamFilterConfig {
match self {
Self::Disabled => SpamFilterConfig {
filter_ordinals: false,
filter_dust: false,
filter_brc20: false,
filter_large_witness: false,
filter_low_fee_rate: false,
filter_high_size_value_ratio: false,
filter_many_small_outputs: false,
..SpamFilterConfig::default()
},
Self::Conservative => SpamFilterConfig {
filter_ordinals: true,
filter_dust: true,
filter_brc20: true,
filter_large_witness: true,
filter_low_fee_rate: false,
filter_high_size_value_ratio: true,
filter_many_small_outputs: true,
max_witness_size: 2000, max_size_value_ratio: 2000.0, max_small_outputs: 20, ..SpamFilterConfig::default()
},
Self::Moderate => SpamFilterConfig::default(),
Self::Aggressive => SpamFilterConfig {
filter_ordinals: true,
filter_dust: true,
filter_brc20: true,
filter_large_witness: true,
filter_low_fee_rate: true, filter_high_size_value_ratio: true,
filter_many_small_outputs: true,
max_witness_size: 500, max_size_value_ratio: 500.0, max_small_outputs: 5, min_fee_rate: 2, ..SpamFilterConfig::default()
},
Self::StrictInscriptions => SpamFilterConfig {
filter_ordinals: true,
filter_dust: true,
filter_brc20: true,
filter_large_witness: true,
filter_low_fee_rate: false,
filter_high_size_value_ratio: true,
filter_many_small_outputs: true,
ordinals_strict_mode: true, max_witness_size: 1500, ..SpamFilterConfig::default()
},
}
}
}
#[derive(Debug, Clone, PartialEq, Eq)]
pub enum SpamType {
Ordinals,
Dust,
BRC20,
LargeWitness,
LowFeeRate,
HighSizeValueRatio,
ManySmallOutputs,
NotSpam,
}
#[derive(Debug, Clone)]
pub struct WitnessSizeThresholds {
pub normal_single_sig: usize,
pub normal_multi_sig: usize,
pub normal_p2wsh: usize,
pub suspicious_threshold: usize,
pub definitely_spam: usize,
}
impl Default for WitnessSizeThresholds {
fn default() -> Self {
Self {
normal_single_sig: 200,
normal_multi_sig: 500,
normal_p2wsh: 800,
suspicious_threshold: 1000,
definitely_spam: 2000,
}
}
}
#[derive(Debug, Clone)]
pub struct WitnessElementAnalysis {
pub total_size: usize,
pub element_count: usize,
pub large_elements: usize,
pub medium_elements: usize,
pub small_elements: usize,
pub suspicious_pattern: bool,
}
#[derive(Debug, Clone)]
pub struct SpamFilterConfig {
pub filter_ordinals: bool,
pub filter_dust: bool,
pub filter_brc20: bool,
pub filter_large_witness: bool,
pub filter_low_fee_rate: bool,
pub filter_high_size_value_ratio: bool,
pub filter_many_small_outputs: bool,
pub dust_threshold: i64,
pub min_output_value: i64,
pub min_fee_rate: u64,
pub max_witness_size: usize,
pub max_size_value_ratio: f64,
pub max_small_outputs: usize,
pub use_adaptive_thresholds: bool,
pub adaptive_thresholds: WitnessSizeThresholds,
pub filter_taproot_spam: bool,
pub max_taproot_control_size: usize,
pub reject_taproot_annexes: bool,
pub filter_large_total_witness: bool,
pub max_total_witness_size: usize,
pub use_improved_envelope_detection: bool,
pub use_json_validation_brc20: bool,
pub require_utxo_for_fee_rate: bool,
pub min_fee_rate_large_tx: u64,
pub large_tx_threshold_bytes: usize,
pub ordinals_strict_mode: bool,
}
impl Default for SpamFilterConfig {
fn default() -> Self {
Self {
filter_ordinals: true,
filter_dust: true,
filter_brc20: true,
filter_large_witness: true,
filter_low_fee_rate: false, filter_high_size_value_ratio: true,
filter_many_small_outputs: true,
dust_threshold: DEFAULT_DUST_THRESHOLD,
min_output_value: DEFAULT_DUST_THRESHOLD,
min_fee_rate: DEFAULT_MIN_FEE_RATE,
max_witness_size: DEFAULT_MAX_WITNESS_SIZE,
max_size_value_ratio: DEFAULT_MAX_SIZE_VALUE_RATIO,
max_small_outputs: 10,
use_adaptive_thresholds: true, adaptive_thresholds: WitnessSizeThresholds::default(),
filter_taproot_spam: true,
max_taproot_control_size: 289, reject_taproot_annexes: true,
filter_large_total_witness: false, max_total_witness_size: 5000,
use_improved_envelope_detection: true,
use_json_validation_brc20: true,
require_utxo_for_fee_rate: false, min_fee_rate_large_tx: 2, large_tx_threshold_bytes: 1000, ordinals_strict_mode: true, }
}
}
#[derive(Debug, Clone)]
pub struct SpamFilterResult {
pub is_spam: bool,
pub spam_type: SpamType,
pub detected_types: Vec<SpamType>,
}
#[derive(Clone)]
pub struct SpamFilter {
config: SpamFilterConfig,
#[cfg(feature = "production")]
#[allow(dead_code)]
pub(crate) script_type_cache: std::sync::Arc<std::sync::RwLock<lru::LruCache<u64, bool>>>,
}
impl SpamFilter {
pub fn new() -> Self {
Self {
config: SpamFilterConfig::default(),
#[cfg(feature = "production")]
script_type_cache: std::sync::Arc::new(std::sync::RwLock::new(lru::LruCache::new(
std::num::NonZeroUsize::new(10_000).unwrap(),
))),
}
}
pub fn with_config(config: SpamFilterConfig) -> Self {
Self {
config,
#[cfg(feature = "production")]
script_type_cache: std::sync::Arc::new(std::sync::RwLock::new(lru::LruCache::new(
std::num::NonZeroUsize::new(10_000).unwrap(),
))),
}
}
pub fn with_preset(preset: SpamFilterPreset) -> Self {
Self::with_config(preset.to_config())
}
pub fn is_spam(&self, tx: &Transaction) -> SpamFilterResult {
self.is_spam_with_witness(tx, None, None)
}
pub fn is_spam_with_witness(
&self,
tx: &Transaction,
witnesses: Option<&[Witness]>,
utxo_set: Option<&UtxoSet>,
) -> SpamFilterResult {
let mut detected_types = Vec::new();
if self.config.filter_ordinals && self.detect_ordinals(tx, witnesses, utxo_set) {
detected_types.push(SpamType::Ordinals);
}
if self.config.filter_dust && self.detect_dust(tx) {
detected_types.push(SpamType::Dust);
}
if self.config.filter_brc20 && self.detect_brc20(tx) {
detected_types.push(SpamType::BRC20);
}
if self.config.filter_large_witness && self.detect_large_witness(tx, witnesses, utxo_set) {
detected_types.push(SpamType::LargeWitness);
}
if self.config.filter_large_total_witness && self.detect_large_total_witness(witnesses) {
detected_types.push(SpamType::LargeWitness);
}
if self.config.filter_low_fee_rate && self.detect_low_fee_rate(tx, witnesses, utxo_set) {
detected_types.push(SpamType::LowFeeRate);
}
if self.config.filter_high_size_value_ratio
&& self.detect_high_size_value_ratio(tx, witnesses)
{
detected_types.push(SpamType::HighSizeValueRatio);
}
if self.config.filter_many_small_outputs && self.detect_many_small_outputs(tx) {
detected_types.push(SpamType::ManySmallOutputs);
}
let is_spam = !detected_types.is_empty();
let spam_type = detected_types.first().cloned().unwrap_or(SpamType::NotSpam);
SpamFilterResult {
is_spam,
spam_type,
detected_types,
}
}
pub fn filter_transaction(&self, tx: &Transaction) -> Option<Transaction> {
let result = self.is_spam(tx);
if result.is_spam {
None } else {
Some(tx.clone()) }
}
fn detect_ordinals(
&self,
tx: &Transaction,
witnesses: Option<&[Witness]>,
utxo_set: Option<&UtxoSet>,
) -> bool {
for output in &tx.outputs {
if self.has_ordinal_pattern(&output.script_pubkey) {
return true;
}
}
for input in &tx.inputs {
if self.has_envelope_pattern(&input.script_sig) {
return true;
}
}
if let Some(witnesses) = witnesses {
for (i, witness) in witnesses.iter().enumerate() {
if i >= tx.inputs.len() {
break;
}
if self.config.filter_taproot_spam {
for output in &tx.outputs {
if self.is_taproot_output(&output.script_pubkey)
&& self.detect_taproot_spam(output, witness)
{
return true;
}
}
}
if self.has_envelope_in_witness(witness) {
return true;
}
if !self.config.ordinals_strict_mode {
if self.config.use_adaptive_thresholds {
if self.has_large_witness_stack_adaptive(witness, tx, i, utxo_set) {
return true;
}
} else if self.has_large_witness_stack(witness) {
return true;
}
if self.has_witness_data_pattern(witness) {
return true;
}
}
}
}
false
}
fn has_envelope_in_witness(&self, witness: &Witness) -> bool {
for element in witness {
if element.len() >= 4 && element[0] == OP_0 && element[1] == OP_IF {
if self.config.use_improved_envelope_detection {
if element.iter().skip(2).any(|&b| b == OP_ENDIF) {
return true;
}
} else {
return true;
}
}
}
false
}
fn is_taproot_output(&self, script_pubkey: &ByteString) -> bool {
script_pubkey.len() == 34 && script_pubkey[0] == OP_1 && script_pubkey[1] == PUSH_32_BYTES
}
fn detect_taproot_spam(
&self,
output: &blvm_consensus::types::TransactionOutput,
witness: &Witness,
) -> bool {
if !self.is_taproot_output(&output.script_pubkey) {
return false;
}
if self.config.reject_taproot_annexes {
if let Some(last) = witness.last() {
if !last.is_empty() && last[0] == blvm_consensus::opcodes::OP_RESERVED {
return true;
}
}
}
if witness.len() >= 2 {
if let Some(control_block) = witness.last() {
if control_block.len() > self.config.max_taproot_control_size {
return true;
}
}
}
false
}
fn has_large_witness_stack(&self, witness: &Witness) -> bool {
let total_size = self.calculate_witness_size(witness);
total_size > self.config.max_witness_size
}
fn has_large_witness_stack_adaptive(
&self,
witness: &Witness,
tx: &Transaction,
input_index: usize,
utxo_set: Option<&UtxoSet>,
) -> bool {
let total_size = self.calculate_witness_size(witness);
if !self.config.use_adaptive_thresholds {
return total_size > self.config.max_witness_size;
}
let threshold =
if let Some(script_type) = detect_script_type_for_input(tx, input_index, utxo_set) {
script_type.recommended_threshold()
} else {
self.config.max_witness_size
};
total_size > threshold
}
fn analyze_witness_elements(&self, witness: &Witness) -> WitnessElementAnalysis {
let total_size = self.calculate_witness_size(witness);
let element_count = witness.len();
let mut large_elements = 0;
let mut medium_elements = 0;
let mut small_elements = 0;
for element in witness {
if element.len() > 200 {
large_elements += 1;
} else if element.len() >= 100 {
medium_elements += 1;
} else {
small_elements += 1;
}
}
let suspicious_pattern = medium_elements >= 10;
WitnessElementAnalysis {
total_size,
element_count,
large_elements,
medium_elements,
small_elements,
suspicious_pattern,
}
}
fn calculate_witness_size(&self, witness: &Witness) -> usize {
let mut size = 1;
for element in witness {
size += if element.len() <= VARINT_1BYTE_MAX as usize {
1
} else if element.len() <= 0xffff {
3 } else if element.len() <= 0xffffffff {
5 } else {
9 };
size += element.len();
}
size
}
fn has_witness_data_pattern(&self, witness: &Witness) -> bool {
if witness.is_empty() {
return false;
}
for element in witness {
if element.len() > 200 {
if element.len() != 64 && (element.is_empty() || element[0] != DER_SIGNATURE_PREFIX)
{
return true;
}
}
}
let large_elements = witness.iter().filter(|elem| elem.len() > 100).count();
if large_elements >= 3 {
return true;
}
let analysis = self.analyze_witness_elements(witness);
if analysis.suspicious_pattern {
return true;
}
false
}
fn has_ordinal_pattern(&self, script: &ByteString) -> bool {
if script.is_empty() {
return false;
}
if script[0] == OP_RETURN && script.len() > 80 {
return true;
}
if self.has_envelope_pattern(script) {
return true;
}
false
}
fn has_envelope_pattern(&self, script: &ByteString) -> bool {
if script.len() < 4 {
return false;
}
if script[0] == OP_0 && script[1] == OP_IF {
if self.config.use_improved_envelope_detection {
if script.iter().skip(2).any(|&b| b == OP_ENDIF) {
return true;
}
} else {
return true;
}
}
false
}
fn detect_dust(&self, tx: &Transaction) -> bool {
let mut all_dust = true;
for output in &tx.outputs {
if output.value >= self.config.dust_threshold {
all_dust = false;
break;
}
}
all_dust && !tx.outputs.is_empty()
}
fn detect_large_witness(
&self,
tx: &Transaction,
witnesses: Option<&[Witness]>,
utxo_set: Option<&UtxoSet>,
) -> bool {
if let Some(witnesses) = witnesses {
for (i, witness) in witnesses.iter().enumerate() {
if self.config.use_adaptive_thresholds {
if self.has_large_witness_stack_adaptive(witness, tx, i, utxo_set) {
return true;
}
} else if self.has_large_witness_stack(witness) {
return true;
}
}
}
false
}
fn detect_low_fee_rate(
&self,
tx: &Transaction,
witnesses: Option<&[Witness]>,
utxo_set: Option<&UtxoSet>,
) -> bool {
let tx_size = self.estimate_transaction_size_with_witness(tx, witnesses);
if self.config.require_utxo_for_fee_rate && utxo_set.is_none() {
return true; }
let fee_rate = if let Some(utxo_set) = utxo_set {
self.calculate_fee_rate_accurate(tx, utxo_set, tx_size)
} else {
return false;
};
let threshold = if tx_size > self.config.large_tx_threshold_bytes {
self.config.min_fee_rate_large_tx
} else {
self.config.min_fee_rate
};
fee_rate < threshold
}
fn calculate_fee_rate_accurate(
&self,
tx: &Transaction,
utxo_set: &UtxoSet,
tx_size: usize,
) -> u64 {
if tx_size == 0 {
return 0;
}
let mut input_total = 0u64;
for input in &tx.inputs {
if let Some(utxo) = utxo_set.get(&input.prevout) {
input_total += utxo.value as u64;
}
}
let output_total: u64 = tx.outputs.iter().map(|out| out.value as u64).sum();
let fee = input_total.saturating_sub(output_total);
if tx_size > 0 { fee / tx_size as u64 } else { 0 }
}
#[allow(dead_code)]
fn calculate_fee_rate_heuristic(&self, tx: &Transaction, tx_size: usize) -> u64 {
if tx_size == 0 {
return 0;
}
let total_output_value: i64 = tx.outputs.iter().map(|out| out.value).sum();
if tx_size > 1000 && total_output_value < 10000 {
1000u64.saturating_div(tx_size as u64)
} else {
self.config.min_fee_rate
}
}
fn detect_large_total_witness(&self, witnesses: Option<&[Witness]>) -> bool {
if !self.config.filter_large_total_witness {
return false; }
if let Some(witnesses) = witnesses {
let total_size: usize = witnesses
.iter()
.map(|w| self.calculate_witness_size(w))
.sum();
total_size > self.config.max_total_witness_size
} else {
false
}
}
fn detect_high_size_value_ratio(
&self,
tx: &Transaction,
witnesses: Option<&[Witness]>,
) -> bool {
let tx_size = self.estimate_transaction_size_with_witness(tx, witnesses) as f64;
let total_output_value: f64 = tx.outputs.iter().map(|out| out.value as f64).sum();
if total_output_value <= 0.0 {
return tx_size > 1000.0;
}
let ratio = tx_size / total_output_value;
let threshold = if self.config.use_adaptive_thresholds {
let tx_type = TransactionType::detect(tx);
tx_type.recommended_size_value_ratio()
} else {
self.config.max_size_value_ratio
};
ratio > threshold
}
fn detect_many_small_outputs(&self, tx: &Transaction) -> bool {
let small_output_count = tx
.outputs
.iter()
.filter(|out| out.value < self.config.dust_threshold)
.count();
small_output_count > self.config.max_small_outputs
}
fn estimate_transaction_size_with_witness(
&self,
tx: &Transaction,
witnesses: Option<&[Witness]>,
) -> usize {
let base_size = estimate_transaction_size(tx) as usize;
if let Some(witnesses) = witnesses {
let witness_size: usize = witnesses
.iter()
.map(|witness| {
let mut size = 1;
for element in witness {
size += 1; size += element.len();
}
size
})
.sum();
let has_witness = witness_size > 0;
if has_witness {
base_size + 2 + witness_size
} else {
base_size
}
} else {
base_size
}
}
fn detect_brc20(&self, tx: &Transaction) -> bool {
for output in &tx.outputs {
if self.has_brc20_pattern(&output.script_pubkey) {
return true;
}
}
false
}
fn has_brc20_pattern(&self, script: &ByteString) -> bool {
if script.len() < 20 {
return false;
}
if script[0] != OP_RETURN {
return false;
}
let data = &script[1..];
let script_str = match String::from_utf8(data.to_vec()) {
Ok(s) => s,
Err(_) => {
return self.has_brc20_pattern_simple(data);
}
};
if self.config.use_json_validation_brc20 {
self.has_brc20_pattern_json(&script_str)
} else {
self.has_brc20_pattern_simple(data)
}
}
fn has_brc20_pattern_json(&self, json_str: &str) -> bool {
let cleaned: String = json_str.chars().filter(|c| !c.is_whitespace()).collect();
if let Ok(json_value) = serde_json::from_str::<serde_json::Value>(&cleaned) {
if let Some(obj) = json_value.as_object() {
if let Some(protocol) = obj.get("p") {
if protocol.as_str() == Some("brc-20") {
if let Some(op) = obj.get("op") {
if let Some(op_str) = op.as_str() {
return matches!(op_str, "mint" | "transfer" | "deploy");
}
}
}
}
}
}
if let Ok(json_value) = serde_json::from_str::<serde_json::Value>(json_str) {
if let Some(obj) = json_value.as_object() {
if let Some(protocol) = obj.get("p") {
if protocol.as_str() == Some("brc-20") {
if let Some(op) = obj.get("op") {
if let Some(op_str) = op.as_str() {
return matches!(op_str, "mint" | "transfer" | "deploy");
}
}
}
}
}
}
false
}
fn has_brc20_pattern_simple(&self, data: &[u8]) -> bool {
if let Ok(script_str) = String::from_utf8(data.to_vec()) {
let lower = script_str.to_lowercase();
lower.contains("brc-20")
|| lower.contains("\"p\":\"brc-20\"")
|| lower.contains("op\":\"mint")
|| lower.contains("op\":\"transfer")
|| lower.contains("op\":\"deploy")
} else {
let pattern = b"brc-20";
let pattern_lower = b"BRC-20";
data.windows(pattern.len())
.any(|window| window == pattern || window == pattern_lower)
}
}
pub fn filter_block(&self, transactions: &[Transaction]) -> (Vec<Transaction>, SpamSummary) {
self.filter_block_with_witness(transactions, None)
}
pub fn filter_block_with_witness(
&self,
transactions: &[Transaction],
witnesses: Option<&[Vec<Witness>]>,
) -> (Vec<Transaction>, SpamSummary) {
let mut filtered_txs = Vec::new();
let mut filtered_count = 0u32;
let mut filtered_size = 0u64;
let mut spam_breakdown = SpamBreakdown::default();
for (i, tx) in transactions.iter().enumerate() {
let tx_witnesses = witnesses.and_then(|w| w.get(i));
let result = if let Some(tx_witnesses) = tx_witnesses {
self.is_spam_with_witness(tx, Some(tx_witnesses), None)
} else {
self.is_spam(tx)
};
if result.is_spam {
filtered_count += 1;
let tx_size = if let Some(tx_witnesses) = tx_witnesses {
self.estimate_transaction_size_with_witness(tx, Some(tx_witnesses)) as u64
} else {
estimate_transaction_size(tx)
};
filtered_size += tx_size;
for spam_type in &result.detected_types {
match spam_type {
SpamType::Ordinals => spam_breakdown.ordinals += 1,
SpamType::Dust => spam_breakdown.dust += 1,
SpamType::BRC20 => spam_breakdown.brc20 += 1,
SpamType::LargeWitness => spam_breakdown.ordinals += 1, SpamType::LowFeeRate => spam_breakdown.dust += 1, SpamType::HighSizeValueRatio => spam_breakdown.ordinals += 1, SpamType::ManySmallOutputs => spam_breakdown.dust += 1, SpamType::NotSpam => {}
}
}
} else {
filtered_txs.push(tx.clone());
}
}
let summary = SpamSummary {
filtered_count,
filtered_size,
by_type: spam_breakdown,
};
(filtered_txs, summary)
}
}
impl Default for SpamFilter {
fn default() -> Self {
Self::new()
}
}
#[derive(Debug, Clone, Default)]
pub struct SpamSummary {
pub filtered_count: u32,
pub filtered_size: u64,
pub by_type: SpamBreakdown,
}
#[derive(Debug, Clone, Default)]
pub struct SpamBreakdown {
pub ordinals: u32,
pub inscriptions: u32,
pub dust: u32,
pub brc20: u32,
}
fn estimate_transaction_size(tx: &Transaction) -> u64 {
blvm_consensus::transaction::calculate_transaction_size(tx) as u64
}
fn detect_script_type_for_input(
tx: &Transaction,
input_index: usize,
utxo_set: Option<&UtxoSet>,
) -> Option<ScriptType> {
if input_index >= tx.inputs.len() {
return None;
}
if let Some(utxo_set) = utxo_set {
if let Some(utxo) = utxo_set.get(&tx.inputs[input_index].prevout) {
let spk: ByteString = utxo.script_pubkey.as_ref().to_vec();
let script_type = ScriptType::detect(&spk);
if script_type != ScriptType::Unknown {
return Some(script_type);
}
}
}
detect_input_script_type(&tx.inputs[input_index].script_sig)
}
#[derive(Debug, Clone, PartialEq, Serialize, Deserialize)]
pub struct WitnessSizeThresholdsSerializable {
#[serde(default = "default_normal_single_sig")]
pub normal_single_sig: usize,
#[serde(default = "default_normal_multi_sig")]
pub normal_multi_sig: usize,
#[serde(default = "default_normal_p2wsh")]
pub normal_p2wsh: usize,
#[serde(default = "default_suspicious_threshold")]
pub suspicious_threshold: usize,
#[serde(default = "default_definitely_spam")]
pub definitely_spam: usize,
}
impl From<WitnessSizeThresholdsSerializable> for WitnessSizeThresholds {
fn from(serializable: WitnessSizeThresholdsSerializable) -> Self {
WitnessSizeThresholds {
normal_single_sig: serializable.normal_single_sig,
normal_multi_sig: serializable.normal_multi_sig,
normal_p2wsh: serializable.normal_p2wsh,
suspicious_threshold: serializable.suspicious_threshold,
definitely_spam: serializable.definitely_spam,
}
}
}
impl From<WitnessSizeThresholds> for WitnessSizeThresholdsSerializable {
fn from(thresholds: WitnessSizeThresholds) -> Self {
WitnessSizeThresholdsSerializable {
normal_single_sig: thresholds.normal_single_sig,
normal_multi_sig: thresholds.normal_multi_sig,
normal_p2wsh: thresholds.normal_p2wsh,
suspicious_threshold: thresholds.suspicious_threshold,
definitely_spam: thresholds.definitely_spam,
}
}
}
#[derive(Debug, Clone, PartialEq, Serialize, Deserialize)]
pub struct SpamFilterConfigSerializable {
#[serde(default = "default_true")]
pub filter_ordinals: bool,
#[serde(default = "default_true")]
pub filter_dust: bool,
#[serde(default = "default_true")]
pub filter_brc20: bool,
#[serde(default = "default_true")]
pub filter_large_witness: bool,
#[serde(default = "default_false")]
pub filter_low_fee_rate: bool,
#[serde(default = "default_true")]
pub filter_high_size_value_ratio: bool,
#[serde(default = "default_true")]
pub filter_many_small_outputs: bool,
#[serde(default = "default_dust_threshold")]
pub dust_threshold: i64,
#[serde(default = "default_dust_threshold")]
pub min_output_value: i64,
#[serde(default = "default_min_fee_rate")]
pub min_fee_rate: u64,
#[serde(default = "default_max_witness_size")]
pub max_witness_size: usize,
#[serde(default = "default_max_size_value_ratio")]
pub max_size_value_ratio: f64,
#[serde(default = "default_max_small_outputs")]
pub max_small_outputs: usize,
#[serde(default = "default_true")]
pub use_adaptive_thresholds: bool,
#[serde(default = "default_adaptive_thresholds")]
pub adaptive_thresholds: WitnessSizeThresholdsSerializable,
#[serde(default = "default_true")]
pub filter_taproot_spam: bool,
#[serde(default = "default_max_taproot_control_size")]
pub max_taproot_control_size: usize,
#[serde(default = "default_true")]
pub reject_taproot_annexes: bool,
#[serde(default = "default_false")]
pub filter_large_total_witness: bool,
#[serde(default = "default_max_total_witness_size")]
pub max_total_witness_size: usize,
#[serde(default = "default_true")]
pub use_improved_envelope_detection: bool,
#[serde(default = "default_true")]
pub use_json_validation_brc20: bool,
#[serde(default = "default_false")]
pub require_utxo_for_fee_rate: bool,
#[serde(default = "default_min_fee_rate_large_tx")]
pub min_fee_rate_large_tx: u64,
#[serde(default = "default_large_tx_threshold_bytes")]
pub large_tx_threshold_bytes: usize,
#[serde(default = "default_true")]
pub ordinals_strict_mode: bool,
}
fn default_true() -> bool {
true
}
fn default_false() -> bool {
false
}
fn default_dust_threshold() -> i64 {
546
}
fn default_min_fee_rate() -> u64 {
1
}
fn default_max_witness_size() -> usize {
1000
}
fn default_max_size_value_ratio() -> f64 {
1000.0
}
fn default_max_small_outputs() -> usize {
10
}
fn default_max_taproot_control_size() -> usize {
289
}
fn default_max_total_witness_size() -> usize {
5000
}
fn default_min_fee_rate_large_tx() -> u64 {
2
}
fn default_large_tx_threshold_bytes() -> usize {
1000
}
fn default_normal_single_sig() -> usize {
200
}
fn default_normal_multi_sig() -> usize {
500
}
fn default_normal_p2wsh() -> usize {
800
}
fn default_suspicious_threshold() -> usize {
1000
}
fn default_definitely_spam() -> usize {
2000
}
fn default_adaptive_thresholds() -> WitnessSizeThresholdsSerializable {
WitnessSizeThresholdsSerializable {
normal_single_sig: 200,
normal_multi_sig: 500,
normal_p2wsh: 800,
suspicious_threshold: 1000,
definitely_spam: 2000,
}
}
impl Default for SpamFilterConfigSerializable {
fn default() -> Self {
Self {
filter_ordinals: default_true(),
filter_dust: default_true(),
filter_brc20: default_true(),
filter_large_witness: default_true(),
filter_low_fee_rate: default_false(),
filter_high_size_value_ratio: default_true(),
filter_many_small_outputs: default_true(),
dust_threshold: default_dust_threshold(),
min_output_value: default_dust_threshold(),
min_fee_rate: default_min_fee_rate(),
max_witness_size: default_max_witness_size(),
max_size_value_ratio: default_max_size_value_ratio(),
max_small_outputs: default_max_small_outputs(),
use_adaptive_thresholds: default_true(),
adaptive_thresholds: default_adaptive_thresholds(),
filter_taproot_spam: default_true(),
max_taproot_control_size: default_max_taproot_control_size(),
reject_taproot_annexes: default_true(),
filter_large_total_witness: default_false(),
max_total_witness_size: default_max_total_witness_size(),
use_improved_envelope_detection: default_true(),
use_json_validation_brc20: default_true(),
require_utxo_for_fee_rate: default_false(),
min_fee_rate_large_tx: default_min_fee_rate_large_tx(),
large_tx_threshold_bytes: default_large_tx_threshold_bytes(),
ordinals_strict_mode: default_true(),
}
}
}
impl From<SpamFilterConfigSerializable> for SpamFilterConfig {
fn from(serializable: SpamFilterConfigSerializable) -> Self {
SpamFilterConfig {
filter_ordinals: serializable.filter_ordinals,
filter_dust: serializable.filter_dust,
filter_brc20: serializable.filter_brc20,
filter_large_witness: serializable.filter_large_witness,
filter_low_fee_rate: serializable.filter_low_fee_rate,
filter_high_size_value_ratio: serializable.filter_high_size_value_ratio,
filter_many_small_outputs: serializable.filter_many_small_outputs,
dust_threshold: serializable.dust_threshold,
min_output_value: serializable.min_output_value,
min_fee_rate: serializable.min_fee_rate,
max_witness_size: serializable.max_witness_size,
max_size_value_ratio: serializable.max_size_value_ratio,
max_small_outputs: serializable.max_small_outputs,
use_adaptive_thresholds: serializable.use_adaptive_thresholds,
adaptive_thresholds: serializable.adaptive_thresholds.into(),
filter_taproot_spam: serializable.filter_taproot_spam,
max_taproot_control_size: serializable.max_taproot_control_size,
reject_taproot_annexes: serializable.reject_taproot_annexes,
filter_large_total_witness: serializable.filter_large_total_witness,
max_total_witness_size: serializable.max_total_witness_size,
use_improved_envelope_detection: serializable.use_improved_envelope_detection,
use_json_validation_brc20: serializable.use_json_validation_brc20,
require_utxo_for_fee_rate: serializable.require_utxo_for_fee_rate,
min_fee_rate_large_tx: serializable.min_fee_rate_large_tx,
large_tx_threshold_bytes: serializable.large_tx_threshold_bytes,
ordinals_strict_mode: serializable.ordinals_strict_mode,
}
}
}
impl From<SpamFilterConfig> for SpamFilterConfigSerializable {
fn from(config: SpamFilterConfig) -> Self {
SpamFilterConfigSerializable {
filter_ordinals: config.filter_ordinals,
filter_dust: config.filter_dust,
filter_brc20: config.filter_brc20,
filter_large_witness: config.filter_large_witness,
filter_low_fee_rate: config.filter_low_fee_rate,
filter_high_size_value_ratio: config.filter_high_size_value_ratio,
filter_many_small_outputs: config.filter_many_small_outputs,
dust_threshold: config.dust_threshold,
min_output_value: config.min_output_value,
min_fee_rate: config.min_fee_rate,
max_witness_size: config.max_witness_size,
max_size_value_ratio: config.max_size_value_ratio,
max_small_outputs: config.max_small_outputs,
use_adaptive_thresholds: config.use_adaptive_thresholds,
adaptive_thresholds: config.adaptive_thresholds.into(),
filter_taproot_spam: config.filter_taproot_spam,
max_taproot_control_size: config.max_taproot_control_size,
reject_taproot_annexes: config.reject_taproot_annexes,
filter_large_total_witness: config.filter_large_total_witness,
max_total_witness_size: config.max_total_witness_size,
use_improved_envelope_detection: config.use_improved_envelope_detection,
use_json_validation_brc20: config.use_json_validation_brc20,
require_utxo_for_fee_rate: config.require_utxo_for_fee_rate,
min_fee_rate_large_tx: config.min_fee_rate_large_tx,
large_tx_threshold_bytes: config.large_tx_threshold_bytes,
ordinals_strict_mode: config.ordinals_strict_mode,
}
}
}