Skip to main content

blvm_protocol/
validation.rs

1//! Protocol validation interface
2//!
3//! This module provides protocol-specific validation that extends
4//! the pure mathematical consensus rules with network-specific
5//! and protocol-specific validation logic.
6
7use crate::error::ProtocolError;
8use crate::features::FeatureRegistry;
9use crate::{BitcoinProtocolEngine, NetworkParameters, ProtocolVersion};
10use crate::{Block, Transaction, ValidationResult};
11use blvm_consensus::types::UtxoSet;
12use serde::{Deserialize, Serialize};
13use std::collections::HashMap;
14
15// Protocol-specific Result type
16type Result<T> = std::result::Result<T, ProtocolError>;
17
18/// Protocol-specific validation rules
19#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize)]
20pub struct ProtocolValidationRules {
21    /// Maximum block size for this protocol
22    pub max_block_size: u32,
23    /// Maximum transaction size for this protocol
24    pub max_tx_size: u32,
25    /// Maximum script size for this protocol
26    pub max_script_size: u32,
27    /// Whether SegWit is enabled
28    pub segwit_enabled: bool,
29    /// Whether Taproot is enabled
30    pub taproot_enabled: bool,
31    /// Whether RBF (Replace-By-Fee) is enabled
32    pub rbf_enabled: bool,
33    /// Minimum transaction fee rate
34    pub min_fee_rate: u64,
35    /// Maximum transaction fee rate
36    pub max_fee_rate: u64,
37}
38
39impl ProtocolValidationRules {
40    /// Get validation rules for a specific protocol version
41    pub fn for_protocol(version: ProtocolVersion) -> Self {
42        match version {
43            ProtocolVersion::BitcoinV1 => Self::mainnet(),
44            ProtocolVersion::Testnet3 => Self::testnet(),
45            ProtocolVersion::Regtest => Self::regtest(),
46            ProtocolVersion::Signet => Self::signet(),
47            ProtocolVersion::Testnet4 => Self::testnet4(),
48        }
49    }
50
51    /// Mainnet validation rules (strict production rules)
52    pub fn mainnet() -> Self {
53        Self {
54            max_block_size: 4_000_000, // 4MB block size limit
55            max_tx_size: 1_000_000,    // 1MB transaction size limit
56            max_script_size: 10_000,   // 10KB script size limit
57            segwit_enabled: true,
58            taproot_enabled: true,
59            rbf_enabled: true,
60            min_fee_rate: 1,         // 1 sat/vB minimum
61            max_fee_rate: 1_000_000, // 1M sat/vB maximum
62        }
63    }
64
65    /// Testnet validation rules (same as mainnet but with testnet parameters)
66    pub fn testnet() -> Self {
67        Self {
68            max_block_size: 4_000_000,
69            max_tx_size: 1_000_000,
70            max_script_size: 10_000,
71            segwit_enabled: true,
72            taproot_enabled: true,
73            rbf_enabled: true,
74            min_fee_rate: 1,
75            max_fee_rate: 1_000_000,
76        }
77    }
78
79    /// Regtest validation rules (relaxed for testing)
80    pub fn regtest() -> Self {
81        Self {
82            max_block_size: 4_000_000,
83            max_tx_size: 1_000_000,
84            max_script_size: 10_000,
85            segwit_enabled: true,
86            taproot_enabled: true,
87            rbf_enabled: true,
88            min_fee_rate: 0, // No minimum fee for testing
89            max_fee_rate: 1_000_000,
90        }
91    }
92
93    /// Testnet4 validation rules (same limits as testnet3).
94    pub fn testnet4() -> Self {
95        Self::testnet()
96    }
97
98    /// Signet validation rules (production-like test network)
99    pub fn signet() -> Self {
100        Self {
101            max_block_size: 4_000_000,
102            max_tx_size: 1_000_000,
103            max_script_size: 10_000,
104            segwit_enabled: true,
105            taproot_enabled: true,
106            rbf_enabled: true,
107            min_fee_rate: 1,
108            max_fee_rate: 1_000_000,
109        }
110    }
111}
112
113/// Protocol-specific validation context
114#[derive(Debug, Clone, PartialEq, Serialize, Deserialize)]
115pub struct ProtocolValidationContext {
116    /// Current block height
117    pub block_height: u64,
118    /// Current network parameters
119    pub network_params: NetworkParameters,
120    /// Protocol validation rules
121    pub validation_rules: ProtocolValidationRules,
122    /// Feature activation registry for this protocol version.
123    ///
124    /// Used by `is_feature_enabled` to derive activation status from
125    /// `block_height` / `network_time` rather than static booleans.
126    pub feature_registry: FeatureRegistry,
127    /// Median time-past used for time-based validation (BIP113)
128    ///
129    /// This is populated by the node using recent headers and is threaded down
130    /// to consensus for timestamp validation.
131    pub median_time_past: u64,
132    /// Current adjusted network time (Unix timestamp)
133    ///
134    /// This is populated by the node from its time source and used by consensus
135    /// to enforce future timestamp limits.
136    pub network_time: u64,
137    /// Additional context data
138    pub context_data: HashMap<String, String>,
139}
140
141impl ProtocolValidationContext {
142    /// Create validation context for a protocol version
143    pub fn new(version: ProtocolVersion, block_height: u64) -> Result<Self> {
144        let network_params = NetworkParameters::for_version(version)?;
145        let validation_rules = ProtocolValidationRules::for_protocol(version);
146
147        let feature_registry = FeatureRegistry::for_protocol(version);
148        Ok(Self {
149            block_height,
150            network_params,
151            validation_rules,
152            feature_registry,
153            // Default to zero; callers that care about time must set these explicitly.
154            median_time_past: 0,
155            network_time: 0,
156            context_data: HashMap::new(),
157        })
158    }
159
160    /// Check if a feature is enabled at the current block height and network time.
161    ///
162    /// Queries the `FeatureRegistry` so activation reflects the actual deployment
163    /// height/timestamp rather than static booleans that ignore `block_height`.
164    pub fn is_feature_enabled(&self, feature: &str) -> bool {
165        self.feature_registry
166            .is_feature_active(feature, self.block_height, self.network_time)
167    }
168
169    /// Get maximum allowed size for a component
170    pub fn get_max_size(&self, component: &str) -> u32 {
171        match component {
172            "block" => self.validation_rules.max_block_size,
173            "transaction" => self.validation_rules.max_tx_size,
174            "script" => self.validation_rules.max_script_size,
175            _ => 0,
176        }
177    }
178}
179
180impl BitcoinProtocolEngine {
181    /// Validate a block with protocol-specific rules (witness-blind).
182    ///
183    /// The block weight check is a lower bound only.  Use
184    /// [`validate_block_with_protocol_and_witnesses`] for accurate BIP141 weight enforcement.
185    pub fn validate_block_with_protocol(
186        &self,
187        block: &Block,
188        _utxos: &UtxoSet,
189        _height: u64,
190        context: &ProtocolValidationContext,
191    ) -> Result<ValidationResult> {
192        self.apply_protocol_validation(block, context)?;
193        Ok(ValidationResult::Valid)
194    }
195
196    /// Validate a block with protocol-specific rules and witness data.
197    ///
198    /// Performs a full BIP141 weight check using the provided witnesses.
199    pub fn validate_block_with_protocol_and_witnesses(
200        &self,
201        block: &Block,
202        witnesses: &[Vec<blvm_consensus::segwit::Witness>],
203        _utxos: &UtxoSet,
204        _height: u64,
205        context: &ProtocolValidationContext,
206    ) -> Result<ValidationResult> {
207        self.apply_protocol_validation_with_witnesses(block, Some(witnesses), context)?;
208        Ok(ValidationResult::Valid)
209    }
210
211    /// Validate a transaction with protocol-specific rules
212    pub fn validate_transaction_with_protocol(
213        &self,
214        tx: &Transaction,
215        context: &ProtocolValidationContext,
216    ) -> Result<ValidationResult> {
217        // First, run consensus validation
218        let consensus_result = self.consensus.validate_transaction(tx)?;
219
220        // Then, apply protocol-specific validation
221        self.apply_transaction_protocol_validation(tx, context)?;
222
223        Ok(consensus_result)
224    }
225
226    /// Apply protocol-specific validation rules (witness-blind; lower-bound weight only).
227    ///
228    /// This variant does not have witness data available, so the block weight check is the
229    /// stripped block: non-witness bytes at ×4. It cannot catch a block that exceeds the
230    /// limit only because of witness data. Prefer
231    /// [`apply_protocol_validation_with_witnesses`] when witness data is available.
232    fn apply_protocol_validation(
233        &self,
234        block: &Block,
235        context: &ProtocolValidationContext,
236    ) -> Result<()> {
237        self.apply_protocol_validation_with_witnesses(block, None, context)
238    }
239
240    /// Apply protocol-specific validation rules with optional witness data.
241    ///
242    /// Block weight is [`blvm_consensus::segwit::calculate_block_weight_from_nested`]:
243    /// non-witness bytes at ×4, and the marker, flag, stack counts, and witness
244    /// elements at ×1. A missing witness list is the stripped block.
245    fn apply_protocol_validation_with_witnesses(
246        &self,
247        block: &Block,
248        witnesses: Option<&[Vec<blvm_consensus::segwit::Witness>]>,
249        context: &ProtocolValidationContext,
250    ) -> Result<()> {
251        let witness_list = witnesses.unwrap_or(&[]);
252        let weight =
253            blvm_consensus::segwit::calculate_block_weight_from_nested(block, witness_list)?;
254        if weight > context.validation_rules.max_block_size as u64 {
255            return Err(ProtocolError::Validation(
256                format!(
257                    "Block weight exceeds maximum: {} WU (max {} WU)",
258                    weight, context.validation_rules.max_block_size
259                )
260                .into(),
261            ));
262        }
263
264        for tx in &block.transactions {
265            self.apply_transaction_protocol_validation(tx, context)?;
266        }
267
268        Ok(())
269    }
270
271    /// Apply protocol-specific transaction validation
272    fn apply_transaction_protocol_validation(
273        &self,
274        tx: &Transaction,
275        context: &ProtocolValidationContext,
276    ) -> Result<()> {
277        // Check transaction size limits
278        let tx_size = self.calculate_transaction_size(tx);
279        if tx_size > context.validation_rules.max_tx_size {
280            return Err(ProtocolError::Validation(
281                format!(
282                    "Transaction size exceeds maximum: {} bytes (max {} bytes)",
283                    tx_size, context.validation_rules.max_tx_size
284                )
285                .into(),
286            ));
287        }
288
289        // Check script size limits
290        for input in &tx.inputs {
291            if input.script_sig.len() > context.validation_rules.max_script_size as usize {
292                return Err(ProtocolError::Validation(
293                    format!(
294                        "Script size exceeds maximum: {} bytes (max {} bytes)",
295                        input.script_sig.len(),
296                        context.validation_rules.max_script_size
297                    )
298                    .into(),
299                ));
300            }
301        }
302
303        Ok(())
304    }
305
306    /// Calculate block size in bytes
307    /// Calculate transaction size in bytes
308    fn calculate_transaction_size(&self, tx: &Transaction) -> u32 {
309        // Use canonical serialization-based size from consensus layer (TX_NO_WITNESS size).
310        //
311        // This keeps protocol-level size limits aligned with the exact serialization
312        // used for consensus checks and transaction size tests.
313        blvm_consensus::transaction::calculate_transaction_size(tx) as u32
314    }
315}
316
317#[cfg(test)]
318mod tests {
319    use super::*;
320    use blvm_consensus::types::{OutPoint, TransactionInput, TransactionOutput};
321    use blvm_consensus::{Block, BlockHeader, Transaction};
322
323    #[test]
324    fn test_validation_rules() {
325        let mainnet_rules = ProtocolValidationRules::mainnet();
326        assert_eq!(mainnet_rules.max_block_size, 4_000_000);
327        assert!(mainnet_rules.segwit_enabled);
328        assert!(mainnet_rules.taproot_enabled);
329
330        let regtest_rules = ProtocolValidationRules::regtest();
331        assert_eq!(regtest_rules.max_block_size, 4_000_000);
332        assert!(regtest_rules.segwit_enabled);
333        assert_eq!(regtest_rules.min_fee_rate, 0); // No minimum fee for testing
334    }
335
336    #[test]
337    fn test_validation_rules_all_protocols() {
338        let mainnet_rules = ProtocolValidationRules::for_protocol(ProtocolVersion::BitcoinV1);
339        let testnet_rules = ProtocolValidationRules::for_protocol(ProtocolVersion::Testnet3);
340        let regtest_rules = ProtocolValidationRules::for_protocol(ProtocolVersion::Regtest);
341
342        // Mainnet and testnet should have same rules
343        assert_eq!(mainnet_rules.max_block_size, testnet_rules.max_block_size);
344        assert_eq!(mainnet_rules.max_tx_size, testnet_rules.max_tx_size);
345        assert_eq!(mainnet_rules.max_script_size, testnet_rules.max_script_size);
346        assert_eq!(mainnet_rules.segwit_enabled, testnet_rules.segwit_enabled);
347        assert_eq!(mainnet_rules.taproot_enabled, testnet_rules.taproot_enabled);
348        assert_eq!(mainnet_rules.rbf_enabled, testnet_rules.rbf_enabled);
349        assert_eq!(mainnet_rules.min_fee_rate, testnet_rules.min_fee_rate);
350        assert_eq!(mainnet_rules.max_fee_rate, testnet_rules.max_fee_rate);
351
352        // Regtest should have relaxed fee rules
353        assert_eq!(regtest_rules.min_fee_rate, 0);
354        assert_eq!(regtest_rules.max_fee_rate, mainnet_rules.max_fee_rate);
355    }
356
357    #[test]
358    fn test_validation_rules_serialization() {
359        let mainnet_rules = ProtocolValidationRules::mainnet();
360        let json = serde_json::to_string(&mainnet_rules).unwrap();
361        let deserialized: ProtocolValidationRules = serde_json::from_str(&json).unwrap();
362
363        assert_eq!(mainnet_rules.max_block_size, deserialized.max_block_size);
364        assert_eq!(mainnet_rules.max_tx_size, deserialized.max_tx_size);
365        assert_eq!(mainnet_rules.max_script_size, deserialized.max_script_size);
366        assert_eq!(mainnet_rules.segwit_enabled, deserialized.segwit_enabled);
367        assert_eq!(mainnet_rules.taproot_enabled, deserialized.taproot_enabled);
368        assert_eq!(mainnet_rules.rbf_enabled, deserialized.rbf_enabled);
369        assert_eq!(mainnet_rules.min_fee_rate, deserialized.min_fee_rate);
370        assert_eq!(mainnet_rules.max_fee_rate, deserialized.max_fee_rate);
371    }
372
373    #[test]
374    fn test_validation_rules_equality() {
375        let mainnet1 = ProtocolValidationRules::mainnet();
376        let mainnet2 = ProtocolValidationRules::mainnet();
377        let testnet = ProtocolValidationRules::testnet();
378
379        assert_eq!(mainnet1, mainnet2);
380        assert_eq!(mainnet1, testnet); // Mainnet and testnet should be identical
381    }
382
383    #[test]
384    fn test_validation_context() {
385        // Use a height past both segwit (481824) and taproot (709632) activation.
386        let context = ProtocolValidationContext::new(ProtocolVersion::BitcoinV1, 800_000).unwrap();
387        assert_eq!(context.block_height, 800_000);
388        assert!(context.is_feature_enabled("segwit"));
389        assert!(!context.is_feature_enabled("nonexistent"));
390        assert_eq!(context.get_max_size("block"), 4_000_000);
391    }
392
393    #[test]
394    fn test_validation_context_all_protocols() {
395        // Per-network heights past segwit+taproot activation:
396        //   mainnet: taproot at 709_632   → use 800_000
397        //   testnet3: taproot at 2_016_000 → use 2_100_000
398        //   regtest: all features AlwaysActive → any height works
399        let mainnet_context =
400            ProtocolValidationContext::new(ProtocolVersion::BitcoinV1, 800_000).unwrap();
401        let testnet_context =
402            ProtocolValidationContext::new(ProtocolVersion::Testnet3, 2_100_000).unwrap();
403        let regtest_context = ProtocolValidationContext::new(ProtocolVersion::Regtest, 1).unwrap();
404
405        assert_eq!(mainnet_context.block_height, 800_000);
406        assert_eq!(testnet_context.block_height, 2_100_000);
407        assert_eq!(regtest_context.block_height, 1);
408
409        // All should support same features
410        assert!(mainnet_context.is_feature_enabled("segwit"));
411        assert!(testnet_context.is_feature_enabled("segwit"));
412        assert!(regtest_context.is_feature_enabled("segwit"));
413
414        assert!(mainnet_context.is_feature_enabled("taproot"));
415        assert!(testnet_context.is_feature_enabled("taproot"));
416        assert!(regtest_context.is_feature_enabled("taproot"));
417
418        assert!(mainnet_context.is_feature_enabled("rbf"));
419        assert!(testnet_context.is_feature_enabled("rbf"));
420        assert!(regtest_context.is_feature_enabled("rbf"));
421    }
422
423    #[test]
424    fn test_validation_context_feature_queries() {
425        // Height past both segwit (481824) and taproot (709632) so all features are active.
426        let context = ProtocolValidationContext::new(ProtocolVersion::BitcoinV1, 800_000).unwrap();
427
428        // Test all supported features
429        assert!(context.is_feature_enabled("segwit"));
430        assert!(context.is_feature_enabled("taproot"));
431        assert!(context.is_feature_enabled("rbf"));
432
433        // Test unsupported features
434        assert!(!context.is_feature_enabled("nonexistent"));
435        assert!(!context.is_feature_enabled(""));
436        assert!(!context.is_feature_enabled("fast_mining"));
437    }
438
439    #[test]
440    fn test_validation_context_size_queries() {
441        let context = ProtocolValidationContext::new(ProtocolVersion::BitcoinV1, 1000).unwrap();
442
443        assert_eq!(context.get_max_size("block"), 4_000_000);
444        assert_eq!(context.get_max_size("transaction"), 1_000_000);
445        assert_eq!(context.get_max_size("script"), 10_000);
446
447        // Test unknown component
448        assert_eq!(context.get_max_size("unknown"), 0);
449    }
450
451    #[test]
452    fn test_validation_context_serialization() {
453        let context = ProtocolValidationContext::new(ProtocolVersion::BitcoinV1, 1000).unwrap();
454        let json = serde_json::to_string(&context).unwrap();
455        let deserialized: ProtocolValidationContext = serde_json::from_str(&json).unwrap();
456
457        assert_eq!(context.block_height, deserialized.block_height);
458        assert_eq!(
459            context.network_params.network_name,
460            deserialized.network_params.network_name
461        );
462        assert_eq!(
463            context.validation_rules.max_block_size,
464            deserialized.validation_rules.max_block_size
465        );
466    }
467
468    #[test]
469    fn test_validation_context_equality() {
470        let context1 = ProtocolValidationContext::new(ProtocolVersion::BitcoinV1, 1000).unwrap();
471        let context2 = ProtocolValidationContext::new(ProtocolVersion::BitcoinV1, 1000).unwrap();
472        let context3 = ProtocolValidationContext::new(ProtocolVersion::Testnet3, 1000).unwrap();
473
474        assert_eq!(context1, context2);
475        assert_ne!(context1, context3); // Different network parameters
476    }
477
478    #[test]
479    fn test_block_size_validation() {
480        let engine = BitcoinProtocolEngine::new(ProtocolVersion::BitcoinV1).unwrap();
481        let context = ProtocolValidationContext::new(ProtocolVersion::BitcoinV1, 1000).unwrap();
482
483        // Create a block that's within size limits with a valid coinbase transaction
484        let coinbase_tx = Transaction {
485            version: 1,
486            inputs: blvm_consensus::tx_inputs![TransactionInput {
487                prevout: OutPoint {
488                    hash: [0u8; 32],
489                    index: 0xffffffff,
490                },
491                script_sig: vec![0x01, 0x00], // Height 0
492                sequence: 0xffffffff,
493            }],
494            outputs: blvm_consensus::tx_outputs![TransactionOutput {
495                value: 50_0000_0000,
496                script_pubkey: vec![blvm_consensus::opcodes::OP_1],
497            }],
498            lock_time: 0,
499        };
500
501        // Calculate proper merkle root
502        let merkle_root = blvm_consensus::mining::calculate_merkle_root(&[coinbase_tx.clone()])
503            .expect("Should calculate merkle root");
504
505        let small_block = Block {
506            header: BlockHeader {
507                version: 1,
508                prev_block_hash: [0u8; 32],
509                merkle_root,
510                timestamp: 1231006505,
511                bits: 0x1d00ffff,
512                nonce: 0,
513            },
514            transactions: vec![coinbase_tx].into_boxed_slice(),
515        };
516
517        // This should pass validation
518        let result =
519            engine.validate_block_with_protocol(&small_block, &UtxoSet::default(), 1000, &context);
520        assert!(result.is_ok());
521    }
522
523    #[test]
524    fn test_transaction_size_validation() {
525        let engine = BitcoinProtocolEngine::new(ProtocolVersion::BitcoinV1).unwrap();
526        let context = ProtocolValidationContext::new(ProtocolVersion::BitcoinV1, 1000).unwrap();
527
528        // Create a small transaction
529        let small_tx = Transaction {
530            version: 1,
531            inputs: vec![TransactionInput {
532                prevout: OutPoint {
533                    hash: [0u8; 32],
534                    index: 0,
535                },
536                script_sig: vec![blvm_consensus::opcodes::PUSH_65_BYTES, 0x04],
537                sequence: 0xffffffff,
538            }]
539            .into(),
540            outputs: vec![TransactionOutput {
541                value: 50_0000_0000,
542                script_pubkey: vec![
543                    blvm_consensus::opcodes::OP_DUP,
544                    blvm_consensus::opcodes::OP_HASH160,
545                    blvm_consensus::opcodes::PUSH_20_BYTES,
546                    0x00,
547                    0x00,
548                    0x00,
549                    0x00,
550                    0x00,
551                    0x00,
552                    0x00,
553                    0x00,
554                    0x00,
555                    0x00,
556                    0x00,
557                    0x00,
558                    0x00,
559                    0x00,
560                    0x00,
561                    0x00,
562                    0x00,
563                    0x00,
564                    0x00,
565                    0x00,
566                    blvm_consensus::opcodes::OP_EQUALVERIFY,
567                    blvm_consensus::opcodes::OP_CHECKSIG,
568                ],
569            }]
570            .into(),
571            lock_time: 0,
572        };
573
574        // This should pass validation
575        let result = engine.validate_transaction_with_protocol(&small_tx, &context);
576        assert!(result.is_ok());
577    }
578
579    #[test]
580    fn test_script_size_validation() {
581        let engine = BitcoinProtocolEngine::new(ProtocolVersion::BitcoinV1).unwrap();
582        let context = ProtocolValidationContext::new(ProtocolVersion::BitcoinV1, 1000).unwrap();
583
584        // Create a transaction with small scripts
585        let tx = Transaction {
586            version: 1,
587            inputs: vec![TransactionInput {
588                prevout: OutPoint {
589                    hash: [0u8; 32],
590                    index: 0,
591                },
592                script_sig: vec![blvm_consensus::opcodes::PUSH_65_BYTES, 0x04],
593                sequence: 0xffffffff,
594            }]
595            .into(),
596            outputs: vec![TransactionOutput {
597                value: 50_0000_0000,
598                script_pubkey: vec![
599                    blvm_consensus::opcodes::OP_DUP,
600                    blvm_consensus::opcodes::OP_HASH160,
601                    blvm_consensus::opcodes::PUSH_20_BYTES,
602                    0x00,
603                    0x00,
604                    0x00,
605                    0x00,
606                    0x00,
607                    0x00,
608                    0x00,
609                    0x00,
610                    0x00,
611                    0x00,
612                    0x00,
613                    0x00,
614                    0x00,
615                    0x00,
616                    0x00,
617                    0x00,
618                    0x00,
619                    0x00,
620                    0x00,
621                    0x00,
622                    blvm_consensus::opcodes::OP_EQUALVERIFY,
623                    blvm_consensus::opcodes::OP_CHECKSIG,
624                ],
625            }]
626            .into(),
627            lock_time: 0,
628        };
629
630        // This should pass validation
631        let result = engine.validate_transaction_with_protocol(&tx, &context);
632        assert!(result.is_ok());
633    }
634
635    #[test]
636    fn test_validation_context_data() {
637        let mut context = ProtocolValidationContext::new(ProtocolVersion::BitcoinV1, 1000).unwrap();
638
639        // Add some context data
640        context
641            .context_data
642            .insert("test_key".to_string(), "test_value".to_string());
643
644        assert_eq!(
645            context.context_data.get("test_key"),
646            Some(&"test_value".to_string())
647        );
648        assert_eq!(context.context_data.get("nonexistent"), None);
649    }
650
651    #[test]
652    fn test_validation_rules_boundary_values() {
653        let rules = ProtocolValidationRules::mainnet();
654
655        // Test boundary values
656        assert!(rules.max_block_size > 0);
657        assert!(rules.max_tx_size > 0);
658        assert!(rules.max_script_size > 0);
659        assert!(rules.max_fee_rate > rules.min_fee_rate);
660
661        // Test that limits are reasonable
662        assert!(rules.max_block_size <= 10_000_000); // Not unreasonably large
663        assert!(rules.max_tx_size <= 5_000_000); // Not unreasonably large
664        assert!(rules.max_script_size <= 50_000); // Not unreasonably large
665    }
666
667    #[test]
668    fn test_block_weight_counts_witness_element_bytes() {
669        use blvm_consensus::opcodes::OP_1;
670        use blvm_consensus::{
671            Block, BlockHeader, OutPoint, Transaction, TransactionInput, TransactionOutput,
672        };
673
674        let engine = BitcoinProtocolEngine::new(ProtocolVersion::Regtest).unwrap();
675        let mut context = ProtocolValidationContext::new(ProtocolVersion::Regtest, 1).unwrap();
676        context.validation_rules.max_block_size = 5_000;
677
678        let coinbase = Transaction {
679            version: 1,
680            inputs: vec![TransactionInput {
681                prevout: OutPoint {
682                    hash: [0u8; 32],
683                    index: 0xffffffff,
684                },
685                script_sig: vec![OP_1].into(),
686                sequence: 0xffffffff,
687            }]
688            .into(),
689            outputs: vec![TransactionOutput {
690                value: 50_0000_0000,
691                script_pubkey: vec![OP_1].into(),
692            }]
693            .into(),
694            lock_time: 0,
695        };
696        let tx = Transaction {
697            version: 2,
698            inputs: vec![TransactionInput {
699                prevout: OutPoint {
700                    hash: [1u8; 32],
701                    index: 0,
702                },
703                script_sig: vec![OP_1].into(),
704                sequence: 0xffffffff,
705            }]
706            .into(),
707            outputs: vec![TransactionOutput {
708                value: 1_000,
709                script_pubkey: vec![OP_1].into(),
710            }]
711            .into(),
712            lock_time: 0,
713        };
714        let block = Block {
715            header: BlockHeader {
716                version: 1,
717                prev_block_hash: [0u8; 32],
718                merkle_root: [0u8; 32],
719                timestamp: 1_700_000_000,
720                bits: 0x207fffff,
721                nonce: 0,
722            },
723            transactions: vec![coinbase, tx].into(),
724        };
725        // One heavy witness element (9000 bytes) — stack depth alone would be 1 WU (REV-P-07).
726        let heavy_elem = vec![0u8; 9000];
727        let witnesses: Vec<Vec<blvm_consensus::segwit::Witness>> =
728            vec![vec![vec![]], vec![vec![heavy_elem]]];
729
730        let result = engine.validate_block_with_protocol_and_witnesses(
731            &block,
732            &witnesses,
733            &UtxoSet::default(),
734            1,
735            &context,
736        );
737        assert!(
738            result.is_err(),
739            "heavy witness bytes must exceed reduced max_block_size weight limit"
740        );
741    }
742
743    #[test]
744    fn protocol_block_weight_matches_consensus_including_witness_framing() {
745        use blvm_consensus::opcodes::OP_1;
746        use blvm_consensus::{
747            Block, BlockHeader, OutPoint, Transaction, TransactionInput, TransactionOutput,
748        };
749
750        let engine = BitcoinProtocolEngine::new(ProtocolVersion::Regtest).unwrap();
751        let mut context = ProtocolValidationContext::new(ProtocolVersion::Regtest, 1).unwrap();
752        let coinbase = Transaction {
753            version: 1,
754            inputs: vec![TransactionInput {
755                prevout: OutPoint {
756                    hash: [0u8; 32],
757                    index: 0xffffffff,
758                },
759                script_sig: vec![OP_1].into(),
760                sequence: 0xffffffff,
761            }]
762            .into(),
763            outputs: vec![TransactionOutput {
764                value: 50_0000_0000,
765                script_pubkey: vec![OP_1].into(),
766            }]
767            .into(),
768            lock_time: 0,
769        };
770        let tx = Transaction {
771            version: 2,
772            inputs: vec![TransactionInput {
773                prevout: OutPoint {
774                    hash: [1u8; 32],
775                    index: 0,
776                },
777                script_sig: vec![OP_1].into(),
778                sequence: 0xffffffff,
779            }]
780            .into(),
781            outputs: vec![TransactionOutput {
782                value: 1_000,
783                script_pubkey: vec![OP_1].into(),
784            }]
785            .into(),
786            lock_time: 0,
787        };
788        let block = Block {
789            header: BlockHeader {
790                version: 1,
791                prev_block_hash: [0u8; 32],
792                merkle_root: [0u8; 32],
793                timestamp: 1_700_000_000,
794                bits: 0x207fffff,
795                nonce: 0,
796            },
797            transactions: vec![coinbase, tx].into(),
798        };
799        let witnesses: Vec<Vec<blvm_consensus::segwit::Witness>> =
800            vec![vec![vec![]], vec![vec![vec![0x11]]]];
801        let weight = blvm_consensus::segwit::calculate_block_weight_from_nested(&block, &witnesses)
802            .expect("witness stacks match the inputs");
803        context.validation_rules.max_block_size = weight as u32;
804        assert!(
805            engine
806                .validate_block_with_protocol_and_witnesses(
807                    &block,
808                    &witnesses,
809                    &UtxoSet::default(),
810                    1,
811                    &context,
812                )
813                .is_ok(),
814            "weight equal to the consensus weight is accepted"
815        );
816        context.validation_rules.max_block_size = (weight - 1) as u32;
817        assert!(
818            engine
819                .validate_block_with_protocol_and_witnesses(
820                    &block,
821                    &witnesses,
822                    &UtxoSet::default(),
823                    1,
824                    &context,
825                )
826                .is_err(),
827            "one weight unit under the consensus weight is rejected"
828        );
829    }
830}