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 {
595 return true;
596 }
597 }
598 }
599
600 false
601 }
602
603 fn has_large_witness_stack(&self, witness: &Witness) -> bool {
607 let total_size = self.calculate_witness_size(witness);
608 total_size > self.config.max_witness_size
609 }
610
611 fn has_large_witness_stack_adaptive(
616 &self,
617 witness: &Witness,
618 tx: &Transaction,
619 input_index: usize,
620 utxo_set: Option<&UtxoSet>,
621 ) -> bool {
622 let total_size = self.calculate_witness_size(witness);
623
624 if !self.config.use_adaptive_thresholds {
626 return total_size > self.config.max_witness_size;
627 }
628
629 let threshold =
630 if let Some(script_type) = detect_script_type_for_input(tx, input_index, utxo_set) {
631 script_type.recommended_threshold()
632 } else {
633 self.config.max_witness_size
634 };
635
636 total_size > threshold
637 }
638
639 fn analyze_witness_elements(&self, witness: &Witness) -> WitnessElementAnalysis {
643 let total_size = self.calculate_witness_size(witness);
644 let element_count = witness.len();
645
646 let mut large_elements = 0;
647 let mut medium_elements = 0;
648 let mut small_elements = 0;
649
650 for element in witness {
651 if element.len() > 200 {
652 large_elements += 1;
653 } else if element.len() >= 100 {
654 medium_elements += 1;
655 } else {
656 small_elements += 1;
657 }
658 }
659
660 let suspicious_pattern = medium_elements >= 10;
662
663 WitnessElementAnalysis {
664 total_size,
665 element_count,
666 large_elements,
667 medium_elements,
668 small_elements,
669 suspicious_pattern,
670 }
671 }
672
673 fn calculate_witness_size(&self, witness: &Witness) -> usize {
681 let mut size = 1;
683
684 for element in witness {
686 size += if element.len() <= VARINT_1BYTE_MAX as usize {
694 1
695 } else if element.len() <= 0xffff {
696 3 } else if element.len() <= 0xffffffff {
698 5 } else {
700 9 };
702 size += element.len();
703 }
704
705 size
706 }
707
708 fn has_witness_data_pattern(&self, witness: &Witness) -> bool {
710 if witness.is_empty() {
711 return false;
712 }
713
714 for element in witness {
717 if element.len() > 200 {
720 if element.len() != 64 && (element.is_empty() || element[0] != DER_SIGNATURE_PREFIX)
723 {
724 return true;
726 }
727 }
728 }
729
730 let large_elements = witness.iter().filter(|elem| elem.len() > 100).count();
732 if large_elements >= 3 {
733 return true;
734 }
735
736 let analysis = self.analyze_witness_elements(witness);
738 if analysis.suspicious_pattern {
739 return true;
740 }
741
742 false
743 }
744
745 fn has_ordinal_pattern(&self, script: &ByteString) -> bool {
751 if script.is_empty() {
752 return false;
753 }
754
755 if script[0] == OP_RETURN && script.len() > 80 {
757 return true;
758 }
759
760 if self.has_envelope_pattern(script) {
762 return true;
763 }
764
765 false
766 }
767
768 fn has_envelope_pattern(&self, script: &ByteString) -> bool {
770 if script.len() < 4 {
772 return false;
773 }
774
775 if script[0] == OP_0 && script[1] == OP_IF {
777 if self.config.use_improved_envelope_detection {
778 if script.iter().skip(2).any(|&b| b == OP_ENDIF) {
781 return true;
782 }
783 } else {
784 return true;
786 }
787 }
788
789 false
790 }
791
792 fn detect_dust(&self, tx: &Transaction) -> bool {
796 let mut all_dust = true;
798
799 for output in &tx.outputs {
800 if output.value >= self.config.dust_threshold {
801 all_dust = false;
802 break;
803 }
804 }
805
806 all_dust && !tx.outputs.is_empty()
807 }
808
809 fn detect_large_witness(
814 &self,
815 tx: &Transaction,
816 witnesses: Option<&[Witness]>,
817 utxo_set: Option<&UtxoSet>,
818 ) -> bool {
819 if let Some(witnesses) = witnesses {
820 for (i, witness) in witnesses.iter().enumerate() {
821 if self.config.use_adaptive_thresholds {
823 if self.has_large_witness_stack_adaptive(witness, tx, i, utxo_set) {
824 return true;
825 }
826 } else if self.has_large_witness_stack(witness) {
827 return true;
828 }
829 }
830 }
831 false
832 }
833
834 fn detect_low_fee_rate(
839 &self,
840 tx: &Transaction,
841 witnesses: Option<&[Witness]>,
842 utxo_set: Option<&UtxoSet>,
843 ) -> bool {
844 let tx_size = self.estimate_transaction_size_with_witness(tx, witnesses);
845
846 if self.config.require_utxo_for_fee_rate && utxo_set.is_none() {
848 return true; }
851
852 let fee_rate = if let Some(utxo_set) = utxo_set {
854 self.calculate_fee_rate_accurate(tx, utxo_set, tx_size)
855 } else {
856 return false;
859 };
860
861 let threshold = if tx_size > self.config.large_tx_threshold_bytes {
863 self.config.min_fee_rate_large_tx
864 } else {
865 self.config.min_fee_rate
866 };
867
868 fee_rate < threshold
869 }
870
871 fn calculate_fee_rate_accurate(
873 &self,
874 tx: &Transaction,
875 utxo_set: &UtxoSet,
876 tx_size: usize,
877 ) -> u64 {
878 if tx_size == 0 {
879 return 0;
880 }
881
882 let mut input_total = 0u64;
884 for input in &tx.inputs {
885 if let Some(utxo) = utxo_set.get(&input.prevout) {
886 input_total += utxo.value as u64;
887 }
888 }
889
890 let output_total: u64 = tx.outputs.iter().map(|out| out.value as u64).sum();
891 let fee = input_total.saturating_sub(output_total);
892
893 if tx_size > 0 { fee / tx_size as u64 } else { 0 }
895 }
896
897 #[allow(dead_code)]
899 fn calculate_fee_rate_heuristic(&self, tx: &Transaction, tx_size: usize) -> u64 {
900 if tx_size == 0 {
901 return 0;
902 }
903
904 let total_output_value: i64 = tx.outputs.iter().map(|out| out.value).sum();
905
906 if tx_size > 1000 && total_output_value < 10000 {
908 1000u64.saturating_div(tx_size as u64)
910 } else {
911 self.config.min_fee_rate
914 }
915 }
916
917 fn detect_large_total_witness(&self, witnesses: Option<&[Witness]>) -> bool {
919 if !self.config.filter_large_total_witness {
920 return false; }
922
923 if let Some(witnesses) = witnesses {
924 let total_size: usize = witnesses
925 .iter()
926 .map(|w| self.calculate_witness_size(w))
927 .sum();
928
929 total_size > self.config.max_total_witness_size
930 } else {
931 false
932 }
933 }
934
935 fn detect_high_size_value_ratio(
941 &self,
942 tx: &Transaction,
943 witnesses: Option<&[Witness]>,
944 ) -> bool {
945 let tx_size = self.estimate_transaction_size_with_witness(tx, witnesses) as f64;
946 let total_output_value: f64 = tx.outputs.iter().map(|out| out.value as f64).sum();
947
948 if total_output_value <= 0.0 {
950 return tx_size > 1000.0;
952 }
953
954 let ratio = tx_size / total_output_value;
955
956 let threshold = if self.config.use_adaptive_thresholds {
958 let tx_type = TransactionType::detect(tx);
959 tx_type.recommended_size_value_ratio()
960 } else {
961 self.config.max_size_value_ratio
962 };
963
964 ratio > threshold
965 }
966
967 fn detect_many_small_outputs(&self, tx: &Transaction) -> bool {
971 let small_output_count = tx
972 .outputs
973 .iter()
974 .filter(|out| out.value < self.config.dust_threshold)
975 .count();
976
977 small_output_count > self.config.max_small_outputs
978 }
979
980 fn estimate_transaction_size_with_witness(
982 &self,
983 tx: &Transaction,
984 witnesses: Option<&[Witness]>,
985 ) -> usize {
986 let base_size = estimate_transaction_size(tx) as usize;
988
989 if let Some(witnesses) = witnesses {
991 let witness_size: usize = witnesses
992 .iter()
993 .map(|witness| {
994 let mut size = 1;
996 for element in witness {
998 size += 1; size += element.len();
1000 }
1001 size
1002 })
1003 .sum();
1004
1005 let has_witness = witness_size > 0;
1007 if has_witness {
1008 base_size + 2 + witness_size
1009 } else {
1010 base_size
1011 }
1012 } else {
1013 base_size
1014 }
1015 }
1016
1017 fn detect_brc20(&self, tx: &Transaction) -> bool {
1023 for output in &tx.outputs {
1025 if self.has_brc20_pattern(&output.script_pubkey) {
1026 return true;
1027 }
1028 }
1029
1030 false
1031 }
1032
1033 fn has_brc20_pattern(&self, script: &ByteString) -> bool {
1040 if script.len() < 20 {
1041 return false;
1042 }
1043
1044 if script[0] != OP_RETURN {
1046 return false;
1047 }
1048
1049 let data = &script[1..];
1051
1052 let script_str = match String::from_utf8(data.to_vec()) {
1054 Ok(s) => s,
1055 Err(_) => {
1056 return self.has_brc20_pattern_simple(data);
1058 }
1059 };
1060
1061 if self.config.use_json_validation_brc20 {
1063 self.has_brc20_pattern_json(&script_str)
1064 } else {
1065 self.has_brc20_pattern_simple(data)
1067 }
1068 }
1069
1070 fn has_brc20_pattern_json(&self, json_str: &str) -> bool {
1072 let cleaned: String = json_str.chars().filter(|c| !c.is_whitespace()).collect();
1074
1075 if let Ok(json_value) = serde_json::from_str::<serde_json::Value>(&cleaned) {
1077 if let Some(obj) = json_value.as_object() {
1079 if let Some(protocol) = obj.get("p") {
1081 if protocol.as_str() == Some("brc-20") {
1082 if let Some(op) = obj.get("op") {
1084 if let Some(op_str) = op.as_str() {
1085 return matches!(op_str, "mint" | "transfer" | "deploy");
1086 }
1087 }
1088 }
1089 }
1090 }
1091 }
1092
1093 if let Ok(json_value) = serde_json::from_str::<serde_json::Value>(json_str) {
1095 if let Some(obj) = json_value.as_object() {
1096 if let Some(protocol) = obj.get("p") {
1097 if protocol.as_str() == Some("brc-20") {
1098 if let Some(op) = obj.get("op") {
1099 if let Some(op_str) = op.as_str() {
1100 return matches!(op_str, "mint" | "transfer" | "deploy");
1101 }
1102 }
1103 }
1104 }
1105 }
1106 }
1107
1108 false
1109 }
1110
1111 fn has_brc20_pattern_simple(&self, data: &[u8]) -> bool {
1113 if let Ok(script_str) = String::from_utf8(data.to_vec()) {
1115 let lower = script_str.to_lowercase();
1117 lower.contains("brc-20")
1118 || lower.contains("\"p\":\"brc-20\"")
1119 || lower.contains("op\":\"mint")
1120 || lower.contains("op\":\"transfer")
1121 || lower.contains("op\":\"deploy")
1122 } else {
1123 let pattern = b"brc-20";
1126 let pattern_lower = b"BRC-20";
1127 data.windows(pattern.len())
1128 .any(|window| window == pattern || window == pattern_lower)
1129 }
1130 }
1131
1132 pub fn filter_block(&self, transactions: &[Transaction]) -> (Vec<Transaction>, SpamSummary) {
1148 self.filter_block_with_witness(transactions, None)
1149 }
1150
1151 pub fn filter_block_with_witness(
1168 &self,
1169 transactions: &[Transaction],
1170 witnesses: Option<&[Vec<Witness>]>,
1171 ) -> (Vec<Transaction>, SpamSummary) {
1172 let mut filtered_txs = Vec::new();
1173 let mut filtered_count = 0u32;
1174 let mut filtered_size = 0u64;
1175 let mut spam_breakdown = SpamBreakdown::default();
1176
1177 for (i, tx) in transactions.iter().enumerate() {
1178 let tx_witnesses = witnesses.and_then(|w| w.get(i));
1180
1181 let result = if let Some(tx_witnesses) = tx_witnesses {
1182 self.is_spam_with_witness(tx, Some(tx_witnesses), None)
1183 } else {
1184 self.is_spam(tx)
1185 };
1186
1187 if result.is_spam {
1188 filtered_count += 1;
1189 let tx_size = if let Some(tx_witnesses) = tx_witnesses {
1190 self.estimate_transaction_size_with_witness(tx, Some(tx_witnesses)) as u64
1191 } else {
1192 estimate_transaction_size(tx)
1193 };
1194 filtered_size += tx_size;
1195
1196 for spam_type in &result.detected_types {
1198 match spam_type {
1199 SpamType::Ordinals => spam_breakdown.ordinals += 1,
1200 SpamType::Dust => spam_breakdown.dust += 1,
1201 SpamType::BRC20 => spam_breakdown.brc20 += 1,
1202 SpamType::LargeWitness => spam_breakdown.ordinals += 1, SpamType::LowFeeRate => spam_breakdown.dust += 1, SpamType::HighSizeValueRatio => spam_breakdown.ordinals += 1, SpamType::ManySmallOutputs => spam_breakdown.dust += 1, SpamType::NotSpam => {}
1207 }
1208 }
1209 } else {
1210 filtered_txs.push(tx.clone());
1211 }
1212 }
1213
1214 let summary = SpamSummary {
1215 filtered_count,
1216 filtered_size,
1217 by_type: spam_breakdown,
1218 };
1219
1220 (filtered_txs, summary)
1221 }
1222}
1223
1224impl Default for SpamFilter {
1225 fn default() -> Self {
1226 Self::new()
1227 }
1228}
1229
1230#[derive(Debug, Clone, Default)]
1232pub struct SpamSummary {
1233 pub filtered_count: u32,
1235 pub filtered_size: u64,
1237 pub by_type: SpamBreakdown,
1239}
1240
1241#[derive(Debug, Clone, Default)]
1243pub struct SpamBreakdown {
1244 pub ordinals: u32,
1245 pub inscriptions: u32,
1246 pub dust: u32,
1247 pub brc20: u32,
1248}
1249
1250fn estimate_transaction_size(tx: &Transaction) -> u64 {
1252 blvm_consensus::transaction::calculate_transaction_size(tx) as u64
1253}
1254
1255fn detect_script_type_for_input(
1257 tx: &Transaction,
1258 input_index: usize,
1259 utxo_set: Option<&UtxoSet>,
1260) -> Option<ScriptType> {
1261 if input_index >= tx.inputs.len() {
1262 return None;
1263 }
1264
1265 if let Some(utxo_set) = utxo_set {
1266 if let Some(utxo) = utxo_set.get(&tx.inputs[input_index].prevout) {
1267 let spk: ByteString = utxo.script_pubkey.as_ref().to_vec();
1268 let script_type = ScriptType::detect(&spk);
1269 if script_type != ScriptType::Unknown {
1270 return Some(script_type);
1271 }
1272 }
1273 }
1274
1275 detect_input_script_type(&tx.inputs[input_index].script_sig)
1276}
1277
1278#[derive(Debug, Clone, PartialEq, Serialize, Deserialize)]
1280pub struct WitnessSizeThresholdsSerializable {
1281 #[serde(default = "default_normal_single_sig")]
1282 pub normal_single_sig: usize,
1283 #[serde(default = "default_normal_multi_sig")]
1284 pub normal_multi_sig: usize,
1285 #[serde(default = "default_normal_p2wsh")]
1286 pub normal_p2wsh: usize,
1287 #[serde(default = "default_suspicious_threshold")]
1288 pub suspicious_threshold: usize,
1289 #[serde(default = "default_definitely_spam")]
1290 pub definitely_spam: usize,
1291}
1292
1293impl From<WitnessSizeThresholdsSerializable> for WitnessSizeThresholds {
1294 fn from(serializable: WitnessSizeThresholdsSerializable) -> Self {
1295 WitnessSizeThresholds {
1296 normal_single_sig: serializable.normal_single_sig,
1297 normal_multi_sig: serializable.normal_multi_sig,
1298 normal_p2wsh: serializable.normal_p2wsh,
1299 suspicious_threshold: serializable.suspicious_threshold,
1300 definitely_spam: serializable.definitely_spam,
1301 }
1302 }
1303}
1304
1305impl From<WitnessSizeThresholds> for WitnessSizeThresholdsSerializable {
1306 fn from(thresholds: WitnessSizeThresholds) -> Self {
1307 WitnessSizeThresholdsSerializable {
1308 normal_single_sig: thresholds.normal_single_sig,
1309 normal_multi_sig: thresholds.normal_multi_sig,
1310 normal_p2wsh: thresholds.normal_p2wsh,
1311 suspicious_threshold: thresholds.suspicious_threshold,
1312 definitely_spam: thresholds.definitely_spam,
1313 }
1314 }
1315}
1316
1317#[derive(Debug, Clone, PartialEq, Serialize, Deserialize)]
1319pub struct SpamFilterConfigSerializable {
1320 #[serde(default = "default_true")]
1321 pub filter_ordinals: bool,
1322 #[serde(default = "default_true")]
1323 pub filter_dust: bool,
1324 #[serde(default = "default_true")]
1325 pub filter_brc20: bool,
1326 #[serde(default = "default_true")]
1327 pub filter_large_witness: bool,
1328 #[serde(default = "default_false")]
1329 pub filter_low_fee_rate: bool,
1330 #[serde(default = "default_true")]
1331 pub filter_high_size_value_ratio: bool,
1332 #[serde(default = "default_true")]
1333 pub filter_many_small_outputs: bool,
1334 #[serde(default = "default_dust_threshold")]
1335 pub dust_threshold: i64,
1336 #[serde(default = "default_dust_threshold")]
1337 pub min_output_value: i64,
1338 #[serde(default = "default_min_fee_rate")]
1339 pub min_fee_rate: u64,
1340 #[serde(default = "default_max_witness_size")]
1341 pub max_witness_size: usize,
1342 #[serde(default = "default_max_size_value_ratio")]
1343 pub max_size_value_ratio: f64,
1344 #[serde(default = "default_max_small_outputs")]
1345 pub max_small_outputs: usize,
1346
1347 #[serde(default = "default_true")]
1349 pub use_adaptive_thresholds: bool,
1350 #[serde(default = "default_adaptive_thresholds")]
1351 pub adaptive_thresholds: WitnessSizeThresholdsSerializable,
1352
1353 #[serde(default = "default_true")]
1355 pub filter_taproot_spam: bool,
1356 #[serde(default = "default_max_taproot_control_size")]
1357 pub max_taproot_control_size: usize,
1358 #[serde(default = "default_true")]
1359 pub reject_taproot_annexes: bool,
1360
1361 #[serde(default = "default_false")]
1363 pub filter_large_total_witness: bool,
1364 #[serde(default = "default_max_total_witness_size")]
1365 pub max_total_witness_size: usize,
1366
1367 #[serde(default = "default_true")]
1369 pub use_improved_envelope_detection: bool,
1370 #[serde(default = "default_true")]
1371 pub use_json_validation_brc20: bool,
1372
1373 #[serde(default = "default_false")]
1375 pub require_utxo_for_fee_rate: bool,
1376 #[serde(default = "default_min_fee_rate_large_tx")]
1377 pub min_fee_rate_large_tx: u64,
1378 #[serde(default = "default_large_tx_threshold_bytes")]
1379 pub large_tx_threshold_bytes: usize,
1380 #[serde(default = "default_true")]
1381 pub ordinals_strict_mode: bool,
1382}
1383
1384fn default_true() -> bool {
1385 true
1386}
1387
1388fn default_false() -> bool {
1389 false
1390}
1391
1392fn default_dust_threshold() -> i64 {
1393 546
1394}
1395
1396fn default_min_fee_rate() -> u64 {
1397 1
1398}
1399
1400fn default_max_witness_size() -> usize {
1401 1000
1402}
1403
1404fn default_max_size_value_ratio() -> f64 {
1405 1000.0
1406}
1407
1408fn default_max_small_outputs() -> usize {
1409 10
1410}
1411
1412fn default_max_taproot_control_size() -> usize {
1413 289
1414}
1415
1416fn default_max_total_witness_size() -> usize {
1417 5000
1418}
1419
1420fn default_min_fee_rate_large_tx() -> u64 {
1421 2
1422}
1423
1424fn default_large_tx_threshold_bytes() -> usize {
1425 1000
1426}
1427
1428fn default_normal_single_sig() -> usize {
1429 200
1430}
1431
1432fn default_normal_multi_sig() -> usize {
1433 500
1434}
1435
1436fn default_normal_p2wsh() -> usize {
1437 800
1438}
1439
1440fn default_suspicious_threshold() -> usize {
1441 1000
1442}
1443
1444fn default_definitely_spam() -> usize {
1445 2000
1446}
1447
1448fn default_adaptive_thresholds() -> WitnessSizeThresholdsSerializable {
1449 WitnessSizeThresholdsSerializable {
1450 normal_single_sig: 200,
1451 normal_multi_sig: 500,
1452 normal_p2wsh: 800,
1453 suspicious_threshold: 1000,
1454 definitely_spam: 2000,
1455 }
1456}
1457
1458impl Default for SpamFilterConfigSerializable {
1459 fn default() -> Self {
1460 Self {
1461 filter_ordinals: default_true(),
1462 filter_dust: default_true(),
1463 filter_brc20: default_true(),
1464 filter_large_witness: default_true(),
1465 filter_low_fee_rate: default_false(),
1466 filter_high_size_value_ratio: default_true(),
1467 filter_many_small_outputs: default_true(),
1468 dust_threshold: default_dust_threshold(),
1469 min_output_value: default_dust_threshold(),
1470 min_fee_rate: default_min_fee_rate(),
1471 max_witness_size: default_max_witness_size(),
1472 max_size_value_ratio: default_max_size_value_ratio(),
1473 max_small_outputs: default_max_small_outputs(),
1474 use_adaptive_thresholds: default_true(),
1475 adaptive_thresholds: default_adaptive_thresholds(),
1476 filter_taproot_spam: default_true(),
1477 max_taproot_control_size: default_max_taproot_control_size(),
1478 reject_taproot_annexes: default_true(),
1479 filter_large_total_witness: default_false(),
1480 max_total_witness_size: default_max_total_witness_size(),
1481 use_improved_envelope_detection: default_true(),
1482 use_json_validation_brc20: default_true(),
1483 require_utxo_for_fee_rate: default_false(),
1484 min_fee_rate_large_tx: default_min_fee_rate_large_tx(),
1485 large_tx_threshold_bytes: default_large_tx_threshold_bytes(),
1486 ordinals_strict_mode: default_true(),
1487 }
1488 }
1489}
1490
1491impl From<SpamFilterConfigSerializable> for SpamFilterConfig {
1492 fn from(serializable: SpamFilterConfigSerializable) -> Self {
1493 SpamFilterConfig {
1494 filter_ordinals: serializable.filter_ordinals,
1495 filter_dust: serializable.filter_dust,
1496 filter_brc20: serializable.filter_brc20,
1497 filter_large_witness: serializable.filter_large_witness,
1498 filter_low_fee_rate: serializable.filter_low_fee_rate,
1499 filter_high_size_value_ratio: serializable.filter_high_size_value_ratio,
1500 filter_many_small_outputs: serializable.filter_many_small_outputs,
1501 dust_threshold: serializable.dust_threshold,
1502 min_output_value: serializable.min_output_value,
1503 min_fee_rate: serializable.min_fee_rate,
1504 max_witness_size: serializable.max_witness_size,
1505 max_size_value_ratio: serializable.max_size_value_ratio,
1506 max_small_outputs: serializable.max_small_outputs,
1507 use_adaptive_thresholds: serializable.use_adaptive_thresholds,
1509 adaptive_thresholds: serializable.adaptive_thresholds.into(),
1510 filter_taproot_spam: serializable.filter_taproot_spam,
1512 max_taproot_control_size: serializable.max_taproot_control_size,
1513 reject_taproot_annexes: serializable.reject_taproot_annexes,
1514 filter_large_total_witness: serializable.filter_large_total_witness,
1515 max_total_witness_size: serializable.max_total_witness_size,
1516 use_improved_envelope_detection: serializable.use_improved_envelope_detection,
1517 use_json_validation_brc20: serializable.use_json_validation_brc20,
1518 require_utxo_for_fee_rate: serializable.require_utxo_for_fee_rate,
1519 min_fee_rate_large_tx: serializable.min_fee_rate_large_tx,
1520 large_tx_threshold_bytes: serializable.large_tx_threshold_bytes,
1521 ordinals_strict_mode: serializable.ordinals_strict_mode,
1522 }
1523 }
1524}
1525
1526impl From<SpamFilterConfig> for SpamFilterConfigSerializable {
1527 fn from(config: SpamFilterConfig) -> Self {
1528 SpamFilterConfigSerializable {
1529 filter_ordinals: config.filter_ordinals,
1530 filter_dust: config.filter_dust,
1531 filter_brc20: config.filter_brc20,
1532 filter_large_witness: config.filter_large_witness,
1533 filter_low_fee_rate: config.filter_low_fee_rate,
1534 filter_high_size_value_ratio: config.filter_high_size_value_ratio,
1535 filter_many_small_outputs: config.filter_many_small_outputs,
1536 dust_threshold: config.dust_threshold,
1537 min_output_value: config.min_output_value,
1538 min_fee_rate: config.min_fee_rate,
1539 max_witness_size: config.max_witness_size,
1540 max_size_value_ratio: config.max_size_value_ratio,
1541 max_small_outputs: config.max_small_outputs,
1542 use_adaptive_thresholds: config.use_adaptive_thresholds,
1544 adaptive_thresholds: config.adaptive_thresholds.into(),
1545 filter_taproot_spam: config.filter_taproot_spam,
1547 max_taproot_control_size: config.max_taproot_control_size,
1548 reject_taproot_annexes: config.reject_taproot_annexes,
1549 filter_large_total_witness: config.filter_large_total_witness,
1550 max_total_witness_size: config.max_total_witness_size,
1551 use_improved_envelope_detection: config.use_improved_envelope_detection,
1552 use_json_validation_brc20: config.use_json_validation_brc20,
1553 require_utxo_for_fee_rate: config.require_utxo_for_fee_rate,
1554 min_fee_rate_large_tx: config.min_fee_rate_large_tx,
1555 large_tx_threshold_bytes: config.large_tx_threshold_bytes,
1556 ordinals_strict_mode: config.ordinals_strict_mode,
1557 }
1558 }
1559}