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