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/// CheckCoinbaseMaturity: spend of coinbase output at height h valid iff h ≄ creation_height + R.
14#[spec_locked("5.1", "CheckCoinbaseMaturity")]
15#[inline]
16pub fn check_coinbase_maturity(
17    spend_height: Natural,
18    utxo_creation_height: Natural,
19    is_coinbase: bool,
20) -> bool {
21    if !is_coinbase {
22        return true;
23    }
24    let required = utxo_creation_height.saturating_add(COINBASE_MATURITY);
25    spend_height >= required
26}
27
28// Cold error construction helpers - these paths are rarely taken
29#[cold]
30fn make_output_sum_overflow_error() -> ConsensusError {
31    ConsensusError::TransactionValidation("Output value sum overflow".into())
32}
33
34#[cold]
35fn make_fee_calculation_underflow_error() -> ConsensusError {
36    ConsensusError::TransactionValidation("Fee calculation underflow".into())
37}
38
39/// Sum all output values with checked overflow arithmetic.
40///
41/// Shared by `check_tx_inputs_with_utxos` and `check_tx_inputs_with_owned_data`
42/// to avoid duplicating the fold/checked-add/error-mapping pattern.
43#[inline]
44fn sum_output_values(outputs: &[TransactionOutput]) -> Result<i64> {
45    outputs
46        .iter()
47        .try_fold(0i64, |acc, output| {
48            assert!(
49                output.value >= 0,
50                "Output value {} must be non-negative",
51                output.value
52            );
53            acc.checked_add(output.value).ok_or_else(|| {
54                ConsensusError::TransactionValidation("Output value overflow".into())
55            })
56        })
57        .map_err(|e| ConsensusError::TransactionValidation(Cow::Owned(e.to_string())))
58}
59
60/// Short-circuits validation for trivially invalid (empty non-coinbase) or coinbase transactions.
61///
62/// Returns `Some(Ok(...))` when the caller should return immediately, `None` to continue.
63/// Eliminates the identical guard block repeated at the top of every `check_tx_inputs_*` variant.
64#[inline]
65fn coinbase_or_empty_short_circuit(
66    tx: &Transaction,
67) -> Option<Result<(ValidationResult, Integer)>> {
68    if tx.inputs.is_empty() && !is_coinbase(tx) {
69        return Some(Ok((
70            ValidationResult::Invalid(
71                "Transaction must have inputs unless it's a coinbase".to_string(),
72            ),
73            0,
74        )));
75    }
76    if is_coinbase(tx) {
77        return Some(Ok((ValidationResult::Valid, 0)));
78    }
79    None
80}
81
82/// Fast-path early-exit checks for transaction validation
83///
84/// Performs quick checks before expensive validation operations.
85/// Returns Some(ValidationResult) if fast-path can determine validity, None if full validation needed.
86#[inline(always)]
87#[cfg(feature = "production")]
88fn check_transaction_fast_path(tx: &Transaction) -> Option<ValidationResult> {
89    // Quick reject: empty inputs or outputs (most common invalid case)
90    if tx.inputs.is_empty() || tx.outputs.is_empty() {
91        return Some(ValidationResult::Invalid("Empty inputs or outputs".into()));
92    }
93
94    // Quick reject: obviously too many inputs/outputs (before expensive size calculation)
95    if tx.inputs.len() > MAX_INPUTS {
96        return Some(ValidationResult::Invalid(format!(
97            "Too many inputs: {}",
98            tx.inputs.len()
99        )));
100    }
101    if tx.outputs.len() > MAX_OUTPUTS {
102        return Some(ValidationResult::Invalid(format!(
103            "Too many outputs: {}",
104            tx.outputs.len()
105        )));
106    }
107
108    // Quick reject: obviously invalid value ranges (before expensive validation)
109    // Check if any output value is negative or exceeds MAX_MONEY
110    // Optimization: Use precomputed constant for u64 comparisons
111    #[cfg(feature = "production")]
112    {
113        use crate::optimizations::precomputed_constants::MAX_MONEY_U64;
114        for output in &tx.outputs {
115            let value_u64 = output.value as u64;
116            if output.value < 0 || value_u64 > MAX_MONEY_U64 {
117                return Some(ValidationResult::Invalid(format!(
118                    "Invalid output value: {}",
119                    output.value
120                )));
121            }
122        }
123    }
124
125    #[cfg(not(feature = "production"))]
126    for output in &tx.outputs {
127        if output.value < 0 || output.value > MAX_MONEY {
128            return Some(ValidationResult::Invalid(format!(
129                "Invalid output value: {}",
130                output.value
131            )));
132        }
133    }
134
135    // Quick reject: coinbase with invalid scriptSig length
136    // Optimization: Use constant folding for zero hash check
137    #[cfg(feature = "production")]
138    let is_coinbase_hash = {
139        use crate::optimizations::constant_folding::is_zero_hash;
140        is_zero_hash(&tx.inputs[0].prevout.hash)
141    };
142
143    #[cfg(not(feature = "production"))]
144    let is_coinbase_hash = tx.inputs[0].prevout.hash == [0u8; 32];
145
146    if tx.inputs.len() == 1 && is_coinbase_hash && tx.inputs[0].prevout.index == 0xffffffff {
147        let script_sig_len = tx.inputs[0].script_sig.len();
148        if !(2..=100).contains(&script_sig_len) {
149            return Some(ValidationResult::Invalid(format!(
150                "Coinbase scriptSig length {script_sig_len} must be between 2 and 100 bytes"
151            )));
152        }
153    }
154
155    // Fast-path can't validate everything, needs full validation
156    None
157}
158
159/// CheckTransaction: š’Æš’³ → {valid, invalid}
160///
161/// A transaction tx = (v, ins, outs, lt) is valid if and only if:
162/// 1. |ins| > 0 ∧ |outs| > 0
163/// 2. āˆ€o ∈ outs: 0 ≤ o.value ≤ M_max
164/// 3. āˆ‘_{o ∈ outs} o.value ≤ M_max (total output sum)
165/// 4. |ins| ≤ M_max_inputs
166/// 5. |outs| ≤ M_max_outputs
167/// 6. |tx| ≤ M_max_tx_size
168/// 7. āˆ€i,j ∈ ins: i ≠ j ⟹ i.prevout ≠ j.prevout (no duplicate inputs)
169/// 8. If tx is coinbase: 2 ≤ |ins[0].scriptSig| ≤ 100
170///
171/// Uses fast-path checks before full validation.
172#[spec_locked("5.1", "CheckTransaction")]
173#[track_caller] // Better error messages showing caller location
174#[cfg_attr(feature = "production", inline(always))]
175#[cfg_attr(not(feature = "production"), inline)]
176pub fn check_transaction(tx: &Transaction) -> Result<ValidationResult> {
177    // Precondition checks: Validate function inputs
178    // Note: We check these conditions and return Invalid rather than asserting,
179    // to allow tests to verify the validation logic properly
180    if tx.inputs.len() > MAX_INPUTS {
181        return Ok(ValidationResult::Invalid(format!(
182            "Input count {} exceeds maximum {}",
183            tx.inputs.len(),
184            MAX_INPUTS
185        )));
186    }
187    if tx.outputs.len() > MAX_OUTPUTS {
188        return Ok(ValidationResult::Invalid(format!(
189            "Output count {} exceeds maximum {}",
190            tx.outputs.len(),
191            MAX_OUTPUTS
192        )));
193    }
194
195    // Fast-path early exit for obviously invalid transactions
196    #[cfg(feature = "production")]
197    if let Some(result) = check_transaction_fast_path(tx) {
198        return Ok(result);
199    }
200
201    // 1. Check inputs and outputs are not empty (redundant if fast-path worked, but safe fallback)
202    // Note: We check this condition and return Invalid rather than asserting, to allow tests
203    // to verify the validation logic properly
204    if tx.inputs.is_empty() {
205        // Coinbase transactions have exactly 1 input, so empty inputs means non-coinbase
206        return Ok(ValidationResult::Invalid(
207            "Transaction must have inputs unless it's a coinbase".to_string(),
208        ));
209    }
210    if tx.outputs.is_empty() {
211        return Ok(ValidationResult::Invalid(
212            "Transaction must have at least one output".to_string(),
213        ));
214    }
215
216    // 2. Check output values are valid and calculate total sum in one pass (Orange Paper Section 5.1, rules 2 & 3)
217    // āˆ€o ∈ outs: 0 ≤ o.value ≤ M_max ∧ āˆ‘_{o ∈ outs} o.value ≤ M_max
218    // Use proven bounds for output access in hot path
219    let mut total_output_value = 0i64;
220    // Invariant assertion: Total output value must start at zero
221    assert!(
222        total_output_value == 0,
223        "Total output value must start at zero"
224    );
225    #[cfg(feature = "production")]
226    {
227        use crate::optimizations::optimized_access::get_proven_by_;
228        use crate::optimizations::precomputed_constants::MAX_MONEY_U64;
229        for i in 0..tx.outputs.len() {
230            if let Some(output) = get_proven_by_(&tx.outputs, i) {
231                let value_u64 = output.value as u64;
232                if output.value < 0 || value_u64 > MAX_MONEY_U64 {
233                    return Ok(ValidationResult::Invalid(format!(
234                        "Invalid output value {} at index {}",
235                        output.value, i
236                    )));
237                }
238                // Accumulate sum with overflow check
239                // Invariant assertion: Output value must be non-negative before addition
240                assert!(
241                    output.value >= 0,
242                    "Output value {} must be non-negative at index {}",
243                    output.value,
244                    i
245                );
246                total_output_value = total_output_value
247                    .checked_add(output.value)
248                    .ok_or_else(make_output_sum_overflow_error)?;
249                // Invariant assertion: Total output value must remain non-negative after addition
250                assert!(
251                    total_output_value >= 0,
252                    "Total output value {total_output_value} must be non-negative after output {i}"
253                );
254            }
255        }
256    }
257
258    #[cfg(not(feature = "production"))]
259    {
260        for (i, output) in tx.outputs.iter().enumerate() {
261            // Bounds checking assertion: Output index must be valid
262            assert!(i < tx.outputs.len(), "Output index {i} out of bounds");
263            // Check output value is valid (non-negative and within MAX_MONEY)
264            // Note: We check this condition and return Invalid rather than asserting,
265            // to allow tests to verify the validation logic properly
266            if output.value < 0 || output.value > MAX_MONEY {
267                return Ok(ValidationResult::Invalid(format!(
268                    "Invalid output value {} at index {}",
269                    output.value, i
270                )));
271            }
272            // Accumulate sum with overflow check
273            total_output_value = total_output_value
274                .checked_add(output.value)
275                .ok_or_else(make_output_sum_overflow_error)?;
276            // Invariant assertion: Total output value must remain non-negative after addition
277            assert!(
278                total_output_value >= 0,
279                "Total output value {total_output_value} must be non-negative after output {i}"
280            );
281        }
282    }
283
284    // 2b. Check total output sum is in valid range (MoneyRange)
285    // MoneyRange(n) = (n >= 0 && n <= MAX_MONEY)
286    // Optimization: Use precomputed constant for comparison
287    // Invariant assertion: Total output value must be non-negative
288    assert!(
289        total_output_value >= 0,
290        "Total output value {total_output_value} must be non-negative"
291    );
292
293    #[cfg(feature = "production")]
294    {
295        use crate::optimizations::precomputed_constants::MAX_MONEY_U64;
296        let total_u64 = total_output_value as u64;
297        // Check for invalid total output value and return error (before assert)
298        if total_output_value < 0 || total_u64 > MAX_MONEY_U64 {
299            return Ok(ValidationResult::Invalid(format!(
300                "Total output value {total_output_value} is out of valid range [0, {MAX_MONEY}]"
301            )));
302        }
303        // Invariant assertion: Total output value must not exceed MAX_MONEY
304        // (This should never fail if the check above is correct)
305        assert!(
306            total_u64 <= MAX_MONEY_U64,
307            "Total output value {total_output_value} must not exceed MAX_MONEY"
308        );
309    }
310
311    #[cfg(not(feature = "production"))]
312    {
313        // Check for invalid total output value and return error (before assert)
314        if !(0..=MAX_MONEY).contains(&total_output_value) {
315            return Ok(ValidationResult::Invalid(format!(
316                "Total output value {total_output_value} is out of valid range [0, {MAX_MONEY}]"
317            )));
318        }
319        // Invariant assertion: Total output value must not exceed MAX_MONEY
320        // (This should never fail if the check above is correct)
321        assert!(
322            total_output_value <= MAX_MONEY,
323            "Total output value {total_output_value} must not exceed MAX_MONEY"
324        );
325    }
326
327    // 3. Check input count limit (redundant if fast-path worked)
328    if tx.inputs.len() > MAX_INPUTS {
329        return Ok(ValidationResult::Invalid(format!(
330            "Too many inputs: {}",
331            tx.inputs.len()
332        )));
333    }
334
335    // 4. Check output count limit (redundant if fast-path worked)
336    if tx.outputs.len() > MAX_OUTPUTS {
337        return Ok(ValidationResult::Invalid(format!(
338            "Too many outputs: {}",
339            tx.outputs.len()
340        )));
341    }
342
343    // 5. Check transaction size limit (consensus CheckTransaction)
344    // GetSerializeSize(TX_NO_WITNESS) * WITNESS_SCALE_FACTOR > MAX_BLOCK_WEIGHT
345    // This checks: stripped_size * 4 > 4,000,000, i.e., stripped_size > 1,000,000
346    // calculate_transaction_size returns stripped size (no witness), matching TX_NO_WITNESS
347    use crate::constants::MAX_BLOCK_WEIGHT;
348    const WITNESS_SCALE_FACTOR: usize = 4;
349    let tx_stripped_size = calculate_transaction_size(tx); // This is TX_NO_WITNESS size
350    if tx_stripped_size * WITNESS_SCALE_FACTOR > MAX_BLOCK_WEIGHT {
351        return Ok(ValidationResult::Invalid(format!(
352            "Transaction too large: stripped size {} bytes (weight {} > {})",
353            tx_stripped_size,
354            tx_stripped_size * WITNESS_SCALE_FACTOR,
355            MAX_BLOCK_WEIGHT
356        )));
357    }
358
359    // 7. Check for duplicate inputs (Orange Paper Section 5.1, rule 4)
360    // āˆ€i,j ∈ ins: i ≠ j ⟹ i.prevout ≠ j.prevout
361    // Optimization: Use HashSet for O(n) duplicate detection instead of O(n²) nested loop
362    use std::collections::HashSet;
363    let mut seen_prevouts = HashSet::with_capacity(tx.inputs.len());
364    for (i, input) in tx.inputs.iter().enumerate() {
365        // Bounds checking assertion: Input index must be valid
366        assert!(i < tx.inputs.len(), "Input index {i} out of bounds");
367        if !seen_prevouts.insert(&input.prevout) {
368            return Ok(ValidationResult::Invalid(format!(
369                "Duplicate input prevout at index {i}"
370            )));
371        }
372    }
373
374    // 8. Check coinbase scriptSig length (Orange Paper Section 5.1, rule 5)
375    // If tx is coinbase: 2 ≤ |ins[0].scriptSig| ≤ 100
376    if is_coinbase(tx) {
377        debug_assert!(
378            !tx.inputs.is_empty(),
379            "Coinbase transaction must have at least one input"
380        );
381        let script_sig_len = tx.inputs[0].script_sig.len();
382        if !(2..=100).contains(&script_sig_len) {
383            return Ok(ValidationResult::Invalid(format!(
384                "Coinbase scriptSig length {script_sig_len} must be between 2 and 100 bytes"
385            )));
386        }
387    }
388
389    // Postcondition assertion: Validation result must be Valid or Invalid
390    // Note: This assertion documents the expected return type
391    // The result is always Valid at this point (we would have returned Invalid earlier)
392
393    Ok(ValidationResult::Valid)
394}
395
396/// CheckTxInputs: š’Æš’³ Ɨ š’°š’® Ɨ ā„• → {valid, invalid} Ɨ ℤ
397///
398/// For transaction tx with UTXO set us at height h:
399/// 1. If tx is coinbase: return (valid, 0)
400/// 2. If tx is not coinbase: āˆ€i ∈ ins: ¬i.prevout.IsNull() (Orange Paper Section 5.1, rule 6)
401/// 3. Let total_in = Σᵢ us(i.prevout).value
402/// 4. Let total_out = Σₒ o.value
403/// 5. If total_in < total_out: return (invalid, 0)
404/// 6. Return (valid, total_in - total_out)
405#[spec_locked("5.1", "CheckTxInputs")]
406#[cfg_attr(feature = "production", inline(always))]
407#[cfg_attr(not(feature = "production"), inline)]
408#[allow(clippy::overly_complex_bool_expr)] // Intentional tautological assertions for formal verification
409pub fn check_tx_inputs<U: UtxoLookup>(
410    tx: &Transaction,
411    utxo_set: &U,
412    height: Natural,
413) -> Result<(ValidationResult, Integer)> {
414    check_tx_inputs_with_utxos(tx, utxo_set, height, None)
415}
416
417/// Optimized version that accepts pre-collected UTXOs to avoid redundant lookups
418pub fn check_tx_inputs_with_utxos<U: UtxoLookup>(
419    tx: &Transaction,
420    utxo_set: &U,
421    height: Natural,
422    pre_collected_utxos: Option<&[Option<&UTXO>]>,
423) -> Result<(ValidationResult, Integer)> {
424    if let Some(result) = coinbase_or_empty_short_circuit(tx) {
425        return result;
426    }
427    assert!(
428        height <= i64::MAX as u64,
429        "Block height {height} must fit in i64"
430    );
431    assert!(
432        utxo_set.len() <= u32::MAX as usize,
433        "UTXO set size {} exceeds maximum",
434        utxo_set.len()
435    );
436
437    // Check that non-coinbase inputs don't have null prevouts (Orange Paper Section 5.1, rule 6)
438    // āˆ€i ∈ ins: ¬i.prevout.IsNull()
439    // Use proven bounds for input access in hot path
440    #[cfg(feature = "production")]
441    {
442        use crate::optimizations::constant_folding::is_zero_hash;
443        use crate::optimizations::optimized_access::get_proven_by_;
444        for i in 0..tx.inputs.len() {
445            if let Some(input) = get_proven_by_(&tx.inputs, i) {
446                if is_zero_hash(&input.prevout.hash) && input.prevout.index == 0xffffffff {
447                    return Ok((
448                        ValidationResult::Invalid(format!(
449                            "Non-coinbase input {i} has null prevout"
450                        )),
451                        0,
452                    ));
453                }
454            }
455        }
456    }
457
458    #[cfg(not(feature = "production"))]
459    {
460        for (i, input) in tx.inputs.iter().enumerate() {
461            if input.prevout.hash == [0u8; 32] && input.prevout.index == 0xffffffff {
462                return Ok((
463                    ValidationResult::Invalid(format!("Non-coinbase input {i} has null prevout")),
464                    0,
465                ));
466            }
467        }
468    }
469
470    // Optimization: Batch UTXO lookups - collect all prevouts first, then lookup
471    // This improves cache locality and reduces HashMap traversal overhead
472    // Optimization: Pre-allocate with known size
473    #[cfg(feature = "production")]
474    {
475        use crate::optimizations::prefetch;
476        // Prefetch ahead for sequential UTXO lookups
477        for i in 0..tx.inputs.len().min(8) {
478            if i + 4 < tx.inputs.len() {
479                prefetch::prefetch_ahead(&tx.inputs, i, 4);
480            }
481        }
482    }
483
484    // OPTIMIZATION: Use pre-collected UTXOs if provided, otherwise collect them
485    let input_utxos: Vec<(usize, Option<&UTXO>)> = if let Some(pre_utxos) = pre_collected_utxos {
486        // Pre-collected UTXOs provided - use them directly (no redundant lookups)
487        pre_utxos
488            .iter()
489            .enumerate()
490            .map(|(i, opt_utxo)| (i, *opt_utxo))
491            .collect()
492    } else {
493        // No pre-collected UTXOs - collect them now
494        let mut result = Vec::with_capacity(tx.inputs.len());
495        for (i, input) in tx.inputs.iter().enumerate() {
496            result.push((i, utxo_set.get(&input.prevout)));
497        }
498        result
499    };
500
501    let mut total_input_value = 0i64;
502    // Invariant assertion: Total input value must start at zero
503    assert!(
504        total_input_value == 0,
505        "Total input value must start at zero"
506    );
507
508    for (i, opt_utxo) in input_utxos {
509        // Bounds checking assertion: Input index must be valid
510        assert!(i < tx.inputs.len(), "Input index {i} out of bounds");
511
512        // Check if input exists in UTXO set
513        if let Some(utxo) = opt_utxo {
514            // Invariant assertion: UTXO value must be non-negative and within MAX_MONEY
515            assert!(
516                utxo.value >= 0,
517                "UTXO value {} must be non-negative at input {}",
518                utxo.value,
519                i
520            );
521            assert!(
522                utxo.value <= MAX_MONEY,
523                "UTXO value {} must not exceed MAX_MONEY at input {}",
524                utxo.value,
525                i
526            );
527
528            // Check coinbase maturity: coinbase outputs cannot be spent until COINBASE_MATURITY blocks deep
529            // Consensus: coinbase outputs require COINBASE_MATURITY confirmations
530            // We check: if utxo.is_coinbase && height < utxo.height + COINBASE_MATURITY
531            if utxo.is_coinbase && !check_coinbase_maturity(height, utxo.height, true) {
532                let required_height = utxo.height.saturating_add(COINBASE_MATURITY);
533                return Ok((
534                    ValidationResult::Invalid(format!(
535                        "Premature spend of coinbase output: input {i} created at height {} cannot be spent until height {} (current: {})",
536                        utxo.height, required_height, height
537                    )),
538                    0,
539                ));
540            }
541
542            // Use checked arithmetic to prevent overflow
543            // Invariant assertion: UTXO value must be non-negative before addition
544            assert!(
545                utxo.value >= 0,
546                "UTXO value {} must be non-negative before addition",
547                utxo.value
548            );
549            total_input_value = total_input_value.checked_add(utxo.value).ok_or_else(|| {
550                ConsensusError::TransactionValidation(
551                    format!("Input value overflow at input {i}").into(),
552                )
553            })?;
554            // Invariant assertion: Total input value must remain non-negative after addition
555            assert!(
556                total_input_value >= 0,
557                "Total input value {total_input_value} must be non-negative after input {i}"
558            );
559        } else {
560            #[cfg(debug_assertions)]
561            {
562                let hash_str: String = tx.inputs[i]
563                    .prevout
564                    .hash
565                    .iter()
566                    .map(|b| format!("{b:02x}"))
567                    .collect();
568                eprintln!(
569                    "   āŒ UTXO NOT FOUND: Input {} prevout {}:{}",
570                    i, hash_str, tx.inputs[i].prevout.index
571                );
572                eprintln!("      UTXO set size: {}", utxo_set.len());
573            }
574            return Ok((
575                ValidationResult::Invalid(format!("Input {i} not found in UTXO set")),
576                0,
577            ));
578        }
579    }
580
581    let total_output_value = sum_output_values(&tx.outputs)?;
582
583    assert!(
584        total_output_value >= 0,
585        "Total output value {total_output_value} must be non-negative"
586    );
587    assert!(
588        total_output_value <= MAX_MONEY,
589        "Total output value {total_output_value} must not exceed MAX_MONEY"
590    );
591    if total_output_value > MAX_MONEY {
592        return Ok((
593            ValidationResult::Invalid(format!(
594                "Total output value {total_output_value} exceeds maximum money supply"
595            )),
596            0,
597        ));
598    }
599
600    // Invariant assertion: Total input must be >= total output for valid transaction
601    if total_input_value < total_output_value {
602        return Ok((
603            ValidationResult::Invalid("Insufficient input value".to_string()),
604            0,
605        ));
606    }
607
608    // Use checked subtraction to prevent underflow (shouldn't happen due to check above, but be safe)
609    let fee = total_input_value
610        .checked_sub(total_output_value)
611        .ok_or_else(make_fee_calculation_underflow_error)?;
612
613    // Postcondition assertions: Validate fee calculation result
614    assert!(fee >= 0, "Fee {fee} must be non-negative");
615    assert!(
616        fee <= total_input_value,
617        "Fee {fee} cannot exceed total input {total_input_value}"
618    );
619    assert!(
620        total_input_value == total_output_value + fee,
621        "Conservation of value: input {total_input_value} must equal output {total_output_value} + fee {fee}"
622    );
623
624    Ok((ValidationResult::Valid, fee))
625}
626
627/// Hot-path: validate inputs using pre-copied UTXO data (value, is_coinbase, height).
628/// Avoids holding overlay refs; enables buffer reuse in block validation.
629pub fn check_tx_inputs_with_owned_data(
630    tx: &Transaction,
631    height: Natural,
632    utxo_data: &[Option<(i64, bool, u64)>],
633) -> Result<(ValidationResult, Integer)> {
634    if let Some(result) = coinbase_or_empty_short_circuit(tx) {
635        return result;
636    }
637    if utxo_data.len() != tx.inputs.len() {
638        return Ok((
639            ValidationResult::Invalid("UTXO data length mismatch".to_string()),
640            0,
641        ));
642    }
643    let mut total_input_value = 0i64;
644    for (i, opt) in utxo_data.iter().enumerate() {
645        if let Some((value, is_coinbase, utxo_height)) = opt {
646            if *value < 0 || *value > MAX_MONEY {
647                return Ok((
648                    ValidationResult::Invalid(format!(
649                        "UTXO value {value} out of bounds at input {i}"
650                    )),
651                    0,
652                ));
653            }
654            if *is_coinbase && !check_coinbase_maturity(height, *utxo_height, true) {
655                return Ok((
656                    ValidationResult::Invalid(format!(
657                        "Premature spend of coinbase output at input {i}"
658                    )),
659                    0,
660                ));
661            }
662            total_input_value = total_input_value.checked_add(*value).ok_or_else(|| {
663                ConsensusError::TransactionValidation(
664                    format!("Input value overflow at input {i}").into(),
665                )
666            })?;
667        } else {
668            return Ok((
669                ValidationResult::Invalid(format!("Input {i} not found in UTXO set")),
670                0,
671            ));
672        }
673    }
674    let total_output_value = sum_output_values(&tx.outputs)?;
675    if total_output_value > MAX_MONEY {
676        return Ok((
677            ValidationResult::Invalid(format!(
678                "Total output value {total_output_value} exceeds maximum"
679            )),
680            0,
681        ));
682    }
683    if total_input_value < total_output_value {
684        return Ok((
685            ValidationResult::Invalid("Insufficient input value".to_string()),
686            0,
687        ));
688    }
689    let fee = total_input_value
690        .checked_sub(total_output_value)
691        .ok_or_else(make_fee_calculation_underflow_error)?;
692    Ok((ValidationResult::Valid, fee))
693}
694
695/// Check if transaction is coinbase
696///
697/// Hot-path function called frequently during validation.
698/// Always inline for maximum performance.
699#[inline(always)]
700#[spec_locked("6.4", "IsCoinbase")]
701pub fn is_coinbase(tx: &Transaction) -> bool {
702    // Optimization: Use constant folding for zero hash check
703    #[cfg(feature = "production")]
704    {
705        use crate::optimizations::constant_folding::is_zero_hash;
706        tx.inputs.len() == 1
707            && is_zero_hash(&tx.inputs[0].prevout.hash)
708            && tx.inputs[0].prevout.index == 0xffffffff
709    }
710
711    #[cfg(not(feature = "production"))]
712    {
713        tx.inputs.len() == 1
714            && tx.inputs[0].prevout.hash == [0u8; 32]
715            && tx.inputs[0].prevout.index == 0xffffffff
716    }
717}
718
719/// Calculate transaction size (non-witness serialization)
720///
721/// Hot-path function called frequently during validation.
722/// Always inline for maximum performance.
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#[inline(always)]
730#[spec_locked("5.1", "CalculateTransactionSize")]
731pub fn calculate_transaction_size(tx: &Transaction) -> usize {
732    // Use actual serialization for consensus compatibility (includes 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],
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![],
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],
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],
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],
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}