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 #[cfg(feature = "production")]
366 #[allow(dead_code)]
367 pub(crate) script_type_cache: std::sync::Arc<std::sync::RwLock<lru::LruCache<u64, bool>>>,
368}
369
370impl SpamFilter {
371 pub fn new() -> Self {
373 Self {
374 config: SpamFilterConfig::default(),
375 #[cfg(feature = "production")]
376 script_type_cache: std::sync::Arc::new(std::sync::RwLock::new(lru::LruCache::new(
377 std::num::NonZeroUsize::new(10_000).unwrap(),
378 ))),
379 }
380 }
381
382 pub fn with_config(config: SpamFilterConfig) -> Self {
384 Self {
385 config,
386 #[cfg(feature = "production")]
387 script_type_cache: std::sync::Arc::new(std::sync::RwLock::new(lru::LruCache::new(
388 std::num::NonZeroUsize::new(10_000).unwrap(),
389 ))),
390 }
391 }
392
393 pub fn with_preset(preset: SpamFilterPreset) -> Self {
401 Self::with_config(preset.to_config())
402 }
403
404 pub fn is_spam(&self, tx: &Transaction) -> SpamFilterResult {
408 self.is_spam_with_witness(tx, None, None)
409 }
410
411 pub fn is_spam_with_witness(
417 &self,
418 tx: &Transaction,
419 witnesses: Option<&[Witness]>,
420 utxo_set: Option<&UtxoSet>,
421 ) -> SpamFilterResult {
422 let mut detected_types = Vec::new();
423
424 match blvm_consensus::script::templates::scan_tx(tx, witnesses) {
425 blvm_consensus::script::templates::ScriptTemplate::UnexecIf { .. }
426 if self.config.filter_unexec_if =>
427 {
428 detected_types.push(SpamType::UnexecIf);
429 }
430 blvm_consensus::script::templates::ScriptTemplate::NullDataOp13
431 if self.config.filter_null_data_op13 =>
432 {
433 detected_types.push(SpamType::NullDataOp13);
434 }
435 blvm_consensus::script::templates::ScriptTemplate::NullDataMagic { .. }
436 if self.config.filter_null_data_magic =>
437 {
438 detected_types.push(SpamType::NullDataMagic);
439 }
440 blvm_consensus::script::templates::ScriptTemplate::DataLikeMs { .. }
441 if self.config.filter_data_like_ms =>
442 {
443 detected_types.push(SpamType::DataLikeMs);
444 }
445 _ => {}
446 }
447
448 if self.config.filter_ordinals && self.detect_ordinals(tx, witnesses, utxo_set) {
450 detected_types.push(SpamType::Ordinals);
451 }
452
453 if self.config.filter_dust && self.detect_dust(tx) {
455 detected_types.push(SpamType::Dust);
456 }
457
458 if self.config.filter_brc20 && self.detect_brc20(tx) {
460 detected_types.push(SpamType::BRC20);
461 }
462
463 if self.config.filter_large_witness && self.detect_large_witness(tx, witnesses, utxo_set) {
465 detected_types.push(SpamType::LargeWitness);
466 }
467
468 if self.config.filter_large_total_witness && self.detect_large_total_witness(witnesses) {
470 detected_types.push(SpamType::LargeWitness);
471 }
472
473 if self.config.filter_low_fee_rate && self.detect_low_fee_rate(tx, witnesses, utxo_set) {
475 detected_types.push(SpamType::LowFeeRate);
476 }
477
478 if self.config.filter_high_size_value_ratio
480 && self.detect_high_size_value_ratio(tx, witnesses)
481 {
482 detected_types.push(SpamType::HighSizeValueRatio);
483 }
484
485 if self.config.filter_many_small_outputs && self.detect_many_small_outputs(tx) {
487 detected_types.push(SpamType::ManySmallOutputs);
488 }
489
490 let is_spam = !detected_types.is_empty();
491 let spam_type = detected_types.first().cloned().unwrap_or(SpamType::NotSpam);
492
493 SpamFilterResult {
494 is_spam,
495 spam_type,
496 detected_types,
497 }
498 }
499
500 pub fn filter_transaction(&self, tx: &Transaction) -> Option<Transaction> {
505 let result = self.is_spam(tx);
506 if result.is_spam {
507 None } else {
509 Some(tx.clone()) }
511 }
512 fn detect_ordinals(
519 &self,
520 tx: &Transaction,
521 witnesses: Option<&[Witness]>,
522 utxo_set: Option<&UtxoSet>,
523 ) -> bool {
524 for output in &tx.outputs {
526 if self.has_ordinal_pattern(&output.script_pubkey) {
527 return true;
528 }
529 }
530
531 for input in &tx.inputs {
533 if self.has_envelope_pattern(&input.script_sig) {
534 return true;
535 }
536 }
537
538 if let Some(witnesses) = witnesses {
540 for (i, witness) in witnesses.iter().enumerate() {
541 if i >= tx.inputs.len() {
542 break;
543 }
544
545 if self.config.filter_taproot_spam {
547 for output in &tx.outputs {
548 if self.is_taproot_output(&output.script_pubkey)
549 && self.detect_taproot_spam(output, witness)
550 {
551 return true;
552 }
553 }
554 }
555
556 if self.has_envelope_in_witness(witness) {
558 return true;
559 }
560
561 if !self.config.ordinals_strict_mode {
564 if self.config.use_adaptive_thresholds {
565 if self.has_large_witness_stack_adaptive(witness, tx, i, utxo_set) {
566 return true;
567 }
568 } else if self.has_large_witness_stack(witness) {
569 return true;
570 }
571 if self.has_witness_data_pattern(witness) {
572 return true;
573 }
574 }
575 }
576 }
577
578 false
579 }
580
581 fn has_envelope_in_witness(&self, witness: &Witness) -> bool {
584 for element in witness {
585 if element.len() >= 4 && element[0] == OP_0 && element[1] == OP_IF {
586 if self.config.use_improved_envelope_detection {
587 if element.iter().skip(2).any(|&b| b == OP_ENDIF) {
588 return true;
589 }
590 } else {
591 return true;
592 }
593 }
594 }
595 false
596 }
597
598 fn is_taproot_output(&self, script_pubkey: &ByteString) -> bool {
602 script_pubkey.len() == 34 && script_pubkey[0] == OP_1 && script_pubkey[1] == PUSH_32_BYTES
604 }
605
606 fn detect_taproot_spam(
612 &self,
613 output: &blvm_consensus::types::TransactionOutput,
614 witness: &Witness,
615 ) -> bool {
616 if !self.is_taproot_output(&output.script_pubkey) {
617 return false;
618 }
619
620 if self.config.reject_taproot_annexes {
623 if let Some(last) = witness.last() {
624 if !last.is_empty() && last[0] == blvm_consensus::opcodes::OP_RESERVED {
625 return true;
627 }
628 }
629 }
630
631 if witness.len() >= 2 {
636 if let Some(control_block) = witness.last() {
639 if control_block.len() > self.config.max_taproot_control_size {
643 return true;
644 }
645 }
646 }
647
648 false
649 }
650
651 fn has_large_witness_stack(&self, witness: &Witness) -> bool {
655 let total_size = self.calculate_witness_size(witness);
656 total_size > self.config.max_witness_size
657 }
658
659 fn has_large_witness_stack_adaptive(
664 &self,
665 witness: &Witness,
666 tx: &Transaction,
667 input_index: usize,
668 utxo_set: Option<&UtxoSet>,
669 ) -> bool {
670 let total_size = self.calculate_witness_size(witness);
671
672 if !self.config.use_adaptive_thresholds {
674 return total_size > self.config.max_witness_size;
675 }
676
677 let threshold =
678 if let Some(script_type) = detect_script_type_for_input(tx, input_index, utxo_set) {
679 script_type.recommended_threshold()
680 } else {
681 self.config.max_witness_size
682 };
683
684 total_size > threshold
685 }
686
687 fn analyze_witness_elements(&self, witness: &Witness) -> WitnessElementAnalysis {
691 let total_size = self.calculate_witness_size(witness);
692 let element_count = witness.len();
693
694 let mut large_elements = 0;
695 let mut medium_elements = 0;
696 let mut small_elements = 0;
697
698 for element in witness {
699 if element.len() > 200 {
700 large_elements += 1;
701 } else if element.len() >= 100 {
702 medium_elements += 1;
703 } else {
704 small_elements += 1;
705 }
706 }
707
708 let suspicious_pattern = medium_elements >= 10;
710
711 WitnessElementAnalysis {
712 total_size,
713 element_count,
714 large_elements,
715 medium_elements,
716 small_elements,
717 suspicious_pattern,
718 }
719 }
720
721 fn calculate_witness_size(&self, witness: &Witness) -> usize {
729 let mut size = 1;
731
732 for element in witness {
734 size += if element.len() <= VARINT_1BYTE_MAX as usize {
742 1
743 } else if element.len() <= 0xffff {
744 3 } else if element.len() <= 0xffffffff {
746 5 } else {
748 9 };
750 size += element.len();
751 }
752
753 size
754 }
755
756 fn has_witness_data_pattern(&self, witness: &Witness) -> bool {
758 if witness.is_empty() {
759 return false;
760 }
761
762 for element in witness {
765 if element.len() > 200 {
768 if element.len() != 64 && (element.is_empty() || element[0] != DER_SIGNATURE_PREFIX)
771 {
772 return true;
774 }
775 }
776 }
777
778 let large_elements = witness.iter().filter(|elem| elem.len() > 100).count();
780 if large_elements >= 3 {
781 return true;
782 }
783
784 let analysis = self.analyze_witness_elements(witness);
786 if analysis.suspicious_pattern {
787 return true;
788 }
789
790 false
791 }
792
793 fn has_ordinal_pattern(&self, script: &ByteString) -> bool {
799 if script.is_empty() {
800 return false;
801 }
802
803 if script[0] == OP_RETURN && script.len() > 80 {
805 return true;
806 }
807
808 if self.has_envelope_pattern(script) {
810 return true;
811 }
812
813 false
814 }
815
816 fn has_envelope_pattern(&self, script: &ByteString) -> bool {
818 if script.len() < 4 {
820 return false;
821 }
822
823 if script[0] == OP_0 && script[1] == OP_IF {
825 if self.config.use_improved_envelope_detection {
826 if script.iter().skip(2).any(|&b| b == OP_ENDIF) {
829 return true;
830 }
831 } else {
832 return true;
834 }
835 }
836
837 false
838 }
839
840 fn detect_dust(&self, tx: &Transaction) -> bool {
844 let mut all_dust = true;
846
847 for output in &tx.outputs {
848 if output.value >= self.config.dust_threshold {
849 all_dust = false;
850 break;
851 }
852 }
853
854 all_dust && !tx.outputs.is_empty()
855 }
856
857 fn detect_large_witness(
862 &self,
863 tx: &Transaction,
864 witnesses: Option<&[Witness]>,
865 utxo_set: Option<&UtxoSet>,
866 ) -> bool {
867 if let Some(witnesses) = witnesses {
868 for (i, witness) in witnesses.iter().enumerate() {
869 if self.config.use_adaptive_thresholds {
871 if self.has_large_witness_stack_adaptive(witness, tx, i, utxo_set) {
872 return true;
873 }
874 } else if self.has_large_witness_stack(witness) {
875 return true;
876 }
877 }
878 }
879 false
880 }
881
882 fn detect_low_fee_rate(
887 &self,
888 tx: &Transaction,
889 witnesses: Option<&[Witness]>,
890 utxo_set: Option<&UtxoSet>,
891 ) -> bool {
892 let tx_size = self.estimate_transaction_size_with_witness(tx, witnesses);
893
894 if self.config.require_utxo_for_fee_rate && utxo_set.is_none() {
896 return true; }
899
900 let fee_rate = if let Some(utxo_set) = utxo_set {
902 self.calculate_fee_rate_accurate(tx, utxo_set, tx_size)
903 } else {
904 return false;
907 };
908
909 let threshold = if tx_size > self.config.large_tx_threshold_bytes {
911 self.config.min_fee_rate_large_tx
912 } else {
913 self.config.min_fee_rate
914 };
915
916 fee_rate < threshold
917 }
918
919 fn calculate_fee_rate_accurate(
921 &self,
922 tx: &Transaction,
923 utxo_set: &UtxoSet,
924 tx_size: usize,
925 ) -> u64 {
926 if tx_size == 0 {
927 return 0;
928 }
929
930 let mut input_total = 0u64;
932 for input in &tx.inputs {
933 if let Some(utxo) = utxo_set.get(&input.prevout) {
934 input_total += utxo.value as u64;
935 }
936 }
937
938 let output_total: u64 = tx.outputs.iter().map(|out| out.value as u64).sum();
939 let fee = input_total.saturating_sub(output_total);
940
941 if tx_size > 0 { fee / tx_size as u64 } else { 0 }
943 }
944
945 fn detect_large_total_witness(&self, witnesses: Option<&[Witness]>) -> bool {
947 if !self.config.filter_large_total_witness {
948 return false; }
950
951 if let Some(witnesses) = witnesses {
952 let total_size: usize = witnesses
953 .iter()
954 .map(|w| self.calculate_witness_size(w))
955 .sum();
956
957 total_size > self.config.max_total_witness_size
958 } else {
959 false
960 }
961 }
962
963 fn detect_high_size_value_ratio(
969 &self,
970 tx: &Transaction,
971 witnesses: Option<&[Witness]>,
972 ) -> bool {
973 let tx_size = self.estimate_transaction_size_with_witness(tx, witnesses) as f64;
974 let total_output_value: f64 = tx.outputs.iter().map(|out| out.value as f64).sum();
975
976 if total_output_value <= 0.0 {
978 return tx_size > 1000.0;
980 }
981
982 let ratio = tx_size / total_output_value;
983
984 let threshold = if self.config.use_adaptive_thresholds {
986 let tx_type = TransactionType::detect(tx);
987 tx_type.recommended_size_value_ratio()
988 } else {
989 self.config.max_size_value_ratio
990 };
991
992 ratio > threshold
993 }
994
995 fn detect_many_small_outputs(&self, tx: &Transaction) -> bool {
999 let small_output_count = tx
1000 .outputs
1001 .iter()
1002 .filter(|out| out.value < self.config.dust_threshold)
1003 .count();
1004
1005 small_output_count > self.config.max_small_outputs
1006 }
1007
1008 fn estimate_transaction_size_with_witness(
1010 &self,
1011 tx: &Transaction,
1012 witnesses: Option<&[Witness]>,
1013 ) -> usize {
1014 let base_size = estimate_transaction_size(tx) as usize;
1016
1017 if let Some(witnesses) = witnesses {
1019 let witness_size: usize = witnesses
1020 .iter()
1021 .map(|witness| {
1022 let mut size = 1;
1024 for element in witness {
1026 size += 1; size += element.len();
1028 }
1029 size
1030 })
1031 .sum();
1032
1033 let has_witness = witness_size > 0;
1035 if has_witness {
1036 base_size + 2 + witness_size
1037 } else {
1038 base_size
1039 }
1040 } else {
1041 base_size
1042 }
1043 }
1044
1045 fn detect_brc20(&self, tx: &Transaction) -> bool {
1051 for output in &tx.outputs {
1053 if self.has_brc20_pattern(&output.script_pubkey) {
1054 return true;
1055 }
1056 }
1057
1058 false
1059 }
1060
1061 fn has_brc20_pattern(&self, script: &ByteString) -> bool {
1068 if script.len() < 20 {
1069 return false;
1070 }
1071
1072 if script[0] != OP_RETURN {
1074 return false;
1075 }
1076
1077 let data = &script[1..];
1079
1080 let script_str = match String::from_utf8(data.to_vec()) {
1082 Ok(s) => s,
1083 Err(_) => {
1084 return self.has_brc20_pattern_simple(data);
1086 }
1087 };
1088
1089 if self.config.use_json_validation_brc20 {
1091 self.has_brc20_pattern_json(&script_str)
1092 } else {
1093 self.has_brc20_pattern_simple(data)
1095 }
1096 }
1097
1098 fn has_brc20_pattern_json(&self, json_str: &str) -> bool {
1100 let cleaned: String = json_str.chars().filter(|c| !c.is_whitespace()).collect();
1102
1103 if let Ok(json_value) = serde_json::from_str::<serde_json::Value>(&cleaned) {
1105 if let Some(obj) = json_value.as_object() {
1107 if let Some(protocol) = obj.get("p") {
1109 if protocol.as_str() == Some("brc-20") {
1110 if let Some(op) = obj.get("op") {
1112 if let Some(op_str) = op.as_str() {
1113 return matches!(op_str, "mint" | "transfer" | "deploy");
1114 }
1115 }
1116 }
1117 }
1118 }
1119 }
1120
1121 if let Ok(json_value) = serde_json::from_str::<serde_json::Value>(json_str) {
1123 if let Some(obj) = json_value.as_object() {
1124 if let Some(protocol) = obj.get("p") {
1125 if protocol.as_str() == Some("brc-20") {
1126 if let Some(op) = obj.get("op") {
1127 if let Some(op_str) = op.as_str() {
1128 return matches!(op_str, "mint" | "transfer" | "deploy");
1129 }
1130 }
1131 }
1132 }
1133 }
1134 }
1135
1136 false
1137 }
1138
1139 fn has_brc20_pattern_simple(&self, data: &[u8]) -> bool {
1141 if let Ok(script_str) = String::from_utf8(data.to_vec()) {
1143 let lower = script_str.to_lowercase();
1145 lower.contains("brc-20")
1146 || lower.contains("\"p\":\"brc-20\"")
1147 || lower.contains("op\":\"mint")
1148 || lower.contains("op\":\"transfer")
1149 || lower.contains("op\":\"deploy")
1150 } else {
1151 let pattern = b"brc-20";
1154 let pattern_lower = b"BRC-20";
1155 data.windows(pattern.len())
1156 .any(|window| window == pattern || window == pattern_lower)
1157 }
1158 }
1159
1160 pub fn filter_block(&self, transactions: &[Transaction]) -> (Vec<Transaction>, SpamSummary) {
1176 self.filter_block_with_witness(transactions, None)
1177 }
1178
1179 pub fn filter_block_with_witness(
1196 &self,
1197 transactions: &[Transaction],
1198 witnesses: Option<&[Vec<Witness>]>,
1199 ) -> (Vec<Transaction>, SpamSummary) {
1200 let mut filtered_txs = Vec::new();
1201 let mut filtered_count = 0u32;
1202 let mut filtered_size = 0u64;
1203 let mut spam_breakdown = SpamBreakdown::default();
1204
1205 for (i, tx) in transactions.iter().enumerate() {
1206 let tx_witnesses = witnesses.and_then(|w| w.get(i));
1208
1209 let result = if let Some(tx_witnesses) = tx_witnesses {
1210 self.is_spam_with_witness(tx, Some(tx_witnesses), None)
1211 } else {
1212 self.is_spam(tx)
1213 };
1214
1215 if result.is_spam {
1216 filtered_count += 1;
1217 let tx_size = if let Some(tx_witnesses) = tx_witnesses {
1218 self.estimate_transaction_size_with_witness(tx, Some(tx_witnesses)) as u64
1219 } else {
1220 estimate_transaction_size(tx)
1221 };
1222 filtered_size += tx_size;
1223
1224 for spam_type in &result.detected_types {
1226 match spam_type {
1227 SpamType::Ordinals => spam_breakdown.ordinals += 1,
1228 SpamType::Dust => spam_breakdown.dust += 1,
1229 SpamType::BRC20 => spam_breakdown.brc20 += 1,
1230 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
1235 | SpamType::NullDataOp13
1236 | SpamType::NullDataMagic
1237 | SpamType::DataLikeMs => spam_breakdown.ordinals += 1,
1238 SpamType::NotSpam => {}
1239 }
1240 }
1241 } else {
1242 filtered_txs.push(tx.clone());
1243 }
1244 }
1245
1246 let summary = SpamSummary {
1247 filtered_count,
1248 filtered_size,
1249 by_type: spam_breakdown,
1250 };
1251
1252 (filtered_txs, summary)
1253 }
1254}
1255
1256impl Default for SpamFilter {
1257 fn default() -> Self {
1258 Self::new()
1259 }
1260}
1261
1262#[derive(Debug, Clone, Default)]
1264pub struct SpamSummary {
1265 pub filtered_count: u32,
1267 pub filtered_size: u64,
1269 pub by_type: SpamBreakdown,
1271}
1272
1273#[derive(Debug, Clone, Default)]
1275pub struct SpamBreakdown {
1276 pub ordinals: u32,
1277 pub inscriptions: u32,
1278 pub dust: u32,
1279 pub brc20: u32,
1280}
1281
1282fn estimate_transaction_size(tx: &Transaction) -> u64 {
1284 blvm_consensus::transaction::calculate_transaction_size(tx) as u64
1285}
1286
1287fn detect_script_type_for_input(
1289 tx: &Transaction,
1290 input_index: usize,
1291 utxo_set: Option<&UtxoSet>,
1292) -> Option<ScriptType> {
1293 if input_index >= tx.inputs.len() {
1294 return None;
1295 }
1296
1297 if let Some(utxo_set) = utxo_set {
1298 if let Some(utxo) = utxo_set.get(&tx.inputs[input_index].prevout) {
1299 let spk: ByteString = utxo.script_pubkey.as_ref().to_vec();
1300 let script_type = ScriptType::detect(&spk);
1301 if script_type != ScriptType::Unknown {
1302 return Some(script_type);
1303 }
1304 }
1305 }
1306
1307 detect_input_script_type(&tx.inputs[input_index].script_sig)
1308}
1309
1310#[derive(Debug, Clone, PartialEq, Serialize, Deserialize)]
1312pub struct WitnessSizeThresholdsSerializable {
1313 #[serde(default = "default_normal_single_sig")]
1314 pub normal_single_sig: usize,
1315 #[serde(default = "default_normal_multi_sig")]
1316 pub normal_multi_sig: usize,
1317 #[serde(default = "default_normal_p2wsh")]
1318 pub normal_p2wsh: usize,
1319 #[serde(default = "default_suspicious_threshold")]
1320 pub suspicious_threshold: usize,
1321 #[serde(default = "default_definitely_spam")]
1322 pub definitely_spam: usize,
1323}
1324
1325impl From<WitnessSizeThresholdsSerializable> for WitnessSizeThresholds {
1326 fn from(serializable: WitnessSizeThresholdsSerializable) -> Self {
1327 WitnessSizeThresholds {
1328 normal_single_sig: serializable.normal_single_sig,
1329 normal_multi_sig: serializable.normal_multi_sig,
1330 normal_p2wsh: serializable.normal_p2wsh,
1331 suspicious_threshold: serializable.suspicious_threshold,
1332 definitely_spam: serializable.definitely_spam,
1333 }
1334 }
1335}
1336
1337impl From<WitnessSizeThresholds> for WitnessSizeThresholdsSerializable {
1338 fn from(thresholds: WitnessSizeThresholds) -> Self {
1339 WitnessSizeThresholdsSerializable {
1340 normal_single_sig: thresholds.normal_single_sig,
1341 normal_multi_sig: thresholds.normal_multi_sig,
1342 normal_p2wsh: thresholds.normal_p2wsh,
1343 suspicious_threshold: thresholds.suspicious_threshold,
1344 definitely_spam: thresholds.definitely_spam,
1345 }
1346 }
1347}
1348
1349#[derive(Debug, Clone, PartialEq, Serialize, Deserialize)]
1351pub struct SpamFilterConfigSerializable {
1352 #[serde(default = "default_true")]
1353 pub filter_ordinals: bool,
1354 #[serde(default = "default_true")]
1355 pub filter_unexec_if: bool,
1356 #[serde(default = "default_true")]
1357 pub filter_null_data_op13: bool,
1358 #[serde(default = "default_true")]
1359 pub filter_null_data_magic: bool,
1360 #[serde(default = "default_true")]
1361 pub filter_data_like_ms: bool,
1362 #[serde(default = "default_true")]
1363 pub filter_dust: bool,
1364 #[serde(default = "default_true")]
1365 pub filter_brc20: bool,
1366 #[serde(default = "default_true")]
1367 pub filter_large_witness: bool,
1368 #[serde(default = "default_false")]
1369 pub filter_low_fee_rate: bool,
1370 #[serde(default = "default_true")]
1371 pub filter_high_size_value_ratio: bool,
1372 #[serde(default = "default_true")]
1373 pub filter_many_small_outputs: bool,
1374 #[serde(default = "default_dust_threshold")]
1375 pub dust_threshold: i64,
1376 #[serde(default = "default_dust_threshold")]
1377 pub min_output_value: i64,
1378 #[serde(default = "default_min_fee_rate")]
1379 pub min_fee_rate: u64,
1380 #[serde(default = "default_max_witness_size")]
1381 pub max_witness_size: usize,
1382 #[serde(default = "default_max_size_value_ratio")]
1383 pub max_size_value_ratio: f64,
1384 #[serde(default = "default_max_small_outputs")]
1385 pub max_small_outputs: usize,
1386
1387 #[serde(default = "default_true")]
1389 pub use_adaptive_thresholds: bool,
1390 #[serde(default = "default_adaptive_thresholds")]
1391 pub adaptive_thresholds: WitnessSizeThresholdsSerializable,
1392
1393 #[serde(default = "default_true")]
1395 pub filter_taproot_spam: bool,
1396 #[serde(default = "default_max_taproot_control_size")]
1397 pub max_taproot_control_size: usize,
1398 #[serde(default = "default_true")]
1399 pub reject_taproot_annexes: bool,
1400
1401 #[serde(default = "default_false")]
1403 pub filter_large_total_witness: bool,
1404 #[serde(default = "default_max_total_witness_size")]
1405 pub max_total_witness_size: usize,
1406
1407 #[serde(default = "default_true")]
1409 pub use_improved_envelope_detection: bool,
1410 #[serde(default = "default_true")]
1411 pub use_json_validation_brc20: bool,
1412
1413 #[serde(default = "default_false")]
1415 pub require_utxo_for_fee_rate: bool,
1416 #[serde(default = "default_min_fee_rate_large_tx")]
1417 pub min_fee_rate_large_tx: u64,
1418 #[serde(default = "default_large_tx_threshold_bytes")]
1419 pub large_tx_threshold_bytes: usize,
1420 #[serde(default = "default_true")]
1421 pub ordinals_strict_mode: bool,
1422}
1423
1424fn default_true() -> bool {
1425 true
1426}
1427
1428fn default_false() -> bool {
1429 false
1430}
1431
1432fn default_dust_threshold() -> i64 {
1433 546
1434}
1435
1436fn default_min_fee_rate() -> u64 {
1437 1
1438}
1439
1440fn default_max_witness_size() -> usize {
1441 1000
1442}
1443
1444fn default_max_size_value_ratio() -> f64 {
1445 1000.0
1446}
1447
1448fn default_max_small_outputs() -> usize {
1449 10
1450}
1451
1452fn default_max_taproot_control_size() -> usize {
1453 289
1454}
1455
1456fn default_max_total_witness_size() -> usize {
1457 5000
1458}
1459
1460fn default_min_fee_rate_large_tx() -> u64 {
1461 2
1462}
1463
1464fn default_large_tx_threshold_bytes() -> usize {
1465 1000
1466}
1467
1468fn default_normal_single_sig() -> usize {
1469 200
1470}
1471
1472fn default_normal_multi_sig() -> usize {
1473 500
1474}
1475
1476fn default_normal_p2wsh() -> usize {
1477 800
1478}
1479
1480fn default_suspicious_threshold() -> usize {
1481 1000
1482}
1483
1484fn default_definitely_spam() -> usize {
1485 2000
1486}
1487
1488fn default_adaptive_thresholds() -> WitnessSizeThresholdsSerializable {
1489 WitnessSizeThresholdsSerializable {
1490 normal_single_sig: 200,
1491 normal_multi_sig: 500,
1492 normal_p2wsh: 800,
1493 suspicious_threshold: 1000,
1494 definitely_spam: 2000,
1495 }
1496}
1497
1498impl Default for SpamFilterConfigSerializable {
1499 fn default() -> Self {
1500 Self {
1501 filter_ordinals: default_true(),
1502 filter_unexec_if: default_true(),
1503 filter_null_data_op13: default_true(),
1504 filter_null_data_magic: default_true(),
1505 filter_data_like_ms: default_true(),
1506 filter_dust: default_true(),
1507 filter_brc20: default_true(),
1508 filter_large_witness: default_true(),
1509 filter_low_fee_rate: default_false(),
1510 filter_high_size_value_ratio: default_true(),
1511 filter_many_small_outputs: default_true(),
1512 dust_threshold: default_dust_threshold(),
1513 min_output_value: default_dust_threshold(),
1514 min_fee_rate: default_min_fee_rate(),
1515 max_witness_size: default_max_witness_size(),
1516 max_size_value_ratio: default_max_size_value_ratio(),
1517 max_small_outputs: default_max_small_outputs(),
1518 use_adaptive_thresholds: default_true(),
1519 adaptive_thresholds: default_adaptive_thresholds(),
1520 filter_taproot_spam: default_true(),
1521 max_taproot_control_size: default_max_taproot_control_size(),
1522 reject_taproot_annexes: default_true(),
1523 filter_large_total_witness: default_false(),
1524 max_total_witness_size: default_max_total_witness_size(),
1525 use_improved_envelope_detection: default_true(),
1526 use_json_validation_brc20: default_true(),
1527 require_utxo_for_fee_rate: default_false(),
1528 min_fee_rate_large_tx: default_min_fee_rate_large_tx(),
1529 large_tx_threshold_bytes: default_large_tx_threshold_bytes(),
1530 ordinals_strict_mode: default_true(),
1531 }
1532 }
1533}
1534
1535impl From<SpamFilterConfigSerializable> for SpamFilterConfig {
1536 fn from(serializable: SpamFilterConfigSerializable) -> Self {
1537 SpamFilterConfig {
1538 filter_ordinals: serializable.filter_ordinals,
1539 filter_unexec_if: serializable.filter_unexec_if,
1540 filter_null_data_op13: serializable.filter_null_data_op13,
1541 filter_null_data_magic: serializable.filter_null_data_magic,
1542 filter_data_like_ms: serializable.filter_data_like_ms,
1543 filter_dust: serializable.filter_dust,
1544 filter_brc20: serializable.filter_brc20,
1545 filter_large_witness: serializable.filter_large_witness,
1546 filter_low_fee_rate: serializable.filter_low_fee_rate,
1547 filter_high_size_value_ratio: serializable.filter_high_size_value_ratio,
1548 filter_many_small_outputs: serializable.filter_many_small_outputs,
1549 dust_threshold: serializable.dust_threshold,
1550 min_output_value: serializable.min_output_value,
1551 min_fee_rate: serializable.min_fee_rate,
1552 max_witness_size: serializable.max_witness_size,
1553 max_size_value_ratio: serializable.max_size_value_ratio,
1554 max_small_outputs: serializable.max_small_outputs,
1555 use_adaptive_thresholds: serializable.use_adaptive_thresholds,
1557 adaptive_thresholds: serializable.adaptive_thresholds.into(),
1558 filter_taproot_spam: serializable.filter_taproot_spam,
1560 max_taproot_control_size: serializable.max_taproot_control_size,
1561 reject_taproot_annexes: serializable.reject_taproot_annexes,
1562 filter_large_total_witness: serializable.filter_large_total_witness,
1563 max_total_witness_size: serializable.max_total_witness_size,
1564 use_improved_envelope_detection: serializable.use_improved_envelope_detection,
1565 use_json_validation_brc20: serializable.use_json_validation_brc20,
1566 require_utxo_for_fee_rate: serializable.require_utxo_for_fee_rate,
1567 min_fee_rate_large_tx: serializable.min_fee_rate_large_tx,
1568 large_tx_threshold_bytes: serializable.large_tx_threshold_bytes,
1569 ordinals_strict_mode: serializable.ordinals_strict_mode,
1570 }
1571 }
1572}
1573
1574impl From<SpamFilterConfig> for SpamFilterConfigSerializable {
1575 fn from(config: SpamFilterConfig) -> Self {
1576 SpamFilterConfigSerializable {
1577 filter_ordinals: config.filter_ordinals,
1578 filter_unexec_if: config.filter_unexec_if,
1579 filter_null_data_op13: config.filter_null_data_op13,
1580 filter_null_data_magic: config.filter_null_data_magic,
1581 filter_data_like_ms: config.filter_data_like_ms,
1582 filter_dust: config.filter_dust,
1583 filter_brc20: config.filter_brc20,
1584 filter_large_witness: config.filter_large_witness,
1585 filter_low_fee_rate: config.filter_low_fee_rate,
1586 filter_high_size_value_ratio: config.filter_high_size_value_ratio,
1587 filter_many_small_outputs: config.filter_many_small_outputs,
1588 dust_threshold: config.dust_threshold,
1589 min_output_value: config.min_output_value,
1590 min_fee_rate: config.min_fee_rate,
1591 max_witness_size: config.max_witness_size,
1592 max_size_value_ratio: config.max_size_value_ratio,
1593 max_small_outputs: config.max_small_outputs,
1594 use_adaptive_thresholds: config.use_adaptive_thresholds,
1596 adaptive_thresholds: config.adaptive_thresholds.into(),
1597 filter_taproot_spam: config.filter_taproot_spam,
1599 max_taproot_control_size: config.max_taproot_control_size,
1600 reject_taproot_annexes: config.reject_taproot_annexes,
1601 filter_large_total_witness: config.filter_large_total_witness,
1602 max_total_witness_size: config.max_total_witness_size,
1603 use_improved_envelope_detection: config.use_improved_envelope_detection,
1604 use_json_validation_brc20: config.use_json_validation_brc20,
1605 require_utxo_for_fee_rate: config.require_utxo_for_fee_rate,
1606 min_fee_rate_large_tx: config.min_fee_rate_large_tx,
1607 large_tx_threshold_bytes: config.large_tx_threshold_bytes,
1608 ordinals_strict_mode: config.ordinals_strict_mode,
1609 }
1610 }
1611}