1use 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#[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]
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#[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#[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#[inline(always)]
87#[cfg(feature = "production")]
88fn check_transaction_fast_path(tx: &Transaction) -> Option<ValidationResult> {
89 if tx.inputs.is_empty() || tx.outputs.is_empty() {
91 return Some(ValidationResult::Invalid("Empty inputs or outputs".into()));
92 }
93
94 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 #[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 #[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 None
157}
158
159#[spec_locked("5.1", "CheckTransaction")]
173#[track_caller] #[cfg_attr(feature = "production", inline(always))]
175#[cfg_attr(not(feature = "production"), inline)]
176pub fn check_transaction(tx: &Transaction) -> Result<ValidationResult> {
177 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 #[cfg(feature = "production")]
197 if let Some(result) = check_transaction_fast_path(tx) {
198 return Ok(result);
199 }
200
201 if tx.inputs.is_empty() {
205 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 let mut total_output_value = 0i64;
220 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 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 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 assert!(i < tx.outputs.len(), "Output index {i} out of bounds");
263 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 total_output_value = total_output_value
274 .checked_add(output.value)
275 .ok_or_else(make_output_sum_overflow_error)?;
276 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 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 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 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 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 assert!(
322 total_output_value <= MAX_MONEY,
323 "Total output value {total_output_value} must not exceed MAX_MONEY"
324 );
325 }
326
327 if tx.inputs.len() > MAX_INPUTS {
329 return Ok(ValidationResult::Invalid(format!(
330 "Too many inputs: {}",
331 tx.inputs.len()
332 )));
333 }
334
335 if tx.outputs.len() > MAX_OUTPUTS {
337 return Ok(ValidationResult::Invalid(format!(
338 "Too many outputs: {}",
339 tx.outputs.len()
340 )));
341 }
342
343 use crate::constants::MAX_BLOCK_WEIGHT;
348 const WITNESS_SCALE_FACTOR: usize = 4;
349 let tx_stripped_size = calculate_transaction_size(tx); 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 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 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 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 Ok(ValidationResult::Valid)
394}
395
396#[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)] pub 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
417pub 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 #[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 #[cfg(feature = "production")]
474 {
475 use crate::optimizations::prefetch;
476 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 let input_utxos: Vec<(usize, Option<&UTXO>)> = if let Some(pre_utxos) = pre_collected_utxos {
486 pre_utxos
488 .iter()
489 .enumerate()
490 .map(|(i, opt_utxo)| (i, *opt_utxo))
491 .collect()
492 } else {
493 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 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 assert!(i < tx.inputs.len(), "Input index {i} out of bounds");
511
512 if let Some(utxo) = opt_utxo {
514 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 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 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 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 if total_input_value < total_output_value {
602 return Ok((
603 ValidationResult::Invalid("Insufficient input value".to_string()),
604 0,
605 ));
606 }
607
608 let fee = total_input_value
610 .checked_sub(total_output_value)
611 .ok_or_else(make_fee_calculation_underflow_error)?;
612
613 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
627pub 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#[inline(always)]
700#[spec_locked("6.4", "IsCoinbase")]
701pub fn is_coinbase(tx: &Transaction) -> bool {
702 #[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#[inline(always)]
730#[spec_locked("5.1", "CalculateTransactionSize")]
731pub fn calculate_transaction_size(tx: &Transaction) -> usize {
732 use crate::serialization::transaction::serialize_transaction;
735 serialize_transaction(tx).len()
736}
737
738#[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 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 proptest! {
873 #[test]
874 fn prop_check_transaction_structure(
875 tx in arb_transaction()
876 ) {
877 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 match result {
890 ValidationResult::Valid => {
891 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 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 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 }
909 }
910 }
911 }
912
913 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 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 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 proptest! {
942 #[test]
943 fn prop_is_coinbase_correct(
944 tx in arb_transaction()
945 ) {
946 let is_cb = is_coinbase(&tx);
947
948 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 proptest! {
959 #[test]
960 fn prop_calculate_transaction_size_consistent(
961 tx in arb_transaction()
962 ) {
963 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 prop_assert!(size >= 10, "Transaction size must be at least 10 bytes (version + varints + lock_time)");
978
979 prop_assert!(size <= MAX_TX_SIZE, "Transaction size must not exceed MAX_TX_SIZE ({})", MAX_TX_SIZE);
983
984 let size2 = calculate_transaction_size(&bounded_tx);
986 prop_assert_eq!(size, size2, "Transaction size calculation must be deterministic");
987 }
988 }
989
990 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 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 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 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 fn make_n_outputs(n: usize, value: i64) -> Vec<TransactionOutput> {
1057 (0..n).map(|_| make_output(value)).collect()
1058 }
1059
1060 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 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 #[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 #[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 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 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 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); }
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 )]))); assert!(!is_coinbase(&bare_tx(vec![make_input([0; 32], 0)]))); assert!(!is_coinbase(&bare_tx(vec![
1331 make_input([0; 32], 0xffffffff),
1332 make_input([1; 32], 0),
1333 ]))); assert!(!is_coinbase(&bare_tx(vec![]))); }
1336
1337 #[test]
1338 fn test_calculate_transaction_size() {
1339 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}