Skip to main content

blvm_consensus/
segwit.rs

1//! Segregated Witness (SegWit) functions from Orange Paper Section 11.1
2//!
3//! **Debug logging (optional):** enable crate feature **`profile`**, then set:
4//! - **`BLVM_WITNESS_DEBUG`** — after computing the witness merkle root from wtxids, print tx count and hex root.
5//! - **`BLVM_WITNESS_COMMIT_DEBUG`** — when a witness commitment output exists but does not match BIP141
6//!   `sha256d(witness_root ‖ reserved_nonce)`, print expected vs found commitment.
7//!
8//! These hooks are omitted from non-`profile` builds so default releases never consult these env vars.
9
10use crate::error::Result;
11use crate::opcodes::*;
12use crate::types::*;
13use crate::types::{ByteString, Hash, Natural};
14use crate::witness;
15use bitcoin_hashes::{sha256d, Hash as BitcoinHash, HashEngine};
16use blvm_spec_lock::spec_locked;
17
18/// Witness Data: 𝒲 = 𝕊* (stack of witness elements)
19///
20/// Uses unified witness type from witness module for consistency with Taproot
21pub use crate::witness::Witness;
22
23/// Calculate transaction weight for SegWit
24/// Weight(tx) = 4 × |Serialize(tx ∖ witness)| + |Serialize(tx)|
25#[spec_locked("11.1.1")]
26pub fn calculate_transaction_weight(
27    tx: &Transaction,
28    witness: Option<&Witness>,
29) -> Result<Natural> {
30    // Calculate base size (transaction without witness data)
31    let base_size = calculate_base_size(tx);
32
33    // Calculate total size (transaction with witness data)
34    let total_size = calculate_total_size(tx, witness);
35
36    // Use unified witness framework for weight formula
37    Ok(witness::calculate_transaction_weight_segwit(
38        base_size, total_size,
39    ))
40}
41
42/// Calculate base size (transaction without witness data).
43///
44/// Simplified consensus-facing estimate (version + inputs + outputs + lock_time). Split into
45/// bounded `usize` steps then a single cast so blvm-spec-lock Z3 can verify `ensures` without
46/// timing out on one huge arithmetic expression.
47#[spec_locked("11.1.1")]
48fn calculate_base_size(tx: &Transaction) -> Natural {
49    const VERSION_AND_LOCKTIME: usize = 4 + 4;
50    const PER_INPUT: usize = 32 + 4 + 1 + 4;
51    const PER_OUTPUT: usize = 8 + 1;
52    let n_in = tx.inputs.len();
53    let n_out = tx.outputs.len();
54    let inputs_part = n_in.saturating_mul(PER_INPUT);
55    let outputs_part = n_out.saturating_mul(PER_OUTPUT);
56    (VERSION_AND_LOCKTIME + inputs_part + outputs_part) as Natural
57}
58
59/// Calculate total size (transaction with witness data)
60#[spec_locked("11.1.1")]
61fn calculate_total_size(tx: &Transaction, witness: Option<&Witness>) -> Natural {
62    let base_size = calculate_base_size(tx);
63
64    if let Some(witness_data) = witness {
65        let witness_size: Natural = witness_data.iter().map(|w| w.len() as Natural).sum();
66        base_size + witness_size
67    } else {
68        base_size
69    }
70}
71
72/// Compute witness merkle root for block
73/// WitnessRoot = ComputeMerkleRoot({Hash(tx.witness) : tx ∈ block.transactions})
74#[spec_locked("11.1.4")]
75pub fn compute_witness_merkle_root(block: &Block, witnesses: &[Witness]) -> Result<Hash> {
76    if block.transactions.is_empty() {
77        return Err(crate::error::ConsensusError::ConsensusRuleViolation(
78            "Cannot compute witness merkle root for empty block".into(),
79        ));
80    }
81
82    // Hash each witness
83    let mut witness_hashes = Vec::new();
84    for (i, witness) in witnesses.iter().enumerate() {
85        if i == 0 {
86            // Coinbase transaction has empty witness
87            witness_hashes.push([0u8; 32]);
88        } else {
89            let witness_hash = hash_witness(witness);
90            witness_hashes.push(witness_hash);
91        }
92    }
93
94    // Compute merkle root of witness hashes
95    compute_merkle_root(&witness_hashes)
96}
97
98/// Double-SHA256 a byte slice, returning a 32-byte hash array.
99fn sha256d_bytes(data: &[u8]) -> Hash {
100    let result = sha256d::Hash::hash(data);
101    let mut hash = [0u8; 32];
102    hash.copy_from_slice(&result[..]);
103    hash
104}
105
106/// Hash witness data
107/// Orange Paper 11.1: Hash(tx.witness) for witness merkle commitment
108#[spec_locked("11.1")]
109fn hash_witness(witness: &Witness) -> Hash {
110    let mut hasher = sha256d::Hash::engine();
111    for element in witness {
112        hasher.input(element);
113    }
114    let result = sha256d::Hash::from_engine(hasher);
115    let mut hash = [0u8; 32];
116    hash.copy_from_slice(&result);
117    hash
118}
119
120/// Hash witness from nested structure (Vec<Witness> per tx) without allocating.
121/// Each tx's witness is the concatenation of its input stacks for merkle commitment.
122/// Orange Paper 11.1.4: Hash(tx.witness) for witness merkle commitment
123#[spec_locked("11.1.4")]
124fn hash_witness_from_nested(tx_witnesses: &[Witness]) -> Hash {
125    let mut hasher = sha256d::Hash::engine();
126    for witness_stack in tx_witnesses {
127        for element in witness_stack {
128            hasher.input(element);
129        }
130    }
131    let result = sha256d::Hash::from_engine(hasher);
132    let mut hash = [0u8; 32];
133    hash.copy_from_slice(&result);
134    hash
135}
136
137/// Compute witness merkle root from nested witnesses without flattening.
138/// Accepts `&[Vec<Witness>]` where each `Vec<Witness>` is one tx's input stacks.
139/// Avoids allocating flattened structure in block validation hot path.
140/// Orange Paper 11.1.4: WitnessRoot = ComputeMerkleRoot({wtxid : tx ∈ block.transactions})
141///
142/// BIP141 wtxid computation:
143///   - coinbase: all zeros (hardcoded, consensus rule)
144///   - non-SegWit tx (no witness data): wtxid = txid = sha256d(legacy serialization)
145///   - SegWit tx (has witness data): wtxid = sha256d(segwit-serialized tx incl. witnesses)
146///
147/// Previously this used `hash_witness_from_nested` which returns sha256d("") for ALL
148/// non-SegWit transactions, causing every non-SegWit tx to map to the same hash.
149/// In a block with many non-SegWit txs (e.g. block 481824), adjacent pairs of these
150/// identical hashes trigger the CVE-2012-2459 mutation check and abort falsely.
151#[spec_locked("11.1.4")]
152pub fn compute_witness_merkle_root_from_nested(
153    block: &Block,
154    witnesses: &[Vec<Witness>],
155) -> Result<Hash> {
156    if block.transactions.is_empty() {
157        return Err(crate::error::ConsensusError::ConsensusRuleViolation(
158            "Cannot compute witness merkle root from empty block".into(),
159        ));
160    }
161    let mut witness_hashes = Vec::with_capacity(block.transactions.len());
162    for (i, (tx, tx_witnesses)) in block.transactions.iter().zip(witnesses.iter()).enumerate() {
163        if i == 0 {
164            // Coinbase wtxid is always all-zeros by consensus (BIP141)
165            witness_hashes.push([0u8; 32]);
166        } else {
167            // Has any non-empty witness stack element across all inputs?
168            let has_witness = tx_witnesses.iter().any(|w| !w.is_empty());
169            let hash = if has_witness {
170                // SegWit tx: wtxid = sha256d(version || 0x00 0x01 || inputs || outputs || witnesses || locktime)
171                let serialized =
172                    crate::serialization::transaction::serialize_transaction_with_witness(
173                        tx,
174                        tx_witnesses,
175                    );
176                sha256d_bytes(&serialized)
177            } else {
178                // Non-SegWit tx: wtxid = txid = sha256d(version || inputs || outputs || locktime)
179                let serialized = crate::serialization::transaction::serialize_transaction(tx);
180                sha256d_bytes(&serialized)
181            };
182            witness_hashes.push(hash);
183        }
184    }
185    let root = compute_merkle_root(&witness_hashes);
186    #[cfg(feature = "profile")]
187    if std::env::var("BLVM_WITNESS_DEBUG").is_ok() {
188        if let Ok(r) = &root {
189            eprintln!(
190                "BLVM_WITNESS_DEBUG: {} txs, root={}",
191                witness_hashes.len(),
192                hex::encode(r)
193            );
194        }
195    }
196    root
197}
198
199/// Compute merkle root from hashes (Orange Paper 8.4.1).
200/// Uses proper pair-and-hash with CVE-2012-2459 mutation detection.
201fn compute_merkle_root(hashes: &[Hash]) -> Result<Hash> {
202    crate::mining::calculate_merkle_root_from_tx_ids(hashes)
203}
204
205/// Validate witness commitment in coinbase transaction.
206///
207/// Per BIP141, the commitment recorded in the coinbase OP_RETURN is:
208///   commitment = sha256d(witness_merkle_root || coinbase_reserved_nonce)
209/// where `coinbase_reserved_nonce` is the 32-byte nonce stored in
210/// the coinbase input's witness stack (witnesses\[0\]\[0\]).
211/// If the coinbase has no witness, the reserved value is 32 zero bytes.
212///
213/// The OP_RETURN output format is:
214///   OP_RETURN 0x24 0xaa21a9ed <commitment_hash:32>
215#[spec_locked("11.1.5")]
216pub fn validate_witness_commitment(
217    coinbase_tx: &Transaction,
218    witness_merkle_root: &Hash,
219    coinbase_witnesses: &[Witness],
220) -> Result<bool> {
221    // Extract the reserved nonce from coinbase input 0's witness stack.
222    // Per BIP141, it MUST be exactly 32 bytes; if absent default to 32 zero bytes.
223    let reserved_nonce: [u8; 32] = coinbase_witnesses
224        .first()
225        .and_then(|w| w.first())
226        .and_then(|item| item.as_slice().try_into().ok())
227        .unwrap_or([0u8; 32]);
228
229    // Compute the expected commitment: sha256d(witness_root || reserved_nonce)
230    let mut preimage = [0u8; 64];
231    preimage[..32].copy_from_slice(witness_merkle_root);
232    preimage[32..].copy_from_slice(&reserved_nonce);
233    let expected_commitment = sha256d_bytes(&preimage);
234
235    // Look for a witness commitment OP_RETURN in coinbase outputs.
236    // BIP141: if multiple outputs match the 0xaa21a9ed prefix, use the LAST one.
237    // Bitcoin Core iterates all outputs and keeps updating the commitment index,
238    // so only the highest-index matching output is used for validation.
239    let mut last_commitment: Option<Hash> = None;
240    for output in &coinbase_tx.outputs {
241        if let Some(commitment) = extract_witness_commitment(&output.script_pubkey) {
242            last_commitment = Some(commitment);
243        }
244    }
245
246    match last_commitment {
247        Some(commitment) => {
248            let ok = commitment == expected_commitment;
249            #[cfg(feature = "profile")]
250            if !ok && std::env::var("BLVM_WITNESS_COMMIT_DEBUG").is_ok() {
251                eprintln!(
252                    "BLVM_WITNESS_COMMIT_DEBUG: root={} nonce={} expected={} got={}",
253                    hex::encode(witness_merkle_root),
254                    hex::encode(reserved_nonce),
255                    hex::encode(expected_commitment),
256                    hex::encode(commitment),
257                );
258            }
259            Ok(ok)
260        }
261        // No witness commitment found — valid for pre-SegWit blocks.
262        None => Ok(true),
263    }
264}
265
266/// Extract the 32-byte commitment hash from a coinbase OP_RETURN witness commitment output.
267/// Orange Paper 11.1.5: Witness commitment in coinbase OP_RETURN output.
268///
269/// Expected format: OP_RETURN 0x24 [0xaa 0x21 0xa9 0xed] [commitment: 32 bytes]
270///   script[0] = 0x6a (OP_RETURN)
271///   script[1] = 0x24 (push 36 bytes)
272///   script[2..6] = 0xaa21a9ed (BIP141 magic prefix)
273///   script[6..38] = commitment hash (32 bytes)
274#[spec_locked("11.1.5")]
275pub(crate) fn extract_witness_commitment(script: &ByteString) -> Option<Hash> {
276    const MAGIC: [u8; 4] = [0xaa, 0x21, 0xa9, 0xed];
277    if script.len() >= 38 && script[0] == OP_RETURN && script[1] == 0x24 && script[2..6] == MAGIC {
278        let mut commitment = [0u8; 32];
279        commitment.copy_from_slice(&script[6..38]);
280        return Some(commitment);
281    }
282    None
283}
284
285/// Check if transaction is SegWit (v0) or Taproot (v1) based on outputs
286#[spec_locked("11.1.6")]
287pub fn is_segwit_transaction(tx: &Transaction) -> bool {
288    use crate::witness::{
289        extract_witness_program, extract_witness_version, validate_witness_program_length,
290    };
291
292    tx.outputs.iter().any(|output| {
293        let script = &output.script_pubkey;
294        if let Some(version) = extract_witness_version(script) {
295            if let Some(program) = extract_witness_program(script, version) {
296                return validate_witness_program_length(&program, version);
297            }
298        }
299        false
300    })
301}
302
303/// Calculate block weight for SegWit blocks
304#[spec_locked("11.1.1")]
305pub fn calculate_block_weight(block: &Block, witnesses: &[Witness]) -> Result<Natural> {
306    let mut total_weight = 0;
307
308    for (i, tx) in block.transactions.iter().enumerate() {
309        let witness = if i < witnesses.len() {
310            Some(&witnesses[i])
311        } else {
312            None
313        };
314
315        total_weight += calculate_transaction_weight(tx, witness)?;
316    }
317
318    Ok(total_weight)
319}
320
321/// Calculate block weight from nested witnesses without flattening.
322/// Accepts `&[Vec<Witness>]` where each `Vec<Witness>` is one tx's input witness stacks.
323/// Avoids allocating the flattened structure in the hot block validation path.
324/// Orange Paper 11.1.1: Weight(tx) = 4 × BaseSize + TotalSize
325#[spec_locked("11.1.1")]
326#[inline]
327pub fn calculate_block_weight_from_nested(
328    block: &Block,
329    witnesses: &[Vec<Witness>],
330) -> Result<Natural> {
331    let mut total_weight = 0;
332    for (i, tx) in block.transactions.iter().enumerate() {
333        let witness_size: Natural = if i < witnesses.len() {
334            witnesses[i]
335                .iter()
336                .flat_map(|w| w.iter())
337                .map(|e| e.len() as Natural)
338                .sum()
339        } else {
340            0
341        };
342        let base_size =
343            (4 + tx.inputs.len() * (32 + 4 + 1 + 4) + tx.outputs.len() * (8 + 1) + 4) as Natural;
344        total_weight +=
345            witness::calculate_transaction_weight_segwit(base_size, base_size + witness_size);
346    }
347    Ok(total_weight)
348}
349
350/// Validate SegWit block
351#[spec_locked("11.1.7")]
352pub fn validate_segwit_block(
353    block: &Block,
354    witnesses: &[Witness],
355    max_block_weight: Natural,
356) -> Result<bool> {
357    // Validate witness structure for all transactions using unified framework
358    for (i, _tx) in block.transactions.iter().enumerate() {
359        if i < witnesses.len() && !witness::validate_segwit_witness_structure(&witnesses[i])? {
360            return Ok(false);
361        }
362    }
363
364    // Check block weight limit
365    let block_weight = calculate_block_weight(block, witnesses)?;
366    if block_weight > max_block_weight {
367        return Ok(false);
368    }
369
370    // Validate witness commitment
371    if !block.transactions.is_empty() && !witnesses.is_empty() {
372        let witness_root = compute_witness_merkle_root(block, witnesses)?;
373        // witnesses[0] is the coinbase input's witness stack (contains the reserved nonce)
374        if !validate_witness_commitment(
375            &block.transactions[0],
376            &witness_root,
377            std::slice::from_ref(&witnesses[0]),
378        )? {
379            return Ok(false);
380        }
381    }
382
383    Ok(true)
384}
385
386#[cfg(test)]
387mod tests {
388    use super::*;
389    use crate::test_utils::create_test_header;
390
391    #[test]
392    fn test_calculate_transaction_weight() {
393        let tx = create_test_transaction();
394        let witness = vec![vec![OP_1], vec![OP_2]]; // OP_1, OP_2
395
396        let weight = calculate_transaction_weight(&tx, Some(&witness)).unwrap();
397        assert!(weight > 0);
398    }
399
400    #[test]
401    fn test_calculate_transaction_weight_no_witness() {
402        let tx = create_test_transaction();
403
404        let weight = calculate_transaction_weight(&tx, None).unwrap();
405        assert!(weight > 0);
406    }
407
408    #[test]
409    fn test_compute_witness_merkle_root() {
410        let block = create_test_block();
411        let witnesses = vec![
412            vec![],           // Coinbase witness (empty)
413            vec![vec![OP_1]], // First transaction witness
414        ];
415
416        let root = compute_witness_merkle_root(&block, &witnesses).unwrap();
417        assert_eq!(root.len(), 32);
418    }
419
420    #[test]
421    fn test_compute_witness_merkle_root_empty_block() {
422        let block = Block {
423            header: create_test_header(1231006505, [0u8; 32]),
424            transactions: vec![].into_boxed_slice(),
425        };
426        let witnesses = vec![];
427
428        let result = compute_witness_merkle_root(&block, &witnesses);
429        assert!(result.is_err());
430    }
431
432    #[test]
433    fn test_validate_witness_commitment() {
434        let mut coinbase_tx = create_test_transaction();
435        let witness_root = [1u8; 32];
436        let nonce = [0u8; 32]; // default reserved nonce
437
438        // Add witness commitment to coinbase script (correct BIP141 format)
439        coinbase_tx.outputs[0].script_pubkey =
440            create_witness_commitment_script(&witness_root, &nonce);
441
442        // Empty coinbase witnesses → nonce defaults to [0u8; 32]
443        let is_valid = validate_witness_commitment(&coinbase_tx, &witness_root, &[]).unwrap();
444        assert!(is_valid);
445    }
446
447    #[test]
448    fn test_is_segwit_transaction() {
449        // SegWit transactions are detected by witness program outputs, not scriptSig
450        // P2WPKH: OP_0 <20-byte-hash>
451        // The format in Bitcoin is: [OP_0, PUSH_20_BYTES, <20-byte-hash>]
452        // Where OP_0 is OP_0 (witness version), PUSH_20_BYTES is push 20 bytes, then 20 bytes of hash
453        let mut tx = create_test_transaction();
454        // Create a P2WPKH output (OP_0 <20-byte-hash>)
455        let p2wpkh_hash = [0x51; 20]; // 20-byte hash
456        let mut script_pubkey = vec![OP_0, PUSH_20_BYTES]; // OP_0, push 20 bytes
457        script_pubkey.extend_from_slice(&p2wpkh_hash);
458        tx.outputs[0].script_pubkey = script_pubkey.into();
459
460        assert!(is_segwit_transaction(&tx));
461    }
462
463    #[test]
464    fn test_calculate_block_weight() {
465        let block = create_test_block();
466        let witnesses = vec![
467            vec![],           // Coinbase
468            vec![vec![OP_1]], // First tx
469        ];
470
471        let weight = calculate_block_weight(&block, &witnesses).unwrap();
472        assert!(weight > 0);
473    }
474
475    #[test]
476    fn test_validate_segwit_block() {
477        let block = create_test_block();
478        let witnesses = vec![
479            vec![],           // Coinbase
480            vec![vec![OP_1]], // First tx
481        ];
482
483        let is_valid = validate_segwit_block(&block, &witnesses, 4_000_000).unwrap();
484        assert!(is_valid);
485    }
486
487    #[test]
488    fn test_validate_segwit_block_exceeds_weight() {
489        let block = create_test_block();
490        let witnesses = vec![
491            vec![],           // Coinbase
492            vec![vec![OP_1]], // First tx
493        ];
494
495        let is_valid = validate_segwit_block(&block, &witnesses, 1).unwrap(); // Very low weight limit
496        assert!(!is_valid);
497    }
498
499    #[test]
500    fn test_validate_segwit_block_invalid_commitment() {
501        let mut block = create_test_block();
502        let witnesses = vec![
503            vec![],           // Coinbase
504            vec![vec![OP_1]], // First tx
505        ];
506
507        // Create coinbase with wrong witness_root in the commitment (won't match actual merkle root)
508        let wrong_root = [2u8; 32];
509        block.transactions[0].outputs[0].script_pubkey =
510            create_witness_commitment_script(&wrong_root, &[0u8; 32]);
511
512        let is_valid = validate_segwit_block(&block, &witnesses, 4_000_000).unwrap();
513        assert!(!is_valid);
514    }
515
516    #[test]
517    fn test_validate_witness_commitment_no_commitment() {
518        let coinbase_tx = create_test_transaction();
519        let witness_root = [1u8; 32];
520
521        // No witness commitment in script
522        let is_valid = validate_witness_commitment(&coinbase_tx, &witness_root, &[]).unwrap();
523        assert!(is_valid); // Should be valid for non-SegWit blocks
524    }
525
526    #[test]
527    fn test_validate_witness_commitment_invalid_commitment() {
528        let mut coinbase_tx = create_test_transaction();
529        let witness_root = [1u8; 32];
530        let invalid_root = [2u8; 32]; // different root → wrong commitment
531
532        // Add commitment computed from a different root
533        coinbase_tx.outputs[0].script_pubkey =
534            create_witness_commitment_script(&invalid_root, &[0u8; 32]);
535
536        let is_valid = validate_witness_commitment(&coinbase_tx, &witness_root, &[]).unwrap();
537        assert!(!is_valid);
538    }
539
540    #[test]
541    fn test_extract_witness_commitment_valid() {
542        let witness_root = [1u8; 32];
543        let nonce = [0u8; 32];
544        let script = create_witness_commitment_script(&witness_root, &nonce);
545
546        // extract_witness_commitment returns sha256d(root||nonce), not the raw root
547        let mut preimage = [0u8; 64];
548        preimage[..32].copy_from_slice(&witness_root);
549        preimage[32..].copy_from_slice(&nonce);
550        let expected = sha256d_bytes(&preimage);
551
552        let extracted = extract_witness_commitment(&script).unwrap();
553        assert_eq!(extracted, expected);
554    }
555
556    #[test]
557    fn test_extract_witness_commitment_invalid_script() {
558        let script = vec![OP_1]; // Not a witness commitment script
559
560        let extracted = extract_witness_commitment(&script);
561        assert!(extracted.is_none());
562    }
563
564    #[test]
565    fn test_extract_witness_commitment_wrong_opcode() {
566        let mut script = vec![0x52, PUSH_36_BYTES]; // Wrong opcode, correct length
567        script.extend_from_slice(&[1u8; 32]);
568
569        let extracted = extract_witness_commitment(&script);
570        assert!(extracted.is_none());
571    }
572
573    #[test]
574    fn test_extract_witness_commitment_wrong_length() {
575        let mut script = vec![OP_RETURN, 0x25]; // OP_RETURN, wrong length (37 bytes)
576        script.extend_from_slice(&[1u8; 32]);
577
578        let extracted = extract_witness_commitment(&script);
579        assert!(extracted.is_none());
580    }
581
582    #[test]
583    fn test_hash_witness() {
584        let witness = vec![vec![OP_1], vec![OP_2]];
585        let hash = hash_witness(&witness);
586
587        assert_eq!(hash.len(), 32);
588
589        // Different witness should produce different hash
590        let witness2 = vec![vec![OP_3], vec![OP_4]];
591        let hash2 = hash_witness(&witness2);
592        assert_ne!(hash, hash2);
593    }
594
595    #[test]
596    fn test_hash_witness_empty() {
597        let witness = vec![];
598        let hash = hash_witness(&witness);
599
600        assert_eq!(hash.len(), 32);
601    }
602
603    #[test]
604    fn test_compute_merkle_root_single_hash() {
605        let hashes = vec![[1u8; 32]];
606        let root = compute_merkle_root(&hashes).unwrap();
607
608        assert_eq!(root, [1u8; 32]);
609    }
610
611    #[test]
612    fn test_compute_merkle_root_empty() {
613        let hashes = vec![];
614        let result = compute_merkle_root(&hashes);
615
616        assert!(result.is_err());
617    }
618
619    #[test]
620    fn test_is_segwit_transaction_false() {
621        let tx = create_test_transaction();
622        // No SegWit markers
623
624        assert!(!is_segwit_transaction(&tx));
625    }
626
627    #[test]
628    fn test_calculate_base_size() {
629        let tx = create_test_transaction();
630        let base_size = calculate_base_size(&tx);
631
632        assert!(base_size > 0);
633    }
634
635    #[test]
636    fn test_calculate_total_size_with_witness() {
637        let tx = create_test_transaction();
638        let witness = vec![vec![OP_1], vec![OP_2]];
639
640        let total_size = calculate_total_size(&tx, Some(&witness));
641        let base_size = calculate_base_size(&tx);
642
643        assert!(total_size > base_size);
644    }
645
646    #[test]
647    fn test_calculate_total_size_without_witness() {
648        let tx = create_test_transaction();
649
650        let total_size = calculate_total_size(&tx, None);
651        let base_size = calculate_base_size(&tx);
652
653        assert_eq!(total_size, base_size);
654    }
655
656    // Helper functions
657    fn create_test_transaction() -> Transaction {
658        Transaction {
659            version: 1,
660            inputs: vec![TransactionInput {
661                prevout: OutPoint {
662                    hash: [0; 32].into(),
663                    index: 0,
664                },
665                script_sig: vec![OP_1],
666                sequence: 0xffffffff,
667            }]
668            .into(),
669            outputs: vec![TransactionOutput {
670                value: 1000,
671                script_pubkey: vec![OP_1].into(),
672            }]
673            .into(),
674            lock_time: 0,
675        }
676    }
677
678    fn create_test_block() -> Block {
679        Block {
680            header: create_test_header(1231006505, [0u8; 32]),
681            transactions: vec![
682                create_test_transaction(), // Coinbase
683                create_test_transaction(), // Regular tx
684            ]
685            .into_boxed_slice(),
686        }
687    }
688
689    fn create_witness_commitment_script(witness_root: &Hash, nonce: &[u8; 32]) -> ByteString {
690        // Compute the correct BIP141 commitment: sha256d(witness_root || nonce)
691        let mut preimage = [0u8; 64];
692        preimage[..32].copy_from_slice(witness_root);
693        preimage[32..].copy_from_slice(nonce);
694        let commitment = sha256d_bytes(&preimage);
695        let mut script = vec![OP_RETURN, PUSH_36_BYTES]; // 0x6a, 0x24
696        script.extend_from_slice(&[0xaa, 0x21, 0xa9, 0xed]); // BIP141 magic prefix
697        script.extend_from_slice(&commitment);
698        script.into()
699    }
700}
701
702#[cfg(test)]
703mod property_tests {
704    use super::*;
705    use proptest::prelude::*;
706
707    /// Property test: transaction weight is non-negative
708    ///
709    /// Mathematical specification:
710    /// ∀ tx ∈ Transaction, witness ∈ Option<Witness>: Weight(tx) ≥ 0
711    proptest! {
712        #[test]
713        fn prop_transaction_weight_non_negative(
714            tx in create_transaction_strategy(),
715            witness in prop::option::of(create_witness_strategy())
716        ) {
717            let _weight = calculate_transaction_weight(&tx, witness.as_ref()).unwrap();
718            // Weight is always non-negative (Natural type) - verified by type system
719        }
720    }
721
722    /// Property test: transaction weight formula is correct
723    ///
724    /// Mathematical specification:
725    /// ∀ tx ∈ Transaction, witness ∈ Option<Witness>:
726    /// Weight(tx) = 3 × base_size + total_size (BIP141; see `witness::calculate_transaction_weight_segwit`)
727    proptest! {
728        #[test]
729        fn prop_transaction_weight_formula(
730            tx in create_transaction_strategy(),
731            witness in prop::option::of(create_witness_strategy())
732        ) {
733            let weight = calculate_transaction_weight(&tx, witness.as_ref()).unwrap();
734            let base_size = calculate_base_size(&tx);
735            let total_size = calculate_total_size(&tx, witness.as_ref());
736            let expected_weight = 3 * base_size + total_size;
737
738            assert_eq!(weight, expected_weight);
739        }
740    }
741
742    /// Property test: block weight validation respects limits
743    ///
744    /// Mathematical specification:
745    /// ∀ block ∈ Block, witnesses ∈ [Witness], max_weight ∈ ℕ:
746    /// If Σ tx_weight > max_weight then validate_segwit_block returns false
747    proptest! {
748        #[test]
749        fn prop_block_weight_validation_limit(
750            block in create_block_strategy(),
751            witnesses in create_witnesses_strategy(),
752            max_weight in 1..10_000_000u64
753        ) {
754            // Handle errors from invalid blocks/witnesses
755            match (calculate_block_weight(&block, &witnesses), validate_segwit_block(&block, &witnesses, max_weight as Natural)) {
756                (Ok(actual_weight), Ok(is_valid)) => {
757                    // If weight exceeds limit, block should be invalid
758                    if actual_weight > max_weight as Natural {
759                        prop_assert!(!is_valid, "Block exceeding weight limit must be invalid");
760                    }
761                },
762                (Err(_), _) | (_, Err(_)) => {
763                    // Invalid blocks/witnesses may cause errors - this is acceptable
764                }
765            }
766        }
767    }
768
769    /// Property test: witness commitment validation is deterministic
770    ///
771    /// Mathematical specification:
772    /// ∀ coinbase_tx ∈ Transaction, witness_root ∈ Hash:
773    /// validate_witness_commitment(coinbase_tx, witness_root) is deterministic
774    proptest! {
775        #[test]
776        fn prop_witness_commitment_deterministic(
777            coinbase_tx in create_transaction_strategy(),
778            witness_root in create_hash_strategy()
779        ) {
780            let result1 = validate_witness_commitment(&coinbase_tx, &witness_root, &[]).unwrap();
781            let result2 = validate_witness_commitment(&coinbase_tx, &witness_root, &[]).unwrap();
782
783            assert_eq!(result1, result2);
784        }
785    }
786
787    /// Property test: witness merkle root computation is deterministic
788    ///
789    /// Mathematical specification:
790    /// ∀ block ∈ Block, witnesses ∈ [Witness]:
791    /// compute_witness_merkle_root(block, witnesses) is deterministic
792    proptest! {
793        #[test]
794        fn prop_witness_merkle_root_deterministic(
795            block in create_block_strategy(),
796            witnesses in create_witnesses_strategy()
797        ) {
798            if !block.transactions.is_empty() {
799                let result1 = compute_witness_merkle_root(&block, &witnesses);
800                let result2 = compute_witness_merkle_root(&block, &witnesses);
801
802                assert_eq!(result1.is_ok(), result2.is_ok());
803                if result1.is_ok() && result2.is_ok() {
804                    assert_eq!(result1.unwrap(), result2.unwrap());
805                }
806            }
807        }
808    }
809
810    /// Property test: SegWit transaction detection is consistent
811    ///
812    /// Mathematical specification:
813    /// ∀ tx ∈ Transaction: is_segwit_transaction(tx) ∈ {true, false}
814    proptest! {
815        #[test]
816        fn prop_segwit_transaction_detection(
817            tx in create_transaction_strategy()
818        ) {
819            let is_segwit = is_segwit_transaction(&tx);
820            // Just test it returns a boolean (is_segwit is either true or false)
821            let _ = is_segwit;
822        }
823    }
824
825    /// Property test: witness hashing is deterministic
826    ///
827    /// Mathematical specification:
828    /// ∀ witness ∈ Witness: hash_witness(witness) is deterministic
829    proptest! {
830        #[test]
831        fn prop_witness_hashing_deterministic(
832            witness in create_witness_strategy()
833        ) {
834            let hash1 = hash_witness(&witness);
835            let hash2 = hash_witness(&witness);
836
837            assert_eq!(hash1, hash2);
838            assert_eq!(hash1.len(), 32);
839        }
840    }
841
842    /// Property test: merkle root computation handles single hash
843    ///
844    /// Mathematical specification:
845    /// ∀ hash ∈ Hash: compute_merkle_root([hash]) = hash
846    proptest! {
847        #[test]
848        fn prop_merkle_root_single_hash(
849            hash in create_hash_strategy()
850        ) {
851            let hashes = vec![hash];
852            let root = compute_merkle_root(&hashes).unwrap();
853
854            assert_eq!(root, hash);
855        }
856    }
857
858    /// Property test: merkle root computation fails on empty input
859    ///
860    /// Mathematical specification:
861    /// compute_merkle_root([]) returns error
862    #[test]
863    fn prop_merkle_root_empty_input() {
864        let hashes: Vec<Hash> = vec![];
865        let result = compute_merkle_root(&hashes);
866
867        assert!(result.is_err());
868    }
869
870    /// Property test: witness commitment extraction is deterministic
871    ///
872    /// Mathematical specification:
873    /// ∀ script ∈ ByteString: extract_witness_commitment(script) is deterministic
874    proptest! {
875        #[test]
876        fn prop_witness_commitment_extraction_deterministic(
877            script in prop::collection::vec(any::<u8>(), 0..100)
878        ) {
879            let result1 = extract_witness_commitment(&script);
880            let result2 = extract_witness_commitment(&script);
881
882            assert_eq!(result1.is_some(), result2.is_some());
883            if result1.is_some() && result2.is_some() {
884                assert_eq!(result1.unwrap(), result2.unwrap());
885            }
886        }
887    }
888
889    /// Property test: base size calculation is monotonic
890    ///
891    /// Mathematical specification:
892    /// ∀ tx1, tx2 ∈ Transaction: |tx1| ≤ |tx2| ⟹ base_size(tx1) ≤ base_size(tx2)
893    proptest! {
894        #[test]
895        fn prop_base_size_monotonic(
896            tx1 in create_transaction_strategy(),
897            tx2 in create_transaction_strategy()
898        ) {
899            let base_size1 = calculate_base_size(&tx1);
900            let base_size2 = calculate_base_size(&tx2);
901
902            // Base size should be positive
903            assert!(base_size1 > 0);
904            assert!(base_size2 > 0);
905        }
906    }
907
908    /// Property test: total size with witness is greater than base size
909    ///
910    /// Mathematical specification:
911    /// ∀ tx ∈ Transaction, witness ∈ Witness: total_size(tx, witness) ≥ base_size(tx)
912    proptest! {
913        #[test]
914        fn prop_total_size_with_witness_greater_than_base(
915            tx in create_transaction_strategy(),
916            witness in create_witness_strategy()
917        ) {
918            let base_size = calculate_base_size(&tx);
919            let total_size = calculate_total_size(&tx, Some(&witness));
920
921            assert!(total_size >= base_size);
922        }
923    }
924
925    // Property test strategies
926    fn create_transaction_strategy() -> impl Strategy<Value = Transaction> {
927        (
928            prop::collection::vec(any::<u8>(), 0..10), // inputs
929            prop::collection::vec(any::<u8>(), 0..10), // outputs
930        )
931            .prop_map(|(input_data, output_data)| {
932                let mut inputs = Vec::new();
933                for (i, _) in input_data.iter().enumerate() {
934                    inputs.push(TransactionInput {
935                        prevout: OutPoint {
936                            hash: [0; 32],
937                            index: i as u32,
938                        },
939                        script_sig: vec![OP_1],
940                        sequence: 0xffffffff,
941                    });
942                }
943
944                let mut outputs = Vec::new();
945                for _ in output_data {
946                    outputs.push(TransactionOutput {
947                        value: 1000,
948                        script_pubkey: vec![OP_1],
949                    });
950                }
951
952                Transaction {
953                    version: 1,
954                    inputs: inputs.into(),
955                    outputs: outputs.into(),
956                    lock_time: 0,
957                }
958            })
959    }
960
961    fn create_block_strategy() -> impl Strategy<Value = Block> {
962        prop::collection::vec(create_transaction_strategy(), 1..5).prop_map(|transactions| Block {
963            header: BlockHeader {
964                version: 1,
965                prev_block_hash: [0; 32],
966                merkle_root: [0; 32],
967                timestamp: 1231006505,
968                bits: 0x1d00ffff,
969                nonce: 0,
970            },
971            transactions: transactions.into_boxed_slice(),
972        })
973    }
974
975    fn create_witness_strategy() -> impl Strategy<Value = Witness> {
976        prop::collection::vec(prop::collection::vec(any::<u8>(), 0..10), 0..5)
977    }
978
979    fn create_witnesses_strategy() -> impl Strategy<Value = Vec<Witness>> {
980        prop::collection::vec(create_witness_strategy(), 0..5)
981    }
982
983    fn create_hash_strategy() -> impl Strategy<Value = Hash> {
984        prop::collection::vec(any::<u8>(), 32..=32).prop_map(|bytes| {
985            let mut hash = [0u8; 32];
986            hash.copy_from_slice(&bytes);
987            hash
988        })
989    }
990}