Skip to main content

blvm_consensus/
test_utils.rs

1//! Property test strategies and shared test fixtures.
2//!
3//! Enables integration tests to use strategies that satisfy Orange Paper constraints
4//! (e.g. |w| = |tx.inputs| for SegWit round-trip) and shared UTXO-set helpers.
5
6#[cfg(any(test, feature = "property-tests"))]
7use proptest::prelude::*;
8
9#[cfg(any(test, feature = "property-tests"))]
10use crate::segwit::Witness;
11use crate::types::{
12    BlockHeader, OutPoint, Transaction, TransactionInput, TransactionOutput, UtxoSet, UTXO,
13};
14
15#[cfg(any(test, feature = "property-tests"))]
16/// Strategy yielding (Transaction, Vec<Witness>) with |w| = |tx.inputs|.
17/// Use for SegWit round-trip property tests per Orange Paper 8.2.2.
18pub fn transaction_with_witness_strategy() -> impl Strategy<Value = (Transaction, Vec<Witness>)> {
19    (1..10usize).prop_flat_map(|input_count| {
20        let tx_strategy = transaction_with_input_count_strategy(input_count);
21        let witness_strategy = prop::collection::vec(
22            prop::collection::vec(prop::collection::vec(any::<u8>(), 0..64), 0..5),
23            input_count,
24        );
25        (tx_strategy, witness_strategy).prop_map(|(tx, witnesses)| (tx, witnesses))
26    })
27}
28
29#[cfg(any(test, feature = "property-tests"))]
30fn transaction_with_input_count_strategy(input_count: usize) -> impl Strategy<Value = Transaction> {
31    prop::collection::vec(any::<u8>(), 0..10).prop_map(move |output_data| {
32        let inputs: Vec<TransactionInput> = (0..input_count)
33            .map(|i| TransactionInput {
34                prevout: OutPoint {
35                    hash: [0; 32],
36                    index: i as u32,
37                },
38                script_sig: vec![0x51], // OP_1
39                sequence: 0xffffffff,
40            })
41            .collect();
42        let outputs: Vec<TransactionOutput> = output_data
43            .iter()
44            .map(|_| TransactionOutput {
45                value: 1000,
46                script_pubkey: vec![0x51],
47            })
48            .collect();
49        Transaction {
50            version: 1,
51            inputs: inputs.into(),
52            outputs: outputs.into(),
53            lock_time: 0,
54        }
55    })
56}
57
58#[cfg(any(test, feature = "property-tests"))]
59/// Strategy yielding Transaction for legacy (non-SegWit) round-trip tests.
60pub fn transaction_strategy() -> impl Strategy<Value = Transaction> {
61    // Wire round-trip matches Bitcoin serialization: at least one vin (vout may be empty).
62    (1..10usize, 0..10usize).prop_map(|(input_count, output_count)| {
63        let inputs: Vec<TransactionInput> = (0..input_count)
64            .map(|i| TransactionInput {
65                prevout: OutPoint {
66                    hash: [0; 32],
67                    index: i as u32,
68                },
69                script_sig: vec![0x51],
70                sequence: 0xffffffff,
71            })
72            .collect();
73        let outputs: Vec<TransactionOutput> = (0..output_count)
74            .map(|_| TransactionOutput {
75                value: 1000,
76                script_pubkey: vec![0x51],
77            })
78            .collect();
79        Transaction {
80            version: 1,
81            inputs: inputs.into(),
82            outputs: outputs.into(),
83            lock_time: 0,
84        }
85    })
86}
87
88/// Create a block header with the given timestamp and previous block hash.
89/// Shared by integration tests so fixture behavior stays consistent.
90pub fn create_test_header(timestamp: u64, prev_hash: [u8; 32]) -> BlockHeader {
91    BlockHeader {
92        version: 1,
93        prev_block_hash: prev_hash,
94        merkle_root: [0; 32],
95        timestamp,
96        bits: 0x1d00ffff,
97        nonce: 0,
98    }
99}
100
101/// Create a valid coinbase transaction (scriptSig 2–100 bytes per consensus).
102/// Shared by integration tests so fixture behavior stays consistent.
103pub fn create_coinbase_tx(value: i64) -> Transaction {
104    Transaction {
105        version: 1,
106        inputs: vec![TransactionInput {
107            prevout: OutPoint {
108                hash: [0; 32],
109                index: 0xffffffff,
110            },
111            script_sig: vec![0x03, 0x01, 0x00, 0x00], // 4 bytes (valid: 2–100)
112            sequence: 0xffffffff,
113        }]
114        .into(),
115        outputs: vec![TransactionOutput {
116            value,
117            script_pubkey: vec![0x51],
118        }]
119        .into(),
120        lock_time: 0,
121    }
122}
123
124/// UTXO set with two outputs for fee-calculation style tests (1 BTC and 0.5 BTC).
125/// Shared by integration tests so fixture behavior stays consistent.
126pub fn create_test_utxo_set_two_outputs() -> UtxoSet {
127    let mut utxo_set = UtxoSet::default();
128    utxo_set.insert(
129        OutPoint {
130            hash: [1; 32],
131            index: 0,
132        },
133        std::sync::Arc::new(UTXO {
134            value: 100_000_000, // 1 BTC
135            script_pubkey: vec![0x51].into(),
136            height: 100,
137            is_coinbase: false,
138        }),
139    );
140    utxo_set.insert(
141        OutPoint {
142            hash: [2; 32],
143            index: 0,
144        },
145        std::sync::Arc::new(UTXO {
146            value: 50_000_000, // 0.5 BTC
147            script_pubkey: vec![0x52].into(),
148            height: 101,
149            is_coinbase: false,
150        }),
151    );
152    utxo_set
153}
154
155#[cfg(all(test, feature = "property-tests"))]
156#[test]
157fn test_transaction_with_witness_strategy_satisfies_constraint() {
158    use proptest::test_runner::Config;
159    proptest!(Config::with_cases(50), |((tx, w) in transaction_with_witness_strategy())| {
160        prop_assert_eq!(w.len(), tx.inputs.len());
161    });
162}