1mod script_analyzer;
22
23pub use script_analyzer::{detect_input_script_type, ScriptType};
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) {
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) {
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(&self, tx: &Transaction, witnesses: Option<&[Witness]>) -> bool {
471 for output in &tx.outputs {
473 if self.has_ordinal_pattern(&output.script_pubkey) {
474 return true;
475 }
476 }
477
478 for input in &tx.inputs {
480 if self.has_envelope_pattern(&input.script_sig) {
481 return true;
482 }
483 }
484
485 if let Some(witnesses) = witnesses {
487 for (i, witness) in witnesses.iter().enumerate() {
488 if i >= tx.inputs.len() {
489 break;
490 }
491
492 if self.config.filter_taproot_spam {
494 for output in &tx.outputs {
495 if self.is_taproot_output(&output.script_pubkey)
496 && self.detect_taproot_spam(output, witness)
497 {
498 return true;
499 }
500 }
501 }
502
503 if self.has_envelope_in_witness(witness) {
505 return true;
506 }
507
508 if !self.config.ordinals_strict_mode {
511 if self.config.use_adaptive_thresholds {
512 if self.has_large_witness_stack_adaptive(witness, tx, i) {
513 return true;
514 }
515 } else if self.has_large_witness_stack(witness) {
516 return true;
517 }
518 if self.has_witness_data_pattern(witness) {
519 return true;
520 }
521 }
522 }
523 }
524
525 false
526 }
527
528 fn has_envelope_in_witness(&self, witness: &Witness) -> bool {
531 for element in witness {
532 if element.len() >= 4 && element[0] == OP_0 && element[1] == OP_IF {
533 if self.config.use_improved_envelope_detection {
534 if element.iter().skip(2).any(|&b| b == OP_ENDIF) {
535 return true;
536 }
537 } else {
538 return true;
539 }
540 }
541 }
542 false
543 }
544
545 fn is_taproot_output(&self, script_pubkey: &ByteString) -> bool {
549 script_pubkey.len() == 34 && script_pubkey[0] == OP_1 && script_pubkey[1] == PUSH_32_BYTES
551 }
552
553 fn detect_taproot_spam(
559 &self,
560 output: &blvm_consensus::types::TransactionOutput,
561 witness: &Witness,
562 ) -> bool {
563 if !self.is_taproot_output(&output.script_pubkey) {
564 return false;
565 }
566
567 if self.config.reject_taproot_annexes {
570 if let Some(last) = witness.last() {
571 if !last.is_empty() && last[0] == blvm_consensus::opcodes::OP_RESERVED {
572 return true;
574 }
575 }
576 }
577
578 if witness.len() >= 2 {
583 if let Some(control_block) = witness.last() {
586 if control_block.len() > self.config.max_taproot_control_size {
591 return true;
592 }
593 }
594 }
595
596 false
597 }
598
599 fn has_large_witness_stack(&self, witness: &Witness) -> bool {
603 let total_size = self.calculate_witness_size(witness);
604 total_size > self.config.max_witness_size
605 }
606
607 fn has_large_witness_stack_adaptive(
612 &self,
613 witness: &Witness,
614 tx: &Transaction,
615 input_index: usize,
616 ) -> bool {
617 let total_size = self.calculate_witness_size(witness);
618
619 if !self.config.use_adaptive_thresholds {
621 return total_size > self.config.max_witness_size;
622 }
623
624 let mut detected_script_type: Option<ScriptType> = None;
628 if input_index < tx.inputs.len() {
629 detected_script_type = detect_input_script_type(&tx.inputs[input_index].script_sig);
630 }
631 if detected_script_type.is_none() {
632 for output in &tx.outputs {
633 let script_type = ScriptType::detect(&output.script_pubkey);
634 if script_type != ScriptType::Unknown {
635 detected_script_type = Some(script_type);
636 break;
637 }
638 }
639 }
640
641 let threshold = if let Some(script_type) = detected_script_type {
643 script_type.recommended_threshold()
644 } else {
645 self.config.max_witness_size
647 };
648
649 total_size > threshold
650 }
651
652 fn analyze_witness_elements(&self, witness: &Witness) -> WitnessElementAnalysis {
656 let total_size = self.calculate_witness_size(witness);
657 let element_count = witness.len();
658
659 let mut large_elements = 0;
660 let mut medium_elements = 0;
661 let mut small_elements = 0;
662
663 for element in witness {
664 if element.len() > 200 {
665 large_elements += 1;
666 } else if element.len() >= 100 {
667 medium_elements += 1;
668 } else {
669 small_elements += 1;
670 }
671 }
672
673 let suspicious_pattern = medium_elements >= 10;
675
676 WitnessElementAnalysis {
677 total_size,
678 element_count,
679 large_elements,
680 medium_elements,
681 small_elements,
682 suspicious_pattern,
683 }
684 }
685
686 fn calculate_witness_size(&self, witness: &Witness) -> usize {
694 let mut size = 1;
696
697 for element in witness {
699 size += if element.len() <= VARINT_1BYTE_MAX as usize {
707 1
708 } else if element.len() <= 0xffff {
709 3 } else if element.len() <= 0xffffffff {
711 5 } else {
713 9 };
715 size += element.len();
716 }
717
718 size
719 }
720
721 fn has_witness_data_pattern(&self, witness: &Witness) -> bool {
723 if witness.is_empty() {
724 return false;
725 }
726
727 for element in witness {
730 if element.len() > 200 {
733 if element.len() != 64 && (element.is_empty() || element[0] != DER_SIGNATURE_PREFIX)
736 {
737 return true;
739 }
740 }
741 }
742
743 let large_elements = witness.iter().filter(|elem| elem.len() > 100).count();
745 if large_elements >= 3 {
746 return true;
747 }
748
749 let analysis = self.analyze_witness_elements(witness);
751 if analysis.suspicious_pattern {
752 return true;
753 }
754
755 false
756 }
757
758 fn has_ordinal_pattern(&self, script: &ByteString) -> bool {
764 if script.is_empty() {
765 return false;
766 }
767
768 if script[0] == OP_RETURN && script.len() > 80 {
770 return true;
771 }
772
773 if self.has_envelope_pattern(script) {
775 return true;
776 }
777
778 false
779 }
780
781 fn has_envelope_pattern(&self, script: &ByteString) -> bool {
783 if script.len() < 4 {
785 return false;
786 }
787
788 if script[0] == OP_0 && script[1] == OP_IF {
790 if self.config.use_improved_envelope_detection {
791 if script.iter().skip(2).any(|&b| b == OP_ENDIF) {
794 return true;
795 }
796 } else {
797 return true;
799 }
800 }
801
802 false
803 }
804
805 fn detect_dust(&self, tx: &Transaction) -> bool {
809 let mut all_dust = true;
811
812 for output in &tx.outputs {
813 if output.value >= self.config.dust_threshold {
814 all_dust = false;
815 break;
816 }
817 }
818
819 all_dust && !tx.outputs.is_empty()
820 }
821
822 fn detect_large_witness(&self, tx: &Transaction, witnesses: Option<&[Witness]>) -> bool {
827 if let Some(witnesses) = witnesses {
828 for (i, witness) in witnesses.iter().enumerate() {
829 if self.config.use_adaptive_thresholds {
831 if self.has_large_witness_stack_adaptive(witness, tx, i) {
832 return true;
833 }
834 } else if self.has_large_witness_stack(witness) {
835 return true;
836 }
837 }
838 }
839 false
840 }
841
842 fn detect_low_fee_rate(
847 &self,
848 tx: &Transaction,
849 witnesses: Option<&[Witness]>,
850 utxo_set: Option<&UtxoSet>,
851 ) -> bool {
852 let tx_size = self.estimate_transaction_size_with_witness(tx, witnesses);
853
854 if self.config.require_utxo_for_fee_rate && utxo_set.is_none() {
856 return true; }
859
860 let fee_rate = if let Some(utxo_set) = utxo_set {
862 self.calculate_fee_rate_accurate(tx, utxo_set, tx_size)
864 } else {
865 self.calculate_fee_rate_heuristic(tx, tx_size)
867 };
868
869 let threshold = if tx_size > self.config.large_tx_threshold_bytes {
871 self.config.min_fee_rate_large_tx
872 } else {
873 self.config.min_fee_rate
874 };
875
876 fee_rate < threshold
877 }
878
879 fn calculate_fee_rate_accurate(
881 &self,
882 tx: &Transaction,
883 utxo_set: &UtxoSet,
884 tx_size: usize,
885 ) -> u64 {
886 if tx_size == 0 {
887 return 0;
888 }
889
890 let mut input_total = 0u64;
892 for input in &tx.inputs {
893 if let Some(utxo) = utxo_set.get(&input.prevout) {
894 input_total += utxo.value as u64;
895 }
896 }
897
898 let output_total: u64 = tx.outputs.iter().map(|out| out.value as u64).sum();
899 let fee = input_total.saturating_sub(output_total);
900
901 if tx_size > 0 {
903 fee / tx_size as u64
904 } else {
905 0
906 }
907 }
908
909 fn calculate_fee_rate_heuristic(&self, tx: &Transaction, tx_size: usize) -> u64 {
911 if tx_size == 0 {
912 return 0;
913 }
914
915 let total_output_value: i64 = tx.outputs.iter().map(|out| out.value).sum();
916
917 if tx_size > 1000 && total_output_value < 10000 {
919 1000u64.saturating_div(tx_size as u64)
921 } else {
922 self.config.min_fee_rate
925 }
926 }
927
928 fn detect_large_total_witness(&self, witnesses: Option<&[Witness]>) -> bool {
930 if !self.config.filter_large_total_witness {
931 return false; }
933
934 if let Some(witnesses) = witnesses {
935 let total_size: usize = witnesses
936 .iter()
937 .map(|w| self.calculate_witness_size(w))
938 .sum();
939
940 total_size > self.config.max_total_witness_size
941 } else {
942 false
943 }
944 }
945
946 fn detect_high_size_value_ratio(
952 &self,
953 tx: &Transaction,
954 witnesses: Option<&[Witness]>,
955 ) -> bool {
956 let tx_size = self.estimate_transaction_size_with_witness(tx, witnesses) as f64;
957 let total_output_value: f64 = tx.outputs.iter().map(|out| out.value as f64).sum();
958
959 if total_output_value <= 0.0 {
961 return tx_size > 1000.0;
963 }
964
965 let ratio = tx_size / total_output_value;
966
967 let threshold = if self.config.use_adaptive_thresholds {
969 let tx_type = TransactionType::detect(tx);
970 tx_type.recommended_size_value_ratio()
971 } else {
972 self.config.max_size_value_ratio
973 };
974
975 ratio > threshold
976 }
977
978 fn detect_many_small_outputs(&self, tx: &Transaction) -> bool {
982 let small_output_count = tx
983 .outputs
984 .iter()
985 .filter(|out| out.value < self.config.dust_threshold)
986 .count();
987
988 small_output_count > self.config.max_small_outputs
989 }
990
991 fn estimate_transaction_size_with_witness(
993 &self,
994 tx: &Transaction,
995 witnesses: Option<&[Witness]>,
996 ) -> usize {
997 let base_size = estimate_transaction_size(tx) as usize;
999
1000 if let Some(witnesses) = witnesses {
1002 let witness_size: usize = witnesses
1003 .iter()
1004 .map(|witness| {
1005 let mut size = 1;
1007 for element in witness {
1009 size += 1; size += element.len();
1011 }
1012 size
1013 })
1014 .sum();
1015
1016 let has_witness = witness_size > 0;
1018 if has_witness {
1019 base_size + 2 + witness_size
1020 } else {
1021 base_size
1022 }
1023 } else {
1024 base_size
1025 }
1026 }
1027
1028 fn detect_brc20(&self, tx: &Transaction) -> bool {
1034 for output in &tx.outputs {
1036 if self.has_brc20_pattern(&output.script_pubkey) {
1037 return true;
1038 }
1039 }
1040
1041 false
1042 }
1043
1044 fn has_brc20_pattern(&self, script: &ByteString) -> bool {
1051 if script.len() < 20 {
1052 return false;
1053 }
1054
1055 if script[0] != OP_RETURN {
1057 return false;
1058 }
1059
1060 let data = &script[1..];
1062
1063 let script_str = match String::from_utf8(data.to_vec()) {
1065 Ok(s) => s,
1066 Err(_) => {
1067 return self.has_brc20_pattern_simple(data);
1069 }
1070 };
1071
1072 if self.config.use_json_validation_brc20 {
1074 self.has_brc20_pattern_json(&script_str)
1075 } else {
1076 self.has_brc20_pattern_simple(data)
1078 }
1079 }
1080
1081 fn has_brc20_pattern_json(&self, json_str: &str) -> bool {
1083 let cleaned: String = json_str.chars().filter(|c| !c.is_whitespace()).collect();
1085
1086 if let Ok(json_value) = serde_json::from_str::<serde_json::Value>(&cleaned) {
1088 if let Some(obj) = json_value.as_object() {
1090 if let Some(protocol) = obj.get("p") {
1092 if protocol.as_str() == Some("brc-20") {
1093 if let Some(op) = obj.get("op") {
1095 if let Some(op_str) = op.as_str() {
1096 return matches!(op_str, "mint" | "transfer" | "deploy");
1097 }
1098 }
1099 }
1100 }
1101 }
1102 }
1103
1104 if let Ok(json_value) = serde_json::from_str::<serde_json::Value>(json_str) {
1106 if let Some(obj) = json_value.as_object() {
1107 if let Some(protocol) = obj.get("p") {
1108 if protocol.as_str() == Some("brc-20") {
1109 if let Some(op) = obj.get("op") {
1110 if let Some(op_str) = op.as_str() {
1111 return matches!(op_str, "mint" | "transfer" | "deploy");
1112 }
1113 }
1114 }
1115 }
1116 }
1117 }
1118
1119 false
1120 }
1121
1122 fn has_brc20_pattern_simple(&self, data: &[u8]) -> bool {
1124 if let Ok(script_str) = String::from_utf8(data.to_vec()) {
1126 let lower = script_str.to_lowercase();
1128 lower.contains("brc-20")
1129 || lower.contains("\"p\":\"brc-20\"")
1130 || lower.contains("op\":\"mint")
1131 || lower.contains("op\":\"transfer")
1132 || lower.contains("op\":\"deploy")
1133 } else {
1134 let pattern = b"brc-20";
1137 let pattern_lower = b"BRC-20";
1138 data.windows(pattern.len())
1139 .any(|window| window == pattern || window == pattern_lower)
1140 }
1141 }
1142
1143 pub fn filter_block(&self, transactions: &[Transaction]) -> (Vec<Transaction>, SpamSummary) {
1159 self.filter_block_with_witness(transactions, None)
1160 }
1161
1162 pub fn filter_block_with_witness(
1179 &self,
1180 transactions: &[Transaction],
1181 witnesses: Option<&[Vec<Witness>]>,
1182 ) -> (Vec<Transaction>, SpamSummary) {
1183 let mut filtered_txs = Vec::new();
1184 let mut filtered_count = 0u32;
1185 let mut filtered_size = 0u64;
1186 let mut spam_breakdown = SpamBreakdown::default();
1187
1188 for (i, tx) in transactions.iter().enumerate() {
1189 let tx_witnesses = witnesses.and_then(|w| w.get(i));
1191
1192 let result = if let Some(tx_witnesses) = tx_witnesses {
1193 self.is_spam_with_witness(tx, Some(tx_witnesses), None)
1194 } else {
1195 self.is_spam(tx)
1196 };
1197
1198 if result.is_spam {
1199 filtered_count += 1;
1200 let tx_size = if let Some(tx_witnesses) = tx_witnesses {
1201 self.estimate_transaction_size_with_witness(tx, Some(tx_witnesses)) as u64
1202 } else {
1203 estimate_transaction_size(tx)
1204 };
1205 filtered_size += tx_size;
1206
1207 for spam_type in &result.detected_types {
1209 match spam_type {
1210 SpamType::Ordinals => spam_breakdown.ordinals += 1,
1211 SpamType::Dust => spam_breakdown.dust += 1,
1212 SpamType::BRC20 => spam_breakdown.brc20 += 1,
1213 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 => {}
1218 }
1219 }
1220 } else {
1221 filtered_txs.push(tx.clone());
1222 }
1223 }
1224
1225 let summary = SpamSummary {
1226 filtered_count,
1227 filtered_size,
1228 by_type: spam_breakdown,
1229 };
1230
1231 (filtered_txs, summary)
1232 }
1233}
1234
1235impl Default for SpamFilter {
1236 fn default() -> Self {
1237 Self::new()
1238 }
1239}
1240
1241#[derive(Debug, Clone, Default)]
1243pub struct SpamSummary {
1244 pub filtered_count: u32,
1246 pub filtered_size: u64,
1248 pub by_type: SpamBreakdown,
1250}
1251
1252#[derive(Debug, Clone, Default)]
1254pub struct SpamBreakdown {
1255 pub ordinals: u32,
1256 pub inscriptions: u32,
1257 pub dust: u32,
1258 pub brc20: u32,
1259}
1260
1261fn estimate_transaction_size(tx: &Transaction) -> u64 {
1263 let base_size: u64 = 4 + 1 + 1 + 4; let input_size = tx.inputs.len() as u64 * 150;
1273 let output_size = tx
1274 .outputs
1275 .iter()
1276 .map(|out| 8 + out.script_pubkey.len() as u64)
1277 .sum::<u64>();
1278
1279 let total_size = base_size
1280 .checked_add(input_size)
1281 .and_then(|sum| sum.checked_add(output_size))
1282 .unwrap_or(u64::MAX); debug_assert!(
1286 total_size <= 1_000_000,
1287 "Transaction size estimate ({total_size}) must not exceed MAX_TX_SIZE (1MB)"
1288 );
1289
1290 total_size
1291}
1292
1293#[derive(Debug, Clone, PartialEq, Serialize, Deserialize)]
1295pub struct WitnessSizeThresholdsSerializable {
1296 #[serde(default = "default_normal_single_sig")]
1297 pub normal_single_sig: usize,
1298 #[serde(default = "default_normal_multi_sig")]
1299 pub normal_multi_sig: usize,
1300 #[serde(default = "default_normal_p2wsh")]
1301 pub normal_p2wsh: usize,
1302 #[serde(default = "default_suspicious_threshold")]
1303 pub suspicious_threshold: usize,
1304 #[serde(default = "default_definitely_spam")]
1305 pub definitely_spam: usize,
1306}
1307
1308impl From<WitnessSizeThresholdsSerializable> for WitnessSizeThresholds {
1309 fn from(serializable: WitnessSizeThresholdsSerializable) -> Self {
1310 WitnessSizeThresholds {
1311 normal_single_sig: serializable.normal_single_sig,
1312 normal_multi_sig: serializable.normal_multi_sig,
1313 normal_p2wsh: serializable.normal_p2wsh,
1314 suspicious_threshold: serializable.suspicious_threshold,
1315 definitely_spam: serializable.definitely_spam,
1316 }
1317 }
1318}
1319
1320impl From<WitnessSizeThresholds> for WitnessSizeThresholdsSerializable {
1321 fn from(thresholds: WitnessSizeThresholds) -> Self {
1322 WitnessSizeThresholdsSerializable {
1323 normal_single_sig: thresholds.normal_single_sig,
1324 normal_multi_sig: thresholds.normal_multi_sig,
1325 normal_p2wsh: thresholds.normal_p2wsh,
1326 suspicious_threshold: thresholds.suspicious_threshold,
1327 definitely_spam: thresholds.definitely_spam,
1328 }
1329 }
1330}
1331
1332#[derive(Debug, Clone, PartialEq, Serialize, Deserialize)]
1334pub struct SpamFilterConfigSerializable {
1335 #[serde(default = "default_true")]
1336 pub filter_ordinals: bool,
1337 #[serde(default = "default_true")]
1338 pub filter_dust: bool,
1339 #[serde(default = "default_true")]
1340 pub filter_brc20: bool,
1341 #[serde(default = "default_true")]
1342 pub filter_large_witness: bool,
1343 #[serde(default = "default_false")]
1344 pub filter_low_fee_rate: bool,
1345 #[serde(default = "default_true")]
1346 pub filter_high_size_value_ratio: bool,
1347 #[serde(default = "default_true")]
1348 pub filter_many_small_outputs: bool,
1349 #[serde(default = "default_dust_threshold")]
1350 pub dust_threshold: i64,
1351 #[serde(default = "default_dust_threshold")]
1352 pub min_output_value: i64,
1353 #[serde(default = "default_min_fee_rate")]
1354 pub min_fee_rate: u64,
1355 #[serde(default = "default_max_witness_size")]
1356 pub max_witness_size: usize,
1357 #[serde(default = "default_max_size_value_ratio")]
1358 pub max_size_value_ratio: f64,
1359 #[serde(default = "default_max_small_outputs")]
1360 pub max_small_outputs: usize,
1361
1362 #[serde(default = "default_true")]
1364 pub use_adaptive_thresholds: bool,
1365 #[serde(default = "default_adaptive_thresholds")]
1366 pub adaptive_thresholds: WitnessSizeThresholdsSerializable,
1367
1368 #[serde(default = "default_true")]
1370 pub filter_taproot_spam: bool,
1371 #[serde(default = "default_max_taproot_control_size")]
1372 pub max_taproot_control_size: usize,
1373 #[serde(default = "default_true")]
1374 pub reject_taproot_annexes: bool,
1375
1376 #[serde(default = "default_false")]
1378 pub filter_large_total_witness: bool,
1379 #[serde(default = "default_max_total_witness_size")]
1380 pub max_total_witness_size: usize,
1381
1382 #[serde(default = "default_true")]
1384 pub use_improved_envelope_detection: bool,
1385 #[serde(default = "default_true")]
1386 pub use_json_validation_brc20: bool,
1387
1388 #[serde(default = "default_false")]
1390 pub require_utxo_for_fee_rate: bool,
1391 #[serde(default = "default_min_fee_rate_large_tx")]
1392 pub min_fee_rate_large_tx: u64,
1393 #[serde(default = "default_large_tx_threshold_bytes")]
1394 pub large_tx_threshold_bytes: usize,
1395 #[serde(default = "default_true")]
1396 pub ordinals_strict_mode: bool,
1397}
1398
1399fn default_true() -> bool {
1400 true
1401}
1402
1403fn default_false() -> bool {
1404 false
1405}
1406
1407fn default_dust_threshold() -> i64 {
1408 546
1409}
1410
1411fn default_min_fee_rate() -> u64 {
1412 1
1413}
1414
1415fn default_max_witness_size() -> usize {
1416 1000
1417}
1418
1419fn default_max_size_value_ratio() -> f64 {
1420 1000.0
1421}
1422
1423fn default_max_small_outputs() -> usize {
1424 10
1425}
1426
1427fn default_max_taproot_control_size() -> usize {
1428 289
1429}
1430
1431fn default_max_total_witness_size() -> usize {
1432 5000
1433}
1434
1435fn default_min_fee_rate_large_tx() -> u64 {
1436 2
1437}
1438
1439fn default_large_tx_threshold_bytes() -> usize {
1440 1000
1441}
1442
1443fn default_normal_single_sig() -> usize {
1444 200
1445}
1446
1447fn default_normal_multi_sig() -> usize {
1448 500
1449}
1450
1451fn default_normal_p2wsh() -> usize {
1452 800
1453}
1454
1455fn default_suspicious_threshold() -> usize {
1456 1000
1457}
1458
1459fn default_definitely_spam() -> usize {
1460 2000
1461}
1462
1463fn default_adaptive_thresholds() -> WitnessSizeThresholdsSerializable {
1464 WitnessSizeThresholdsSerializable {
1465 normal_single_sig: 200,
1466 normal_multi_sig: 500,
1467 normal_p2wsh: 800,
1468 suspicious_threshold: 1000,
1469 definitely_spam: 2000,
1470 }
1471}
1472
1473impl Default for SpamFilterConfigSerializable {
1474 fn default() -> Self {
1475 Self {
1476 filter_ordinals: default_true(),
1477 filter_dust: default_true(),
1478 filter_brc20: default_true(),
1479 filter_large_witness: default_true(),
1480 filter_low_fee_rate: default_false(),
1481 filter_high_size_value_ratio: default_true(),
1482 filter_many_small_outputs: default_true(),
1483 dust_threshold: default_dust_threshold(),
1484 min_output_value: default_dust_threshold(),
1485 min_fee_rate: default_min_fee_rate(),
1486 max_witness_size: default_max_witness_size(),
1487 max_size_value_ratio: default_max_size_value_ratio(),
1488 max_small_outputs: default_max_small_outputs(),
1489 use_adaptive_thresholds: default_true(),
1490 adaptive_thresholds: default_adaptive_thresholds(),
1491 filter_taproot_spam: default_true(),
1492 max_taproot_control_size: default_max_taproot_control_size(),
1493 reject_taproot_annexes: default_true(),
1494 filter_large_total_witness: default_false(),
1495 max_total_witness_size: default_max_total_witness_size(),
1496 use_improved_envelope_detection: default_true(),
1497 use_json_validation_brc20: default_true(),
1498 require_utxo_for_fee_rate: default_false(),
1499 min_fee_rate_large_tx: default_min_fee_rate_large_tx(),
1500 large_tx_threshold_bytes: default_large_tx_threshold_bytes(),
1501 ordinals_strict_mode: default_true(),
1502 }
1503 }
1504}
1505
1506impl From<SpamFilterConfigSerializable> for SpamFilterConfig {
1507 fn from(serializable: SpamFilterConfigSerializable) -> Self {
1508 SpamFilterConfig {
1509 filter_ordinals: serializable.filter_ordinals,
1510 filter_dust: serializable.filter_dust,
1511 filter_brc20: serializable.filter_brc20,
1512 filter_large_witness: serializable.filter_large_witness,
1513 filter_low_fee_rate: serializable.filter_low_fee_rate,
1514 filter_high_size_value_ratio: serializable.filter_high_size_value_ratio,
1515 filter_many_small_outputs: serializable.filter_many_small_outputs,
1516 dust_threshold: serializable.dust_threshold,
1517 min_output_value: serializable.min_output_value,
1518 min_fee_rate: serializable.min_fee_rate,
1519 max_witness_size: serializable.max_witness_size,
1520 max_size_value_ratio: serializable.max_size_value_ratio,
1521 max_small_outputs: serializable.max_small_outputs,
1522 use_adaptive_thresholds: serializable.use_adaptive_thresholds,
1524 adaptive_thresholds: serializable.adaptive_thresholds.into(),
1525 filter_taproot_spam: serializable.filter_taproot_spam,
1527 max_taproot_control_size: serializable.max_taproot_control_size,
1528 reject_taproot_annexes: serializable.reject_taproot_annexes,
1529 filter_large_total_witness: serializable.filter_large_total_witness,
1530 max_total_witness_size: serializable.max_total_witness_size,
1531 use_improved_envelope_detection: serializable.use_improved_envelope_detection,
1532 use_json_validation_brc20: serializable.use_json_validation_brc20,
1533 require_utxo_for_fee_rate: serializable.require_utxo_for_fee_rate,
1534 min_fee_rate_large_tx: serializable.min_fee_rate_large_tx,
1535 large_tx_threshold_bytes: serializable.large_tx_threshold_bytes,
1536 ordinals_strict_mode: serializable.ordinals_strict_mode,
1537 }
1538 }
1539}
1540
1541impl From<SpamFilterConfig> for SpamFilterConfigSerializable {
1542 fn from(config: SpamFilterConfig) -> Self {
1543 SpamFilterConfigSerializable {
1544 filter_ordinals: config.filter_ordinals,
1545 filter_dust: config.filter_dust,
1546 filter_brc20: config.filter_brc20,
1547 filter_large_witness: config.filter_large_witness,
1548 filter_low_fee_rate: config.filter_low_fee_rate,
1549 filter_high_size_value_ratio: config.filter_high_size_value_ratio,
1550 filter_many_small_outputs: config.filter_many_small_outputs,
1551 dust_threshold: config.dust_threshold,
1552 min_output_value: config.min_output_value,
1553 min_fee_rate: config.min_fee_rate,
1554 max_witness_size: config.max_witness_size,
1555 max_size_value_ratio: config.max_size_value_ratio,
1556 max_small_outputs: config.max_small_outputs,
1557 use_adaptive_thresholds: config.use_adaptive_thresholds,
1559 adaptive_thresholds: config.adaptive_thresholds.into(),
1560 filter_taproot_spam: config.filter_taproot_spam,
1562 max_taproot_control_size: config.max_taproot_control_size,
1563 reject_taproot_annexes: config.reject_taproot_annexes,
1564 filter_large_total_witness: config.filter_large_total_witness,
1565 max_total_witness_size: config.max_total_witness_size,
1566 use_improved_envelope_detection: config.use_improved_envelope_detection,
1567 use_json_validation_brc20: config.use_json_validation_brc20,
1568 require_utxo_for_fee_rate: config.require_utxo_for_fee_rate,
1569 min_fee_rate_large_tx: config.min_fee_rate_large_tx,
1570 large_tx_threshold_bytes: config.large_tx_threshold_bytes,
1571 ordinals_strict_mode: config.ordinals_strict_mode,
1572 }
1573 }
1574}