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
733    // This replaces the simplified calculation that didn't account for varint encoding
734    use crate::serialization::transaction::serialize_transaction;
735    serialize_transaction(tx).len()
736}
737
738// ============================================================================
739// FORMAL VERIFICATION
740// ============================================================================
741
742/// Mathematical Specification for Transaction Validation (Orange Paper Section 5.1):
743/// āˆ€ tx ∈ š’Æš’³: CheckTransaction(tx) = valid ⟺
744///   (|tx.inputs| > 0 ∧ |tx.outputs| > 0 ∧
745///    āˆ€o ∈ tx.outputs: 0 ≤ o.value ≤ M_max ∧
746///    āˆ‘_{o ∈ tx.outputs} o.value ≤ M_max ∧
747///    |tx.inputs| ≤ M_max_inputs ∧ |tx.outputs| ≤ M_max_outputs ∧
748///    |tx| ≤ M_max_tx_size ∧
749///    āˆ€i,j ∈ tx.inputs: i ≠ j ⟹ i.prevout ≠ j.prevout ∧
750///    (IsCoinbase(tx) ⟹ 2 ≤ |tx.inputs[0].scriptSig| ≤ 100))
751///
752/// Invariants:
753/// - Valid transactions have non-empty inputs and outputs
754/// - Output values are bounded [0, MAX_MONEY] individually (rule 2)
755/// - Total output sum doesn't exceed MAX_MONEY (rule 3)
756/// - Input/output counts respect limits
757/// - Transaction size respects limits
758/// - No duplicate prevouts in inputs (rule 4)
759/// - Coinbase transactions have scriptSig length [2, 100] bytes (rule 5)
760/// - Non-coinbase inputs must not have null prevouts (rule 6, checked in check_tx_inputs)
761
762/// Proptest strategies for transaction-shaped values (no `Arbitrary` impl — orphan rules).
763#[cfg(test)]
764pub(crate) mod transaction_proptest {
765    use super::*;
766    use proptest::prelude::*;
767
768    pub fn arb_transaction() -> BoxedStrategy<Transaction> {
769        (
770            any::<u64>(),
771            prop::collection::vec(
772                (
773                    any::<[u8; 32]>(),
774                    any::<u64>(),
775                    prop::collection::vec(any::<u8>(), 0..100),
776                    any::<u64>(),
777                ),
778                0..10,
779            ),
780            prop::collection::vec(
781                (any::<i64>(), prop::collection::vec(any::<u8>(), 0..100)),
782                0..10,
783            ),
784            any::<u64>(),
785        )
786            .prop_map(|(version, inputs, outputs, lock_time)| {
787                let inputs: Vec<TransactionInput> = inputs
788                    .into_iter()
789                    .map(|(hash, index, script_sig, sequence)| TransactionInput {
790                        prevout: OutPoint {
791                            hash,
792                            index: index as u32,
793                        },
794                        script_sig,
795                        sequence,
796                    })
797                    .collect();
798                let outputs: Vec<TransactionOutput> = outputs
799                    .into_iter()
800                    .map(|(value, script_pubkey)| TransactionOutput {
801                        value,
802                        script_pubkey,
803                    })
804                    .collect();
805                Transaction {
806                    version,
807                    #[cfg(feature = "production")]
808                    inputs: inputs.into(),
809                    #[cfg(not(feature = "production"))]
810                    inputs,
811                    #[cfg(feature = "production")]
812                    outputs: outputs.into(),
813                    #[cfg(not(feature = "production"))]
814                    outputs,
815                    lock_time,
816                }
817            })
818            .boxed()
819    }
820
821    pub fn arb_outpoint() -> impl Strategy<Value = OutPoint> {
822        (any::<[u8; 32]>(), any::<u32>()).prop_map(|(hash, index)| OutPoint { hash, index })
823    }
824
825    pub fn arb_utxo() -> impl Strategy<Value = UTXO> {
826        (
827            any::<i64>(),
828            prop::collection::vec(any::<u8>(), 0..40),
829            any::<u64>(),
830            any::<bool>(),
831        )
832            .prop_map(|(value, script_pubkey, height, is_coinbase)| UTXO {
833                value,
834                script_pubkey: script_pubkey.into(),
835                height,
836                is_coinbase,
837            })
838    }
839
840    /// Minimal single-input transaction with one output of the given value.
841    /// Used by `prop_output_value_bounds` to avoid inline Transaction boilerplate.
842    pub fn make_tx_single_output(value: i64) -> Transaction {
843        Transaction {
844            version: 1,
845            inputs: vec![TransactionInput {
846                prevout: OutPoint {
847                    hash: [0; 32],
848                    index: 0,
849                },
850                script_sig: vec![],
851                sequence: 0xffffffff,
852            }]
853            .into(),
854            outputs: vec![TransactionOutput {
855                value,
856                script_pubkey: vec![],
857            }]
858            .into(),
859            lock_time: 0,
860        }
861    }
862}
863
864#[cfg(test)]
865#[allow(unused_doc_comments)]
866mod property_tests {
867    use super::transaction_proptest::{arb_outpoint, arb_transaction, arb_utxo};
868    use super::*;
869    use proptest::prelude::*;
870
871    /// Property test: check_transaction validates structure correctly
872    proptest! {
873        #[test]
874        fn prop_check_transaction_structure(
875            tx in arb_transaction()
876        ) {
877            // Bound for tractability
878            let mut bounded_tx = tx;
879            if bounded_tx.inputs.len() > 10 {
880                bounded_tx.inputs.truncate(10);
881            }
882            if bounded_tx.outputs.len() > 10 {
883                bounded_tx.outputs.truncate(10);
884            }
885
886            let result = check_transaction(&bounded_tx).unwrap_or_else(|_| ValidationResult::Invalid("Error".to_string()));
887
888            // Structure properties
889            match result {
890                ValidationResult::Valid => {
891                    // Valid transactions must have non-empty inputs and outputs
892                    prop_assert!(!bounded_tx.inputs.is_empty(), "Valid transaction must have inputs");
893                    prop_assert!(!bounded_tx.outputs.is_empty(), "Valid transaction must have outputs");
894
895                    // Valid transactions must respect limits
896                    prop_assert!(bounded_tx.inputs.len() <= MAX_INPUTS, "Valid transaction must respect input limit");
897                    prop_assert!(bounded_tx.outputs.len() <= MAX_OUTPUTS, "Valid transaction must respect output limit");
898
899                    // Valid transactions must have valid output values
900                    for output in &bounded_tx.outputs {
901                        prop_assert!(output.value >= 0, "Valid transaction outputs must be non-negative");
902                        prop_assert!(output.value <= MAX_MONEY, "Valid transaction outputs must not exceed max money");
903                    }
904                },
905                ValidationResult::Invalid(_) => {
906                    // Invalid transactions may violate any rule
907                    // This is acceptable - we're testing the validation logic
908                }
909            }
910        }
911    }
912
913    /// Property test: check_tx_inputs handles coinbase correctly
914    proptest! {
915        #[test]
916        fn prop_check_tx_inputs_coinbase(
917            tx in arb_transaction(),
918            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>()),
919            height in 0u64..1000u64
920        ) {
921            // Bound for tractability
922            let mut bounded_tx = tx;
923            if bounded_tx.inputs.len() > 5 {
924                bounded_tx.inputs.truncate(5);
925            }
926            if bounded_tx.outputs.len() > 5 {
927                bounded_tx.outputs.truncate(5);
928            }
929
930            let result = check_tx_inputs(&bounded_tx, &utxo_set, height).unwrap_or((ValidationResult::Invalid("Error".to_string()), 0));
931
932            // Coinbase property
933            if is_coinbase(&bounded_tx) {
934                prop_assert!(matches!(result.0, ValidationResult::Valid), "Coinbase transactions must be valid");
935                prop_assert_eq!(result.1, 0, "Coinbase transactions must have zero fee");
936            }
937        }
938    }
939
940    /// Property test: is_coinbase correctly identifies coinbase transactions
941    proptest! {
942        #[test]
943        fn prop_is_coinbase_correct(
944            tx in arb_transaction()
945        ) {
946            let is_cb = is_coinbase(&tx);
947
948            // Coinbase identification property
949            if is_cb {
950                prop_assert_eq!(tx.inputs.len(), 1, "Coinbase must have exactly one input");
951                prop_assert_eq!(tx.inputs[0].prevout.hash, [0u8; 32], "Coinbase input must have zero hash");
952                prop_assert_eq!(tx.inputs[0].prevout.index, 0xffffffffu32, "Coinbase input must have max index");
953            }
954        }
955    }
956
957    /// Property test: calculate_transaction_size is consistent
958    proptest! {
959        #[test]
960        fn prop_calculate_transaction_size_consistent(
961            tx in arb_transaction()
962        ) {
963            // Bound for tractability
964            let mut bounded_tx = tx;
965            if bounded_tx.inputs.len() > 10 {
966                bounded_tx.inputs.truncate(10);
967            }
968            if bounded_tx.outputs.len() > 10 {
969                bounded_tx.outputs.truncate(10);
970            }
971
972            let size = calculate_transaction_size(&bounded_tx);
973
974            // Size calculation properties
975            // Minimum: version(4) + input_count_varint(1) + output_count_varint(1) + lock_time(4) = 10 bytes
976            // (Even with 0 inputs/outputs, we need varints for counts)
977            prop_assert!(size >= 10, "Transaction size must be at least 10 bytes (version + varints + lock_time)");
978
979            // Maximum: Use MAX_TX_SIZE as the upper bound (actual serialization can be larger than simplified calculation)
980            // The simplified calculation was: 4 + 10*41 + 10*9 + 4 = 508
981            // But actual serialization with varints and real script sizes can be larger
982            prop_assert!(size <= MAX_TX_SIZE, "Transaction size must not exceed MAX_TX_SIZE ({})", MAX_TX_SIZE);
983
984            // Size should be deterministic
985            let size2 = calculate_transaction_size(&bounded_tx);
986            prop_assert_eq!(size, size2, "Transaction size calculation must be deterministic");
987        }
988    }
989
990    /// Property test: output value bounds are respected
991    proptest! {
992        #[test]
993        fn prop_output_value_bounds(
994            value in 0i64..(MAX_MONEY + 1000)
995        ) {
996            use super::transaction_proptest::make_tx_single_output;
997            let tx = make_tx_single_output(value);
998            let result = check_transaction(&tx).unwrap_or(ValidationResult::Invalid("Error".to_string()));
999
1000            if !(0..=MAX_MONEY).contains(&value) {
1001                prop_assert!(matches!(result, ValidationResult::Invalid(_)),
1002                    "Transactions with invalid output values must be invalid");
1003            } else {
1004                prop_assert!(matches!(result, ValidationResult::Valid),
1005                    "Transactions with valid output values should be valid");
1006            }
1007        }
1008    }
1009}
1010
1011#[cfg(test)]
1012mod tests {
1013    use super::*;
1014
1015    // ============================================================================
1016    // TEST BUILDERS
1017    // ============================================================================
1018
1019    fn make_input(hash: [u8; 32], index: u32) -> TransactionInput {
1020        TransactionInput {
1021            prevout: OutPoint { hash, index },
1022            script_sig: vec![],
1023            sequence: 0xffffffff,
1024        }
1025    }
1026
1027    fn make_output(value: i64) -> TransactionOutput {
1028        TransactionOutput {
1029            value,
1030            script_pubkey: vec![],
1031        }
1032    }
1033
1034    /// Minimal valid non-coinbase transaction with one input (hash=0, index=0).
1035    fn make_simple_tx(value: i64) -> Transaction {
1036        Transaction {
1037            version: 1,
1038            inputs: vec![make_input([0; 32], 0)].into(),
1039            outputs: vec![make_output(value)].into(),
1040            lock_time: 0,
1041        }
1042    }
1043
1044    /// N unique inputs with distinct hashes (hash bytes 0-3 encode `i` as little-endian u32).
1045    fn make_n_inputs(n: usize) -> Vec<TransactionInput> {
1046        (0..n)
1047            .map(|i| {
1048                let mut hash = [0u8; 32];
1049                hash[..4].copy_from_slice(&(i as u32).to_le_bytes());
1050                make_input(hash, i as u32)
1051            })
1052            .collect()
1053    }
1054
1055    /// N outputs each with the given value.
1056    fn make_n_outputs(n: usize, value: i64) -> Vec<TransactionOutput> {
1057        (0..n).map(|_| make_output(value)).collect()
1058    }
1059
1060    /// N inputs with large `script_sig` bytes (for size-limit tests).
1061    fn make_n_large_inputs(n: usize, script_size: usize) -> Vec<TransactionInput> {
1062        (0..n)
1063            .map(|i| TransactionInput {
1064                prevout: OutPoint {
1065                    hash: {
1066                        let mut h = [0u8; 32];
1067                        h[..4].copy_from_slice(&(i as u32).to_le_bytes());
1068                        h
1069                    },
1070                    index: 0,
1071                },
1072                script_sig: vec![0u8; script_size],
1073                sequence: 0xffffffff,
1074            })
1075            .collect()
1076    }
1077
1078    /// Coinbase transaction (null prevout, index=0xffffffff).
1079    fn make_coinbase_tx(value: i64) -> Transaction {
1080        Transaction {
1081            version: 1,
1082            inputs: vec![TransactionInput {
1083                prevout: OutPoint {
1084                    hash: [0; 32],
1085                    index: 0xffffffff,
1086                },
1087                script_sig: vec![],
1088                sequence: 0xffffffff,
1089            }]
1090            .into(),
1091            outputs: vec![make_output(value)].into(),
1092            lock_time: 0,
1093        }
1094    }
1095
1096    // ============================================================================
1097    // TESTS
1098    // ============================================================================
1099
1100    #[test]
1101    fn test_check_transaction_valid() {
1102        assert_eq!(
1103            check_transaction(&make_simple_tx(1000)).unwrap(),
1104            ValidationResult::Valid
1105        );
1106    }
1107
1108    #[test]
1109    fn test_check_transaction_empty_inputs() {
1110        let tx = Transaction {
1111            version: 1,
1112            inputs: vec![].into(),
1113            outputs: vec![make_output(1000)].into(),
1114            lock_time: 0,
1115        };
1116        assert!(matches!(
1117            check_transaction(&tx).unwrap(),
1118            ValidationResult::Invalid(_)
1119        ));
1120    }
1121
1122    #[test]
1123    fn test_check_tx_inputs_coinbase() {
1124        let utxo_set = UtxoSet::default();
1125        let (result, fee) =
1126            check_tx_inputs(&make_coinbase_tx(5_000_000_000), &utxo_set, 0).unwrap();
1127        assert_eq!(result, ValidationResult::Valid);
1128        assert_eq!(fee, 0);
1129    }
1130
1131    // ============================================================================
1132    // COMPREHENSIVE TRANSACTION TESTS
1133    // ============================================================================
1134
1135    #[test]
1136    fn test_check_transaction_empty_outputs() {
1137        let tx = Transaction {
1138            version: 1,
1139            inputs: vec![make_input([0; 32], 0)].into(),
1140            outputs: vec![].into(),
1141            lock_time: 0,
1142        };
1143        assert!(matches!(
1144            check_transaction(&tx).unwrap(),
1145            ValidationResult::Invalid(_)
1146        ));
1147    }
1148
1149    #[test]
1150    fn test_check_transaction_invalid_output_value_negative() {
1151        assert!(matches!(
1152            check_transaction(&make_simple_tx(-1)).unwrap(),
1153            ValidationResult::Invalid(_)
1154        ));
1155    }
1156
1157    #[test]
1158    fn test_check_transaction_invalid_output_value_too_large() {
1159        assert!(matches!(
1160            check_transaction(&make_simple_tx(MAX_MONEY + 1)).unwrap(),
1161            ValidationResult::Invalid(_)
1162        ));
1163    }
1164
1165    #[test]
1166    fn test_check_transaction_max_output_value() {
1167        assert_eq!(
1168            check_transaction(&make_simple_tx(MAX_MONEY)).unwrap(),
1169            ValidationResult::Valid
1170        );
1171    }
1172
1173    #[test]
1174    fn test_check_transaction_too_many_inputs() {
1175        // MAX_INPUTS + 1 inputs → must be rejected
1176        let tx = Transaction {
1177            version: 1,
1178            inputs: make_n_inputs(MAX_INPUTS + 1).into(),
1179            outputs: vec![make_output(1000)].into(),
1180            lock_time: 0,
1181        };
1182        assert!(matches!(
1183            check_transaction(&tx).unwrap(),
1184            ValidationResult::Invalid(_)
1185        ));
1186    }
1187
1188    #[test]
1189    fn test_check_transaction_max_inputs() {
1190        // 20_000 unique inputs — fits within the 1 MB stripped-size limit
1191        // (each ā‰ˆ 41 bytes; 20_000 Ɨ 41 ā‰ˆ 820 kB < 1 MB)
1192        let tx = Transaction {
1193            version: 1,
1194            inputs: make_n_inputs(20_000).into(),
1195            outputs: vec![make_output(1000)].into(),
1196            lock_time: 0,
1197        };
1198        assert_eq!(check_transaction(&tx).unwrap(), ValidationResult::Valid);
1199    }
1200
1201    #[test]
1202    fn test_check_transaction_too_many_outputs() {
1203        let tx = Transaction {
1204            version: 1,
1205            inputs: vec![make_input([0; 32], 0)].into(),
1206            outputs: make_n_outputs(MAX_OUTPUTS + 1, 1000).into(),
1207            lock_time: 0,
1208        };
1209        assert!(matches!(
1210            check_transaction(&tx).unwrap(),
1211            ValidationResult::Invalid(_)
1212        ));
1213    }
1214
1215    #[test]
1216    fn test_check_transaction_max_outputs() {
1217        let tx = Transaction {
1218            version: 1,
1219            inputs: vec![make_input([0; 32], 0)].into(),
1220            outputs: make_n_outputs(MAX_OUTPUTS, 1000).into(),
1221            lock_time: 0,
1222        };
1223        assert_eq!(check_transaction(&tx).unwrap(), ValidationResult::Valid);
1224    }
1225
1226    #[test]
1227    fn test_check_transaction_too_large() {
1228        // MAX_INPUTS inputs each with a 1 kB script_sig pushes the stripped size
1229        // past 1 MB (MAX_BLOCK_WEIGHT / WITNESS_SCALE_FACTOR).
1230        let tx = Transaction {
1231            version: 1,
1232            inputs: make_n_large_inputs(MAX_INPUTS, 1000).into(),
1233            outputs: vec![make_output(1000)].into(),
1234            lock_time: 0,
1235        };
1236        assert!(matches!(
1237            check_transaction(&tx).unwrap(),
1238            ValidationResult::Invalid(_)
1239        ));
1240    }
1241
1242    fn make_utxo_set(entries: &[([u8; 32], u32, i64)]) -> UtxoSet {
1243        let mut utxo_set = UtxoSet::default();
1244        for &(hash, index, value) in entries {
1245            utxo_set.insert(
1246                OutPoint { hash, index },
1247                std::sync::Arc::new(UTXO {
1248                    value,
1249                    script_pubkey: vec![].into(),
1250                    height: 0,
1251                    is_coinbase: false,
1252                }),
1253            );
1254        }
1255        utxo_set
1256    }
1257
1258    #[test]
1259    fn test_check_tx_inputs_regular_transaction() {
1260        let utxo_set = make_utxo_set(&[([1; 32], 0, 1_000_000_000)]);
1261        let tx = Transaction {
1262            version: 1,
1263            inputs: vec![make_input([1; 32], 0)].into(),
1264            outputs: vec![make_output(900_000_000)].into(),
1265            lock_time: 0,
1266        };
1267        let (result, fee) = check_tx_inputs(&tx, &utxo_set, 0).unwrap();
1268        assert_eq!(result, ValidationResult::Valid);
1269        assert_eq!(fee, 100_000_000);
1270    }
1271
1272    #[test]
1273    fn test_check_tx_inputs_missing_utxo() {
1274        let utxo_set = UtxoSet::default();
1275        let tx = Transaction {
1276            version: 1,
1277            inputs: vec![make_input([1; 32], 0)].into(),
1278            outputs: vec![make_output(100_000_000)].into(),
1279            lock_time: 0,
1280        };
1281        let (result, fee) = check_tx_inputs(&tx, &utxo_set, 0).unwrap();
1282        assert!(matches!(result, ValidationResult::Invalid(_)));
1283        assert_eq!(fee, 0);
1284    }
1285
1286    #[test]
1287    fn test_check_tx_inputs_insufficient_funds() {
1288        let utxo_set = make_utxo_set(&[([1; 32], 0, 100_000_000)]);
1289        let tx = Transaction {
1290            version: 1,
1291            inputs: vec![make_input([1; 32], 0)].into(),
1292            outputs: vec![make_output(200_000_000)].into(),
1293            lock_time: 0,
1294        };
1295        let (result, fee) = check_tx_inputs(&tx, &utxo_set, 0).unwrap();
1296        assert!(matches!(result, ValidationResult::Invalid(_)));
1297        assert_eq!(fee, 0);
1298    }
1299
1300    #[test]
1301    fn test_check_tx_inputs_multiple_inputs() {
1302        let utxo_set = make_utxo_set(&[([1; 32], 0, 500_000_000), ([2; 32], 0, 300_000_000)]);
1303        let tx = Transaction {
1304            version: 1,
1305            inputs: vec![make_input([1; 32], 0), make_input([2; 32], 0)].into(),
1306            outputs: vec![make_output(700_000_000)].into(),
1307            lock_time: 0,
1308        };
1309        let (result, fee) = check_tx_inputs(&tx, &utxo_set, 0).unwrap();
1310        assert_eq!(result, ValidationResult::Valid);
1311        assert_eq!(fee, 100_000_000); // 8 BTC in - 7 BTC out
1312    }
1313
1314    fn bare_tx(inputs: Vec<TransactionInput>) -> Transaction {
1315        Transaction {
1316            version: 1,
1317            inputs: inputs.into(),
1318            outputs: vec![].into(),
1319            lock_time: 0,
1320        }
1321    }
1322
1323    #[test]
1324    fn test_is_coinbase_edge_cases() {
1325        assert!(is_coinbase(&bare_tx(vec![make_input([0; 32], 0xffffffff)])));
1326        assert!(!is_coinbase(&bare_tx(vec![make_input(
1327            [1; 32], 0xffffffff
1328        )]))); // wrong hash
1329        assert!(!is_coinbase(&bare_tx(vec![make_input([0; 32], 0)]))); // wrong index
1330        assert!(!is_coinbase(&bare_tx(vec![
1331            make_input([0; 32], 0xffffffff),
1332            make_input([1; 32], 0),
1333        ]))); // multiple inputs
1334        assert!(!is_coinbase(&bare_tx(vec![]))); // no inputs
1335    }
1336
1337    #[test]
1338    fn test_calculate_transaction_size() {
1339        // 4 (version) + 1 (input_count) +
1340        // 2 Ɨ (32 + 4 + 1 + 3 + 4)  [hash + index + script_len varint + script + sequence] +
1341        // 1 (output_count) +
1342        // 2 Ɨ (8 + 1 + 3)            [value + script_len varint + script] +
1343        // 4 (lock_time) = 122
1344        let tx = Transaction {
1345            version: 1,
1346            inputs: vec![
1347                TransactionInput {
1348                    prevout: OutPoint {
1349                        hash: [0; 32],
1350                        index: 0,
1351                    },
1352                    script_sig: vec![1, 2, 3],
1353                    sequence: 0xffffffff,
1354                },
1355                TransactionInput {
1356                    prevout: OutPoint {
1357                        hash: [1; 32],
1358                        index: 1,
1359                    },
1360                    script_sig: vec![4, 5, 6],
1361                    sequence: 0xffffffff,
1362                },
1363            ]
1364            .into(),
1365            outputs: vec![
1366                TransactionOutput {
1367                    value: 1000,
1368                    script_pubkey: vec![7, 8, 9],
1369                },
1370                TransactionOutput {
1371                    value: 2000,
1372                    script_pubkey: vec![10, 11, 12],
1373                },
1374            ]
1375            .into(),
1376            lock_time: 12345,
1377        };
1378        assert_eq!(calculate_transaction_size(&tx), 122);
1379    }
1380}