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