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