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    /// Relay policy default: 289 bytes (depth 8); tighter thresholds may reject deeper trees
244    /// Default: 289 bytes (allows depth 8)
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 — reject under relay policy when enabled
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                // Relay policy uses a configurable threshold (default 289 bytes, depth 8)
594                if control_block.len() > self.config.max_taproot_control_size {
595                    return true;
596                }
597            }
598        }
599
600        false
601    }
602
603    /// Check if witness stack is suspiciously large (suggests data embedding)
604    ///
605    /// Uses adaptive thresholds based on script type if enabled.
606    fn has_large_witness_stack(&self, witness: &Witness) -> bool {
607        let total_size = self.calculate_witness_size(witness);
608        total_size > self.config.max_witness_size
609    }
610
611    /// Check if witness stack is suspiciously large using adaptive thresholds
612    ///
613    /// This method uses script type detection to apply appropriate thresholds.
614    /// Falls back to fixed threshold if adaptive thresholds are disabled or script type cannot be determined.
615    fn has_large_witness_stack_adaptive(
616        &self,
617        witness: &Witness,
618        tx: &Transaction,
619        input_index: usize,
620        utxo_set: Option<&UtxoSet>,
621    ) -> bool {
622        let total_size = self.calculate_witness_size(witness);
623
624        // If adaptive thresholds disabled, use fixed threshold
625        if !self.config.use_adaptive_thresholds {
626            return total_size > self.config.max_witness_size;
627        }
628
629        let threshold =
630            if let Some(script_type) = detect_script_type_for_input(tx, input_index, utxo_set) {
631                script_type.recommended_threshold()
632            } else {
633                self.config.max_witness_size
634            };
635
636        total_size > threshold
637    }
638
639    /// Analyze witness elements for suspicious patterns
640    ///
641    /// Detects data splitting patterns (many medium-sized elements).
642    fn analyze_witness_elements(&self, witness: &Witness) -> WitnessElementAnalysis {
643        let total_size = self.calculate_witness_size(witness);
644        let element_count = witness.len();
645
646        let mut large_elements = 0;
647        let mut medium_elements = 0;
648        let mut small_elements = 0;
649
650        for element in witness {
651            if element.len() > 200 {
652                large_elements += 1;
653            } else if element.len() >= 100 {
654                medium_elements += 1;
655            } else {
656                small_elements += 1;
657            }
658        }
659
660        // Suspicious pattern: many medium elements (suggests data splitting)
661        let suspicious_pattern = medium_elements >= 10;
662
663        WitnessElementAnalysis {
664            total_size,
665            element_count,
666            large_elements,
667            medium_elements,
668            small_elements,
669            suspicious_pattern,
670        }
671    }
672
673    /// Calculate accurate witness size including varint overhead
674    ///
675    /// Witness size includes:
676    /// - Stack count varint (1 byte typically for small stacks)
677    /// - For each element: length varint (1-9 bytes) + element data
678    ///
679    /// This matches the actual serialized size of witness data in Bitcoin transactions.
680    fn calculate_witness_size(&self, witness: &Witness) -> usize {
681        // Stack count varint (typically 1 byte for small stacks)
682        let mut size = 1;
683
684        // Each element: length varint + element data
685        for element in witness {
686            // Varint encoding: 1 byte for <128, 2 for <16384, etc.
687            // Bitcoin varint encoding: values < 0xfd use 1 byte, larger values use prefix + data
688            // For witness element lengths, we use compact size encoding:
689            // - < 0xfd: 1 byte
690            // - 0xfd-0xffff: 0xfd prefix (1 byte) + 2 bytes data
691            // - 0x10000-0xffffffff: 0xfe prefix (1 byte) + 4 bytes data
692            // - > 0xffffffff: 0xff prefix (1 byte) + 8 bytes data
693            size += if element.len() <= VARINT_1BYTE_MAX as usize {
694                1
695            } else if element.len() <= 0xffff {
696                3 // VARINT_2BYTE_PREFIX + 2 bytes
697            } else if element.len() <= 0xffffffff {
698                5 // VARINT_4BYTE_PREFIX + 4 bytes
699            } else {
700                9 // VARINT_8BYTE_PREFIX + 8 bytes
701            };
702            size += element.len();
703        }
704
705        size
706    }
707
708    /// Check if witness contains data patterns (non-signature data)
709    fn has_witness_data_pattern(&self, witness: &Witness) -> bool {
710        if witness.is_empty() {
711            return false;
712        }
713
714        // Check for very large witness elements (>520 bytes is max for signatures)
715        // Elements larger than typical signature size suggest data embedding
716        for element in witness {
717            // Typical signatures are 71-73 bytes (DER-encoded) or 64 bytes (Schnorr)
718            // Witness elements >200 bytes are suspicious for data embedding
719            if element.len() > 200 {
720                // Check if it looks like data (not a signature)
721                // Signatures typically start with 0x30 (DER) or are exactly 64 bytes (Schnorr)
722                if element.len() != 64 && (element.is_empty() || element[0] != DER_SIGNATURE_PREFIX)
723                {
724                    // Likely data embedding
725                    return true;
726                }
727            }
728        }
729
730        // Check for multiple large elements (suggests data chunks)
731        let large_elements = witness.iter().filter(|elem| elem.len() > 100).count();
732        if large_elements >= 3 {
733            return true;
734        }
735
736        // Check for suspicious pattern (many medium elements - data splitting)
737        let analysis = self.analyze_witness_elements(witness);
738        if analysis.suspicious_pattern {
739            return true;
740        }
741
742        false
743    }
744
745    /// Check if script has Ordinals pattern
746    ///
747    /// Ordinals typically use:
748    /// - OP_RETURN followed by data (>80 bytes)
749    /// - Envelope protocol (OP_0 OP_IF ... OP_ENDIF) in output/scriptSig
750    fn has_ordinal_pattern(&self, script: &ByteString) -> bool {
751        if script.is_empty() {
752            return false;
753        }
754
755        // OP_RETURN >80 bytes (relay-policy heuristic; larger suggests data embedding)
756        if script[0] == OP_RETURN && script.len() > 80 {
757            return true;
758        }
759
760        // Envelope protocol in output or scriptSig
761        if self.has_envelope_pattern(script) {
762            return true;
763        }
764
765        false
766    }
767
768    /// Check if script has envelope protocol pattern
769    fn has_envelope_pattern(&self, script: &ByteString) -> bool {
770        // Envelope protocol: OP_FALSE OP_IF ... OP_ENDIF
771        if script.len() < 4 {
772            return false;
773        }
774
775        // Check for OP_FALSE OP_IF pattern (common in inscriptions)
776        if script[0] == OP_0 && script[1] == OP_IF {
777            if self.config.use_improved_envelope_detection {
778                // Improved: Verify OP_ENDIF exists later in script
779                // Envelope protocol: OP_FALSE OP_IF ... OP_ENDIF
780                if script.iter().skip(2).any(|&b| b == OP_ENDIF) {
781                    return true;
782                }
783            } else {
784                // Original simple check (backward compatibility)
785                return true;
786            }
787        }
788
789        false
790    }
791
792    /// Detect dust outputs
793    ///
794    /// Dust outputs are outputs with value below threshold (default: 546 satoshis).
795    fn detect_dust(&self, tx: &Transaction) -> bool {
796        // Check if all outputs are below threshold
797        let mut all_dust = true;
798
799        for output in &tx.outputs {
800            if output.value >= self.config.dust_threshold {
801                all_dust = false;
802                break;
803            }
804        }
805
806        all_dust && !tx.outputs.is_empty()
807    }
808
809    /// Detect transactions with large witness data
810    ///
811    /// Large witness stacks often indicate data embedding (Ordinals, inscriptions).
812    /// Now uses adaptive thresholds based on script type.
813    fn detect_large_witness(
814        &self,
815        tx: &Transaction,
816        witnesses: Option<&[Witness]>,
817        utxo_set: Option<&UtxoSet>,
818    ) -> bool {
819        if let Some(witnesses) = witnesses {
820            for (i, witness) in witnesses.iter().enumerate() {
821                // Use adaptive thresholds if enabled
822                if self.config.use_adaptive_thresholds {
823                    if self.has_large_witness_stack_adaptive(witness, tx, i, utxo_set) {
824                        return true;
825                    }
826                } else if self.has_large_witness_stack(witness) {
827                    return true;
828                }
829            }
830        }
831        false
832    }
833
834    /// Detect transactions with low fee rate
835    ///
836    /// Non-monetary transactions often pay minimal fees relative to size.
837    /// Now accepts optional UTXO set for accurate fee calculation.
838    fn detect_low_fee_rate(
839        &self,
840        tx: &Transaction,
841        witnesses: Option<&[Witness]>,
842        utxo_set: Option<&UtxoSet>,
843    ) -> bool {
844        let tx_size = self.estimate_transaction_size_with_witness(tx, witnesses);
845
846        // If require_utxo_for_fee_rate is true and UTXO set unavailable, reject
847        if self.config.require_utxo_for_fee_rate && utxo_set.is_none() {
848            // Cannot calculate accurate fee rate, reject if strict mode enabled
849            return true; // Reject as spam (conservative)
850        }
851
852        // Calculate fee rate
853        let fee_rate = if let Some(utxo_set) = utxo_set {
854            self.calculate_fee_rate_accurate(tx, utxo_set, tx_size)
855        } else {
856            // Without UTXO inputs we cannot compute fee rate; skip to avoid false negatives
857            // from the old min_fee_rate heuristic (REV-P-21).
858            return false;
859        };
860
861        // Check against threshold (use large tx threshold if applicable)
862        let threshold = if tx_size > self.config.large_tx_threshold_bytes {
863            self.config.min_fee_rate_large_tx
864        } else {
865            self.config.min_fee_rate
866        };
867
868        fee_rate < threshold
869    }
870
871    /// Calculate fee rate accurately using UTXO set
872    fn calculate_fee_rate_accurate(
873        &self,
874        tx: &Transaction,
875        utxo_set: &UtxoSet,
876        tx_size: usize,
877    ) -> u64 {
878        if tx_size == 0 {
879            return 0;
880        }
881
882        // Calculate actual fee
883        let mut input_total = 0u64;
884        for input in &tx.inputs {
885            if let Some(utxo) = utxo_set.get(&input.prevout) {
886                input_total += utxo.value as u64;
887            }
888        }
889
890        let output_total: u64 = tx.outputs.iter().map(|out| out.value as u64).sum();
891        let fee = input_total.saturating_sub(output_total);
892
893        // Fee rate in satoshis per vbyte
894        if tx_size > 0 { fee / tx_size as u64 } else { 0 }
895    }
896
897    /// Calculate fee rate using heuristics (fallback)
898    #[allow(dead_code)]
899    fn calculate_fee_rate_heuristic(&self, tx: &Transaction, tx_size: usize) -> u64 {
900        if tx_size == 0 {
901            return 0;
902        }
903
904        let total_output_value: i64 = tx.outputs.iter().map(|out| out.value).sum();
905
906        // Heuristic: large transactions with small output value likely have low fee rate
907        if tx_size > 1000 && total_output_value < 10000 {
908            // Assume minimal fee (1000 sats) for large transactions
909            1000u64.saturating_div(tx_size as u64)
910        } else {
911            // For other transactions, assume reasonable fee rate
912            // This is conservative - may have false negatives
913            self.config.min_fee_rate
914        }
915    }
916
917    /// Detect transactions with large total witness size across all inputs
918    fn detect_large_total_witness(&self, witnesses: Option<&[Witness]>) -> bool {
919        if !self.config.filter_large_total_witness {
920            return false; // Feature disabled
921        }
922
923        if let Some(witnesses) = witnesses {
924            let total_size: usize = witnesses
925                .iter()
926                .map(|w| self.calculate_witness_size(w))
927                .sum();
928
929            total_size > self.config.max_total_witness_size
930        } else {
931            false
932        }
933    }
934
935    /// Detect transactions with high size-to-value ratio
936    ///
937    /// Non-monetary transactions often have very large size relative to value transferred.
938    /// Now uses transaction type detection to adjust thresholds for legitimate transactions
939    /// (consolidations, CoinJoins) that legitimately have high ratios.
940    fn detect_high_size_value_ratio(
941        &self,
942        tx: &Transaction,
943        witnesses: Option<&[Witness]>,
944    ) -> bool {
945        let tx_size = self.estimate_transaction_size_with_witness(tx, witnesses) as f64;
946        let total_output_value: f64 = tx.outputs.iter().map(|out| out.value as f64).sum();
947
948        // Avoid division by zero
949        if total_output_value <= 0.0 {
950            // Transaction with zero outputs is suspicious
951            return tx_size > 1000.0;
952        }
953
954        let ratio = tx_size / total_output_value;
955
956        // Use transaction type to adjust threshold
957        let threshold = if self.config.use_adaptive_thresholds {
958            let tx_type = TransactionType::detect(tx);
959            tx_type.recommended_size_value_ratio()
960        } else {
961            self.config.max_size_value_ratio
962        };
963
964        ratio > threshold
965    }
966
967    /// Detect transactions with many small outputs
968    ///
969    /// Token distributions and Ordinal transfers often create many small outputs.
970    fn detect_many_small_outputs(&self, tx: &Transaction) -> bool {
971        let small_output_count = tx
972            .outputs
973            .iter()
974            .filter(|out| out.value < self.config.dust_threshold)
975            .count();
976
977        small_output_count > self.config.max_small_outputs
978    }
979
980    /// Estimate transaction size including witness data
981    fn estimate_transaction_size_with_witness(
982        &self,
983        tx: &Transaction,
984        witnesses: Option<&[Witness]>,
985    ) -> usize {
986        // Base transaction size (non-witness)
987        let base_size = estimate_transaction_size(tx) as usize;
988
989        // Add witness size if available
990        if let Some(witnesses) = witnesses {
991            let witness_size: usize = witnesses
992                .iter()
993                .map(|witness| {
994                    // Witness stack count (varint, ~1 byte)
995                    let mut size = 1;
996                    // Each witness element: length (varint, ~1 byte) + element data
997                    for element in witness {
998                        size += 1; // varint for length
999                        size += element.len();
1000                    }
1001                    size
1002                })
1003                .sum();
1004
1005            // SegWit marker and flag (2 bytes)
1006            let has_witness = witness_size > 0;
1007            if has_witness {
1008                base_size + 2 + witness_size
1009            } else {
1010                base_size
1011            }
1012        } else {
1013            base_size
1014        }
1015    }
1016
1017    /// Detect BRC-20 token transactions
1018    ///
1019    /// BRC-20 transactions typically have:
1020    /// - OP_RETURN outputs with JSON data
1021    /// - Specific JSON patterns (mint, transfer, deploy)
1022    fn detect_brc20(&self, tx: &Transaction) -> bool {
1023        // Check outputs for OP_RETURN with JSON-like data
1024        for output in &tx.outputs {
1025            if self.has_brc20_pattern(&output.script_pubkey) {
1026                return true;
1027            }
1028        }
1029
1030        false
1031    }
1032
1033    /// Check if script has BRC-20 pattern
1034    ///
1035    /// BRC-20 transactions use OP_RETURN with JSON:
1036    /// - {"p":"brc-20","op":"mint",...}
1037    /// - {"p":"brc-20","op":"transfer",...}
1038    /// - {"p":"brc-20","op":"deploy",...}
1039    fn has_brc20_pattern(&self, script: &ByteString) -> bool {
1040        if script.len() < 20 {
1041            return false;
1042        }
1043
1044        // Check for OP_RETURN
1045        if script[0] != OP_RETURN {
1046            return false;
1047        }
1048
1049        // Extract data after OP_RETURN
1050        let data = &script[1..];
1051
1052        // Try to decode as UTF-8
1053        let script_str = match String::from_utf8(data.to_vec()) {
1054            Ok(s) => s,
1055            Err(_) => {
1056                // Not valid UTF-8, use simple pattern matching
1057                return self.has_brc20_pattern_simple(data);
1058            }
1059        };
1060
1061        // Use JSON validation if enabled
1062        if self.config.use_json_validation_brc20 {
1063            self.has_brc20_pattern_json(&script_str)
1064        } else {
1065            // Fallback to simple string matching
1066            self.has_brc20_pattern_simple(data)
1067        }
1068    }
1069
1070    /// Check for BRC-20 pattern using JSON validation
1071    fn has_brc20_pattern_json(&self, json_str: &str) -> bool {
1072        // Remove whitespace for more robust matching
1073        let cleaned: String = json_str.chars().filter(|c| !c.is_whitespace()).collect();
1074
1075        // Try to parse as JSON
1076        if let Ok(json_value) = serde_json::from_str::<serde_json::Value>(&cleaned) {
1077            // Check if it's a valid BRC-20 transaction
1078            if let Some(obj) = json_value.as_object() {
1079                // Check for protocol field: "p": "brc-20"
1080                if let Some(protocol) = obj.get("p") {
1081                    if protocol.as_str() == Some("brc-20") {
1082                        // Check for operation field: "op": "mint" | "transfer" | "deploy"
1083                        if let Some(op) = obj.get("op") {
1084                            if let Some(op_str) = op.as_str() {
1085                                return matches!(op_str, "mint" | "transfer" | "deploy");
1086                            }
1087                        }
1088                    }
1089                }
1090            }
1091        }
1092
1093        // Fallback: try parsing original string (with whitespace)
1094        if let Ok(json_value) = serde_json::from_str::<serde_json::Value>(json_str) {
1095            if let Some(obj) = json_value.as_object() {
1096                if let Some(protocol) = obj.get("p") {
1097                    if protocol.as_str() == Some("brc-20") {
1098                        if let Some(op) = obj.get("op") {
1099                            if let Some(op_str) = op.as_str() {
1100                                return matches!(op_str, "mint" | "transfer" | "deploy");
1101                            }
1102                        }
1103                    }
1104                }
1105            }
1106        }
1107
1108        false
1109    }
1110
1111    /// Check for BRC-20 pattern using simple string matching (fallback)
1112    fn has_brc20_pattern_simple(&self, data: &[u8]) -> bool {
1113        // Convert to string for pattern matching
1114        if let Ok(script_str) = String::from_utf8(data.to_vec()) {
1115            // Check for BRC-20 markers (case-insensitive)
1116            let lower = script_str.to_lowercase();
1117            lower.contains("brc-20")
1118                || lower.contains("\"p\":\"brc-20\"")
1119                || lower.contains("op\":\"mint")
1120                || lower.contains("op\":\"transfer")
1121                || lower.contains("op\":\"deploy")
1122        } else {
1123            // Not valid UTF-8, try byte pattern matching
1124            // Look for "brc-20" in bytes (case-insensitive)
1125            let pattern = b"brc-20";
1126            let pattern_lower = b"BRC-20";
1127            data.windows(pattern.len())
1128                .any(|window| window == pattern || window == pattern_lower)
1129        }
1130    }
1131
1132    /// Filter transactions from a block (without witness data)
1133    ///
1134    /// Returns filtered transactions (non-spam only) and summary of filtered spam.
1135    ///
1136    /// **Important**: This function filters entire transactions. For UTXO commitment processing,
1137    /// use `process_filtered_block` in `initial_sync.rs` which correctly handles spam
1138    /// transactions by removing spent inputs while filtering outputs.
1139    ///
1140    /// This function is primarily used for:
1141    /// - Bandwidth estimation (calculating filtered size)
1142    /// - Statistics and reporting
1143    /// - Network message filtering (where entire transactions can be dropped)
1144    ///
1145    /// **Do not use this for UTXO tree updates** - it will cause UTXO set inconsistency
1146    /// when spam transactions spend non-spam inputs.
1147    pub fn filter_block(&self, transactions: &[Transaction]) -> (Vec<Transaction>, SpamSummary) {
1148        self.filter_block_with_witness(transactions, None)
1149    }
1150
1151    /// Filter transactions from a block (with optional witness data)
1152    ///
1153    /// Returns filtered transactions (non-spam only) and summary of filtered spam.
1154    /// Witness data improves detection accuracy for SegWit/Taproot-based spam.
1155    ///
1156    /// **Important**: This function filters entire transactions. For UTXO commitment processing,
1157    /// use `process_filtered_block` in `initial_sync.rs` which correctly handles spam
1158    /// transactions by removing spent inputs while filtering outputs.
1159    ///
1160    /// This function is primarily used for:
1161    /// - Bandwidth estimation (calculating filtered size)
1162    /// - Statistics and reporting
1163    /// - Network message filtering (where entire transactions can be dropped)
1164    ///
1165    /// **Do not use this for UTXO tree updates** - it will cause UTXO set inconsistency
1166    /// when spam transactions spend non-spam inputs.
1167    pub fn filter_block_with_witness(
1168        &self,
1169        transactions: &[Transaction],
1170        witnesses: Option<&[Vec<Witness>]>,
1171    ) -> (Vec<Transaction>, SpamSummary) {
1172        let mut filtered_txs = Vec::new();
1173        let mut filtered_count = 0u32;
1174        let mut filtered_size = 0u64;
1175        let mut spam_breakdown = SpamBreakdown::default();
1176
1177        for (i, tx) in transactions.iter().enumerate() {
1178            // Get witness data for this transaction if available
1179            let tx_witnesses = witnesses.and_then(|w| w.get(i));
1180
1181            let result = if let Some(tx_witnesses) = tx_witnesses {
1182                self.is_spam_with_witness(tx, Some(tx_witnesses), None)
1183            } else {
1184                self.is_spam(tx)
1185            };
1186
1187            if result.is_spam {
1188                filtered_count += 1;
1189                let tx_size = if let Some(tx_witnesses) = tx_witnesses {
1190                    self.estimate_transaction_size_with_witness(tx, Some(tx_witnesses)) as u64
1191                } else {
1192                    estimate_transaction_size(tx)
1193                };
1194                filtered_size += tx_size;
1195
1196                // Update breakdown
1197                for spam_type in &result.detected_types {
1198                    match spam_type {
1199                        SpamType::Ordinals => spam_breakdown.ordinals += 1,
1200                        SpamType::Dust => spam_breakdown.dust += 1,
1201                        SpamType::BRC20 => spam_breakdown.brc20 += 1,
1202                        SpamType::LargeWitness => spam_breakdown.ordinals += 1, // Count as Ordinals
1203                        SpamType::LowFeeRate => spam_breakdown.dust += 1, // Count as suspicious
1204                        SpamType::HighSizeValueRatio => spam_breakdown.ordinals += 1, // Count as Ordinals
1205                        SpamType::ManySmallOutputs => spam_breakdown.dust += 1, // Count as dust-like
1206                        SpamType::NotSpam => {}
1207                    }
1208                }
1209            } else {
1210                filtered_txs.push(tx.clone());
1211            }
1212        }
1213
1214        let summary = SpamSummary {
1215            filtered_count,
1216            filtered_size,
1217            by_type: spam_breakdown,
1218        };
1219
1220        (filtered_txs, summary)
1221    }
1222}
1223
1224impl Default for SpamFilter {
1225    fn default() -> Self {
1226        Self::new()
1227    }
1228}
1229
1230/// Summary of filtered spam
1231#[derive(Debug, Clone, Default)]
1232pub struct SpamSummary {
1233    /// Number of transactions filtered
1234    pub filtered_count: u32,
1235    /// Total size of filtered transactions (bytes, estimated)
1236    pub filtered_size: u64,
1237    /// Breakdown by spam type
1238    pub by_type: SpamBreakdown,
1239}
1240
1241/// Breakdown of spam by category
1242#[derive(Debug, Clone, Default)]
1243pub struct SpamBreakdown {
1244    pub ordinals: u32,
1245    pub inscriptions: u32,
1246    pub dust: u32,
1247    pub brc20: u32,
1248}
1249
1250/// Estimate transaction size in bytes (consensus serialization length).
1251fn estimate_transaction_size(tx: &Transaction) -> u64 {
1252    blvm_consensus::transaction::calculate_transaction_size(tx) as u64
1253}
1254
1255/// Detect script type for a specific input using prevout UTXO script when available.
1256fn detect_script_type_for_input(
1257    tx: &Transaction,
1258    input_index: usize,
1259    utxo_set: Option<&UtxoSet>,
1260) -> Option<ScriptType> {
1261    if input_index >= tx.inputs.len() {
1262        return None;
1263    }
1264
1265    if let Some(utxo_set) = utxo_set {
1266        if let Some(utxo) = utxo_set.get(&tx.inputs[input_index].prevout) {
1267            let spk: ByteString = utxo.script_pubkey.as_ref().to_vec();
1268            let script_type = ScriptType::detect(&spk);
1269            if script_type != ScriptType::Unknown {
1270                return Some(script_type);
1271            }
1272        }
1273    }
1274
1275    detect_input_script_type(&tx.inputs[input_index].script_sig)
1276}
1277
1278/// Serializable adaptive thresholds
1279#[derive(Debug, Clone, PartialEq, Serialize, Deserialize)]
1280pub struct WitnessSizeThresholdsSerializable {
1281    #[serde(default = "default_normal_single_sig")]
1282    pub normal_single_sig: usize,
1283    #[serde(default = "default_normal_multi_sig")]
1284    pub normal_multi_sig: usize,
1285    #[serde(default = "default_normal_p2wsh")]
1286    pub normal_p2wsh: usize,
1287    #[serde(default = "default_suspicious_threshold")]
1288    pub suspicious_threshold: usize,
1289    #[serde(default = "default_definitely_spam")]
1290    pub definitely_spam: usize,
1291}
1292
1293impl From<WitnessSizeThresholdsSerializable> for WitnessSizeThresholds {
1294    fn from(serializable: WitnessSizeThresholdsSerializable) -> Self {
1295        WitnessSizeThresholds {
1296            normal_single_sig: serializable.normal_single_sig,
1297            normal_multi_sig: serializable.normal_multi_sig,
1298            normal_p2wsh: serializable.normal_p2wsh,
1299            suspicious_threshold: serializable.suspicious_threshold,
1300            definitely_spam: serializable.definitely_spam,
1301        }
1302    }
1303}
1304
1305impl From<WitnessSizeThresholds> for WitnessSizeThresholdsSerializable {
1306    fn from(thresholds: WitnessSizeThresholds) -> Self {
1307        WitnessSizeThresholdsSerializable {
1308            normal_single_sig: thresholds.normal_single_sig,
1309            normal_multi_sig: thresholds.normal_multi_sig,
1310            normal_p2wsh: thresholds.normal_p2wsh,
1311            suspicious_threshold: thresholds.suspicious_threshold,
1312            definitely_spam: thresholds.definitely_spam,
1313        }
1314    }
1315}
1316
1317/// Serializable spam filter configuration (for config files)
1318#[derive(Debug, Clone, PartialEq, Serialize, Deserialize)]
1319pub struct SpamFilterConfigSerializable {
1320    #[serde(default = "default_true")]
1321    pub filter_ordinals: bool,
1322    #[serde(default = "default_true")]
1323    pub filter_dust: bool,
1324    #[serde(default = "default_true")]
1325    pub filter_brc20: bool,
1326    #[serde(default = "default_true")]
1327    pub filter_large_witness: bool,
1328    #[serde(default = "default_false")]
1329    pub filter_low_fee_rate: bool,
1330    #[serde(default = "default_true")]
1331    pub filter_high_size_value_ratio: bool,
1332    #[serde(default = "default_true")]
1333    pub filter_many_small_outputs: bool,
1334    #[serde(default = "default_dust_threshold")]
1335    pub dust_threshold: i64,
1336    #[serde(default = "default_dust_threshold")]
1337    pub min_output_value: i64,
1338    #[serde(default = "default_min_fee_rate")]
1339    pub min_fee_rate: u64,
1340    #[serde(default = "default_max_witness_size")]
1341    pub max_witness_size: usize,
1342    #[serde(default = "default_max_size_value_ratio")]
1343    pub max_size_value_ratio: f64,
1344    #[serde(default = "default_max_small_outputs")]
1345    pub max_small_outputs: usize,
1346
1347    // NEW: Adaptive thresholds
1348    #[serde(default = "default_true")]
1349    pub use_adaptive_thresholds: bool,
1350    #[serde(default = "default_adaptive_thresholds")]
1351    pub adaptive_thresholds: WitnessSizeThresholdsSerializable,
1352
1353    // NEW: Taproot-specific options
1354    #[serde(default = "default_true")]
1355    pub filter_taproot_spam: bool,
1356    #[serde(default = "default_max_taproot_control_size")]
1357    pub max_taproot_control_size: usize,
1358    #[serde(default = "default_true")]
1359    pub reject_taproot_annexes: bool,
1360
1361    // NEW: Total witness size check
1362    #[serde(default = "default_false")]
1363    pub filter_large_total_witness: bool,
1364    #[serde(default = "default_max_total_witness_size")]
1365    pub max_total_witness_size: usize,
1366
1367    // NEW: Enhanced detection options
1368    #[serde(default = "default_true")]
1369    pub use_improved_envelope_detection: bool,
1370    #[serde(default = "default_true")]
1371    pub use_json_validation_brc20: bool,
1372
1373    // NEW: Fee rate calculation options
1374    #[serde(default = "default_false")]
1375    pub require_utxo_for_fee_rate: bool,
1376    #[serde(default = "default_min_fee_rate_large_tx")]
1377    pub min_fee_rate_large_tx: u64,
1378    #[serde(default = "default_large_tx_threshold_bytes")]
1379    pub large_tx_threshold_bytes: usize,
1380    #[serde(default = "default_true")]
1381    pub ordinals_strict_mode: bool,
1382}
1383
1384fn default_true() -> bool {
1385    true
1386}
1387
1388fn default_false() -> bool {
1389    false
1390}
1391
1392fn default_dust_threshold() -> i64 {
1393    546
1394}
1395
1396fn default_min_fee_rate() -> u64 {
1397    1
1398}
1399
1400fn default_max_witness_size() -> usize {
1401    1000
1402}
1403
1404fn default_max_size_value_ratio() -> f64 {
1405    1000.0
1406}
1407
1408fn default_max_small_outputs() -> usize {
1409    10
1410}
1411
1412fn default_max_taproot_control_size() -> usize {
1413    289
1414}
1415
1416fn default_max_total_witness_size() -> usize {
1417    5000
1418}
1419
1420fn default_min_fee_rate_large_tx() -> u64 {
1421    2
1422}
1423
1424fn default_large_tx_threshold_bytes() -> usize {
1425    1000
1426}
1427
1428fn default_normal_single_sig() -> usize {
1429    200
1430}
1431
1432fn default_normal_multi_sig() -> usize {
1433    500
1434}
1435
1436fn default_normal_p2wsh() -> usize {
1437    800
1438}
1439
1440fn default_suspicious_threshold() -> usize {
1441    1000
1442}
1443
1444fn default_definitely_spam() -> usize {
1445    2000
1446}
1447
1448fn default_adaptive_thresholds() -> WitnessSizeThresholdsSerializable {
1449    WitnessSizeThresholdsSerializable {
1450        normal_single_sig: 200,
1451        normal_multi_sig: 500,
1452        normal_p2wsh: 800,
1453        suspicious_threshold: 1000,
1454        definitely_spam: 2000,
1455    }
1456}
1457
1458impl Default for SpamFilterConfigSerializable {
1459    fn default() -> Self {
1460        Self {
1461            filter_ordinals: default_true(),
1462            filter_dust: default_true(),
1463            filter_brc20: default_true(),
1464            filter_large_witness: default_true(),
1465            filter_low_fee_rate: default_false(),
1466            filter_high_size_value_ratio: default_true(),
1467            filter_many_small_outputs: default_true(),
1468            dust_threshold: default_dust_threshold(),
1469            min_output_value: default_dust_threshold(),
1470            min_fee_rate: default_min_fee_rate(),
1471            max_witness_size: default_max_witness_size(),
1472            max_size_value_ratio: default_max_size_value_ratio(),
1473            max_small_outputs: default_max_small_outputs(),
1474            use_adaptive_thresholds: default_true(),
1475            adaptive_thresholds: default_adaptive_thresholds(),
1476            filter_taproot_spam: default_true(),
1477            max_taproot_control_size: default_max_taproot_control_size(),
1478            reject_taproot_annexes: default_true(),
1479            filter_large_total_witness: default_false(),
1480            max_total_witness_size: default_max_total_witness_size(),
1481            use_improved_envelope_detection: default_true(),
1482            use_json_validation_brc20: default_true(),
1483            require_utxo_for_fee_rate: default_false(),
1484            min_fee_rate_large_tx: default_min_fee_rate_large_tx(),
1485            large_tx_threshold_bytes: default_large_tx_threshold_bytes(),
1486            ordinals_strict_mode: default_true(),
1487        }
1488    }
1489}
1490
1491impl From<SpamFilterConfigSerializable> for SpamFilterConfig {
1492    fn from(serializable: SpamFilterConfigSerializable) -> Self {
1493        SpamFilterConfig {
1494            filter_ordinals: serializable.filter_ordinals,
1495            filter_dust: serializable.filter_dust,
1496            filter_brc20: serializable.filter_brc20,
1497            filter_large_witness: serializable.filter_large_witness,
1498            filter_low_fee_rate: serializable.filter_low_fee_rate,
1499            filter_high_size_value_ratio: serializable.filter_high_size_value_ratio,
1500            filter_many_small_outputs: serializable.filter_many_small_outputs,
1501            dust_threshold: serializable.dust_threshold,
1502            min_output_value: serializable.min_output_value,
1503            min_fee_rate: serializable.min_fee_rate,
1504            max_witness_size: serializable.max_witness_size,
1505            max_size_value_ratio: serializable.max_size_value_ratio,
1506            max_small_outputs: serializable.max_small_outputs,
1507            // NEW: Adaptive thresholds
1508            use_adaptive_thresholds: serializable.use_adaptive_thresholds,
1509            adaptive_thresholds: serializable.adaptive_thresholds.into(),
1510            // NEW fields
1511            filter_taproot_spam: serializable.filter_taproot_spam,
1512            max_taproot_control_size: serializable.max_taproot_control_size,
1513            reject_taproot_annexes: serializable.reject_taproot_annexes,
1514            filter_large_total_witness: serializable.filter_large_total_witness,
1515            max_total_witness_size: serializable.max_total_witness_size,
1516            use_improved_envelope_detection: serializable.use_improved_envelope_detection,
1517            use_json_validation_brc20: serializable.use_json_validation_brc20,
1518            require_utxo_for_fee_rate: serializable.require_utxo_for_fee_rate,
1519            min_fee_rate_large_tx: serializable.min_fee_rate_large_tx,
1520            large_tx_threshold_bytes: serializable.large_tx_threshold_bytes,
1521            ordinals_strict_mode: serializable.ordinals_strict_mode,
1522        }
1523    }
1524}
1525
1526impl From<SpamFilterConfig> for SpamFilterConfigSerializable {
1527    fn from(config: SpamFilterConfig) -> Self {
1528        SpamFilterConfigSerializable {
1529            filter_ordinals: config.filter_ordinals,
1530            filter_dust: config.filter_dust,
1531            filter_brc20: config.filter_brc20,
1532            filter_large_witness: config.filter_large_witness,
1533            filter_low_fee_rate: config.filter_low_fee_rate,
1534            filter_high_size_value_ratio: config.filter_high_size_value_ratio,
1535            filter_many_small_outputs: config.filter_many_small_outputs,
1536            dust_threshold: config.dust_threshold,
1537            min_output_value: config.min_output_value,
1538            min_fee_rate: config.min_fee_rate,
1539            max_witness_size: config.max_witness_size,
1540            max_size_value_ratio: config.max_size_value_ratio,
1541            max_small_outputs: config.max_small_outputs,
1542            // NEW: Adaptive thresholds
1543            use_adaptive_thresholds: config.use_adaptive_thresholds,
1544            adaptive_thresholds: config.adaptive_thresholds.into(),
1545            // NEW fields
1546            filter_taproot_spam: config.filter_taproot_spam,
1547            max_taproot_control_size: config.max_taproot_control_size,
1548            reject_taproot_annexes: config.reject_taproot_annexes,
1549            filter_large_total_witness: config.filter_large_total_witness,
1550            max_total_witness_size: config.max_total_witness_size,
1551            use_improved_envelope_detection: config.use_improved_envelope_detection,
1552            use_json_validation_brc20: config.use_json_validation_brc20,
1553            require_utxo_for_fee_rate: config.require_utxo_for_fee_rate,
1554            min_fee_rate_large_tx: config.min_fee_rate_large_tx,
1555            large_tx_threshold_bytes: config.large_tx_threshold_bytes,
1556            ordinals_strict_mode: config.ordinals_strict_mode,
1557        }
1558    }
1559}