1mod 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
31pub const DEFAULT_DUST_THRESHOLD: i64 = 546;
33
34pub const DEFAULT_MIN_FEE_RATE: u64 = 1;
37
38pub const DEFAULT_MAX_WITNESS_SIZE: usize = 1000;
40
41pub const DEFAULT_MAX_SIZE_VALUE_RATIO: f64 = 1000.0; #[derive(Debug, Clone, Copy, PartialEq, Eq)]
49pub enum SpamFilterPreset {
50 Disabled,
52 Conservative,
56 Moderate,
60 Aggressive,
64 StrictInscriptions,
69}
70
71impl SpamFilterPreset {
72 pub fn to_config(&self) -> SpamFilterConfig {
74 match self {
75 Self::Disabled => SpamFilterConfig {
76 filter_ordinals: false,
77 filter_dust: false,
78 filter_brc20: false,
79 filter_large_witness: false,
80 filter_low_fee_rate: false,
81 filter_high_size_value_ratio: false,
82 filter_many_small_outputs: false,
83 ..SpamFilterConfig::default()
84 },
85 Self::Conservative => SpamFilterConfig {
86 filter_ordinals: true,
87 filter_dust: true,
88 filter_brc20: true,
89 filter_large_witness: true,
90 filter_low_fee_rate: false,
91 filter_high_size_value_ratio: true,
92 filter_many_small_outputs: true,
93 max_witness_size: 2000, max_size_value_ratio: 2000.0, max_small_outputs: 20, ..SpamFilterConfig::default()
97 },
98 Self::Moderate => SpamFilterConfig::default(),
99 Self::Aggressive => SpamFilterConfig {
100 filter_ordinals: true,
101 filter_dust: true,
102 filter_brc20: true,
103 filter_large_witness: true,
104 filter_low_fee_rate: true, filter_high_size_value_ratio: true,
106 filter_many_small_outputs: true,
107 max_witness_size: 500, max_size_value_ratio: 500.0, max_small_outputs: 5, min_fee_rate: 2, ..SpamFilterConfig::default()
112 },
113 Self::StrictInscriptions => SpamFilterConfig {
114 filter_ordinals: true,
115 filter_dust: true,
116 filter_brc20: true,
117 filter_large_witness: true,
118 filter_low_fee_rate: false,
119 filter_high_size_value_ratio: true,
120 filter_many_small_outputs: true,
121 ordinals_strict_mode: true, max_witness_size: 1500, ..SpamFilterConfig::default()
124 },
125 }
126 }
127}
128
129#[derive(Debug, Clone, PartialEq, Eq)]
131pub enum SpamType {
132 Ordinals,
134 Dust,
136 BRC20,
138 LargeWitness,
140 LowFeeRate,
142 HighSizeValueRatio,
144 ManySmallOutputs,
146 NotSpam,
148}
149
150#[derive(Debug, Clone)]
155pub struct WitnessSizeThresholds {
156 pub normal_single_sig: usize,
158 pub normal_multi_sig: usize,
160 pub normal_p2wsh: usize,
162 pub suspicious_threshold: usize,
164 pub definitely_spam: usize,
166}
167
168impl Default for WitnessSizeThresholds {
169 fn default() -> Self {
170 Self {
173 normal_single_sig: 200,
174 normal_multi_sig: 500,
175 normal_p2wsh: 800,
176 suspicious_threshold: 1000,
177 definitely_spam: 2000,
178 }
179 }
180}
181
182#[derive(Debug, Clone)]
184pub struct WitnessElementAnalysis {
185 pub total_size: usize,
187 pub element_count: usize,
189 pub large_elements: usize,
191 pub medium_elements: usize,
193 pub small_elements: usize,
195 pub suspicious_pattern: bool,
197}
198
199#[derive(Debug, Clone)]
201pub struct SpamFilterConfig {
202 pub filter_ordinals: bool,
204 pub filter_dust: bool,
206 pub filter_brc20: bool,
208 pub filter_large_witness: bool,
210 pub filter_low_fee_rate: bool,
212 pub filter_high_size_value_ratio: bool,
214 pub filter_many_small_outputs: bool,
216 pub dust_threshold: i64,
218 pub min_output_value: i64,
220 pub min_fee_rate: u64,
222 pub max_witness_size: usize,
225 pub max_size_value_ratio: f64,
227 pub max_small_outputs: usize,
229
230 pub use_adaptive_thresholds: bool,
234 pub adaptive_thresholds: WitnessSizeThresholds,
236
237 pub filter_taproot_spam: bool,
241 pub max_taproot_control_size: usize,
246 pub reject_taproot_annexes: bool,
249
250 pub filter_large_total_witness: bool,
254 pub max_total_witness_size: usize,
257
258 pub use_improved_envelope_detection: bool,
262 pub use_json_validation_brc20: bool,
265
266 pub require_utxo_for_fee_rate: bool,
271 pub min_fee_rate_large_tx: u64,
275 pub large_tx_threshold_bytes: usize,
279
280 pub ordinals_strict_mode: bool,
286}
287
288impl Default for SpamFilterConfig {
289 fn default() -> Self {
290 Self {
291 filter_ordinals: true,
292 filter_dust: true,
293 filter_brc20: true,
294 filter_large_witness: true,
295 filter_low_fee_rate: false, filter_high_size_value_ratio: true,
297 filter_many_small_outputs: true,
298 dust_threshold: DEFAULT_DUST_THRESHOLD,
299 min_output_value: DEFAULT_DUST_THRESHOLD,
300 min_fee_rate: DEFAULT_MIN_FEE_RATE,
301 max_witness_size: DEFAULT_MAX_WITNESS_SIZE,
302 max_size_value_ratio: DEFAULT_MAX_SIZE_VALUE_RATIO,
303 max_small_outputs: 10, use_adaptive_thresholds: true, adaptive_thresholds: WitnessSizeThresholds::default(),
308
309 filter_taproot_spam: true,
311 max_taproot_control_size: 289, reject_taproot_annexes: true,
313 filter_large_total_witness: false, max_total_witness_size: 5000,
315 use_improved_envelope_detection: true,
316 use_json_validation_brc20: true,
317 require_utxo_for_fee_rate: false, min_fee_rate_large_tx: 2, large_tx_threshold_bytes: 1000, ordinals_strict_mode: true, }
322 }
323}
324
325#[derive(Debug, Clone)]
327pub struct SpamFilterResult {
328 pub is_spam: bool,
330 pub spam_type: SpamType,
332 pub detected_types: Vec<SpamType>,
334}
335
336#[derive(Clone)]
338pub struct SpamFilter {
339 config: SpamFilterConfig,
340 #[cfg(feature = "production")]
342 #[allow(dead_code)]
343 pub(crate) script_type_cache: std::sync::Arc<std::sync::RwLock<lru::LruCache<u64, bool>>>,
344}
345
346impl SpamFilter {
347 pub fn new() -> Self {
349 Self {
350 config: SpamFilterConfig::default(),
351 #[cfg(feature = "production")]
352 script_type_cache: std::sync::Arc::new(std::sync::RwLock::new(lru::LruCache::new(
353 std::num::NonZeroUsize::new(10_000).unwrap(),
354 ))),
355 }
356 }
357
358 pub fn with_config(config: SpamFilterConfig) -> Self {
360 Self {
361 config,
362 #[cfg(feature = "production")]
363 script_type_cache: std::sync::Arc::new(std::sync::RwLock::new(lru::LruCache::new(
364 std::num::NonZeroUsize::new(10_000).unwrap(),
365 ))),
366 }
367 }
368
369 pub fn with_preset(preset: SpamFilterPreset) -> Self {
377 Self::with_config(preset.to_config())
378 }
379
380 pub fn is_spam(&self, tx: &Transaction) -> SpamFilterResult {
384 self.is_spam_with_witness(tx, None, None)
385 }
386
387 pub fn is_spam_with_witness(
393 &self,
394 tx: &Transaction,
395 witnesses: Option<&[Witness]>,
396 utxo_set: Option<&UtxoSet>,
397 ) -> SpamFilterResult {
398 let mut detected_types = Vec::new();
399
400 if self.config.filter_ordinals && self.detect_ordinals(tx, witnesses, utxo_set) {
402 detected_types.push(SpamType::Ordinals);
403 }
404
405 if self.config.filter_dust && self.detect_dust(tx) {
407 detected_types.push(SpamType::Dust);
408 }
409
410 if self.config.filter_brc20 && self.detect_brc20(tx) {
412 detected_types.push(SpamType::BRC20);
413 }
414
415 if self.config.filter_large_witness && self.detect_large_witness(tx, witnesses, utxo_set) {
417 detected_types.push(SpamType::LargeWitness);
418 }
419
420 if self.config.filter_large_total_witness && self.detect_large_total_witness(witnesses) {
422 detected_types.push(SpamType::LargeWitness);
423 }
424
425 if self.config.filter_low_fee_rate && self.detect_low_fee_rate(tx, witnesses, utxo_set) {
427 detected_types.push(SpamType::LowFeeRate);
428 }
429
430 if self.config.filter_high_size_value_ratio
432 && self.detect_high_size_value_ratio(tx, witnesses)
433 {
434 detected_types.push(SpamType::HighSizeValueRatio);
435 }
436
437 if self.config.filter_many_small_outputs && self.detect_many_small_outputs(tx) {
439 detected_types.push(SpamType::ManySmallOutputs);
440 }
441
442 let is_spam = !detected_types.is_empty();
443 let spam_type = detected_types.first().cloned().unwrap_or(SpamType::NotSpam);
444
445 SpamFilterResult {
446 is_spam,
447 spam_type,
448 detected_types,
449 }
450 }
451
452 pub fn filter_transaction(&self, tx: &Transaction) -> Option<Transaction> {
457 let result = self.is_spam(tx);
458 if result.is_spam {
459 None } else {
461 Some(tx.clone()) }
463 }
464 fn detect_ordinals(
471 &self,
472 tx: &Transaction,
473 witnesses: Option<&[Witness]>,
474 utxo_set: Option<&UtxoSet>,
475 ) -> bool {
476 for output in &tx.outputs {
478 if self.has_ordinal_pattern(&output.script_pubkey) {
479 return true;
480 }
481 }
482
483 for input in &tx.inputs {
485 if self.has_envelope_pattern(&input.script_sig) {
486 return true;
487 }
488 }
489
490 if let Some(witnesses) = witnesses {
492 for (i, witness) in witnesses.iter().enumerate() {
493 if i >= tx.inputs.len() {
494 break;
495 }
496
497 if self.config.filter_taproot_spam {
499 for output in &tx.outputs {
500 if self.is_taproot_output(&output.script_pubkey)
501 && self.detect_taproot_spam(output, witness)
502 {
503 return true;
504 }
505 }
506 }
507
508 if self.has_envelope_in_witness(witness) {
510 return true;
511 }
512
513 if !self.config.ordinals_strict_mode {
516 if self.config.use_adaptive_thresholds {
517 if self.has_large_witness_stack_adaptive(witness, tx, i, utxo_set) {
518 return true;
519 }
520 } else if self.has_large_witness_stack(witness) {
521 return true;
522 }
523 if self.has_witness_data_pattern(witness) {
524 return true;
525 }
526 }
527 }
528 }
529
530 false
531 }
532
533 fn has_envelope_in_witness(&self, witness: &Witness) -> bool {
536 for element in witness {
537 if element.len() >= 4 && element[0] == OP_0 && element[1] == OP_IF {
538 if self.config.use_improved_envelope_detection {
539 if element.iter().skip(2).any(|&b| b == OP_ENDIF) {
540 return true;
541 }
542 } else {
543 return true;
544 }
545 }
546 }
547 false
548 }
549
550 fn is_taproot_output(&self, script_pubkey: &ByteString) -> bool {
554 script_pubkey.len() == 34 && script_pubkey[0] == OP_1 && script_pubkey[1] == PUSH_32_BYTES
556 }
557
558 fn detect_taproot_spam(
564 &self,
565 output: &blvm_consensus::types::TransactionOutput,
566 witness: &Witness,
567 ) -> bool {
568 if !self.is_taproot_output(&output.script_pubkey) {
569 return false;
570 }
571
572 if self.config.reject_taproot_annexes {
575 if let Some(last) = witness.last() {
576 if !last.is_empty() && last[0] == blvm_consensus::opcodes::OP_RESERVED {
577 return true;
579 }
580 }
581 }
582
583 if witness.len() >= 2 {
588 if let Some(control_block) = witness.last() {
591 if control_block.len() > self.config.max_taproot_control_size {
596 return true;
597 }
598 }
599 }
600
601 false
602 }
603
604 fn has_large_witness_stack(&self, witness: &Witness) -> bool {
608 let total_size = self.calculate_witness_size(witness);
609 total_size > self.config.max_witness_size
610 }
611
612 fn has_large_witness_stack_adaptive(
617 &self,
618 witness: &Witness,
619 tx: &Transaction,
620 input_index: usize,
621 utxo_set: Option<&UtxoSet>,
622 ) -> bool {
623 let total_size = self.calculate_witness_size(witness);
624
625 if !self.config.use_adaptive_thresholds {
627 return total_size > self.config.max_witness_size;
628 }
629
630 let threshold =
631 if let Some(script_type) = detect_script_type_for_input(tx, input_index, utxo_set) {
632 script_type.recommended_threshold()
633 } else {
634 self.config.max_witness_size
635 };
636
637 total_size > threshold
638 }
639
640 fn analyze_witness_elements(&self, witness: &Witness) -> WitnessElementAnalysis {
644 let total_size = self.calculate_witness_size(witness);
645 let element_count = witness.len();
646
647 let mut large_elements = 0;
648 let mut medium_elements = 0;
649 let mut small_elements = 0;
650
651 for element in witness {
652 if element.len() > 200 {
653 large_elements += 1;
654 } else if element.len() >= 100 {
655 medium_elements += 1;
656 } else {
657 small_elements += 1;
658 }
659 }
660
661 let suspicious_pattern = medium_elements >= 10;
663
664 WitnessElementAnalysis {
665 total_size,
666 element_count,
667 large_elements,
668 medium_elements,
669 small_elements,
670 suspicious_pattern,
671 }
672 }
673
674 fn calculate_witness_size(&self, witness: &Witness) -> usize {
682 let mut size = 1;
684
685 for element in witness {
687 size += if element.len() <= VARINT_1BYTE_MAX as usize {
695 1
696 } else if element.len() <= 0xffff {
697 3 } else if element.len() <= 0xffffffff {
699 5 } else {
701 9 };
703 size += element.len();
704 }
705
706 size
707 }
708
709 fn has_witness_data_pattern(&self, witness: &Witness) -> bool {
711 if witness.is_empty() {
712 return false;
713 }
714
715 for element in witness {
718 if element.len() > 200 {
721 if element.len() != 64 && (element.is_empty() || element[0] != DER_SIGNATURE_PREFIX)
724 {
725 return true;
727 }
728 }
729 }
730
731 let large_elements = witness.iter().filter(|elem| elem.len() > 100).count();
733 if large_elements >= 3 {
734 return true;
735 }
736
737 let analysis = self.analyze_witness_elements(witness);
739 if analysis.suspicious_pattern {
740 return true;
741 }
742
743 false
744 }
745
746 fn has_ordinal_pattern(&self, script: &ByteString) -> bool {
752 if script.is_empty() {
753 return false;
754 }
755
756 if script[0] == OP_RETURN && script.len() > 80 {
758 return true;
759 }
760
761 if self.has_envelope_pattern(script) {
763 return true;
764 }
765
766 false
767 }
768
769 fn has_envelope_pattern(&self, script: &ByteString) -> bool {
771 if script.len() < 4 {
773 return false;
774 }
775
776 if script[0] == OP_0 && script[1] == OP_IF {
778 if self.config.use_improved_envelope_detection {
779 if script.iter().skip(2).any(|&b| b == OP_ENDIF) {
782 return true;
783 }
784 } else {
785 return true;
787 }
788 }
789
790 false
791 }
792
793 fn detect_dust(&self, tx: &Transaction) -> bool {
797 let mut all_dust = true;
799
800 for output in &tx.outputs {
801 if output.value >= self.config.dust_threshold {
802 all_dust = false;
803 break;
804 }
805 }
806
807 all_dust && !tx.outputs.is_empty()
808 }
809
810 fn detect_large_witness(
815 &self,
816 tx: &Transaction,
817 witnesses: Option<&[Witness]>,
818 utxo_set: Option<&UtxoSet>,
819 ) -> bool {
820 if let Some(witnesses) = witnesses {
821 for (i, witness) in witnesses.iter().enumerate() {
822 if self.config.use_adaptive_thresholds {
824 if self.has_large_witness_stack_adaptive(witness, tx, i, utxo_set) {
825 return true;
826 }
827 } else if self.has_large_witness_stack(witness) {
828 return true;
829 }
830 }
831 }
832 false
833 }
834
835 fn detect_low_fee_rate(
840 &self,
841 tx: &Transaction,
842 witnesses: Option<&[Witness]>,
843 utxo_set: Option<&UtxoSet>,
844 ) -> bool {
845 let tx_size = self.estimate_transaction_size_with_witness(tx, witnesses);
846
847 if self.config.require_utxo_for_fee_rate && utxo_set.is_none() {
849 return true; }
852
853 let fee_rate = if let Some(utxo_set) = utxo_set {
855 self.calculate_fee_rate_accurate(tx, utxo_set, tx_size)
856 } else {
857 return false;
860 };
861
862 let threshold = if tx_size > self.config.large_tx_threshold_bytes {
864 self.config.min_fee_rate_large_tx
865 } else {
866 self.config.min_fee_rate
867 };
868
869 fee_rate < threshold
870 }
871
872 fn calculate_fee_rate_accurate(
874 &self,
875 tx: &Transaction,
876 utxo_set: &UtxoSet,
877 tx_size: usize,
878 ) -> u64 {
879 if tx_size == 0 {
880 return 0;
881 }
882
883 let mut input_total = 0u64;
885 for input in &tx.inputs {
886 if let Some(utxo) = utxo_set.get(&input.prevout) {
887 input_total += utxo.value as u64;
888 }
889 }
890
891 let output_total: u64 = tx.outputs.iter().map(|out| out.value as u64).sum();
892 let fee = input_total.saturating_sub(output_total);
893
894 if tx_size > 0 { fee / tx_size as u64 } else { 0 }
896 }
897
898 #[allow(dead_code)]
900 fn calculate_fee_rate_heuristic(&self, tx: &Transaction, tx_size: usize) -> u64 {
901 if tx_size == 0 {
902 return 0;
903 }
904
905 let total_output_value: i64 = tx.outputs.iter().map(|out| out.value).sum();
906
907 if tx_size > 1000 && total_output_value < 10000 {
909 1000u64.saturating_div(tx_size as u64)
911 } else {
912 self.config.min_fee_rate
915 }
916 }
917
918 fn detect_large_total_witness(&self, witnesses: Option<&[Witness]>) -> bool {
920 if !self.config.filter_large_total_witness {
921 return false; }
923
924 if let Some(witnesses) = witnesses {
925 let total_size: usize = witnesses
926 .iter()
927 .map(|w| self.calculate_witness_size(w))
928 .sum();
929
930 total_size > self.config.max_total_witness_size
931 } else {
932 false
933 }
934 }
935
936 fn detect_high_size_value_ratio(
942 &self,
943 tx: &Transaction,
944 witnesses: Option<&[Witness]>,
945 ) -> bool {
946 let tx_size = self.estimate_transaction_size_with_witness(tx, witnesses) as f64;
947 let total_output_value: f64 = tx.outputs.iter().map(|out| out.value as f64).sum();
948
949 if total_output_value <= 0.0 {
951 return tx_size > 1000.0;
953 }
954
955 let ratio = tx_size / total_output_value;
956
957 let threshold = if self.config.use_adaptive_thresholds {
959 let tx_type = TransactionType::detect(tx);
960 tx_type.recommended_size_value_ratio()
961 } else {
962 self.config.max_size_value_ratio
963 };
964
965 ratio > threshold
966 }
967
968 fn detect_many_small_outputs(&self, tx: &Transaction) -> bool {
972 let small_output_count = tx
973 .outputs
974 .iter()
975 .filter(|out| out.value < self.config.dust_threshold)
976 .count();
977
978 small_output_count > self.config.max_small_outputs
979 }
980
981 fn estimate_transaction_size_with_witness(
983 &self,
984 tx: &Transaction,
985 witnesses: Option<&[Witness]>,
986 ) -> usize {
987 let base_size = estimate_transaction_size(tx) as usize;
989
990 if let Some(witnesses) = witnesses {
992 let witness_size: usize = witnesses
993 .iter()
994 .map(|witness| {
995 let mut size = 1;
997 for element in witness {
999 size += 1; size += element.len();
1001 }
1002 size
1003 })
1004 .sum();
1005
1006 let has_witness = witness_size > 0;
1008 if has_witness {
1009 base_size + 2 + witness_size
1010 } else {
1011 base_size
1012 }
1013 } else {
1014 base_size
1015 }
1016 }
1017
1018 fn detect_brc20(&self, tx: &Transaction) -> bool {
1024 for output in &tx.outputs {
1026 if self.has_brc20_pattern(&output.script_pubkey) {
1027 return true;
1028 }
1029 }
1030
1031 false
1032 }
1033
1034 fn has_brc20_pattern(&self, script: &ByteString) -> bool {
1041 if script.len() < 20 {
1042 return false;
1043 }
1044
1045 if script[0] != OP_RETURN {
1047 return false;
1048 }
1049
1050 let data = &script[1..];
1052
1053 let script_str = match String::from_utf8(data.to_vec()) {
1055 Ok(s) => s,
1056 Err(_) => {
1057 return self.has_brc20_pattern_simple(data);
1059 }
1060 };
1061
1062 if self.config.use_json_validation_brc20 {
1064 self.has_brc20_pattern_json(&script_str)
1065 } else {
1066 self.has_brc20_pattern_simple(data)
1068 }
1069 }
1070
1071 fn has_brc20_pattern_json(&self, json_str: &str) -> bool {
1073 let cleaned: String = json_str.chars().filter(|c| !c.is_whitespace()).collect();
1075
1076 if let Ok(json_value) = serde_json::from_str::<serde_json::Value>(&cleaned) {
1078 if let Some(obj) = json_value.as_object() {
1080 if let Some(protocol) = obj.get("p") {
1082 if protocol.as_str() == Some("brc-20") {
1083 if let Some(op) = obj.get("op") {
1085 if let Some(op_str) = op.as_str() {
1086 return matches!(op_str, "mint" | "transfer" | "deploy");
1087 }
1088 }
1089 }
1090 }
1091 }
1092 }
1093
1094 if let Ok(json_value) = serde_json::from_str::<serde_json::Value>(json_str) {
1096 if let Some(obj) = json_value.as_object() {
1097 if let Some(protocol) = obj.get("p") {
1098 if protocol.as_str() == Some("brc-20") {
1099 if let Some(op) = obj.get("op") {
1100 if let Some(op_str) = op.as_str() {
1101 return matches!(op_str, "mint" | "transfer" | "deploy");
1102 }
1103 }
1104 }
1105 }
1106 }
1107 }
1108
1109 false
1110 }
1111
1112 fn has_brc20_pattern_simple(&self, data: &[u8]) -> bool {
1114 if let Ok(script_str) = String::from_utf8(data.to_vec()) {
1116 let lower = script_str.to_lowercase();
1118 lower.contains("brc-20")
1119 || lower.contains("\"p\":\"brc-20\"")
1120 || lower.contains("op\":\"mint")
1121 || lower.contains("op\":\"transfer")
1122 || lower.contains("op\":\"deploy")
1123 } else {
1124 let pattern = b"brc-20";
1127 let pattern_lower = b"BRC-20";
1128 data.windows(pattern.len())
1129 .any(|window| window == pattern || window == pattern_lower)
1130 }
1131 }
1132
1133 pub fn filter_block(&self, transactions: &[Transaction]) -> (Vec<Transaction>, SpamSummary) {
1149 self.filter_block_with_witness(transactions, None)
1150 }
1151
1152 pub fn filter_block_with_witness(
1169 &self,
1170 transactions: &[Transaction],
1171 witnesses: Option<&[Vec<Witness>]>,
1172 ) -> (Vec<Transaction>, SpamSummary) {
1173 let mut filtered_txs = Vec::new();
1174 let mut filtered_count = 0u32;
1175 let mut filtered_size = 0u64;
1176 let mut spam_breakdown = SpamBreakdown::default();
1177
1178 for (i, tx) in transactions.iter().enumerate() {
1179 let tx_witnesses = witnesses.and_then(|w| w.get(i));
1181
1182 let result = if let Some(tx_witnesses) = tx_witnesses {
1183 self.is_spam_with_witness(tx, Some(tx_witnesses), None)
1184 } else {
1185 self.is_spam(tx)
1186 };
1187
1188 if result.is_spam {
1189 filtered_count += 1;
1190 let tx_size = if let Some(tx_witnesses) = tx_witnesses {
1191 self.estimate_transaction_size_with_witness(tx, Some(tx_witnesses)) as u64
1192 } else {
1193 estimate_transaction_size(tx)
1194 };
1195 filtered_size += tx_size;
1196
1197 for spam_type in &result.detected_types {
1199 match spam_type {
1200 SpamType::Ordinals => spam_breakdown.ordinals += 1,
1201 SpamType::Dust => spam_breakdown.dust += 1,
1202 SpamType::BRC20 => spam_breakdown.brc20 += 1,
1203 SpamType::LargeWitness => spam_breakdown.ordinals += 1, SpamType::LowFeeRate => spam_breakdown.dust += 1, SpamType::HighSizeValueRatio => spam_breakdown.ordinals += 1, SpamType::ManySmallOutputs => spam_breakdown.dust += 1, SpamType::NotSpam => {}
1208 }
1209 }
1210 } else {
1211 filtered_txs.push(tx.clone());
1212 }
1213 }
1214
1215 let summary = SpamSummary {
1216 filtered_count,
1217 filtered_size,
1218 by_type: spam_breakdown,
1219 };
1220
1221 (filtered_txs, summary)
1222 }
1223}
1224
1225impl Default for SpamFilter {
1226 fn default() -> Self {
1227 Self::new()
1228 }
1229}
1230
1231#[derive(Debug, Clone, Default)]
1233pub struct SpamSummary {
1234 pub filtered_count: u32,
1236 pub filtered_size: u64,
1238 pub by_type: SpamBreakdown,
1240}
1241
1242#[derive(Debug, Clone, Default)]
1244pub struct SpamBreakdown {
1245 pub ordinals: u32,
1246 pub inscriptions: u32,
1247 pub dust: u32,
1248 pub brc20: u32,
1249}
1250
1251fn estimate_transaction_size(tx: &Transaction) -> u64 {
1253 blvm_consensus::transaction::calculate_transaction_size(tx) as u64
1254}
1255
1256fn detect_script_type_for_input(
1258 tx: &Transaction,
1259 input_index: usize,
1260 utxo_set: Option<&UtxoSet>,
1261) -> Option<ScriptType> {
1262 if input_index >= tx.inputs.len() {
1263 return None;
1264 }
1265
1266 if let Some(utxo_set) = utxo_set {
1267 if let Some(utxo) = utxo_set.get(&tx.inputs[input_index].prevout) {
1268 let spk: ByteString = utxo.script_pubkey.as_ref().to_vec();
1269 let script_type = ScriptType::detect(&spk);
1270 if script_type != ScriptType::Unknown {
1271 return Some(script_type);
1272 }
1273 }
1274 }
1275
1276 detect_input_script_type(&tx.inputs[input_index].script_sig)
1277}
1278
1279#[derive(Debug, Clone, PartialEq, Serialize, Deserialize)]
1281pub struct WitnessSizeThresholdsSerializable {
1282 #[serde(default = "default_normal_single_sig")]
1283 pub normal_single_sig: usize,
1284 #[serde(default = "default_normal_multi_sig")]
1285 pub normal_multi_sig: usize,
1286 #[serde(default = "default_normal_p2wsh")]
1287 pub normal_p2wsh: usize,
1288 #[serde(default = "default_suspicious_threshold")]
1289 pub suspicious_threshold: usize,
1290 #[serde(default = "default_definitely_spam")]
1291 pub definitely_spam: usize,
1292}
1293
1294impl From<WitnessSizeThresholdsSerializable> for WitnessSizeThresholds {
1295 fn from(serializable: WitnessSizeThresholdsSerializable) -> Self {
1296 WitnessSizeThresholds {
1297 normal_single_sig: serializable.normal_single_sig,
1298 normal_multi_sig: serializable.normal_multi_sig,
1299 normal_p2wsh: serializable.normal_p2wsh,
1300 suspicious_threshold: serializable.suspicious_threshold,
1301 definitely_spam: serializable.definitely_spam,
1302 }
1303 }
1304}
1305
1306impl From<WitnessSizeThresholds> for WitnessSizeThresholdsSerializable {
1307 fn from(thresholds: WitnessSizeThresholds) -> Self {
1308 WitnessSizeThresholdsSerializable {
1309 normal_single_sig: thresholds.normal_single_sig,
1310 normal_multi_sig: thresholds.normal_multi_sig,
1311 normal_p2wsh: thresholds.normal_p2wsh,
1312 suspicious_threshold: thresholds.suspicious_threshold,
1313 definitely_spam: thresholds.definitely_spam,
1314 }
1315 }
1316}
1317
1318#[derive(Debug, Clone, PartialEq, Serialize, Deserialize)]
1320pub struct SpamFilterConfigSerializable {
1321 #[serde(default = "default_true")]
1322 pub filter_ordinals: bool,
1323 #[serde(default = "default_true")]
1324 pub filter_dust: bool,
1325 #[serde(default = "default_true")]
1326 pub filter_brc20: bool,
1327 #[serde(default = "default_true")]
1328 pub filter_large_witness: bool,
1329 #[serde(default = "default_false")]
1330 pub filter_low_fee_rate: bool,
1331 #[serde(default = "default_true")]
1332 pub filter_high_size_value_ratio: bool,
1333 #[serde(default = "default_true")]
1334 pub filter_many_small_outputs: bool,
1335 #[serde(default = "default_dust_threshold")]
1336 pub dust_threshold: i64,
1337 #[serde(default = "default_dust_threshold")]
1338 pub min_output_value: i64,
1339 #[serde(default = "default_min_fee_rate")]
1340 pub min_fee_rate: u64,
1341 #[serde(default = "default_max_witness_size")]
1342 pub max_witness_size: usize,
1343 #[serde(default = "default_max_size_value_ratio")]
1344 pub max_size_value_ratio: f64,
1345 #[serde(default = "default_max_small_outputs")]
1346 pub max_small_outputs: usize,
1347
1348 #[serde(default = "default_true")]
1350 pub use_adaptive_thresholds: bool,
1351 #[serde(default = "default_adaptive_thresholds")]
1352 pub adaptive_thresholds: WitnessSizeThresholdsSerializable,
1353
1354 #[serde(default = "default_true")]
1356 pub filter_taproot_spam: bool,
1357 #[serde(default = "default_max_taproot_control_size")]
1358 pub max_taproot_control_size: usize,
1359 #[serde(default = "default_true")]
1360 pub reject_taproot_annexes: bool,
1361
1362 #[serde(default = "default_false")]
1364 pub filter_large_total_witness: bool,
1365 #[serde(default = "default_max_total_witness_size")]
1366 pub max_total_witness_size: usize,
1367
1368 #[serde(default = "default_true")]
1370 pub use_improved_envelope_detection: bool,
1371 #[serde(default = "default_true")]
1372 pub use_json_validation_brc20: bool,
1373
1374 #[serde(default = "default_false")]
1376 pub require_utxo_for_fee_rate: bool,
1377 #[serde(default = "default_min_fee_rate_large_tx")]
1378 pub min_fee_rate_large_tx: u64,
1379 #[serde(default = "default_large_tx_threshold_bytes")]
1380 pub large_tx_threshold_bytes: usize,
1381 #[serde(default = "default_true")]
1382 pub ordinals_strict_mode: bool,
1383}
1384
1385fn default_true() -> bool {
1386 true
1387}
1388
1389fn default_false() -> bool {
1390 false
1391}
1392
1393fn default_dust_threshold() -> i64 {
1394 546
1395}
1396
1397fn default_min_fee_rate() -> u64 {
1398 1
1399}
1400
1401fn default_max_witness_size() -> usize {
1402 1000
1403}
1404
1405fn default_max_size_value_ratio() -> f64 {
1406 1000.0
1407}
1408
1409fn default_max_small_outputs() -> usize {
1410 10
1411}
1412
1413fn default_max_taproot_control_size() -> usize {
1414 289
1415}
1416
1417fn default_max_total_witness_size() -> usize {
1418 5000
1419}
1420
1421fn default_min_fee_rate_large_tx() -> u64 {
1422 2
1423}
1424
1425fn default_large_tx_threshold_bytes() -> usize {
1426 1000
1427}
1428
1429fn default_normal_single_sig() -> usize {
1430 200
1431}
1432
1433fn default_normal_multi_sig() -> usize {
1434 500
1435}
1436
1437fn default_normal_p2wsh() -> usize {
1438 800
1439}
1440
1441fn default_suspicious_threshold() -> usize {
1442 1000
1443}
1444
1445fn default_definitely_spam() -> usize {
1446 2000
1447}
1448
1449fn default_adaptive_thresholds() -> WitnessSizeThresholdsSerializable {
1450 WitnessSizeThresholdsSerializable {
1451 normal_single_sig: 200,
1452 normal_multi_sig: 500,
1453 normal_p2wsh: 800,
1454 suspicious_threshold: 1000,
1455 definitely_spam: 2000,
1456 }
1457}
1458
1459impl Default for SpamFilterConfigSerializable {
1460 fn default() -> Self {
1461 Self {
1462 filter_ordinals: default_true(),
1463 filter_dust: default_true(),
1464 filter_brc20: default_true(),
1465 filter_large_witness: default_true(),
1466 filter_low_fee_rate: default_false(),
1467 filter_high_size_value_ratio: default_true(),
1468 filter_many_small_outputs: default_true(),
1469 dust_threshold: default_dust_threshold(),
1470 min_output_value: default_dust_threshold(),
1471 min_fee_rate: default_min_fee_rate(),
1472 max_witness_size: default_max_witness_size(),
1473 max_size_value_ratio: default_max_size_value_ratio(),
1474 max_small_outputs: default_max_small_outputs(),
1475 use_adaptive_thresholds: default_true(),
1476 adaptive_thresholds: default_adaptive_thresholds(),
1477 filter_taproot_spam: default_true(),
1478 max_taproot_control_size: default_max_taproot_control_size(),
1479 reject_taproot_annexes: default_true(),
1480 filter_large_total_witness: default_false(),
1481 max_total_witness_size: default_max_total_witness_size(),
1482 use_improved_envelope_detection: default_true(),
1483 use_json_validation_brc20: default_true(),
1484 require_utxo_for_fee_rate: default_false(),
1485 min_fee_rate_large_tx: default_min_fee_rate_large_tx(),
1486 large_tx_threshold_bytes: default_large_tx_threshold_bytes(),
1487 ordinals_strict_mode: default_true(),
1488 }
1489 }
1490}
1491
1492impl From<SpamFilterConfigSerializable> for SpamFilterConfig {
1493 fn from(serializable: SpamFilterConfigSerializable) -> Self {
1494 SpamFilterConfig {
1495 filter_ordinals: serializable.filter_ordinals,
1496 filter_dust: serializable.filter_dust,
1497 filter_brc20: serializable.filter_brc20,
1498 filter_large_witness: serializable.filter_large_witness,
1499 filter_low_fee_rate: serializable.filter_low_fee_rate,
1500 filter_high_size_value_ratio: serializable.filter_high_size_value_ratio,
1501 filter_many_small_outputs: serializable.filter_many_small_outputs,
1502 dust_threshold: serializable.dust_threshold,
1503 min_output_value: serializable.min_output_value,
1504 min_fee_rate: serializable.min_fee_rate,
1505 max_witness_size: serializable.max_witness_size,
1506 max_size_value_ratio: serializable.max_size_value_ratio,
1507 max_small_outputs: serializable.max_small_outputs,
1508 use_adaptive_thresholds: serializable.use_adaptive_thresholds,
1510 adaptive_thresholds: serializable.adaptive_thresholds.into(),
1511 filter_taproot_spam: serializable.filter_taproot_spam,
1513 max_taproot_control_size: serializable.max_taproot_control_size,
1514 reject_taproot_annexes: serializable.reject_taproot_annexes,
1515 filter_large_total_witness: serializable.filter_large_total_witness,
1516 max_total_witness_size: serializable.max_total_witness_size,
1517 use_improved_envelope_detection: serializable.use_improved_envelope_detection,
1518 use_json_validation_brc20: serializable.use_json_validation_brc20,
1519 require_utxo_for_fee_rate: serializable.require_utxo_for_fee_rate,
1520 min_fee_rate_large_tx: serializable.min_fee_rate_large_tx,
1521 large_tx_threshold_bytes: serializable.large_tx_threshold_bytes,
1522 ordinals_strict_mode: serializable.ordinals_strict_mode,
1523 }
1524 }
1525}
1526
1527impl From<SpamFilterConfig> for SpamFilterConfigSerializable {
1528 fn from(config: SpamFilterConfig) -> Self {
1529 SpamFilterConfigSerializable {
1530 filter_ordinals: config.filter_ordinals,
1531 filter_dust: config.filter_dust,
1532 filter_brc20: config.filter_brc20,
1533 filter_large_witness: config.filter_large_witness,
1534 filter_low_fee_rate: config.filter_low_fee_rate,
1535 filter_high_size_value_ratio: config.filter_high_size_value_ratio,
1536 filter_many_small_outputs: config.filter_many_small_outputs,
1537 dust_threshold: config.dust_threshold,
1538 min_output_value: config.min_output_value,
1539 min_fee_rate: config.min_fee_rate,
1540 max_witness_size: config.max_witness_size,
1541 max_size_value_ratio: config.max_size_value_ratio,
1542 max_small_outputs: config.max_small_outputs,
1543 use_adaptive_thresholds: config.use_adaptive_thresholds,
1545 adaptive_thresholds: config.adaptive_thresholds.into(),
1546 filter_taproot_spam: config.filter_taproot_spam,
1548 max_taproot_control_size: config.max_taproot_control_size,
1549 reject_taproot_annexes: config.reject_taproot_annexes,
1550 filter_large_total_witness: config.filter_large_total_witness,
1551 max_total_witness_size: config.max_total_witness_size,
1552 use_improved_envelope_detection: config.use_improved_envelope_detection,
1553 use_json_validation_brc20: config.use_json_validation_brc20,
1554 require_utxo_for_fee_rate: config.require_utxo_for_fee_rate,
1555 min_fee_rate_large_tx: config.min_fee_rate_large_tx,
1556 large_tx_threshold_bytes: config.large_tx_threshold_bytes,
1557 ordinals_strict_mode: config.ordinals_strict_mode,
1558 }
1559 }
1560}