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_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, max_size_value_ratio: 2000.0, max_small_outputs: 20, ..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, filter_high_size_value_ratio: true,
110 filter_many_small_outputs: true,
111 max_witness_size: 500, max_size_value_ratio: 500.0, max_small_outputs: 5, min_fee_rate: 2, ..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, max_witness_size: 1500, ..SpamFilterConfig::default()
128 },
129 }
130 }
131}
132
133#[derive(Debug, Clone, PartialEq, Eq)]
135pub enum SpamType {
136 Ordinals,
138 Dust,
140 BRC20,
142 LargeWitness,
144 LowFeeRate,
146 HighSizeValueRatio,
148 ManySmallOutputs,
150 NotSpam,
152 UnexecIf,
154 NullDataOp13,
156 NullDataMagic,
158 DataLikeMs,
160}
161
162#[derive(Debug, Clone)]
167pub struct WitnessSizeThresholds {
168 pub normal_single_sig: usize,
170 pub normal_multi_sig: usize,
172 pub normal_p2wsh: usize,
174 pub suspicious_threshold: usize,
176 pub definitely_spam: usize,
178}
179
180impl Default for WitnessSizeThresholds {
181 fn default() -> Self {
182 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#[derive(Debug, Clone)]
196pub struct WitnessElementAnalysis {
197 pub total_size: usize,
199 pub element_count: usize,
201 pub large_elements: usize,
203 pub medium_elements: usize,
205 pub small_elements: usize,
207 pub suspicious_pattern: bool,
209}
210
211#[derive(Debug, Clone)]
213pub struct SpamFilterConfig {
214 pub filter_ordinals: bool,
216 pub filter_unexec_if: bool,
218 pub filter_null_data_op13: bool,
220 pub filter_null_data_magic: bool,
222 pub filter_data_like_ms: bool,
224 pub filter_dust: bool,
226 pub filter_brc20: bool,
228 pub filter_large_witness: bool,
230 pub filter_low_fee_rate: bool,
232 pub filter_high_size_value_ratio: bool,
234 pub filter_many_small_outputs: bool,
236 pub dust_threshold: i64,
238 pub min_output_value: i64,
240 pub min_fee_rate: u64,
242 pub max_witness_size: usize,
245 pub max_size_value_ratio: f64,
247 pub max_small_outputs: usize,
249
250 pub use_adaptive_thresholds: bool,
254 pub adaptive_thresholds: WitnessSizeThresholds,
256
257 pub filter_taproot_spam: bool,
261 pub max_taproot_control_size: usize,
266 pub reject_taproot_annexes: bool,
269
270 pub filter_large_total_witness: bool,
274 pub max_total_witness_size: usize,
277
278 pub use_improved_envelope_detection: bool,
282 pub use_json_validation_brc20: bool,
285
286 pub require_utxo_for_fee_rate: bool,
291 pub min_fee_rate_large_tx: u64,
295 pub large_tx_threshold_bytes: usize,
299
300 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, 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, use_adaptive_thresholds: true, adaptive_thresholds: WitnessSizeThresholds::default(),
332
333 filter_taproot_spam: true,
335 max_taproot_control_size: 289, reject_taproot_annexes: true,
337 filter_large_total_witness: false, max_total_witness_size: 5000,
339 use_improved_envelope_detection: true,
340 use_json_validation_brc20: true,
341 require_utxo_for_fee_rate: false, min_fee_rate_large_tx: 2, large_tx_threshold_bytes: 1000, ordinals_strict_mode: true, }
346 }
347}
348
349#[derive(Debug, Clone)]
351pub struct SpamFilterResult {
352 pub is_spam: bool,
354 pub spam_type: SpamType,
356 pub detected_types: Vec<SpamType>,
358}
359
360#[derive(Clone)]
362pub struct SpamFilter {
363 config: SpamFilterConfig,
364}
365
366impl SpamFilter {
367 pub fn new() -> Self {
369 Self {
370 config: SpamFilterConfig::default(),
371 }
372 }
373
374 pub fn with_config(config: SpamFilterConfig) -> Self {
376 Self { config }
377 }
378
379 pub fn with_preset(preset: SpamFilterPreset) -> Self {
387 Self::with_config(preset.to_config())
388 }
389
390 pub fn is_spam(&self, tx: &Transaction) -> SpamFilterResult {
394 self.is_spam_with_witness(tx, None, None)
395 }
396
397 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 if self.config.filter_ordinals && self.detect_ordinals(tx, witnesses, utxo_set) {
436 detected_types.push(SpamType::Ordinals);
437 }
438
439 if self.config.filter_dust && self.detect_dust(tx) {
441 detected_types.push(SpamType::Dust);
442 }
443
444 if self.config.filter_brc20 && self.detect_brc20(tx) {
446 detected_types.push(SpamType::BRC20);
447 }
448
449 if self.config.filter_large_witness && self.detect_large_witness(tx, witnesses, utxo_set) {
451 detected_types.push(SpamType::LargeWitness);
452 }
453
454 if self.config.filter_large_total_witness && self.detect_large_total_witness(witnesses) {
456 detected_types.push(SpamType::LargeWitness);
457 }
458
459 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 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 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 pub fn filter_transaction(&self, tx: &Transaction) -> Option<Transaction> {
491 let result = self.is_spam(tx);
492 if result.is_spam {
493 None } else {
495 Some(tx.clone()) }
497 }
498 fn detect_ordinals(
505 &self,
506 tx: &Transaction,
507 witnesses: Option<&[Witness]>,
508 utxo_set: Option<&UtxoSet>,
509 ) -> bool {
510 for output in &tx.outputs {
512 if self.has_ordinal_pattern(&output.script_pubkey) {
513 return true;
514 }
515 }
516
517 for input in &tx.inputs {
519 if self.has_envelope_pattern(&input.script_sig) {
520 return true;
521 }
522 }
523
524 if let Some(witnesses) = witnesses {
526 for (i, witness) in witnesses.iter().enumerate() {
527 if i >= tx.inputs.len() {
528 break;
529 }
530
531 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 if self.has_envelope_in_witness(witness) {
544 return true;
545 }
546
547 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 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 fn is_taproot_output(&self, script_pubkey: &ByteString) -> bool {
588 script_pubkey.len() == 34 && script_pubkey[0] == OP_1 && script_pubkey[1] == PUSH_32_BYTES
590 }
591
592 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 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 return true;
613 }
614 }
615 }
616
617 if witness.len() >= 2 {
622 if let Some(control_block) = witness.last() {
625 if control_block.len() > self.config.max_taproot_control_size {
629 return true;
630 }
631 }
632 }
633
634 false
635 }
636
637 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 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 !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 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 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 fn calculate_witness_size(&self, witness: &Witness) -> usize {
715 let mut size = 1;
717
718 for element in witness {
720 size += if element.len() <= VARINT_1BYTE_MAX as usize {
728 1
729 } else if element.len() <= 0xffff {
730 3 } else if element.len() <= 0xffffffff {
732 5 } else {
734 9 };
736 size += element.len();
737 }
738
739 size
740 }
741
742 fn has_witness_data_pattern(&self, witness: &Witness) -> bool {
744 if witness.is_empty() {
745 return false;
746 }
747
748 for element in witness {
751 if element.len() > 200 {
754 if element.len() != 64 && (element.is_empty() || element[0] != DER_SIGNATURE_PREFIX)
757 {
758 return true;
760 }
761 }
762 }
763
764 let large_elements = witness.iter().filter(|elem| elem.len() > 100).count();
766 if large_elements >= 3 {
767 return true;
768 }
769
770 let analysis = self.analyze_witness_elements(witness);
772 if analysis.suspicious_pattern {
773 return true;
774 }
775
776 false
777 }
778
779 fn has_ordinal_pattern(&self, script: &ByteString) -> bool {
785 if script.is_empty() {
786 return false;
787 }
788
789 if script[0] == OP_RETURN && script.len() > 80 {
791 return true;
792 }
793
794 if self.has_envelope_pattern(script) {
796 return true;
797 }
798
799 false
800 }
801
802 fn has_envelope_pattern(&self, script: &ByteString) -> bool {
804 if script.len() < 4 {
806 return false;
807 }
808
809 if script[0] == OP_0 && script[1] == OP_IF {
811 if self.config.use_improved_envelope_detection {
812 if script.iter().skip(2).any(|&b| b == OP_ENDIF) {
815 return true;
816 }
817 } else {
818 return true;
820 }
821 }
822
823 false
824 }
825
826 fn detect_dust(&self, tx: &Transaction) -> bool {
830 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 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 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 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 self.config.require_utxo_for_fee_rate && utxo_set.is_none() {
882 return true; }
885
886 let fee_rate = if let Some(utxo_set) = utxo_set {
888 self.calculate_fee_rate_accurate(tx, utxo_set, tx_size)
889 } else {
890 return false;
893 };
894
895 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 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 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 if tx_size > 0 { fee / tx_size as u64 } else { 0 }
929 }
930
931 fn detect_large_total_witness(&self, witnesses: Option<&[Witness]>) -> bool {
933 if !self.config.filter_large_total_witness {
934 return false; }
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 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 if total_output_value <= 0.0 {
964 return tx_size > 1000.0;
966 }
967
968 let ratio = tx_size / total_output_value;
969
970 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 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 fn estimate_transaction_size_with_witness(
996 &self,
997 tx: &Transaction,
998 witnesses: Option<&[Witness]>,
999 ) -> usize {
1000 let base_size = estimate_transaction_size(tx) as usize;
1002
1003 if let Some(witnesses) = witnesses {
1005 let witness_size: usize = witnesses
1006 .iter()
1007 .map(|witness| {
1008 let mut size = 1;
1010 for element in witness {
1012 size += 1; size += element.len();
1014 }
1015 size
1016 })
1017 .sum();
1018
1019 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 fn detect_brc20(&self, tx: &Transaction) -> bool {
1037 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 fn has_brc20_pattern(&self, script: &ByteString) -> bool {
1054 if script.len() < 20 {
1055 return false;
1056 }
1057
1058 if script[0] != OP_RETURN {
1060 return false;
1061 }
1062
1063 let data = &script[1..];
1065
1066 let script_str = match std::str::from_utf8(data) {
1068 Ok(s) => s,
1069 Err(_) => {
1070 return self.has_brc20_pattern_simple(data);
1072 }
1073 };
1074
1075 if self.config.use_json_validation_brc20 {
1077 self.has_brc20_pattern_json(script_str)
1078 } else {
1079 self.has_brc20_pattern_simple(data)
1081 }
1082 }
1083
1084 fn has_brc20_pattern_json(&self, json_str: &str) -> bool {
1086 let cleaned: String = json_str.chars().filter(|c| !c.is_whitespace()).collect();
1088
1089 if let Ok(json_value) = serde_json::from_str::<serde_json::Value>(&cleaned) {
1091 if let Some(obj) = json_value.as_object() {
1093 if let Some(protocol) = obj.get("p") {
1095 if protocol.as_str() == Some("brc-20") {
1096 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 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 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 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 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 pub fn filter_block(&self, transactions: &[Transaction]) -> (Vec<Transaction>, SpamSummary) {
1171 self.filter_block_with_witness(transactions, None)
1172 }
1173
1174 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 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 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, SpamType::LowFeeRate => spam_breakdown.dust += 1, SpamType::HighSizeValueRatio => spam_breakdown.ordinals += 1, SpamType::ManySmallOutputs => spam_breakdown.dust += 1, 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#[derive(Debug, Clone, Default)]
1259pub struct SpamSummary {
1260 pub filtered_count: u32,
1262 pub filtered_size: u64,
1264 pub by_type: SpamBreakdown,
1266}
1267
1268#[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
1277fn estimate_transaction_size(tx: &Transaction) -> u64 {
1279 blvm_consensus::transaction::calculate_transaction_size(tx) as u64
1280}
1281
1282fn 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#[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#[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 #[serde(default = "default_true")]
1383 pub use_adaptive_thresholds: bool,
1384 #[serde(default = "default_adaptive_thresholds")]
1385 pub adaptive_thresholds: WitnessSizeThresholdsSerializable,
1386
1387 #[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 #[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 #[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 #[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 use_adaptive_thresholds: serializable.use_adaptive_thresholds,
1551 adaptive_thresholds: serializable.adaptive_thresholds.into(),
1552 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 use_adaptive_thresholds: config.use_adaptive_thresholds,
1590 adaptive_thresholds: config.adaptive_thresholds.into(),
1591 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}