Skip to main content

blvm_protocol/spam_filter/
mod.rs

1//! Spam Filtering for UTXO Commitments
2//!
3//! Implements spam detection and filtering for Bitcoin transactions:
4//! - Ordinals/Inscriptions detection
5//! - Dust output filtering
6//! - BRC-20 pattern detection
7//! - Adaptive witness size thresholds based on script type
8//!
9//! This filter enables 40-60% bandwidth savings by skipping spam transactions
10//! during ongoing sync while maintaining consensus correctness.
11//!
12//! **Critical Design Note**: Spam filtering applies to OUTPUTS only, not entire transactions.
13//! When a spam transaction is processed:
14//! - Its spent INPUTS are still removed from the UTXO tree (maintains consistency)
15//! - Its OUTPUTS are filtered out (bandwidth savings)
16//!
17//! This ensures the UTXO tree remains consistent even when spam transactions spend
18//! non-spam inputs. The `process_filtered_block` function in `initial_sync.rs` implements
19//! this correctly by processing all transactions but only adding non-spam outputs.
20
21mod script_analyzer;
22
23pub use script_analyzer::{ScriptType, detect_input_script_type};
24
25use blvm_consensus::opcodes::*;
26use blvm_consensus::segwit::Witness;
27use blvm_consensus::types::{ByteString, Transaction, UtxoSet};
28use script_analyzer::TransactionType;
29use serde::{Deserialize, Serialize};
30
31/// Default dust threshold (546 satoshis = 0.00000546 BTC)
32pub const DEFAULT_DUST_THRESHOLD: i64 = 546;
33
34/// Default minimum fee rate threshold (satoshis per vbyte)
35/// Transactions with fee rate below this are suspicious
36pub const DEFAULT_MIN_FEE_RATE: u64 = 1;
37
38/// Default maximum witness size (bytes) - larger witness stacks suggest data embedding
39pub const DEFAULT_MAX_WITNESS_SIZE: usize = 1000;
40
41/// Default maximum transaction size to value ratio
42/// Non-monetary transactions often have very large size relative to value transferred
43pub const DEFAULT_MAX_SIZE_VALUE_RATIO: f64 = 1000.0; // bytes per satoshi
44
45/// Spam filter preset configurations
46///
47/// Presets provide easy-to-use configurations for common use cases.
48#[derive(Debug, Clone, Copy, PartialEq, Eq)]
49pub enum SpamFilterPreset {
50    /// No spam filtering (all transactions pass)
51    Disabled,
52    /// Lenient filtering, minimal false positives
53    /// - Higher thresholds
54    /// - Only obvious spam patterns
55    Conservative,
56    /// Balanced filtering (default)
57    /// - Standard thresholds
58    /// - Comprehensive detection
59    Moderate,
60    /// Strong filtering, may have false positives
61    /// - Lower thresholds
62    /// - Aggressive detection
63    Aggressive,
64    /// Research/strict: Ordinals = envelope/pattern only, LargeWitness separate
65    /// - ordinals_strict_mode: true (no large-witness heuristic in Ordinals)
66    /// - All spam categories enabled but properly separated
67    /// - Minimizes false positives (Miniscript, vaults not misclassified)
68    StrictInscriptions,
69}
70
71impl SpamFilterPreset {
72    /// Convert preset to configuration
73    pub fn to_config(&self) -> SpamFilterConfig {
74        match self {
75            Self::Disabled => SpamFilterConfig {
76                filter_ordinals: false,
77                filter_dust: false,
78                filter_brc20: false,
79                filter_large_witness: false,
80                filter_low_fee_rate: false,
81                filter_high_size_value_ratio: false,
82                filter_many_small_outputs: false,
83                ..SpamFilterConfig::default()
84            },
85            Self::Conservative => SpamFilterConfig {
86                filter_ordinals: true,
87                filter_dust: true,
88                filter_brc20: true,
89                filter_large_witness: true,
90                filter_low_fee_rate: false,
91                filter_high_size_value_ratio: true,
92                filter_many_small_outputs: true,
93                max_witness_size: 2000,       // Higher threshold
94                max_size_value_ratio: 2000.0, // Higher ratio
95                max_small_outputs: 20,        // More lenient
96                ..SpamFilterConfig::default()
97            },
98            Self::Moderate => SpamFilterConfig::default(),
99            Self::Aggressive => SpamFilterConfig {
100                filter_ordinals: true,
101                filter_dust: true,
102                filter_brc20: true,
103                filter_large_witness: true,
104                filter_low_fee_rate: true, // Enable fee rate filtering
105                filter_high_size_value_ratio: true,
106                filter_many_small_outputs: true,
107                max_witness_size: 500,       // Lower threshold
108                max_size_value_ratio: 500.0, // Lower ratio
109                max_small_outputs: 5,        // More strict
110                min_fee_rate: 2,             // Higher fee rate requirement
111                ..SpamFilterConfig::default()
112            },
113            Self::StrictInscriptions => SpamFilterConfig {
114                filter_ordinals: true,
115                filter_dust: true,
116                filter_brc20: true,
117                filter_large_witness: true,
118                filter_low_fee_rate: false,
119                filter_high_size_value_ratio: true,
120                filter_many_small_outputs: true,
121                ordinals_strict_mode: true, // Envelope/pattern only; LargeWitness separate
122                max_witness_size: 1500,     // Slightly higher; LargeWitness is separate category
123                ..SpamFilterConfig::default()
124            },
125        }
126    }
127}
128
129/// Spam classification for a transaction
130#[derive(Debug, Clone, PartialEq, Eq)]
131pub enum SpamType {
132    /// Ordinals/Inscriptions (data embedded in witness or script)
133    Ordinals,
134    /// Dust outputs (< threshold satoshis)
135    Dust,
136    /// BRC-20 token transactions
137    BRC20,
138    /// Large witness data (suggests data embedding in witness)
139    LargeWitness,
140    /// Low fee rate (suggests non-monetary use)
141    LowFeeRate,
142    /// High size-to-value ratio (large transaction, small value transfer)
143    HighSizeValueRatio,
144    /// Many small outputs (common in token/ordinal distribution)
145    ManySmallOutputs,
146    /// Not spam (valid transaction)
147    NotSpam,
148}
149
150/// Adaptive witness size thresholds based on script type
151///
152/// These thresholds will be refined with real-world data collection.
153/// For now, they use conservative estimates based on typical transaction patterns.
154#[derive(Debug, Clone)]
155pub struct WitnessSizeThresholds {
156    /// Normal single-sig witness size (95th percentile)
157    pub normal_single_sig: usize,
158    /// Normal multi-sig witness size (95th percentile for 2-of-3)
159    pub normal_multi_sig: usize,
160    /// Normal P2WSH witness size (95th percentile)
161    pub normal_p2wsh: usize,
162    /// Suspicious threshold (current default)
163    pub suspicious_threshold: usize,
164    /// Definitely spam threshold (99.9th percentile)
165    pub definitely_spam: usize,
166}
167
168impl Default for WitnessSizeThresholds {
169    fn default() -> Self {
170        // These will be populated from real-world data collection
171        // For now, use conservative estimates
172        Self {
173            normal_single_sig: 200,
174            normal_multi_sig: 500,
175            normal_p2wsh: 800,
176            suspicious_threshold: 1000,
177            definitely_spam: 2000,
178        }
179    }
180}
181
182/// Witness element analysis result
183#[derive(Debug, Clone)]
184pub struct WitnessElementAnalysis {
185    /// Total witness size (including varint overhead)
186    pub total_size: usize,
187    /// Number of witness elements
188    pub element_count: usize,
189    /// Number of large elements (> 200 bytes)
190    pub large_elements: usize,
191    /// Number of medium elements (100-200 bytes)
192    pub medium_elements: usize,
193    /// Number of small elements (< 100 bytes)
194    pub small_elements: usize,
195    /// Whether pattern suggests data splitting (many medium elements)
196    pub suspicious_pattern: bool,
197}
198
199/// Spam filter configuration
200#[derive(Debug, Clone)]
201pub struct SpamFilterConfig {
202    /// Filter Ordinals/Inscriptions
203    pub filter_ordinals: bool,
204    /// Filter dust outputs
205    pub filter_dust: bool,
206    /// Filter BRC-20 patterns
207    pub filter_brc20: bool,
208    /// Filter transactions with large witness data
209    pub filter_large_witness: bool,
210    /// Filter transactions with low fee rate
211    pub filter_low_fee_rate: bool,
212    /// Filter transactions with high size-to-value ratio
213    pub filter_high_size_value_ratio: bool,
214    /// Filter transactions with many small outputs
215    pub filter_many_small_outputs: bool,
216    /// Minimum output value to consider non-dust (satoshis)
217    pub dust_threshold: i64,
218    /// Minimum output value to include in filtered blocks (satoshis)
219    pub min_output_value: i64,
220    /// Minimum fee rate threshold (satoshis per vbyte)
221    pub min_fee_rate: u64,
222    /// Maximum witness size before flagging (bytes)
223    /// Note: This is now adaptive based on script type when `use_adaptive_thresholds` is enabled
224    pub max_witness_size: usize,
225    /// Maximum size-to-value ratio (bytes per satoshi)
226    pub max_size_value_ratio: f64,
227    /// Maximum number of small outputs before flagging
228    pub max_small_outputs: usize,
229
230    // NEW: Adaptive thresholds
231    /// Use adaptive witness size thresholds based on script type
232    /// Default: true (enables data-driven thresholds)
233    pub use_adaptive_thresholds: bool,
234    /// Adaptive threshold configuration
235    pub adaptive_thresholds: WitnessSizeThresholds,
236
237    // NEW: Taproot-specific options
238    /// Filter Taproot-specific spam patterns (control blocks, annexes)
239    /// Default: true
240    pub filter_taproot_spam: bool,
241    /// Maximum Taproot control block size (bytes)
242    /// Control blocks: 33 bytes base + 32 bytes per tree level
243    /// BIP-110 limits to 257 bytes (depth 7), we use 289 bytes (depth 8) for policy
244    /// Default: 289 bytes (allows depth 8, more lenient than BIP-110)
245    pub max_taproot_control_size: usize,
246    /// Reject Taproot annexes (last witness element starting with OP_RESERVED)
247    /// Default: true
248    pub reject_taproot_annexes: bool,
249
250    // NEW: Total witness size check
251    /// Filter transactions with large total witness size across all inputs
252    /// Default: false (disabled by default, can be aggressive)
253    pub filter_large_total_witness: bool,
254    /// Maximum total witness size across all inputs (bytes)
255    /// Default: 5000 bytes
256    pub max_total_witness_size: usize,
257
258    // NEW: Enhanced detection options
259    /// Use improved envelope protocol detection (checks for OP_ENDIF)
260    /// Default: true
261    pub use_improved_envelope_detection: bool,
262    /// Use JSON validation for BRC-20 detection (requires serde_json)
263    /// Default: true (if serde_json available)
264    pub use_json_validation_brc20: bool,
265
266    // NEW: Fee rate calculation options
267    /// Require UTXO set for fee rate calculation (reject if unavailable)
268    /// If false, falls back to heuristic when UTXO set unavailable
269    /// Default: false (use heuristic fallback)
270    pub require_utxo_for_fee_rate: bool,
271    /// Minimum fee rate for large transactions (satoshis per vbyte)
272    /// Transactions larger than large_tx_threshold_bytes require this fee rate
273    /// Default: 2 sat/vB (higher than standard 1 sat/vB)
274    pub min_fee_rate_large_tx: u64,
275    /// Large transaction threshold (bytes)
276    /// Transactions larger than this require min_fee_rate_large_tx
277    /// Default: 1000 bytes
278    pub large_tx_threshold_bytes: usize,
279
280    /// Ordinals detection mode: strict (envelope/pattern only) vs legacy (includes large witness heuristics)
281    /// When true: Ordinals = envelope protocol, ordinal output patterns, Taproot annex/control-block.
282    ///            Large witness alone is NOT Ordinals (handled by LargeWitness category).
283    /// When false: Legacy behavior - large witness and witness data patterns also trigger Ordinals.
284    /// Default: true (minimize false positives; Miniscript/vaults no longer misclassified as Ordinals)
285    pub ordinals_strict_mode: bool,
286}
287
288impl Default for SpamFilterConfig {
289    fn default() -> Self {
290        Self {
291            filter_ordinals: true,
292            filter_dust: true,
293            filter_brc20: true,
294            filter_large_witness: true,
295            filter_low_fee_rate: false, // Disabled by default (too aggressive)
296            filter_high_size_value_ratio: true,
297            filter_many_small_outputs: true,
298            dust_threshold: DEFAULT_DUST_THRESHOLD,
299            min_output_value: DEFAULT_DUST_THRESHOLD,
300            min_fee_rate: DEFAULT_MIN_FEE_RATE,
301            max_witness_size: DEFAULT_MAX_WITNESS_SIZE,
302            max_size_value_ratio: DEFAULT_MAX_SIZE_VALUE_RATIO,
303            max_small_outputs: 10, // Flag if more than 10 small outputs
304
305            // NEW: Adaptive thresholds
306            use_adaptive_thresholds: true, // Enable by default
307            adaptive_thresholds: WitnessSizeThresholds::default(),
308
309            // NEW defaults
310            filter_taproot_spam: true,
311            max_taproot_control_size: 289, // 33 + 32*8 (depth 8)
312            reject_taproot_annexes: true,
313            filter_large_total_witness: false, // Disabled by default (can be aggressive)
314            max_total_witness_size: 5000,
315            use_improved_envelope_detection: true,
316            use_json_validation_brc20: true,
317            require_utxo_for_fee_rate: false, // When true, reject if UTXO set missing; else skip fee check
318            min_fee_rate_large_tx: 2,         // 2 sat/vB
319            large_tx_threshold_bytes: 1000,   // 1 KB
320            ordinals_strict_mode: true,       // Envelope/pattern only; LargeWitness is separate
321        }
322    }
323}
324
325/// Spam filter result
326#[derive(Debug, Clone)]
327pub struct SpamFilterResult {
328    /// Whether transaction is spam
329    pub is_spam: bool,
330    /// Primary spam type detected
331    pub spam_type: SpamType,
332    /// All detected spam types (transaction may match multiple)
333    pub detected_types: Vec<SpamType>,
334}
335
336/// Spam filter implementation
337#[derive(Clone)]
338pub struct SpamFilter {
339    config: SpamFilterConfig,
340    /// Reserved for script-type LRU; wired incrementally in hot paths.
341    #[cfg(feature = "production")]
342    #[allow(dead_code)]
343    pub(crate) script_type_cache: std::sync::Arc<std::sync::RwLock<lru::LruCache<u64, bool>>>,
344}
345
346impl SpamFilter {
347    /// Create a new spam filter with default configuration
348    pub fn new() -> Self {
349        Self {
350            config: SpamFilterConfig::default(),
351            #[cfg(feature = "production")]
352            script_type_cache: std::sync::Arc::new(std::sync::RwLock::new(lru::LruCache::new(
353                std::num::NonZeroUsize::new(10_000).unwrap(),
354            ))),
355        }
356    }
357
358    /// Create a new spam filter with custom configuration
359    pub fn with_config(config: SpamFilterConfig) -> Self {
360        Self {
361            config,
362            #[cfg(feature = "production")]
363            script_type_cache: std::sync::Arc::new(std::sync::RwLock::new(lru::LruCache::new(
364                std::num::NonZeroUsize::new(10_000).unwrap(),
365            ))),
366        }
367    }
368
369    /// Create a new spam filter with a preset configuration
370    ///
371    /// Presets provide easy-to-use configurations for common use cases:
372    /// - `Disabled`: No spam filtering
373    /// - `Conservative`: Lenient filtering, minimal false positives
374    /// - `Moderate`: Balanced filtering (default)
375    /// - `Aggressive`: Strong filtering, may have false positives
376    pub fn with_preset(preset: SpamFilterPreset) -> Self {
377        Self::with_config(preset.to_config())
378    }
379
380    /// Check if a transaction is spam (without witness data)
381    ///
382    /// This is the backward-compatible method. For better detection, use `is_spam_with_witness`.
383    pub fn is_spam(&self, tx: &Transaction) -> SpamFilterResult {
384        self.is_spam_with_witness(tx, None, None)
385    }
386
387    /// Check if a transaction is spam (with optional witness data and UTXO set)
388    ///
389    /// Witness data is required for detecting Taproot/SegWit-based Ordinals.
390    /// UTXO set is optional but improves fee rate calculation accuracy.
391    /// If witness data is not provided, detection will be less accurate.
392    pub fn is_spam_with_witness(
393        &self,
394        tx: &Transaction,
395        witnesses: Option<&[Witness]>,
396        utxo_set: Option<&UtxoSet>,
397    ) -> SpamFilterResult {
398        let mut detected_types = Vec::new();
399
400        // Check for Ordinals/Inscriptions (now with witness data support)
401        if self.config.filter_ordinals && self.detect_ordinals(tx, witnesses, utxo_set) {
402            detected_types.push(SpamType::Ordinals);
403        }
404
405        // Check for dust outputs
406        if self.config.filter_dust && self.detect_dust(tx) {
407            detected_types.push(SpamType::Dust);
408        }
409
410        // Check for BRC-20 patterns
411        if self.config.filter_brc20 && self.detect_brc20(tx) {
412            detected_types.push(SpamType::BRC20);
413        }
414
415        // Check for large witness data (now with adaptive thresholds)
416        if self.config.filter_large_witness && self.detect_large_witness(tx, witnesses, utxo_set) {
417            detected_types.push(SpamType::LargeWitness);
418        }
419
420        // Check for large total witness size (across all inputs)
421        if self.config.filter_large_total_witness && self.detect_large_total_witness(witnesses) {
422            detected_types.push(SpamType::LargeWitness);
423        }
424
425        // Check for low fee rate (requires fee calculation)
426        if self.config.filter_low_fee_rate && self.detect_low_fee_rate(tx, witnesses, utxo_set) {
427            detected_types.push(SpamType::LowFeeRate);
428        }
429
430        // Check for high size-to-value ratio
431        if self.config.filter_high_size_value_ratio
432            && self.detect_high_size_value_ratio(tx, witnesses)
433        {
434            detected_types.push(SpamType::HighSizeValueRatio);
435        }
436
437        // Check for many small outputs
438        if self.config.filter_many_small_outputs && self.detect_many_small_outputs(tx) {
439            detected_types.push(SpamType::ManySmallOutputs);
440        }
441
442        let is_spam = !detected_types.is_empty();
443        let spam_type = detected_types.first().cloned().unwrap_or(SpamType::NotSpam);
444
445        SpamFilterResult {
446            is_spam,
447            spam_type,
448            detected_types,
449        }
450    }
451
452    /// Filter a transaction based on spam detection
453    ///
454    /// Returns `Some(tx)` if transaction should be included (not spam),
455    /// or `None` if transaction should be filtered (spam).
456    pub fn filter_transaction(&self, tx: &Transaction) -> Option<Transaction> {
457        let result = self.is_spam(tx);
458        if result.is_spam {
459            None // Filter out spam
460        } else {
461            Some(tx.clone()) // Include non-spam
462        }
463    }
464    /// Detect Ordinals/Inscriptions in transaction
465    ///
466    /// Ordinals typically embed data in:
467    /// - Witness scripts (SegWit v0 or Taproot) - PRIMARY METHOD
468    /// - Script pubkey (OP_RETURN or data push)
469    /// - Envelope protocol patterns
470    fn detect_ordinals(
471        &self,
472        tx: &Transaction,
473        witnesses: Option<&[Witness]>,
474        utxo_set: Option<&UtxoSet>,
475    ) -> bool {
476        // Check outputs for OP_RETURN or data pushes (common Ordinals pattern)
477        for output in &tx.outputs {
478            if self.has_ordinal_pattern(&output.script_pubkey) {
479                return true;
480            }
481        }
482
483        // Check inputs for envelope protocol in scriptSig
484        for input in &tx.inputs {
485            if self.has_envelope_pattern(&input.script_sig) {
486                return true;
487            }
488        }
489
490        // Check witness data (PRIMARY METHOD for Taproot/SegWit Ordinals)
491        if let Some(witnesses) = witnesses {
492            for (i, witness) in witnesses.iter().enumerate() {
493                if i >= tx.inputs.len() {
494                    break;
495                }
496
497                // Check for Taproot-specific spam patterns (annex, oversized control block)
498                if self.config.filter_taproot_spam {
499                    for output in &tx.outputs {
500                        if self.is_taproot_output(&output.script_pubkey)
501                            && self.detect_taproot_spam(output, witness)
502                        {
503                            return true;
504                        }
505                    }
506                }
507
508                // Envelope protocol in witness (inscription format: OP_0 OP_IF ... OP_ENDIF)
509                if self.has_envelope_in_witness(witness) {
510                    return true;
511                }
512
513                // Legacy mode: large witness and data patterns also trigger Ordinals
514                // Strict mode: LargeWitness is handled separately by detect_large_witness
515                if !self.config.ordinals_strict_mode {
516                    if self.config.use_adaptive_thresholds {
517                        if self.has_large_witness_stack_adaptive(witness, tx, i, utxo_set) {
518                            return true;
519                        }
520                    } else if self.has_large_witness_stack(witness) {
521                        return true;
522                    }
523                    if self.has_witness_data_pattern(witness) {
524                        return true;
525                    }
526                }
527            }
528        }
529
530        false
531    }
532
533    /// Check if witness contains envelope protocol (OP_0 OP_IF ... OP_ENDIF)
534    /// Inscriptions embed data using this pattern in Taproot script-path witness.
535    fn has_envelope_in_witness(&self, witness: &Witness) -> bool {
536        for element in witness {
537            if element.len() >= 4 && element[0] == OP_0 && element[1] == OP_IF {
538                if self.config.use_improved_envelope_detection {
539                    if element.iter().skip(2).any(|&b| b == OP_ENDIF) {
540                        return true;
541                    }
542                } else {
543                    return true;
544                }
545            }
546        }
547        false
548    }
549
550    /// Check if output is Taproot (P2TR)
551    ///
552    /// P2TR format: OP_1 + PUSH_32_BYTES + 32-byte x-only pubkey = 34 bytes
553    fn is_taproot_output(&self, script_pubkey: &ByteString) -> bool {
554        // P2TR: OP_1 + PUSH_32_BYTES + 32-byte x-only pubkey = 34 bytes
555        script_pubkey.len() == 34 && script_pubkey[0] == OP_1 && script_pubkey[1] == PUSH_32_BYTES
556    }
557
558    /// Detect Taproot-specific spam patterns
559    ///
560    /// Checks for:
561    /// - Taproot annexes (last witness element starting with OP_RESERVED)
562    /// - Large control blocks (script path spends with deep trees)
563    fn detect_taproot_spam(
564        &self,
565        output: &blvm_consensus::types::TransactionOutput,
566        witness: &Witness,
567    ) -> bool {
568        if !self.is_taproot_output(&output.script_pubkey) {
569            return false;
570        }
571
572        // Check for annex (last witness element starting with OP_RESERVED)
573        // BIP-341: Annex is the last witness element if it starts with 0x50
574        if self.config.reject_taproot_annexes {
575            if let Some(last) = witness.last() {
576                if !last.is_empty() && last[0] == blvm_consensus::opcodes::OP_RESERVED {
577                    // Annex detected - BIP-110 invalidates these
578                    return true;
579                }
580            }
581        }
582
583        // Check for large control blocks (script path spends)
584        // Control blocks are typically the last element in Taproot script path spends
585        // Format: 33 + 32*n bytes (where n is tree depth)
586        // Large control blocks suggest deep trees (potential data embedding)
587        if witness.len() >= 2 {
588            // Script path spend: script + control block + witness items
589            // Control block is typically the last element
590            if let Some(control_block) = witness.last() {
591                // Control block: 33 bytes base + 32 bytes per tree level
592                // TAPROOT_CONTROL_BASE_SIZE = 33, TAPROOT_CONTROL_NODE_SIZE = 32
593                // BIP-110 limits to TAPROOT_CONTROL_MAX_SIZE_REDUCED (257 bytes, depth 7)
594                // For policy, we use a configurable threshold (289 bytes, depth 8)
595                if control_block.len() > self.config.max_taproot_control_size {
596                    return true;
597                }
598            }
599        }
600
601        false
602    }
603
604    /// Check if witness stack is suspiciously large (suggests data embedding)
605    ///
606    /// Uses adaptive thresholds based on script type if enabled.
607    fn has_large_witness_stack(&self, witness: &Witness) -> bool {
608        let total_size = self.calculate_witness_size(witness);
609        total_size > self.config.max_witness_size
610    }
611
612    /// Check if witness stack is suspiciously large using adaptive thresholds
613    ///
614    /// This method uses script type detection to apply appropriate thresholds.
615    /// Falls back to fixed threshold if adaptive thresholds are disabled or script type cannot be determined.
616    fn has_large_witness_stack_adaptive(
617        &self,
618        witness: &Witness,
619        tx: &Transaction,
620        input_index: usize,
621        utxo_set: Option<&UtxoSet>,
622    ) -> bool {
623        let total_size = self.calculate_witness_size(witness);
624
625        // If adaptive thresholds disabled, use fixed threshold
626        if !self.config.use_adaptive_thresholds {
627            return total_size > self.config.max_witness_size;
628        }
629
630        let threshold =
631            if let Some(script_type) = detect_script_type_for_input(tx, input_index, utxo_set) {
632                script_type.recommended_threshold()
633            } else {
634                self.config.max_witness_size
635            };
636
637        total_size > threshold
638    }
639
640    /// Analyze witness elements for suspicious patterns
641    ///
642    /// Detects data splitting patterns (many medium-sized elements).
643    fn analyze_witness_elements(&self, witness: &Witness) -> WitnessElementAnalysis {
644        let total_size = self.calculate_witness_size(witness);
645        let element_count = witness.len();
646
647        let mut large_elements = 0;
648        let mut medium_elements = 0;
649        let mut small_elements = 0;
650
651        for element in witness {
652            if element.len() > 200 {
653                large_elements += 1;
654            } else if element.len() >= 100 {
655                medium_elements += 1;
656            } else {
657                small_elements += 1;
658            }
659        }
660
661        // Suspicious pattern: many medium elements (suggests data splitting)
662        let suspicious_pattern = medium_elements >= 10;
663
664        WitnessElementAnalysis {
665            total_size,
666            element_count,
667            large_elements,
668            medium_elements,
669            small_elements,
670            suspicious_pattern,
671        }
672    }
673
674    /// Calculate accurate witness size including varint overhead
675    ///
676    /// Witness size includes:
677    /// - Stack count varint (1 byte typically for small stacks)
678    /// - For each element: length varint (1-9 bytes) + element data
679    ///
680    /// This matches the actual serialized size of witness data in Bitcoin transactions.
681    fn calculate_witness_size(&self, witness: &Witness) -> usize {
682        // Stack count varint (typically 1 byte for small stacks)
683        let mut size = 1;
684
685        // Each element: length varint + element data
686        for element in witness {
687            // Varint encoding: 1 byte for <128, 2 for <16384, etc.
688            // Bitcoin varint encoding: values < 0xfd use 1 byte, larger values use prefix + data
689            // For witness element lengths, we use compact size encoding:
690            // - < 0xfd: 1 byte
691            // - 0xfd-0xffff: 0xfd prefix (1 byte) + 2 bytes data
692            // - 0x10000-0xffffffff: 0xfe prefix (1 byte) + 4 bytes data
693            // - > 0xffffffff: 0xff prefix (1 byte) + 8 bytes data
694            size += if element.len() <= VARINT_1BYTE_MAX as usize {
695                1
696            } else if element.len() <= 0xffff {
697                3 // VARINT_2BYTE_PREFIX + 2 bytes
698            } else if element.len() <= 0xffffffff {
699                5 // VARINT_4BYTE_PREFIX + 4 bytes
700            } else {
701                9 // VARINT_8BYTE_PREFIX + 8 bytes
702            };
703            size += element.len();
704        }
705
706        size
707    }
708
709    /// Check if witness contains data patterns (non-signature data)
710    fn has_witness_data_pattern(&self, witness: &Witness) -> bool {
711        if witness.is_empty() {
712            return false;
713        }
714
715        // Check for very large witness elements (>520 bytes is max for signatures)
716        // Elements larger than typical signature size suggest data embedding
717        for element in witness {
718            // Typical signatures are 71-73 bytes (DER-encoded) or 64 bytes (Schnorr)
719            // Witness elements >200 bytes are suspicious for data embedding
720            if element.len() > 200 {
721                // Check if it looks like data (not a signature)
722                // Signatures typically start with 0x30 (DER) or are exactly 64 bytes (Schnorr)
723                if element.len() != 64 && (element.is_empty() || element[0] != DER_SIGNATURE_PREFIX)
724                {
725                    // Likely data embedding
726                    return true;
727                }
728            }
729        }
730
731        // Check for multiple large elements (suggests data chunks)
732        let large_elements = witness.iter().filter(|elem| elem.len() > 100).count();
733        if large_elements >= 3 {
734            return true;
735        }
736
737        // Check for suspicious pattern (many medium elements - data splitting)
738        let analysis = self.analyze_witness_elements(witness);
739        if analysis.suspicious_pattern {
740            return true;
741        }
742
743        false
744    }
745
746    /// Check if script has Ordinals pattern
747    ///
748    /// Ordinals typically use:
749    /// - OP_RETURN followed by data (>80 bytes)
750    /// - Envelope protocol (OP_0 OP_IF ... OP_ENDIF) in output/scriptSig
751    fn has_ordinal_pattern(&self, script: &ByteString) -> bool {
752        if script.is_empty() {
753            return false;
754        }
755
756        // OP_RETURN >80 bytes (BIP-110 limit is 83; larger suggests data embedding)
757        if script[0] == OP_RETURN && script.len() > 80 {
758            return true;
759        }
760
761        // Envelope protocol in output or scriptSig
762        if self.has_envelope_pattern(script) {
763            return true;
764        }
765
766        false
767    }
768
769    /// Check if script has envelope protocol pattern
770    fn has_envelope_pattern(&self, script: &ByteString) -> bool {
771        // Envelope protocol: OP_FALSE OP_IF ... OP_ENDIF
772        if script.len() < 4 {
773            return false;
774        }
775
776        // Check for OP_FALSE OP_IF pattern (common in inscriptions)
777        if script[0] == OP_0 && script[1] == OP_IF {
778            if self.config.use_improved_envelope_detection {
779                // Improved: Verify OP_ENDIF exists later in script
780                // Envelope protocol: OP_FALSE OP_IF ... OP_ENDIF
781                if script.iter().skip(2).any(|&b| b == OP_ENDIF) {
782                    return true;
783                }
784            } else {
785                // Original simple check (backward compatibility)
786                return true;
787            }
788        }
789
790        false
791    }
792
793    /// Detect dust outputs
794    ///
795    /// Dust outputs are outputs with value below threshold (default: 546 satoshis).
796    fn detect_dust(&self, tx: &Transaction) -> bool {
797        // Check if all outputs are below threshold
798        let mut all_dust = true;
799
800        for output in &tx.outputs {
801            if output.value >= self.config.dust_threshold {
802                all_dust = false;
803                break;
804            }
805        }
806
807        all_dust && !tx.outputs.is_empty()
808    }
809
810    /// Detect transactions with large witness data
811    ///
812    /// Large witness stacks often indicate data embedding (Ordinals, inscriptions).
813    /// Now uses adaptive thresholds based on script type.
814    fn detect_large_witness(
815        &self,
816        tx: &Transaction,
817        witnesses: Option<&[Witness]>,
818        utxo_set: Option<&UtxoSet>,
819    ) -> bool {
820        if let Some(witnesses) = witnesses {
821            for (i, witness) in witnesses.iter().enumerate() {
822                // Use adaptive thresholds if enabled
823                if self.config.use_adaptive_thresholds {
824                    if self.has_large_witness_stack_adaptive(witness, tx, i, utxo_set) {
825                        return true;
826                    }
827                } else if self.has_large_witness_stack(witness) {
828                    return true;
829                }
830            }
831        }
832        false
833    }
834
835    /// Detect transactions with low fee rate
836    ///
837    /// Non-monetary transactions often pay minimal fees relative to size.
838    /// Now accepts optional UTXO set for accurate fee calculation.
839    fn detect_low_fee_rate(
840        &self,
841        tx: &Transaction,
842        witnesses: Option<&[Witness]>,
843        utxo_set: Option<&UtxoSet>,
844    ) -> bool {
845        let tx_size = self.estimate_transaction_size_with_witness(tx, witnesses);
846
847        // If require_utxo_for_fee_rate is true and UTXO set unavailable, reject
848        if self.config.require_utxo_for_fee_rate && utxo_set.is_none() {
849            // Cannot calculate accurate fee rate, reject if strict mode enabled
850            return true; // Reject as spam (conservative)
851        }
852
853        // Calculate fee rate
854        let fee_rate = if let Some(utxo_set) = utxo_set {
855            self.calculate_fee_rate_accurate(tx, utxo_set, tx_size)
856        } else {
857            // Without UTXO inputs we cannot compute fee rate; skip to avoid false negatives
858            // from the old min_fee_rate heuristic (REV-P-21).
859            return false;
860        };
861
862        // Check against threshold (use large tx threshold if applicable)
863        let threshold = if tx_size > self.config.large_tx_threshold_bytes {
864            self.config.min_fee_rate_large_tx
865        } else {
866            self.config.min_fee_rate
867        };
868
869        fee_rate < threshold
870    }
871
872    /// Calculate fee rate accurately using UTXO set
873    fn calculate_fee_rate_accurate(
874        &self,
875        tx: &Transaction,
876        utxo_set: &UtxoSet,
877        tx_size: usize,
878    ) -> u64 {
879        if tx_size == 0 {
880            return 0;
881        }
882
883        // Calculate actual fee
884        let mut input_total = 0u64;
885        for input in &tx.inputs {
886            if let Some(utxo) = utxo_set.get(&input.prevout) {
887                input_total += utxo.value as u64;
888            }
889        }
890
891        let output_total: u64 = tx.outputs.iter().map(|out| out.value as u64).sum();
892        let fee = input_total.saturating_sub(output_total);
893
894        // Fee rate in satoshis per vbyte
895        if tx_size > 0 { fee / tx_size as u64 } else { 0 }
896    }
897
898    /// Calculate fee rate using heuristics (fallback)
899    #[allow(dead_code)]
900    fn calculate_fee_rate_heuristic(&self, tx: &Transaction, tx_size: usize) -> u64 {
901        if tx_size == 0 {
902            return 0;
903        }
904
905        let total_output_value: i64 = tx.outputs.iter().map(|out| out.value).sum();
906
907        // Heuristic: large transactions with small output value likely have low fee rate
908        if tx_size > 1000 && total_output_value < 10000 {
909            // Assume minimal fee (1000 sats) for large transactions
910            1000u64.saturating_div(tx_size as u64)
911        } else {
912            // For other transactions, assume reasonable fee rate
913            // This is conservative - may have false negatives
914            self.config.min_fee_rate
915        }
916    }
917
918    /// Detect transactions with large total witness size across all inputs
919    fn detect_large_total_witness(&self, witnesses: Option<&[Witness]>) -> bool {
920        if !self.config.filter_large_total_witness {
921            return false; // Feature disabled
922        }
923
924        if let Some(witnesses) = witnesses {
925            let total_size: usize = witnesses
926                .iter()
927                .map(|w| self.calculate_witness_size(w))
928                .sum();
929
930            total_size > self.config.max_total_witness_size
931        } else {
932            false
933        }
934    }
935
936    /// Detect transactions with high size-to-value ratio
937    ///
938    /// Non-monetary transactions often have very large size relative to value transferred.
939    /// Now uses transaction type detection to adjust thresholds for legitimate transactions
940    /// (consolidations, CoinJoins) that legitimately have high ratios.
941    fn detect_high_size_value_ratio(
942        &self,
943        tx: &Transaction,
944        witnesses: Option<&[Witness]>,
945    ) -> bool {
946        let tx_size = self.estimate_transaction_size_with_witness(tx, witnesses) as f64;
947        let total_output_value: f64 = tx.outputs.iter().map(|out| out.value as f64).sum();
948
949        // Avoid division by zero
950        if total_output_value <= 0.0 {
951            // Transaction with zero outputs is suspicious
952            return tx_size > 1000.0;
953        }
954
955        let ratio = tx_size / total_output_value;
956
957        // Use transaction type to adjust threshold
958        let threshold = if self.config.use_adaptive_thresholds {
959            let tx_type = TransactionType::detect(tx);
960            tx_type.recommended_size_value_ratio()
961        } else {
962            self.config.max_size_value_ratio
963        };
964
965        ratio > threshold
966    }
967
968    /// Detect transactions with many small outputs
969    ///
970    /// Token distributions and Ordinal transfers often create many small outputs.
971    fn detect_many_small_outputs(&self, tx: &Transaction) -> bool {
972        let small_output_count = tx
973            .outputs
974            .iter()
975            .filter(|out| out.value < self.config.dust_threshold)
976            .count();
977
978        small_output_count > self.config.max_small_outputs
979    }
980
981    /// Estimate transaction size including witness data
982    fn estimate_transaction_size_with_witness(
983        &self,
984        tx: &Transaction,
985        witnesses: Option<&[Witness]>,
986    ) -> usize {
987        // Base transaction size (non-witness)
988        let base_size = estimate_transaction_size(tx) as usize;
989
990        // Add witness size if available
991        if let Some(witnesses) = witnesses {
992            let witness_size: usize = witnesses
993                .iter()
994                .map(|witness| {
995                    // Witness stack count (varint, ~1 byte)
996                    let mut size = 1;
997                    // Each witness element: length (varint, ~1 byte) + element data
998                    for element in witness {
999                        size += 1; // varint for length
1000                        size += element.len();
1001                    }
1002                    size
1003                })
1004                .sum();
1005
1006            // SegWit marker and flag (2 bytes)
1007            let has_witness = witness_size > 0;
1008            if has_witness {
1009                base_size + 2 + witness_size
1010            } else {
1011                base_size
1012            }
1013        } else {
1014            base_size
1015        }
1016    }
1017
1018    /// Detect BRC-20 token transactions
1019    ///
1020    /// BRC-20 transactions typically have:
1021    /// - OP_RETURN outputs with JSON data
1022    /// - Specific JSON patterns (mint, transfer, deploy)
1023    fn detect_brc20(&self, tx: &Transaction) -> bool {
1024        // Check outputs for OP_RETURN with JSON-like data
1025        for output in &tx.outputs {
1026            if self.has_brc20_pattern(&output.script_pubkey) {
1027                return true;
1028            }
1029        }
1030
1031        false
1032    }
1033
1034    /// Check if script has BRC-20 pattern
1035    ///
1036    /// BRC-20 transactions use OP_RETURN with JSON:
1037    /// - {"p":"brc-20","op":"mint",...}
1038    /// - {"p":"brc-20","op":"transfer",...}
1039    /// - {"p":"brc-20","op":"deploy",...}
1040    fn has_brc20_pattern(&self, script: &ByteString) -> bool {
1041        if script.len() < 20 {
1042            return false;
1043        }
1044
1045        // Check for OP_RETURN
1046        if script[0] != OP_RETURN {
1047            return false;
1048        }
1049
1050        // Extract data after OP_RETURN
1051        let data = &script[1..];
1052
1053        // Try to decode as UTF-8
1054        let script_str = match String::from_utf8(data.to_vec()) {
1055            Ok(s) => s,
1056            Err(_) => {
1057                // Not valid UTF-8, use simple pattern matching
1058                return self.has_brc20_pattern_simple(data);
1059            }
1060        };
1061
1062        // Use JSON validation if enabled
1063        if self.config.use_json_validation_brc20 {
1064            self.has_brc20_pattern_json(&script_str)
1065        } else {
1066            // Fallback to simple string matching
1067            self.has_brc20_pattern_simple(data)
1068        }
1069    }
1070
1071    /// Check for BRC-20 pattern using JSON validation
1072    fn has_brc20_pattern_json(&self, json_str: &str) -> bool {
1073        // Remove whitespace for more robust matching
1074        let cleaned: String = json_str.chars().filter(|c| !c.is_whitespace()).collect();
1075
1076        // Try to parse as JSON
1077        if let Ok(json_value) = serde_json::from_str::<serde_json::Value>(&cleaned) {
1078            // Check if it's a valid BRC-20 transaction
1079            if let Some(obj) = json_value.as_object() {
1080                // Check for protocol field: "p": "brc-20"
1081                if let Some(protocol) = obj.get("p") {
1082                    if protocol.as_str() == Some("brc-20") {
1083                        // Check for operation field: "op": "mint" | "transfer" | "deploy"
1084                        if let Some(op) = obj.get("op") {
1085                            if let Some(op_str) = op.as_str() {
1086                                return matches!(op_str, "mint" | "transfer" | "deploy");
1087                            }
1088                        }
1089                    }
1090                }
1091            }
1092        }
1093
1094        // Fallback: try parsing original string (with whitespace)
1095        if let Ok(json_value) = serde_json::from_str::<serde_json::Value>(json_str) {
1096            if let Some(obj) = json_value.as_object() {
1097                if let Some(protocol) = obj.get("p") {
1098                    if protocol.as_str() == Some("brc-20") {
1099                        if let Some(op) = obj.get("op") {
1100                            if let Some(op_str) = op.as_str() {
1101                                return matches!(op_str, "mint" | "transfer" | "deploy");
1102                            }
1103                        }
1104                    }
1105                }
1106            }
1107        }
1108
1109        false
1110    }
1111
1112    /// Check for BRC-20 pattern using simple string matching (fallback)
1113    fn has_brc20_pattern_simple(&self, data: &[u8]) -> bool {
1114        // Convert to string for pattern matching
1115        if let Ok(script_str) = String::from_utf8(data.to_vec()) {
1116            // Check for BRC-20 markers (case-insensitive)
1117            let lower = script_str.to_lowercase();
1118            lower.contains("brc-20")
1119                || lower.contains("\"p\":\"brc-20\"")
1120                || lower.contains("op\":\"mint")
1121                || lower.contains("op\":\"transfer")
1122                || lower.contains("op\":\"deploy")
1123        } else {
1124            // Not valid UTF-8, try byte pattern matching
1125            // Look for "brc-20" in bytes (case-insensitive)
1126            let pattern = b"brc-20";
1127            let pattern_lower = b"BRC-20";
1128            data.windows(pattern.len())
1129                .any(|window| window == pattern || window == pattern_lower)
1130        }
1131    }
1132
1133    /// Filter transactions from a block (without witness data)
1134    ///
1135    /// Returns filtered transactions (non-spam only) and summary of filtered spam.
1136    ///
1137    /// **Important**: This function filters entire transactions. For UTXO commitment processing,
1138    /// use `process_filtered_block` in `initial_sync.rs` which correctly handles spam
1139    /// transactions by removing spent inputs while filtering outputs.
1140    ///
1141    /// This function is primarily used for:
1142    /// - Bandwidth estimation (calculating filtered size)
1143    /// - Statistics and reporting
1144    /// - Network message filtering (where entire transactions can be dropped)
1145    ///
1146    /// **Do not use this for UTXO tree updates** - it will cause UTXO set inconsistency
1147    /// when spam transactions spend non-spam inputs.
1148    pub fn filter_block(&self, transactions: &[Transaction]) -> (Vec<Transaction>, SpamSummary) {
1149        self.filter_block_with_witness(transactions, None)
1150    }
1151
1152    /// Filter transactions from a block (with optional witness data)
1153    ///
1154    /// Returns filtered transactions (non-spam only) and summary of filtered spam.
1155    /// Witness data improves detection accuracy for SegWit/Taproot-based spam.
1156    ///
1157    /// **Important**: This function filters entire transactions. For UTXO commitment processing,
1158    /// use `process_filtered_block` in `initial_sync.rs` which correctly handles spam
1159    /// transactions by removing spent inputs while filtering outputs.
1160    ///
1161    /// This function is primarily used for:
1162    /// - Bandwidth estimation (calculating filtered size)
1163    /// - Statistics and reporting
1164    /// - Network message filtering (where entire transactions can be dropped)
1165    ///
1166    /// **Do not use this for UTXO tree updates** - it will cause UTXO set inconsistency
1167    /// when spam transactions spend non-spam inputs.
1168    pub fn filter_block_with_witness(
1169        &self,
1170        transactions: &[Transaction],
1171        witnesses: Option<&[Vec<Witness>]>,
1172    ) -> (Vec<Transaction>, SpamSummary) {
1173        let mut filtered_txs = Vec::new();
1174        let mut filtered_count = 0u32;
1175        let mut filtered_size = 0u64;
1176        let mut spam_breakdown = SpamBreakdown::default();
1177
1178        for (i, tx) in transactions.iter().enumerate() {
1179            // Get witness data for this transaction if available
1180            let tx_witnesses = witnesses.and_then(|w| w.get(i));
1181
1182            let result = if let Some(tx_witnesses) = tx_witnesses {
1183                self.is_spam_with_witness(tx, Some(tx_witnesses), None)
1184            } else {
1185                self.is_spam(tx)
1186            };
1187
1188            if result.is_spam {
1189                filtered_count += 1;
1190                let tx_size = if let Some(tx_witnesses) = tx_witnesses {
1191                    self.estimate_transaction_size_with_witness(tx, Some(tx_witnesses)) as u64
1192                } else {
1193                    estimate_transaction_size(tx)
1194                };
1195                filtered_size += tx_size;
1196
1197                // Update breakdown
1198                for spam_type in &result.detected_types {
1199                    match spam_type {
1200                        SpamType::Ordinals => spam_breakdown.ordinals += 1,
1201                        SpamType::Dust => spam_breakdown.dust += 1,
1202                        SpamType::BRC20 => spam_breakdown.brc20 += 1,
1203                        SpamType::LargeWitness => spam_breakdown.ordinals += 1, // Count as Ordinals
1204                        SpamType::LowFeeRate => spam_breakdown.dust += 1, // Count as suspicious
1205                        SpamType::HighSizeValueRatio => spam_breakdown.ordinals += 1, // Count as Ordinals
1206                        SpamType::ManySmallOutputs => spam_breakdown.dust += 1, // Count as dust-like
1207                        SpamType::NotSpam => {}
1208                    }
1209                }
1210            } else {
1211                filtered_txs.push(tx.clone());
1212            }
1213        }
1214
1215        let summary = SpamSummary {
1216            filtered_count,
1217            filtered_size,
1218            by_type: spam_breakdown,
1219        };
1220
1221        (filtered_txs, summary)
1222    }
1223}
1224
1225impl Default for SpamFilter {
1226    fn default() -> Self {
1227        Self::new()
1228    }
1229}
1230
1231/// Summary of filtered spam
1232#[derive(Debug, Clone, Default)]
1233pub struct SpamSummary {
1234    /// Number of transactions filtered
1235    pub filtered_count: u32,
1236    /// Total size of filtered transactions (bytes, estimated)
1237    pub filtered_size: u64,
1238    /// Breakdown by spam type
1239    pub by_type: SpamBreakdown,
1240}
1241
1242/// Breakdown of spam by category
1243#[derive(Debug, Clone, Default)]
1244pub struct SpamBreakdown {
1245    pub ordinals: u32,
1246    pub inscriptions: u32,
1247    pub dust: u32,
1248    pub brc20: u32,
1249}
1250
1251/// Estimate transaction size in bytes (consensus serialization length).
1252fn estimate_transaction_size(tx: &Transaction) -> u64 {
1253    blvm_consensus::transaction::calculate_transaction_size(tx) as u64
1254}
1255
1256/// Detect script type for a specific input using prevout UTXO script when available.
1257fn detect_script_type_for_input(
1258    tx: &Transaction,
1259    input_index: usize,
1260    utxo_set: Option<&UtxoSet>,
1261) -> Option<ScriptType> {
1262    if input_index >= tx.inputs.len() {
1263        return None;
1264    }
1265
1266    if let Some(utxo_set) = utxo_set {
1267        if let Some(utxo) = utxo_set.get(&tx.inputs[input_index].prevout) {
1268            let spk: ByteString = utxo.script_pubkey.as_ref().to_vec();
1269            let script_type = ScriptType::detect(&spk);
1270            if script_type != ScriptType::Unknown {
1271                return Some(script_type);
1272            }
1273        }
1274    }
1275
1276    detect_input_script_type(&tx.inputs[input_index].script_sig)
1277}
1278
1279/// Serializable adaptive thresholds
1280#[derive(Debug, Clone, PartialEq, Serialize, Deserialize)]
1281pub struct WitnessSizeThresholdsSerializable {
1282    #[serde(default = "default_normal_single_sig")]
1283    pub normal_single_sig: usize,
1284    #[serde(default = "default_normal_multi_sig")]
1285    pub normal_multi_sig: usize,
1286    #[serde(default = "default_normal_p2wsh")]
1287    pub normal_p2wsh: usize,
1288    #[serde(default = "default_suspicious_threshold")]
1289    pub suspicious_threshold: usize,
1290    #[serde(default = "default_definitely_spam")]
1291    pub definitely_spam: usize,
1292}
1293
1294impl From<WitnessSizeThresholdsSerializable> for WitnessSizeThresholds {
1295    fn from(serializable: WitnessSizeThresholdsSerializable) -> Self {
1296        WitnessSizeThresholds {
1297            normal_single_sig: serializable.normal_single_sig,
1298            normal_multi_sig: serializable.normal_multi_sig,
1299            normal_p2wsh: serializable.normal_p2wsh,
1300            suspicious_threshold: serializable.suspicious_threshold,
1301            definitely_spam: serializable.definitely_spam,
1302        }
1303    }
1304}
1305
1306impl From<WitnessSizeThresholds> for WitnessSizeThresholdsSerializable {
1307    fn from(thresholds: WitnessSizeThresholds) -> Self {
1308        WitnessSizeThresholdsSerializable {
1309            normal_single_sig: thresholds.normal_single_sig,
1310            normal_multi_sig: thresholds.normal_multi_sig,
1311            normal_p2wsh: thresholds.normal_p2wsh,
1312            suspicious_threshold: thresholds.suspicious_threshold,
1313            definitely_spam: thresholds.definitely_spam,
1314        }
1315    }
1316}
1317
1318/// Serializable spam filter configuration (for config files)
1319#[derive(Debug, Clone, PartialEq, Serialize, Deserialize)]
1320pub struct SpamFilterConfigSerializable {
1321    #[serde(default = "default_true")]
1322    pub filter_ordinals: bool,
1323    #[serde(default = "default_true")]
1324    pub filter_dust: bool,
1325    #[serde(default = "default_true")]
1326    pub filter_brc20: bool,
1327    #[serde(default = "default_true")]
1328    pub filter_large_witness: bool,
1329    #[serde(default = "default_false")]
1330    pub filter_low_fee_rate: bool,
1331    #[serde(default = "default_true")]
1332    pub filter_high_size_value_ratio: bool,
1333    #[serde(default = "default_true")]
1334    pub filter_many_small_outputs: bool,
1335    #[serde(default = "default_dust_threshold")]
1336    pub dust_threshold: i64,
1337    #[serde(default = "default_dust_threshold")]
1338    pub min_output_value: i64,
1339    #[serde(default = "default_min_fee_rate")]
1340    pub min_fee_rate: u64,
1341    #[serde(default = "default_max_witness_size")]
1342    pub max_witness_size: usize,
1343    #[serde(default = "default_max_size_value_ratio")]
1344    pub max_size_value_ratio: f64,
1345    #[serde(default = "default_max_small_outputs")]
1346    pub max_small_outputs: usize,
1347
1348    // NEW: Adaptive thresholds
1349    #[serde(default = "default_true")]
1350    pub use_adaptive_thresholds: bool,
1351    #[serde(default = "default_adaptive_thresholds")]
1352    pub adaptive_thresholds: WitnessSizeThresholdsSerializable,
1353
1354    // NEW: Taproot-specific options
1355    #[serde(default = "default_true")]
1356    pub filter_taproot_spam: bool,
1357    #[serde(default = "default_max_taproot_control_size")]
1358    pub max_taproot_control_size: usize,
1359    #[serde(default = "default_true")]
1360    pub reject_taproot_annexes: bool,
1361
1362    // NEW: Total witness size check
1363    #[serde(default = "default_false")]
1364    pub filter_large_total_witness: bool,
1365    #[serde(default = "default_max_total_witness_size")]
1366    pub max_total_witness_size: usize,
1367
1368    // NEW: Enhanced detection options
1369    #[serde(default = "default_true")]
1370    pub use_improved_envelope_detection: bool,
1371    #[serde(default = "default_true")]
1372    pub use_json_validation_brc20: bool,
1373
1374    // NEW: Fee rate calculation options
1375    #[serde(default = "default_false")]
1376    pub require_utxo_for_fee_rate: bool,
1377    #[serde(default = "default_min_fee_rate_large_tx")]
1378    pub min_fee_rate_large_tx: u64,
1379    #[serde(default = "default_large_tx_threshold_bytes")]
1380    pub large_tx_threshold_bytes: usize,
1381    #[serde(default = "default_true")]
1382    pub ordinals_strict_mode: bool,
1383}
1384
1385fn default_true() -> bool {
1386    true
1387}
1388
1389fn default_false() -> bool {
1390    false
1391}
1392
1393fn default_dust_threshold() -> i64 {
1394    546
1395}
1396
1397fn default_min_fee_rate() -> u64 {
1398    1
1399}
1400
1401fn default_max_witness_size() -> usize {
1402    1000
1403}
1404
1405fn default_max_size_value_ratio() -> f64 {
1406    1000.0
1407}
1408
1409fn default_max_small_outputs() -> usize {
1410    10
1411}
1412
1413fn default_max_taproot_control_size() -> usize {
1414    289
1415}
1416
1417fn default_max_total_witness_size() -> usize {
1418    5000
1419}
1420
1421fn default_min_fee_rate_large_tx() -> u64 {
1422    2
1423}
1424
1425fn default_large_tx_threshold_bytes() -> usize {
1426    1000
1427}
1428
1429fn default_normal_single_sig() -> usize {
1430    200
1431}
1432
1433fn default_normal_multi_sig() -> usize {
1434    500
1435}
1436
1437fn default_normal_p2wsh() -> usize {
1438    800
1439}
1440
1441fn default_suspicious_threshold() -> usize {
1442    1000
1443}
1444
1445fn default_definitely_spam() -> usize {
1446    2000
1447}
1448
1449fn default_adaptive_thresholds() -> WitnessSizeThresholdsSerializable {
1450    WitnessSizeThresholdsSerializable {
1451        normal_single_sig: 200,
1452        normal_multi_sig: 500,
1453        normal_p2wsh: 800,
1454        suspicious_threshold: 1000,
1455        definitely_spam: 2000,
1456    }
1457}
1458
1459impl Default for SpamFilterConfigSerializable {
1460    fn default() -> Self {
1461        Self {
1462            filter_ordinals: default_true(),
1463            filter_dust: default_true(),
1464            filter_brc20: default_true(),
1465            filter_large_witness: default_true(),
1466            filter_low_fee_rate: default_false(),
1467            filter_high_size_value_ratio: default_true(),
1468            filter_many_small_outputs: default_true(),
1469            dust_threshold: default_dust_threshold(),
1470            min_output_value: default_dust_threshold(),
1471            min_fee_rate: default_min_fee_rate(),
1472            max_witness_size: default_max_witness_size(),
1473            max_size_value_ratio: default_max_size_value_ratio(),
1474            max_small_outputs: default_max_small_outputs(),
1475            use_adaptive_thresholds: default_true(),
1476            adaptive_thresholds: default_adaptive_thresholds(),
1477            filter_taproot_spam: default_true(),
1478            max_taproot_control_size: default_max_taproot_control_size(),
1479            reject_taproot_annexes: default_true(),
1480            filter_large_total_witness: default_false(),
1481            max_total_witness_size: default_max_total_witness_size(),
1482            use_improved_envelope_detection: default_true(),
1483            use_json_validation_brc20: default_true(),
1484            require_utxo_for_fee_rate: default_false(),
1485            min_fee_rate_large_tx: default_min_fee_rate_large_tx(),
1486            large_tx_threshold_bytes: default_large_tx_threshold_bytes(),
1487            ordinals_strict_mode: default_true(),
1488        }
1489    }
1490}
1491
1492impl From<SpamFilterConfigSerializable> for SpamFilterConfig {
1493    fn from(serializable: SpamFilterConfigSerializable) -> Self {
1494        SpamFilterConfig {
1495            filter_ordinals: serializable.filter_ordinals,
1496            filter_dust: serializable.filter_dust,
1497            filter_brc20: serializable.filter_brc20,
1498            filter_large_witness: serializable.filter_large_witness,
1499            filter_low_fee_rate: serializable.filter_low_fee_rate,
1500            filter_high_size_value_ratio: serializable.filter_high_size_value_ratio,
1501            filter_many_small_outputs: serializable.filter_many_small_outputs,
1502            dust_threshold: serializable.dust_threshold,
1503            min_output_value: serializable.min_output_value,
1504            min_fee_rate: serializable.min_fee_rate,
1505            max_witness_size: serializable.max_witness_size,
1506            max_size_value_ratio: serializable.max_size_value_ratio,
1507            max_small_outputs: serializable.max_small_outputs,
1508            // NEW: Adaptive thresholds
1509            use_adaptive_thresholds: serializable.use_adaptive_thresholds,
1510            adaptive_thresholds: serializable.adaptive_thresholds.into(),
1511            // NEW fields
1512            filter_taproot_spam: serializable.filter_taproot_spam,
1513            max_taproot_control_size: serializable.max_taproot_control_size,
1514            reject_taproot_annexes: serializable.reject_taproot_annexes,
1515            filter_large_total_witness: serializable.filter_large_total_witness,
1516            max_total_witness_size: serializable.max_total_witness_size,
1517            use_improved_envelope_detection: serializable.use_improved_envelope_detection,
1518            use_json_validation_brc20: serializable.use_json_validation_brc20,
1519            require_utxo_for_fee_rate: serializable.require_utxo_for_fee_rate,
1520            min_fee_rate_large_tx: serializable.min_fee_rate_large_tx,
1521            large_tx_threshold_bytes: serializable.large_tx_threshold_bytes,
1522            ordinals_strict_mode: serializable.ordinals_strict_mode,
1523        }
1524    }
1525}
1526
1527impl From<SpamFilterConfig> for SpamFilterConfigSerializable {
1528    fn from(config: SpamFilterConfig) -> Self {
1529        SpamFilterConfigSerializable {
1530            filter_ordinals: config.filter_ordinals,
1531            filter_dust: config.filter_dust,
1532            filter_brc20: config.filter_brc20,
1533            filter_large_witness: config.filter_large_witness,
1534            filter_low_fee_rate: config.filter_low_fee_rate,
1535            filter_high_size_value_ratio: config.filter_high_size_value_ratio,
1536            filter_many_small_outputs: config.filter_many_small_outputs,
1537            dust_threshold: config.dust_threshold,
1538            min_output_value: config.min_output_value,
1539            min_fee_rate: config.min_fee_rate,
1540            max_witness_size: config.max_witness_size,
1541            max_size_value_ratio: config.max_size_value_ratio,
1542            max_small_outputs: config.max_small_outputs,
1543            // NEW: Adaptive thresholds
1544            use_adaptive_thresholds: config.use_adaptive_thresholds,
1545            adaptive_thresholds: config.adaptive_thresholds.into(),
1546            // NEW fields
1547            filter_taproot_spam: config.filter_taproot_spam,
1548            max_taproot_control_size: config.max_taproot_control_size,
1549            reject_taproot_annexes: config.reject_taproot_annexes,
1550            filter_large_total_witness: config.filter_large_total_witness,
1551            max_total_witness_size: config.max_total_witness_size,
1552            use_improved_envelope_detection: config.use_improved_envelope_detection,
1553            use_json_validation_brc20: config.use_json_validation_brc20,
1554            require_utxo_for_fee_rate: config.require_utxo_for_fee_rate,
1555            min_fee_rate_large_tx: config.min_fee_rate_large_tx,
1556            large_tx_threshold_bytes: config.large_tx_threshold_bytes,
1557            ordinals_strict_mode: config.ordinals_strict_mode,
1558        }
1559    }
1560}