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