Skip to main content

blvm_consensus/
transaction.rs

1//! Transaction validation functions from Orange Paper Section 5.1
2//!
3//! Performance optimizations:
4//! - Early-exit fast-path checks for obviously invalid transactions
5
6use crate::constants::*;
7use crate::error::{ConsensusError, Result};
8use crate::types::*;
9use crate::utxo_overlay::UtxoLookup;
10use blvm_spec_lock::spec_locked;
11use std::borrow::Cow;
12
13// Cold error construction helpers - these paths are rarely taken
14#[cold]
15fn make_output_sum_overflow_error() -> ConsensusError {
16    ConsensusError::TransactionValidation("Output value sum overflow".into())
17}
18
19#[cold]
20fn make_fee_calculation_underflow_error() -> ConsensusError {
21    ConsensusError::TransactionValidation("Fee calculation underflow".into())
22}
23
24/// Fast-path early-exit checks for transaction validation
25///
26/// Performs quick checks before expensive validation operations.
27/// Returns Some(ValidationResult) if fast-path can determine validity, None if full validation needed.
28#[inline(always)]
29#[cfg(feature = "production")]
30fn check_transaction_fast_path(tx: &Transaction) -> Option<ValidationResult> {
31    // Quick reject: empty inputs or outputs (most common invalid case)
32    if tx.inputs.is_empty() || tx.outputs.is_empty() {
33        return Some(ValidationResult::Invalid("Empty inputs or outputs".into()));
34    }
35
36    // Quick reject: obviously too many inputs/outputs (before expensive size calculation)
37    if tx.inputs.len() > MAX_INPUTS {
38        return Some(ValidationResult::Invalid(format!(
39            "Too many inputs: {}",
40            tx.inputs.len()
41        )));
42    }
43    if tx.outputs.len() > MAX_OUTPUTS {
44        return Some(ValidationResult::Invalid(format!(
45            "Too many outputs: {}",
46            tx.outputs.len()
47        )));
48    }
49
50    // Quick reject: obviously invalid value ranges (before expensive validation)
51    // Check if any output value is negative or exceeds MAX_MONEY
52    // Optimization: Use precomputed constant for u64 comparisons
53    #[cfg(feature = "production")]
54    {
55        use crate::optimizations::precomputed_constants::MAX_MONEY_U64;
56        for output in &tx.outputs {
57            let value_u64 = output.value as u64;
58            if output.value < 0 || value_u64 > MAX_MONEY_U64 {
59                return Some(ValidationResult::Invalid(format!(
60                    "Invalid output value: {}",
61                    output.value
62                )));
63            }
64        }
65    }
66
67    #[cfg(not(feature = "production"))]
68    for output in &tx.outputs {
69        if output.value < 0 || output.value > MAX_MONEY {
70            return Some(ValidationResult::Invalid(format!(
71                "Invalid output value: {}",
72                output.value
73            )));
74        }
75    }
76
77    // Quick reject: coinbase with invalid scriptSig length
78    // Optimization: Use constant folding for zero hash check
79    #[cfg(feature = "production")]
80    let is_coinbase_hash = {
81        use crate::optimizations::constant_folding::is_zero_hash;
82        is_zero_hash(&tx.inputs[0].prevout.hash)
83    };
84
85    #[cfg(not(feature = "production"))]
86    let is_coinbase_hash = tx.inputs[0].prevout.hash == [0u8; 32];
87
88    if tx.inputs.len() == 1 && is_coinbase_hash && tx.inputs[0].prevout.index == 0xffffffff {
89        let script_sig_len = tx.inputs[0].script_sig.len();
90        if !(2..=100).contains(&script_sig_len) {
91            return Some(ValidationResult::Invalid(format!(
92                "Coinbase scriptSig length {script_sig_len} must be between 2 and 100 bytes"
93            )));
94        }
95    }
96
97    // Fast-path can't validate everything, needs full validation
98    None
99}
100
101/// CheckTransaction: š’Æš’³ → {valid, invalid}
102///
103/// A transaction tx = (v, ins, outs, lt) is valid if and only if:
104/// 1. |ins| > 0 ∧ |outs| > 0
105/// 2. āˆ€o ∈ outs: 0 ≤ o.value ≤ M_max
106/// 3. āˆ‘_{o ∈ outs} o.value ≤ M_max (total output sum)
107/// 4. |ins| ≤ M_max_inputs
108/// 5. |outs| ≤ M_max_outputs
109/// 6. |tx| ≤ M_max_tx_size
110/// 7. āˆ€i,j ∈ ins: i ≠ j ⟹ i.prevout ≠ j.prevout (no duplicate inputs)
111/// 8. If tx is coinbase: 2 ≤ |ins[0].scriptSig| ≤ 100
112///
113/// Uses fast-path checks before full validation.
114#[spec_locked("5.1")]
115#[track_caller] // Better error messages showing caller location
116#[cfg_attr(feature = "production", inline(always))]
117#[cfg_attr(not(feature = "production"), inline)]
118pub fn check_transaction(tx: &Transaction) -> Result<ValidationResult> {
119    // Precondition checks: Validate function inputs
120    // Note: We check these conditions and return Invalid rather than asserting,
121    // to allow tests to verify the validation logic properly
122    if tx.inputs.len() > MAX_INPUTS {
123        return Ok(ValidationResult::Invalid(format!(
124            "Input count {} exceeds maximum {}",
125            tx.inputs.len(),
126            MAX_INPUTS
127        )));
128    }
129    if tx.outputs.len() > MAX_OUTPUTS {
130        return Ok(ValidationResult::Invalid(format!(
131            "Output count {} exceeds maximum {}",
132            tx.outputs.len(),
133            MAX_OUTPUTS
134        )));
135    }
136
137    // Fast-path early exit for obviously invalid transactions
138    #[cfg(feature = "production")]
139    if let Some(result) = check_transaction_fast_path(tx) {
140        return Ok(result);
141    }
142
143    // 1. Check inputs and outputs are not empty (redundant if fast-path worked, but safe fallback)
144    // Note: We check this condition and return Invalid rather than asserting, to allow tests
145    // to verify the validation logic properly
146    if tx.inputs.is_empty() {
147        // Coinbase transactions have exactly 1 input, so empty inputs means non-coinbase
148        return Ok(ValidationResult::Invalid(
149            "Transaction must have inputs unless it's a coinbase".to_string(),
150        ));
151    }
152    if tx.outputs.is_empty() {
153        return Ok(ValidationResult::Invalid(
154            "Transaction must have at least one output".to_string(),
155        ));
156    }
157
158    // 2. Check output values are valid and calculate total sum in one pass (Orange Paper Section 5.1, rules 2 & 3)
159    // āˆ€o ∈ outs: 0 ≤ o.value ≤ M_max ∧ āˆ‘_{o ∈ outs} o.value ≤ M_max
160    // Use proven bounds for output access in hot path
161    let mut total_output_value = 0i64;
162    // Invariant assertion: Total output value must start at zero
163    assert!(
164        total_output_value == 0,
165        "Total output value must start at zero"
166    );
167    #[cfg(feature = "production")]
168    {
169        use crate::optimizations::optimized_access::get_proven_by_;
170        use crate::optimizations::precomputed_constants::MAX_MONEY_U64;
171        for i in 0..tx.outputs.len() {
172            if let Some(output) = get_proven_by_(&tx.outputs, i) {
173                let value_u64 = output.value as u64;
174                if output.value < 0 || value_u64 > MAX_MONEY_U64 {
175                    return Ok(ValidationResult::Invalid(format!(
176                        "Invalid output value {} at index {}",
177                        output.value, i
178                    )));
179                }
180                // Accumulate sum with overflow check
181                // Invariant assertion: Output value must be non-negative before addition
182                assert!(
183                    output.value >= 0,
184                    "Output value {} must be non-negative at index {}",
185                    output.value,
186                    i
187                );
188                total_output_value = total_output_value
189                    .checked_add(output.value)
190                    .ok_or_else(make_output_sum_overflow_error)?;
191                // Invariant assertion: Total output value must remain non-negative after addition
192                assert!(
193                    total_output_value >= 0,
194                    "Total output value {total_output_value} must be non-negative after output {i}"
195                );
196            }
197        }
198    }
199
200    #[cfg(not(feature = "production"))]
201    {
202        for (i, output) in tx.outputs.iter().enumerate() {
203            // Bounds checking assertion: Output index must be valid
204            assert!(i < tx.outputs.len(), "Output index {i} out of bounds");
205            // Check output value is valid (non-negative and within MAX_MONEY)
206            // Note: We check this condition and return Invalid rather than asserting,
207            // to allow tests to verify the validation logic properly
208            if output.value < 0 || output.value > MAX_MONEY {
209                return Ok(ValidationResult::Invalid(format!(
210                    "Invalid output value {} at index {}",
211                    output.value, i
212                )));
213            }
214            // Accumulate sum with overflow check
215            total_output_value = total_output_value
216                .checked_add(output.value)
217                .ok_or_else(make_output_sum_overflow_error)?;
218            // Invariant assertion: Total output value must remain non-negative after addition
219            assert!(
220                total_output_value >= 0,
221                "Total output value {total_output_value} must be non-negative after output {i}"
222            );
223        }
224    }
225
226    // 2b. Check total output sum is in valid range (MoneyRange)
227    // MoneyRange(n) = (n >= 0 && n <= MAX_MONEY)
228    // Optimization: Use precomputed constant for comparison
229    // Invariant assertion: Total output value must be non-negative
230    assert!(
231        total_output_value >= 0,
232        "Total output value {total_output_value} must be non-negative"
233    );
234
235    #[cfg(feature = "production")]
236    {
237        use crate::optimizations::precomputed_constants::MAX_MONEY_U64;
238        let total_u64 = total_output_value as u64;
239        // Check for invalid total output value and return error (before assert)
240        if total_output_value < 0 || total_u64 > MAX_MONEY_U64 {
241            return Ok(ValidationResult::Invalid(format!(
242                "Total output value {total_output_value} is out of valid range [0, {MAX_MONEY}]"
243            )));
244        }
245        // Invariant assertion: Total output value must not exceed MAX_MONEY
246        // (This should never fail if the check above is correct)
247        assert!(
248            total_u64 <= MAX_MONEY_U64,
249            "Total output value {total_output_value} must not exceed MAX_MONEY"
250        );
251    }
252
253    #[cfg(not(feature = "production"))]
254    {
255        // Check for invalid total output value and return error (before assert)
256        if !(0..=MAX_MONEY).contains(&total_output_value) {
257            return Ok(ValidationResult::Invalid(format!(
258                "Total output value {total_output_value} is out of valid range [0, {MAX_MONEY}]"
259            )));
260        }
261        // Invariant assertion: Total output value must not exceed MAX_MONEY
262        // (This should never fail if the check above is correct)
263        assert!(
264            total_output_value <= MAX_MONEY,
265            "Total output value {total_output_value} must not exceed MAX_MONEY"
266        );
267    }
268
269    // 3. Check input count limit (redundant if fast-path worked)
270    if tx.inputs.len() > MAX_INPUTS {
271        return Ok(ValidationResult::Invalid(format!(
272            "Too many inputs: {}",
273            tx.inputs.len()
274        )));
275    }
276
277    // 4. Check output count limit (redundant if fast-path worked)
278    if tx.outputs.len() > MAX_OUTPUTS {
279        return Ok(ValidationResult::Invalid(format!(
280            "Too many outputs: {}",
281            tx.outputs.len()
282        )));
283    }
284
285    // 5. Check transaction size limit (consensus CheckTransaction)
286    // GetSerializeSize(TX_NO_WITNESS) * WITNESS_SCALE_FACTOR > MAX_BLOCK_WEIGHT
287    // This checks: stripped_size * 4 > 4,000,000, i.e., stripped_size > 1,000,000
288    // calculate_transaction_size returns stripped size (no witness), matching TX_NO_WITNESS
289    use crate::constants::MAX_BLOCK_WEIGHT;
290    const WITNESS_SCALE_FACTOR: usize = 4;
291    let tx_stripped_size = calculate_transaction_size(tx); // This is TX_NO_WITNESS size
292    if tx_stripped_size * WITNESS_SCALE_FACTOR > MAX_BLOCK_WEIGHT {
293        return Ok(ValidationResult::Invalid(format!(
294            "Transaction too large: stripped size {} bytes (weight {} > {})",
295            tx_stripped_size,
296            tx_stripped_size * WITNESS_SCALE_FACTOR,
297            MAX_BLOCK_WEIGHT
298        )));
299    }
300
301    // 7. Check for duplicate inputs (Orange Paper Section 5.1, rule 4)
302    // āˆ€i,j ∈ ins: i ≠ j ⟹ i.prevout ≠ j.prevout
303    // Optimization: Use HashSet for O(n) duplicate detection instead of O(n²) nested loop
304    use std::collections::HashSet;
305    let mut seen_prevouts = HashSet::with_capacity(tx.inputs.len());
306    for (i, input) in tx.inputs.iter().enumerate() {
307        // Bounds checking assertion: Input index must be valid
308        assert!(i < tx.inputs.len(), "Input index {i} out of bounds");
309        if !seen_prevouts.insert(&input.prevout) {
310            return Ok(ValidationResult::Invalid(format!(
311                "Duplicate input prevout at index {i}"
312            )));
313        }
314    }
315
316    // 8. Check coinbase scriptSig length (Orange Paper Section 5.1, rule 5)
317    // If tx is coinbase: 2 ≤ |ins[0].scriptSig| ≤ 100
318    if is_coinbase(tx) {
319        debug_assert!(
320            !tx.inputs.is_empty(),
321            "Coinbase transaction must have at least one input"
322        );
323        let script_sig_len = tx.inputs[0].script_sig.len();
324        if !(2..=100).contains(&script_sig_len) {
325            return Ok(ValidationResult::Invalid(format!(
326                "Coinbase scriptSig length {script_sig_len} must be between 2 and 100 bytes"
327            )));
328        }
329    }
330
331    // Postcondition assertion: Validation result must be Valid or Invalid
332    // Note: This assertion documents the expected return type
333    // The result is always Valid at this point (we would have returned Invalid earlier)
334
335    Ok(ValidationResult::Valid)
336}
337
338/// CheckTxInputs: š’Æš’³ Ɨ š’°š’® Ɨ ā„• → {valid, invalid} Ɨ ℤ
339///
340/// For transaction tx with UTXO set us at height h:
341/// 1. If tx is coinbase: return (valid, 0)
342/// 2. If tx is not coinbase: āˆ€i ∈ ins: ¬i.prevout.IsNull() (Orange Paper Section 5.1, rule 6)
343/// 3. Let total_in = Σᵢ us(i.prevout).value
344/// 4. Let total_out = Σₒ o.value
345/// 5. If total_in < total_out: return (invalid, 0)
346/// 6. Return (valid, total_in - total_out)
347#[spec_locked("5.1")]
348#[cfg_attr(feature = "production", inline(always))]
349#[cfg_attr(not(feature = "production"), inline)]
350#[allow(clippy::overly_complex_bool_expr)] // Intentional tautological assertions for formal verification
351pub fn check_tx_inputs<U: UtxoLookup>(
352    tx: &Transaction,
353    utxo_set: &U,
354    height: Natural,
355) -> Result<(ValidationResult, Integer)> {
356    check_tx_inputs_with_utxos(tx, utxo_set, height, None)
357}
358
359/// Optimized version that accepts pre-collected UTXOs to avoid redundant lookups
360pub fn check_tx_inputs_with_utxos<U: UtxoLookup>(
361    tx: &Transaction,
362    utxo_set: &U,
363    height: Natural,
364    pre_collected_utxos: Option<&[Option<&UTXO>]>,
365) -> Result<(ValidationResult, Integer)> {
366    // Precondition checks: Validate function inputs
367    // Note: We check this condition and return Invalid rather than asserting,
368    // to allow tests to verify the validation logic properly
369    if tx.inputs.is_empty() && !is_coinbase(tx) {
370        return Ok((
371            ValidationResult::Invalid(
372                "Transaction must have inputs unless it's a coinbase".to_string(),
373            ),
374            0,
375        ));
376    }
377    assert!(
378        height <= i64::MAX as u64,
379        "Block height {height} must fit in i64"
380    );
381    assert!(
382        utxo_set.len() <= u32::MAX as usize,
383        "UTXO set size {} exceeds maximum",
384        utxo_set.len()
385    );
386
387    // Check if this is a coinbase transaction
388    if is_coinbase(tx) {
389        // Postcondition assertion: Coinbase fee must be zero
390        #[allow(clippy::eq_op)]
391        {
392            // Coinbase fee must be zero (tautology for formal verification)
393        }
394        return Ok((ValidationResult::Valid, 0));
395    }
396
397    // Check that non-coinbase inputs don't have null prevouts (Orange Paper Section 5.1, rule 6)
398    // āˆ€i ∈ ins: ¬i.prevout.IsNull()
399    // Use proven bounds for input access in hot path
400    #[cfg(feature = "production")]
401    {
402        use crate::optimizations::constant_folding::is_zero_hash;
403        use crate::optimizations::optimized_access::get_proven_by_;
404        for i in 0..tx.inputs.len() {
405            if let Some(input) = get_proven_by_(&tx.inputs, i) {
406                if is_zero_hash(&input.prevout.hash) && input.prevout.index == 0xffffffff {
407                    return Ok((
408                        ValidationResult::Invalid(format!(
409                            "Non-coinbase input {i} has null prevout"
410                        )),
411                        0,
412                    ));
413                }
414            }
415        }
416    }
417
418    #[cfg(not(feature = "production"))]
419    {
420        for (i, input) in tx.inputs.iter().enumerate() {
421            if input.prevout.hash == [0u8; 32] && input.prevout.index == 0xffffffff {
422                return Ok((
423                    ValidationResult::Invalid(format!("Non-coinbase input {i} has null prevout")),
424                    0,
425                ));
426            }
427        }
428    }
429
430    // Optimization: Batch UTXO lookups - collect all prevouts first, then lookup
431    // This improves cache locality and reduces HashMap traversal overhead
432    // Optimization: Pre-allocate with known size
433    #[cfg(feature = "production")]
434    {
435        use crate::optimizations::prefetch;
436        // Prefetch ahead for sequential UTXO lookups
437        for i in 0..tx.inputs.len().min(8) {
438            if i + 4 < tx.inputs.len() {
439                prefetch::prefetch_ahead(&tx.inputs, i, 4);
440            }
441        }
442    }
443
444    // OPTIMIZATION: Use pre-collected UTXOs if provided, otherwise collect them
445    let input_utxos: Vec<(usize, Option<&UTXO>)> = if let Some(pre_utxos) = pre_collected_utxos {
446        // Pre-collected UTXOs provided - use them directly (no redundant lookups)
447        pre_utxos
448            .iter()
449            .enumerate()
450            .map(|(i, opt_utxo)| (i, *opt_utxo))
451            .collect()
452    } else {
453        // No pre-collected UTXOs - collect them now
454        let mut result = Vec::with_capacity(tx.inputs.len());
455        for (i, input) in tx.inputs.iter().enumerate() {
456            result.push((i, utxo_set.get(&input.prevout)));
457        }
458        result
459    };
460
461    let mut total_input_value = 0i64;
462    // Invariant assertion: Total input value must start at zero
463    assert!(
464        total_input_value == 0,
465        "Total input value must start at zero"
466    );
467
468    for (i, opt_utxo) in input_utxos {
469        // Bounds checking assertion: Input index must be valid
470        assert!(i < tx.inputs.len(), "Input index {i} out of bounds");
471
472        // Check if input exists in UTXO set
473        if let Some(utxo) = opt_utxo {
474            // Invariant assertion: UTXO value must be non-negative and within MAX_MONEY
475            assert!(
476                utxo.value >= 0,
477                "UTXO value {} must be non-negative at input {}",
478                utxo.value,
479                i
480            );
481            assert!(
482                utxo.value <= MAX_MONEY,
483                "UTXO value {} must not exceed MAX_MONEY at input {}",
484                utxo.value,
485                i
486            );
487
488            // Check coinbase maturity: coinbase outputs cannot be spent until COINBASE_MATURITY blocks deep
489            // Consensus: coinbase outputs require COINBASE_MATURITY confirmations
490            // We check: if utxo.is_coinbase && height < utxo.height + COINBASE_MATURITY
491            if utxo.is_coinbase {
492                use crate::constants::COINBASE_MATURITY;
493                let required_height = utxo.height.saturating_add(COINBASE_MATURITY);
494                // Invariant assertion: Height must be sufficient for coinbase maturity
495                assert!(
496                    height >= utxo.height,
497                    "Current height {} must be >= UTXO creation height {}",
498                    height,
499                    utxo.height
500                );
501                if height < required_height {
502                    return Ok((
503                        ValidationResult::Invalid(format!(
504                            "Premature spend of coinbase output: input {i} created at height {} cannot be spent until height {} (current: {})",
505                            utxo.height, required_height, height
506                        )),
507                        0,
508                    ));
509                }
510            }
511
512            // Use checked arithmetic to prevent overflow
513            // Invariant assertion: UTXO value must be non-negative before addition
514            assert!(
515                utxo.value >= 0,
516                "UTXO value {} must be non-negative before addition",
517                utxo.value
518            );
519            total_input_value = total_input_value.checked_add(utxo.value).ok_or_else(|| {
520                ConsensusError::TransactionValidation(
521                    format!("Input value overflow at input {i}").into(),
522                )
523            })?;
524            // Invariant assertion: Total input value must remain non-negative after addition
525            assert!(
526                total_input_value >= 0,
527                "Total input value {total_input_value} must be non-negative after input {i}"
528            );
529        } else {
530            #[cfg(debug_assertions)]
531            {
532                let hash_str: String = tx.inputs[i]
533                    .prevout
534                    .hash
535                    .iter()
536                    .map(|b| format!("{b:02x}"))
537                    .collect();
538                eprintln!(
539                    "   āŒ UTXO NOT FOUND: Input {} prevout {}:{}",
540                    i, hash_str, tx.inputs[i].prevout.index
541                );
542                eprintln!("      UTXO set size: {}", utxo_set.len());
543            }
544            return Ok((
545                ValidationResult::Invalid(format!("Input {i} not found in UTXO set")),
546                0,
547            ));
548        }
549    }
550
551    // Use checked sum to prevent overflow when summing outputs
552    let total_output_value: i64 = tx
553        .outputs
554        .iter()
555        .try_fold(0i64, |acc, output| {
556            // Invariant assertion: Output value must be non-negative
557            assert!(
558                output.value >= 0,
559                "Output value {} must be non-negative",
560                output.value
561            );
562            acc.checked_add(output.value).ok_or_else(|| {
563                ConsensusError::TransactionValidation("Output value overflow".into())
564            })
565        })
566        .map_err(|e| ConsensusError::TransactionValidation(Cow::Owned(e.to_string())))?;
567
568    // Invariant assertion: Total output value must be non-negative
569    assert!(
570        total_output_value >= 0,
571        "Total output value {total_output_value} must be non-negative"
572    );
573    // Check that output total doesn't exceed MAX_MONEY
574    assert!(
575        total_output_value <= MAX_MONEY,
576        "Total output value {total_output_value} must not exceed MAX_MONEY"
577    );
578    if total_output_value > MAX_MONEY {
579        return Ok((
580            ValidationResult::Invalid(format!(
581                "Total output value {total_output_value} exceeds maximum money supply"
582            )),
583            0,
584        ));
585    }
586
587    // Invariant assertion: Total input must be >= total output for valid transaction
588    if total_input_value < total_output_value {
589        return Ok((
590            ValidationResult::Invalid("Insufficient input value".to_string()),
591            0,
592        ));
593    }
594
595    // Use checked subtraction to prevent underflow (shouldn't happen due to check above, but be safe)
596    let fee = total_input_value
597        .checked_sub(total_output_value)
598        .ok_or_else(make_fee_calculation_underflow_error)?;
599
600    // Postcondition assertions: Validate fee calculation result
601    assert!(fee >= 0, "Fee {fee} must be non-negative");
602    assert!(
603        fee <= total_input_value,
604        "Fee {fee} cannot exceed total input {total_input_value}"
605    );
606    assert!(
607        total_input_value == total_output_value + fee,
608        "Conservation of value: input {total_input_value} must equal output {total_output_value} + fee {fee}"
609    );
610
611    Ok((ValidationResult::Valid, fee))
612}
613
614/// Hot-path: validate inputs using pre-copied UTXO data (value, is_coinbase, height).
615/// Avoids holding overlay refs; enables buffer reuse in block validation.
616pub fn check_tx_inputs_with_owned_data(
617    tx: &Transaction,
618    height: Natural,
619    utxo_data: &[Option<(i64, bool, u64)>],
620) -> Result<(ValidationResult, Integer)> {
621    if tx.inputs.is_empty() && !is_coinbase(tx) {
622        return Ok((
623            ValidationResult::Invalid(
624                "Transaction must have inputs unless it's a coinbase".to_string(),
625            ),
626            0,
627        ));
628    }
629    if is_coinbase(tx) {
630        return Ok((ValidationResult::Valid, 0));
631    }
632    if utxo_data.len() != tx.inputs.len() {
633        return Ok((
634            ValidationResult::Invalid("UTXO data length mismatch".to_string()),
635            0,
636        ));
637    }
638    let mut total_input_value = 0i64;
639    for (i, opt) in utxo_data.iter().enumerate() {
640        if let Some((value, is_coinbase, utxo_height)) = opt {
641            if *value < 0 || *value > MAX_MONEY {
642                return Ok((
643                    ValidationResult::Invalid(format!(
644                        "UTXO value {value} out of bounds at input {i}"
645                    )),
646                    0,
647                ));
648            }
649            if *is_coinbase {
650                use crate::constants::COINBASE_MATURITY;
651                let required_height = utxo_height.saturating_add(COINBASE_MATURITY);
652                if height < required_height {
653                    return Ok((
654                        ValidationResult::Invalid(format!(
655                            "Premature spend of coinbase output at input {i}"
656                        )),
657                        0,
658                    ));
659                }
660            }
661            total_input_value = total_input_value.checked_add(*value).ok_or_else(|| {
662                ConsensusError::TransactionValidation(
663                    format!("Input value overflow at input {i}").into(),
664                )
665            })?;
666        } else {
667            return Ok((
668                ValidationResult::Invalid(format!("Input {i} not found in UTXO set")),
669                0,
670            ));
671        }
672    }
673    let total_output_value: i64 = tx
674        .outputs
675        .iter()
676        .try_fold(0i64, |acc, output| {
677            acc.checked_add(output.value).ok_or_else(|| {
678                ConsensusError::TransactionValidation("Output value overflow".into())
679            })
680        })
681        .map_err(|e| ConsensusError::TransactionValidation(Cow::Owned(e.to_string())))?;
682    if total_output_value > MAX_MONEY {
683        return Ok((
684            ValidationResult::Invalid(format!(
685                "Total output value {total_output_value} exceeds maximum"
686            )),
687            0,
688        ));
689    }
690    if total_input_value < total_output_value {
691        return Ok((
692            ValidationResult::Invalid("Insufficient input value".to_string()),
693            0,
694        ));
695    }
696    let fee = total_input_value
697        .checked_sub(total_output_value)
698        .ok_or_else(make_fee_calculation_underflow_error)?;
699    Ok((ValidationResult::Valid, fee))
700}
701
702/// Check if transaction is coinbase
703///
704/// Hot-path function called frequently during validation.
705/// Always inline for maximum performance.
706#[inline(always)]
707#[spec_locked("6.4")]
708pub fn is_coinbase(tx: &Transaction) -> bool {
709    // Optimization: Use constant folding for zero hash check
710    #[cfg(feature = "production")]
711    {
712        use crate::optimizations::constant_folding::is_zero_hash;
713        tx.inputs.len() == 1
714            && is_zero_hash(&tx.inputs[0].prevout.hash)
715            && tx.inputs[0].prevout.index == 0xffffffff
716    }
717
718    #[cfg(not(feature = "production"))]
719    {
720        tx.inputs.len() == 1
721            && tx.inputs[0].prevout.hash == [0u8; 32]
722            && tx.inputs[0].prevout.index == 0xffffffff
723    }
724}
725
726/// Calculate transaction size (non-witness serialization)
727///
728/// Hot-path function called frequently during validation.
729/// Always inline for maximum performance.
730#[inline(always)]
731///
732/// This function calculates the size of a transaction when serialized
733/// without witness data (base serialization size).
734///
735/// CRITICAL: This must match the actual serialized size exactly to ensure
736/// consensus compatibility.
737#[spec_locked("5.1")]
738pub fn calculate_transaction_size(tx: &Transaction) -> usize {
739    // Use actual serialization for consensus compatibility
740    // This replaces the simplified calculation that didn't account for varint encoding
741    use crate::serialization::transaction::serialize_transaction;
742    serialize_transaction(tx).len()
743}
744
745// ============================================================================
746// FORMAL VERIFICATION
747// ============================================================================
748
749/// Mathematical Specification for Transaction Validation (Orange Paper Section 5.1):
750/// āˆ€ tx ∈ š’Æš’³: CheckTransaction(tx) = valid ⟺
751///   (|tx.inputs| > 0 ∧ |tx.outputs| > 0 ∧
752///    āˆ€o ∈ tx.outputs: 0 ≤ o.value ≤ M_max ∧
753///    āˆ‘_{o ∈ tx.outputs} o.value ≤ M_max ∧
754///    |tx.inputs| ≤ M_max_inputs ∧ |tx.outputs| ≤ M_max_outputs ∧
755///    |tx| ≤ M_max_tx_size ∧
756///    āˆ€i,j ∈ tx.inputs: i ≠ j ⟹ i.prevout ≠ j.prevout ∧
757///    (IsCoinbase(tx) ⟹ 2 ≤ |tx.inputs[0].scriptSig| ≤ 100))
758///
759/// Invariants:
760/// - Valid transactions have non-empty inputs and outputs
761/// - Output values are bounded [0, MAX_MONEY] individually (rule 2)
762/// - Total output sum doesn't exceed MAX_MONEY (rule 3)
763/// - Input/output counts respect limits
764/// - Transaction size respects limits
765/// - No duplicate prevouts in inputs (rule 4)
766/// - Coinbase transactions have scriptSig length [2, 100] bytes (rule 5)
767/// - Non-coinbase inputs must not have null prevouts (rule 6, checked in check_tx_inputs)
768
769/// Proptest strategies for transaction-shaped values (no `Arbitrary` impl — orphan rules).
770#[cfg(test)]
771pub(crate) mod transaction_proptest {
772    use super::*;
773    use proptest::prelude::*;
774
775    pub fn arb_transaction() -> BoxedStrategy<Transaction> {
776        (
777            any::<u64>(),
778            prop::collection::vec(
779                (
780                    any::<[u8; 32]>(),
781                    any::<u64>(),
782                    prop::collection::vec(any::<u8>(), 0..100),
783                    any::<u64>(),
784                ),
785                0..10,
786            ),
787            prop::collection::vec(
788                (any::<i64>(), prop::collection::vec(any::<u8>(), 0..100)),
789                0..10,
790            ),
791            any::<u64>(),
792        )
793            .prop_map(|(version, inputs, outputs, lock_time)| {
794                let inputs: Vec<TransactionInput> = inputs
795                    .into_iter()
796                    .map(|(hash, index, script_sig, sequence)| TransactionInput {
797                        prevout: OutPoint {
798                            hash,
799                            index: index as u32,
800                        },
801                        script_sig,
802                        sequence,
803                    })
804                    .collect();
805                let outputs: Vec<TransactionOutput> = outputs
806                    .into_iter()
807                    .map(|(value, script_pubkey)| TransactionOutput {
808                        value,
809                        script_pubkey,
810                    })
811                    .collect();
812                Transaction {
813                    version,
814                    #[cfg(feature = "production")]
815                    inputs: inputs.into(),
816                    #[cfg(not(feature = "production"))]
817                    inputs,
818                    #[cfg(feature = "production")]
819                    outputs: outputs.into(),
820                    #[cfg(not(feature = "production"))]
821                    outputs,
822                    lock_time,
823                }
824            })
825            .boxed()
826    }
827
828    pub fn arb_outpoint() -> impl Strategy<Value = OutPoint> {
829        (any::<[u8; 32]>(), any::<u32>()).prop_map(|(hash, index)| OutPoint { hash, index })
830    }
831
832    pub fn arb_utxo() -> impl Strategy<Value = UTXO> {
833        (
834            any::<i64>(),
835            prop::collection::vec(any::<u8>(), 0..40),
836            any::<u64>(),
837            any::<bool>(),
838        )
839            .prop_map(|(value, script_pubkey, height, is_coinbase)| UTXO {
840                value,
841                script_pubkey: script_pubkey.into(),
842                height,
843                is_coinbase,
844            })
845    }
846}
847
848#[cfg(test)]
849#[allow(unused_doc_comments)]
850mod property_tests {
851    use super::transaction_proptest::{arb_outpoint, arb_transaction, arb_utxo};
852    use super::*;
853    use proptest::prelude::*;
854
855    /// Property test: check_transaction validates structure correctly
856    proptest! {
857        #[test]
858        fn prop_check_transaction_structure(
859            tx in arb_transaction()
860        ) {
861            // Bound for tractability
862            let mut bounded_tx = tx;
863            if bounded_tx.inputs.len() > 10 {
864                bounded_tx.inputs.truncate(10);
865            }
866            if bounded_tx.outputs.len() > 10 {
867                bounded_tx.outputs.truncate(10);
868            }
869
870            let result = check_transaction(&bounded_tx).unwrap_or_else(|_| ValidationResult::Invalid("Error".to_string()));
871
872            // Structure properties
873            match result {
874                ValidationResult::Valid => {
875                    // Valid transactions must have non-empty inputs and outputs
876                    prop_assert!(!bounded_tx.inputs.is_empty(), "Valid transaction must have inputs");
877                    prop_assert!(!bounded_tx.outputs.is_empty(), "Valid transaction must have outputs");
878
879                    // Valid transactions must respect limits
880                    prop_assert!(bounded_tx.inputs.len() <= MAX_INPUTS, "Valid transaction must respect input limit");
881                    prop_assert!(bounded_tx.outputs.len() <= MAX_OUTPUTS, "Valid transaction must respect output limit");
882
883                    // Valid transactions must have valid output values
884                    for output in &bounded_tx.outputs {
885                        prop_assert!(output.value >= 0, "Valid transaction outputs must be non-negative");
886                        prop_assert!(output.value <= MAX_MONEY, "Valid transaction outputs must not exceed max money");
887                    }
888                },
889                ValidationResult::Invalid(_) => {
890                    // Invalid transactions may violate any rule
891                    // This is acceptable - we're testing the validation logic
892                }
893            }
894        }
895    }
896
897    /// Property test: check_tx_inputs handles coinbase correctly
898    proptest! {
899        #[test]
900        fn prop_check_tx_inputs_coinbase(
901            tx in arb_transaction(),
902            utxo_set in prop::collection::vec((arb_outpoint(), arb_utxo()), 0..50).prop_map(|v| v.into_iter().map(|(op, u)| (op, std::sync::Arc::new(u))).collect::<UtxoSet>()),
903            height in 0u64..1000u64
904        ) {
905            // Bound for tractability
906            let mut bounded_tx = tx;
907            if bounded_tx.inputs.len() > 5 {
908                bounded_tx.inputs.truncate(5);
909            }
910            if bounded_tx.outputs.len() > 5 {
911                bounded_tx.outputs.truncate(5);
912            }
913
914            let result = check_tx_inputs(&bounded_tx, &utxo_set, height).unwrap_or((ValidationResult::Invalid("Error".to_string()), 0));
915
916            // Coinbase property
917            if is_coinbase(&bounded_tx) {
918                prop_assert!(matches!(result.0, ValidationResult::Valid), "Coinbase transactions must be valid");
919                prop_assert_eq!(result.1, 0, "Coinbase transactions must have zero fee");
920            }
921        }
922    }
923
924    /// Property test: is_coinbase correctly identifies coinbase transactions
925    proptest! {
926        #[test]
927        fn prop_is_coinbase_correct(
928            tx in arb_transaction()
929        ) {
930            let is_cb = is_coinbase(&tx);
931
932            // Coinbase identification property
933            if is_cb {
934                prop_assert_eq!(tx.inputs.len(), 1, "Coinbase must have exactly one input");
935                prop_assert_eq!(tx.inputs[0].prevout.hash, [0u8; 32], "Coinbase input must have zero hash");
936                prop_assert_eq!(tx.inputs[0].prevout.index, 0xffffffffu32, "Coinbase input must have max index");
937            }
938        }
939    }
940
941    /// Property test: calculate_transaction_size is consistent
942    proptest! {
943        #[test]
944        fn prop_calculate_transaction_size_consistent(
945            tx in arb_transaction()
946        ) {
947            // Bound for tractability
948            let mut bounded_tx = tx;
949            if bounded_tx.inputs.len() > 10 {
950                bounded_tx.inputs.truncate(10);
951            }
952            if bounded_tx.outputs.len() > 10 {
953                bounded_tx.outputs.truncate(10);
954            }
955
956            let size = calculate_transaction_size(&bounded_tx);
957
958            // Size calculation properties
959            // Minimum: version(4) + input_count_varint(1) + output_count_varint(1) + lock_time(4) = 10 bytes
960            // (Even with 0 inputs/outputs, we need varints for counts)
961            prop_assert!(size >= 10, "Transaction size must be at least 10 bytes (version + varints + lock_time)");
962
963            // Maximum: Use MAX_TX_SIZE as the upper bound (actual serialization can be larger than simplified calculation)
964            // The simplified calculation was: 4 + 10*41 + 10*9 + 4 = 508
965            // But actual serialization with varints and real script sizes can be larger
966            prop_assert!(size <= MAX_TX_SIZE, "Transaction size must not exceed MAX_TX_SIZE ({})", MAX_TX_SIZE);
967
968            // Size should be deterministic
969            let size2 = calculate_transaction_size(&bounded_tx);
970            prop_assert_eq!(size, size2, "Transaction size calculation must be deterministic");
971        }
972    }
973
974    /// Property test: output value bounds are respected
975    proptest! {
976        #[test]
977        fn prop_output_value_bounds(
978            value in 0i64..(MAX_MONEY + 1000)
979        ) {
980            let tx = Transaction {
981                version: 1,
982                inputs: vec![TransactionInput {
983                    prevout: OutPoint { hash: [0; 32].into(), index: 0 },
984                    script_sig: vec![],
985                    sequence: 0xffffffff,
986                }].into(),
987                outputs: vec![TransactionOutput {
988                    value,
989                    script_pubkey: vec![],
990                }].into(),
991                lock_time: 0,
992            };
993
994            let result = check_transaction(&tx).unwrap_or(ValidationResult::Invalid("Error".to_string()));
995
996            // Value bounds property
997            if !(0..=MAX_MONEY).contains(&value) {
998                prop_assert!(matches!(result, ValidationResult::Invalid(_)),
999                    "Transactions with invalid output values must be invalid");
1000            } else {
1001                // Valid values should pass other checks too
1002                if !tx.inputs.is_empty() && !tx.outputs.is_empty() {
1003                    prop_assert!(matches!(result, ValidationResult::Valid),
1004                        "Transactions with valid output values should be valid");
1005                }
1006            }
1007        }
1008    }
1009}
1010
1011#[cfg(test)]
1012mod tests {
1013    use super::*;
1014
1015    #[test]
1016    fn test_check_transaction_valid() {
1017        let tx = Transaction {
1018            version: 1,
1019            inputs: vec![TransactionInput {
1020                prevout: OutPoint {
1021                    hash: [0; 32].into(),
1022                    index: 0,
1023                },
1024                script_sig: vec![],
1025                sequence: 0xffffffff,
1026            }]
1027            .into(),
1028            outputs: vec![TransactionOutput {
1029                value: 1000,
1030                script_pubkey: vec![].into(),
1031            }]
1032            .into(),
1033            lock_time: 0,
1034        };
1035
1036        assert_eq!(check_transaction(&tx).unwrap(), ValidationResult::Valid);
1037    }
1038
1039    #[test]
1040    fn test_check_transaction_empty_inputs() {
1041        let tx = Transaction {
1042            version: 1,
1043            inputs: vec![].into(),
1044            outputs: vec![TransactionOutput {
1045                value: 1000,
1046                script_pubkey: vec![].into(),
1047            }]
1048            .into(),
1049            lock_time: 0,
1050        };
1051
1052        assert!(matches!(
1053            check_transaction(&tx).unwrap(),
1054            ValidationResult::Invalid(_)
1055        ));
1056    }
1057
1058    #[test]
1059    fn test_check_tx_inputs_coinbase() {
1060        let tx = Transaction {
1061            version: 1,
1062            inputs: vec![TransactionInput {
1063                prevout: OutPoint {
1064                    hash: [0; 32].into(),
1065                    index: 0xffffffff,
1066                },
1067                script_sig: vec![],
1068                sequence: 0xffffffff,
1069            }]
1070            .into(),
1071            outputs: vec![TransactionOutput {
1072                value: 5000000000, // 50 BTC
1073                script_pubkey: vec![].into(),
1074            }]
1075            .into(),
1076            lock_time: 0,
1077        };
1078
1079        let utxo_set = UtxoSet::default();
1080        let (result, fee) = check_tx_inputs(&tx, &utxo_set, 0).unwrap();
1081
1082        assert_eq!(result, ValidationResult::Valid);
1083        assert_eq!(fee, 0);
1084    }
1085
1086    // ============================================================================
1087    // COMPREHENSIVE TRANSACTION TESTS
1088    // ============================================================================
1089
1090    #[test]
1091    fn test_check_transaction_empty_outputs() {
1092        let tx = Transaction {
1093            version: 1,
1094            inputs: vec![TransactionInput {
1095                prevout: OutPoint {
1096                    hash: [0; 32].into(),
1097                    index: 0,
1098                },
1099                script_sig: vec![],
1100                sequence: 0xffffffff,
1101            }]
1102            .into(),
1103            outputs: vec![].into(),
1104            lock_time: 0,
1105        };
1106
1107        assert!(matches!(
1108            check_transaction(&tx).unwrap(),
1109            ValidationResult::Invalid(_)
1110        ));
1111    }
1112
1113    #[test]
1114    fn test_check_transaction_invalid_output_value_negative() {
1115        let tx = Transaction {
1116            version: 1,
1117            inputs: vec![TransactionInput {
1118                prevout: OutPoint {
1119                    hash: [0; 32].into(),
1120                    index: 0,
1121                },
1122                script_sig: vec![],
1123                sequence: 0xffffffff,
1124            }]
1125            .into(),
1126            outputs: vec![TransactionOutput {
1127                value: -1, // Invalid negative value
1128                script_pubkey: vec![].into(),
1129            }]
1130            .into(),
1131            lock_time: 0,
1132        };
1133
1134        assert!(matches!(
1135            check_transaction(&tx).unwrap(),
1136            ValidationResult::Invalid(_)
1137        ));
1138    }
1139
1140    #[test]
1141    fn test_check_transaction_invalid_output_value_too_large() {
1142        let tx = Transaction {
1143            version: 1,
1144            inputs: vec![TransactionInput {
1145                prevout: OutPoint {
1146                    hash: [0; 32].into(),
1147                    index: 0,
1148                },
1149                script_sig: vec![],
1150                sequence: 0xffffffff,
1151            }]
1152            .into(),
1153            outputs: vec![TransactionOutput {
1154                value: MAX_MONEY + 1, // Invalid value exceeding max
1155                script_pubkey: vec![].into(),
1156            }]
1157            .into(),
1158            lock_time: 0,
1159        };
1160
1161        assert!(matches!(
1162            check_transaction(&tx).unwrap(),
1163            ValidationResult::Invalid(_)
1164        ));
1165    }
1166
1167    #[test]
1168    fn test_check_transaction_max_output_value() {
1169        let tx = Transaction {
1170            version: 1,
1171            inputs: vec![TransactionInput {
1172                prevout: OutPoint {
1173                    hash: [0; 32].into(),
1174                    index: 0,
1175                },
1176                script_sig: vec![],
1177                sequence: 0xffffffff,
1178            }]
1179            .into(),
1180            outputs: vec![TransactionOutput {
1181                value: MAX_MONEY, // Valid max value
1182                script_pubkey: vec![].into(),
1183            }]
1184            .into(),
1185            lock_time: 0,
1186        };
1187
1188        assert_eq!(check_transaction(&tx).unwrap(), ValidationResult::Valid);
1189    }
1190
1191    #[test]
1192    fn test_check_transaction_too_many_inputs() {
1193        let mut inputs = Vec::new();
1194        for i in 0..=MAX_INPUTS {
1195            inputs.push(TransactionInput {
1196                prevout: OutPoint {
1197                    hash: [i as u8; 32],
1198                    index: 0,
1199                },
1200                script_sig: vec![],
1201                sequence: 0xffffffff,
1202            });
1203        }
1204
1205        let tx = Transaction {
1206            version: 1,
1207            inputs: inputs.into(),
1208            outputs: vec![TransactionOutput {
1209                value: 1000,
1210                script_pubkey: vec![].into(),
1211            }]
1212            .into(),
1213            lock_time: 0,
1214        };
1215
1216        assert!(matches!(
1217            check_transaction(&tx).unwrap(),
1218            ValidationResult::Invalid(_)
1219        ));
1220    }
1221
1222    #[test]
1223    fn test_check_transaction_max_inputs() {
1224        // Use a reasonable number of inputs that fits within the block weight limit.
1225        // Each input ā‰ˆ 41 bytes stripped. Weight limit = 4,000,000. Max stripped = 1,000,000.
1226        // Max inputs ā‰ˆ 1,000,000 / 41 ā‰ˆ 24,390. Use 20,000 to stay safe.
1227        let num_inputs = 20_000;
1228        let mut inputs = Vec::new();
1229        for i in 0..num_inputs {
1230            let mut hash = [0u8; 32];
1231            // Use unique hash for each input to avoid duplicates
1232            hash[0] = (i & 0xff) as u8;
1233            hash[1] = ((i >> 8) & 0xff) as u8;
1234            hash[2] = ((i >> 16) & 0xff) as u8;
1235            hash[3] = ((i >> 24) & 0xff) as u8;
1236            inputs.push(TransactionInput {
1237                prevout: OutPoint {
1238                    hash,
1239                    index: i as u32,
1240                },
1241                script_sig: vec![],
1242                sequence: 0xffffffff,
1243            });
1244        }
1245
1246        let tx = Transaction {
1247            version: 1,
1248            inputs: inputs.into(),
1249            outputs: vec![TransactionOutput {
1250                value: 1000,
1251                script_pubkey: vec![].into(),
1252            }]
1253            .into(),
1254            lock_time: 0,
1255        };
1256
1257        assert_eq!(check_transaction(&tx).unwrap(), ValidationResult::Valid);
1258    }
1259
1260    #[test]
1261    fn test_check_transaction_too_many_outputs() {
1262        let mut outputs = Vec::new();
1263        for _ in 0..=MAX_OUTPUTS {
1264            outputs.push(TransactionOutput {
1265                value: 1000,
1266                script_pubkey: vec![].into(),
1267            });
1268        }
1269
1270        let tx = Transaction {
1271            version: 1,
1272            inputs: vec![TransactionInput {
1273                prevout: OutPoint {
1274                    hash: [0; 32].into(),
1275                    index: 0,
1276                },
1277                script_sig: vec![],
1278                sequence: 0xffffffff,
1279            }]
1280            .into(),
1281            outputs: outputs.into(),
1282            lock_time: 0,
1283        };
1284
1285        assert!(matches!(
1286            check_transaction(&tx).unwrap(),
1287            ValidationResult::Invalid(_)
1288        ));
1289    }
1290
1291    #[test]
1292    fn test_check_transaction_max_outputs() {
1293        let mut outputs = Vec::new();
1294        for _ in 0..MAX_OUTPUTS {
1295            outputs.push(TransactionOutput {
1296                value: 1000,
1297                script_pubkey: vec![].into(),
1298            });
1299        }
1300
1301        let tx = Transaction {
1302            version: 1,
1303            inputs: vec![TransactionInput {
1304                prevout: OutPoint {
1305                    hash: [0; 32].into(),
1306                    index: 0,
1307                },
1308                script_sig: vec![],
1309                sequence: 0xffffffff,
1310            }]
1311            .into(),
1312            outputs: outputs.into(),
1313            lock_time: 0,
1314        };
1315
1316        assert_eq!(check_transaction(&tx).unwrap(), ValidationResult::Valid);
1317    }
1318
1319    #[test]
1320    fn test_check_transaction_too_large() {
1321        // Create a transaction that will exceed MAX_BLOCK_WEIGHT / WITNESS_SCALE_FACTOR
1322        // MAX_BLOCK_WEIGHT is 4,000,000, so MAX_TX_SIZE is effectively 1,000,000 bytes
1323        // calculate_transaction_size now uses actual serialization, so we need to create
1324        // a transaction with large scripts to exceed the size limit while staying within input limits
1325        use crate::constants::MAX_INPUTS;
1326        let mut inputs = Vec::new();
1327        // Use MAX_INPUTS inputs with large scripts to exceed size limit
1328        // Each input: 32 (hash) + 4 (index) + varint(script_len) + script + 4 (sequence)
1329        // With 1000 inputs and ~1000 byte scripts each, we get ~1MB+ transaction
1330        for i in 0..MAX_INPUTS {
1331            inputs.push(TransactionInput {
1332                prevout: OutPoint {
1333                    hash: [i as u8; 32],
1334                    index: 0,
1335                },
1336                script_sig: vec![0u8; 1000], // Large script to increase size (1000 bytes each)
1337                sequence: 0xffffffff,
1338            });
1339        }
1340
1341        let tx = Transaction {
1342            version: 1,
1343            inputs: inputs.into(),
1344            outputs: vec![TransactionOutput {
1345                value: 1000,
1346                script_pubkey: vec![].into(),
1347            }]
1348            .into(),
1349            lock_time: 0,
1350        };
1351
1352        assert!(matches!(
1353            check_transaction(&tx).unwrap(),
1354            ValidationResult::Invalid(_)
1355        ));
1356    }
1357
1358    #[test]
1359    fn test_check_tx_inputs_regular_transaction() {
1360        let mut utxo_set = UtxoSet::default();
1361
1362        // Add UTXO to the set
1363        let outpoint = OutPoint {
1364            hash: [1; 32],
1365            index: 0,
1366        };
1367        let utxo = UTXO {
1368            value: 1000000000, // 10 BTC
1369            script_pubkey: vec![].into(),
1370            height: 0,
1371            is_coinbase: false,
1372        };
1373        utxo_set.insert(outpoint, std::sync::Arc::new(utxo));
1374
1375        let tx = Transaction {
1376            version: 1,
1377            inputs: vec![TransactionInput {
1378                prevout: OutPoint {
1379                    hash: [1; 32].into(),
1380                    index: 0,
1381                },
1382                script_sig: vec![],
1383                sequence: 0xffffffff,
1384            }]
1385            .into(),
1386            outputs: vec![TransactionOutput {
1387                value: 900000000, // 9 BTC output
1388                script_pubkey: vec![],
1389            }]
1390            .into(),
1391            lock_time: 0,
1392        };
1393
1394        let (result, fee) = check_tx_inputs(&tx, &utxo_set, 0).unwrap();
1395
1396        assert_eq!(result, ValidationResult::Valid);
1397        assert_eq!(fee, 100000000); // 1 BTC fee
1398    }
1399
1400    #[test]
1401    fn test_check_tx_inputs_missing_utxo() {
1402        let utxo_set = UtxoSet::default(); // Empty UTXO set
1403
1404        let tx = Transaction {
1405            version: 1,
1406            inputs: vec![TransactionInput {
1407                prevout: OutPoint {
1408                    hash: [1; 32].into(),
1409                    index: 0,
1410                },
1411                script_sig: vec![],
1412                sequence: 0xffffffff,
1413            }]
1414            .into(),
1415            outputs: vec![TransactionOutput {
1416                value: 100000000,
1417                script_pubkey: vec![],
1418            }]
1419            .into(),
1420            lock_time: 0,
1421        };
1422
1423        let (result, fee) = check_tx_inputs(&tx, &utxo_set, 0).unwrap();
1424
1425        assert!(matches!(result, ValidationResult::Invalid(_)));
1426        assert_eq!(fee, 0);
1427    }
1428
1429    #[test]
1430    fn test_check_tx_inputs_insufficient_funds() {
1431        let mut utxo_set = UtxoSet::default();
1432
1433        // Add UTXO with insufficient value
1434        let outpoint = OutPoint {
1435            hash: [1; 32],
1436            index: 0,
1437        };
1438        let utxo = UTXO {
1439            value: 100000000, // 1 BTC
1440            script_pubkey: vec![].into(),
1441            height: 0,
1442            is_coinbase: false,
1443        };
1444        utxo_set.insert(outpoint, std::sync::Arc::new(utxo));
1445
1446        let tx = Transaction {
1447            version: 1,
1448            inputs: vec![TransactionInput {
1449                prevout: OutPoint {
1450                    hash: [1; 32].into(),
1451                    index: 0,
1452                },
1453                script_sig: vec![],
1454                sequence: 0xffffffff,
1455            }]
1456            .into(),
1457            outputs: vec![TransactionOutput {
1458                value: 200000000, // 2 BTC output (more than input)
1459                script_pubkey: vec![],
1460            }]
1461            .into(),
1462            lock_time: 0,
1463        };
1464
1465        let (result, fee) = check_tx_inputs(&tx, &utxo_set, 0).unwrap();
1466
1467        assert!(matches!(result, ValidationResult::Invalid(_)));
1468        assert_eq!(fee, 0);
1469    }
1470
1471    #[test]
1472    fn test_check_tx_inputs_multiple_inputs() {
1473        let mut utxo_set = UtxoSet::default();
1474
1475        // Add two UTXOs
1476        let outpoint1 = OutPoint {
1477            hash: [1; 32],
1478            index: 0,
1479        };
1480        let utxo1 = UTXO {
1481            value: 500000000, // 5 BTC
1482            script_pubkey: vec![].into(),
1483            height: 0,
1484            is_coinbase: false,
1485        };
1486        utxo_set.insert(outpoint1, std::sync::Arc::new(utxo1));
1487
1488        let outpoint2 = OutPoint {
1489            hash: [2; 32],
1490            index: 0,
1491        };
1492        let utxo2 = UTXO {
1493            value: 300000000, // 3 BTC
1494            script_pubkey: vec![].into(),
1495            height: 0,
1496            is_coinbase: false,
1497        };
1498        utxo_set.insert(outpoint2, std::sync::Arc::new(utxo2));
1499
1500        let tx = Transaction {
1501            version: 1,
1502            inputs: vec![
1503                TransactionInput {
1504                    prevout: OutPoint {
1505                        hash: [1; 32].into(),
1506                        index: 0,
1507                    },
1508                    script_sig: vec![],
1509                    sequence: 0xffffffff,
1510                },
1511                TransactionInput {
1512                    prevout: OutPoint {
1513                        hash: [2; 32],
1514                        index: 0,
1515                    },
1516                    script_sig: vec![],
1517                    sequence: 0xffffffff,
1518                },
1519            ]
1520            .into(),
1521            outputs: vec![TransactionOutput {
1522                value: 700000000, // 7 BTC output
1523                script_pubkey: vec![].into(),
1524            }]
1525            .into(),
1526            lock_time: 0,
1527        };
1528
1529        let (result, fee) = check_tx_inputs(&tx, &utxo_set, 0).unwrap();
1530
1531        assert_eq!(result, ValidationResult::Valid);
1532        assert_eq!(fee, 100000000); // 1 BTC fee (8 BTC input - 7 BTC output)
1533    }
1534
1535    #[test]
1536    fn test_is_coinbase_edge_cases() {
1537        // Valid coinbase
1538        let valid_coinbase = Transaction {
1539            version: 1,
1540            inputs: vec![TransactionInput {
1541                prevout: OutPoint {
1542                    hash: [0; 32].into(),
1543                    index: 0xffffffff,
1544                },
1545                script_sig: vec![],
1546                sequence: 0xffffffff,
1547            }]
1548            .into(),
1549            outputs: vec![].into(),
1550            lock_time: 0,
1551        };
1552        assert!(is_coinbase(&valid_coinbase));
1553
1554        // Wrong hash
1555        let wrong_hash = Transaction {
1556            version: 1,
1557            inputs: vec![TransactionInput {
1558                prevout: OutPoint {
1559                    hash: [1; 32].into(),
1560                    index: 0xffffffff,
1561                },
1562                script_sig: vec![],
1563                sequence: 0xffffffff,
1564            }]
1565            .into(),
1566            outputs: vec![].into(),
1567            lock_time: 0,
1568        };
1569        assert!(!is_coinbase(&wrong_hash));
1570
1571        // Wrong index
1572        let wrong_index = Transaction {
1573            version: 1,
1574            inputs: vec![TransactionInput {
1575                prevout: OutPoint {
1576                    hash: [0; 32].into(),
1577                    index: 0,
1578                },
1579                script_sig: vec![],
1580                sequence: 0xffffffff,
1581            }]
1582            .into(),
1583            outputs: vec![].into(),
1584            lock_time: 0,
1585        };
1586        assert!(!is_coinbase(&wrong_index));
1587
1588        // Multiple inputs
1589        let multiple_inputs = Transaction {
1590            version: 1,
1591            inputs: vec![
1592                TransactionInput {
1593                    prevout: OutPoint {
1594                        hash: [0; 32].into(),
1595                        index: 0xffffffff,
1596                    },
1597                    script_sig: vec![],
1598                    sequence: 0xffffffff,
1599                },
1600                TransactionInput {
1601                    prevout: OutPoint {
1602                        hash: [1; 32],
1603                        index: 0,
1604                    },
1605                    script_sig: vec![],
1606                    sequence: 0xffffffff,
1607                },
1608            ]
1609            .into(),
1610            outputs: vec![].into(),
1611            lock_time: 0,
1612        };
1613        assert!(!is_coinbase(&multiple_inputs));
1614
1615        // No inputs
1616        let no_inputs = Transaction {
1617            version: 1,
1618            inputs: vec![].into(),
1619            outputs: vec![].into(),
1620            lock_time: 0,
1621        };
1622        assert!(!is_coinbase(&no_inputs));
1623    }
1624
1625    #[test]
1626    fn test_calculate_transaction_size() {
1627        let tx = Transaction {
1628            version: 1,
1629            inputs: vec![
1630                TransactionInput {
1631                    prevout: OutPoint {
1632                        hash: [0; 32].into(),
1633                        index: 0,
1634                    },
1635                    script_sig: vec![1, 2, 3],
1636                    sequence: 0xffffffff,
1637                },
1638                TransactionInput {
1639                    prevout: OutPoint {
1640                        hash: [1; 32],
1641                        index: 1,
1642                    },
1643                    script_sig: vec![4, 5, 6],
1644                    sequence: 0xffffffff,
1645                },
1646            ]
1647            .into(),
1648            outputs: vec![
1649                TransactionOutput {
1650                    value: 1000,
1651                    script_pubkey: vec![7, 8, 9].into(),
1652                },
1653                TransactionOutput {
1654                    value: 2000,
1655                    script_pubkey: vec![10, 11, 12],
1656                },
1657            ]
1658            .into(),
1659            lock_time: 12345,
1660        };
1661
1662        let size = calculate_transaction_size(&tx);
1663        // Expected actual serialized size:
1664        // 4 (version) + 1 (input_count varint) +
1665        // 2 * (32 + 4 + 1 + 3 + 4) (inputs: hash + index + script_len_varint + script + sequence) +
1666        // 1 (output_count varint) +
1667        // 2 * (8 + 1 + 3) (outputs: value + script_len_varint + script) +
1668        // 4 (lock_time) = 4 + 1 + 88 + 1 + 24 + 4 = 122
1669        // This matches actual serialization (not simplified calculation)
1670        assert_eq!(size, 122);
1671    }
1672}