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#[cold]
15fn make_output_sum_overflow_error() -> ConsensusError {
16 ConsensusError::TransactionValidation("Output value sum overflow".into())
17}
18
19#[cold]
20fn make_fee_calculation_underflow_error() -> ConsensusError {
21 ConsensusError::TransactionValidation("Fee calculation underflow".into())
22}
23
24#[inline]
29fn sum_output_values(outputs: &[TransactionOutput]) -> Result<i64> {
30 outputs
31 .iter()
32 .try_fold(0i64, |acc, output| {
33 assert!(
34 output.value >= 0,
35 "Output value {} must be non-negative",
36 output.value
37 );
38 acc.checked_add(output.value).ok_or_else(|| {
39 ConsensusError::TransactionValidation("Output value overflow".into())
40 })
41 })
42 .map_err(|e| ConsensusError::TransactionValidation(Cow::Owned(e.to_string())))
43}
44
45#[inline]
50fn coinbase_or_empty_short_circuit(
51 tx: &Transaction,
52) -> Option<Result<(ValidationResult, Integer)>> {
53 if tx.inputs.is_empty() && !is_coinbase(tx) {
54 return Some(Ok((
55 ValidationResult::Invalid(
56 "Transaction must have inputs unless it's a coinbase".to_string(),
57 ),
58 0,
59 )));
60 }
61 if is_coinbase(tx) {
62 return Some(Ok((ValidationResult::Valid, 0)));
63 }
64 None
65}
66
67#[inline(always)]
72#[cfg(feature = "production")]
73fn check_transaction_fast_path(tx: &Transaction) -> Option<ValidationResult> {
74 if tx.inputs.is_empty() || tx.outputs.is_empty() {
76 return Some(ValidationResult::Invalid("Empty inputs or outputs".into()));
77 }
78
79 if tx.inputs.len() > MAX_INPUTS {
81 return Some(ValidationResult::Invalid(format!(
82 "Too many inputs: {}",
83 tx.inputs.len()
84 )));
85 }
86 if tx.outputs.len() > MAX_OUTPUTS {
87 return Some(ValidationResult::Invalid(format!(
88 "Too many outputs: {}",
89 tx.outputs.len()
90 )));
91 }
92
93 #[cfg(feature = "production")]
97 {
98 use crate::optimizations::precomputed_constants::MAX_MONEY_U64;
99 for output in &tx.outputs {
100 let value_u64 = output.value as u64;
101 if output.value < 0 || value_u64 > MAX_MONEY_U64 {
102 return Some(ValidationResult::Invalid(format!(
103 "Invalid output value: {}",
104 output.value
105 )));
106 }
107 }
108 }
109
110 #[cfg(not(feature = "production"))]
111 for output in &tx.outputs {
112 if output.value < 0 || output.value > MAX_MONEY {
113 return Some(ValidationResult::Invalid(format!(
114 "Invalid output value: {}",
115 output.value
116 )));
117 }
118 }
119
120 #[cfg(feature = "production")]
123 let is_coinbase_hash = {
124 use crate::optimizations::constant_folding::is_zero_hash;
125 is_zero_hash(&tx.inputs[0].prevout.hash)
126 };
127
128 #[cfg(not(feature = "production"))]
129 let is_coinbase_hash = tx.inputs[0].prevout.hash == [0u8; 32];
130
131 if tx.inputs.len() == 1 && is_coinbase_hash && tx.inputs[0].prevout.index == 0xffffffff {
132 let script_sig_len = tx.inputs[0].script_sig.len();
133 if !(2..=100).contains(&script_sig_len) {
134 return Some(ValidationResult::Invalid(format!(
135 "Coinbase scriptSig length {script_sig_len} must be between 2 and 100 bytes"
136 )));
137 }
138 }
139
140 None
142}
143
144#[spec_locked("5.1")]
158#[track_caller] #[cfg_attr(feature = "production", inline(always))]
160#[cfg_attr(not(feature = "production"), inline)]
161pub fn check_transaction(tx: &Transaction) -> Result<ValidationResult> {
162 if tx.inputs.len() > MAX_INPUTS {
166 return Ok(ValidationResult::Invalid(format!(
167 "Input count {} exceeds maximum {}",
168 tx.inputs.len(),
169 MAX_INPUTS
170 )));
171 }
172 if tx.outputs.len() > MAX_OUTPUTS {
173 return Ok(ValidationResult::Invalid(format!(
174 "Output count {} exceeds maximum {}",
175 tx.outputs.len(),
176 MAX_OUTPUTS
177 )));
178 }
179
180 #[cfg(feature = "production")]
182 if let Some(result) = check_transaction_fast_path(tx) {
183 return Ok(result);
184 }
185
186 if tx.inputs.is_empty() {
190 return Ok(ValidationResult::Invalid(
192 "Transaction must have inputs unless it's a coinbase".to_string(),
193 ));
194 }
195 if tx.outputs.is_empty() {
196 return Ok(ValidationResult::Invalid(
197 "Transaction must have at least one output".to_string(),
198 ));
199 }
200
201 let mut total_output_value = 0i64;
205 assert!(
207 total_output_value == 0,
208 "Total output value must start at zero"
209 );
210 #[cfg(feature = "production")]
211 {
212 use crate::optimizations::optimized_access::get_proven_by_;
213 use crate::optimizations::precomputed_constants::MAX_MONEY_U64;
214 for i in 0..tx.outputs.len() {
215 if let Some(output) = get_proven_by_(&tx.outputs, i) {
216 let value_u64 = output.value as u64;
217 if output.value < 0 || value_u64 > MAX_MONEY_U64 {
218 return Ok(ValidationResult::Invalid(format!(
219 "Invalid output value {} at index {}",
220 output.value, i
221 )));
222 }
223 assert!(
226 output.value >= 0,
227 "Output value {} must be non-negative at index {}",
228 output.value,
229 i
230 );
231 total_output_value = total_output_value
232 .checked_add(output.value)
233 .ok_or_else(make_output_sum_overflow_error)?;
234 assert!(
236 total_output_value >= 0,
237 "Total output value {total_output_value} must be non-negative after output {i}"
238 );
239 }
240 }
241 }
242
243 #[cfg(not(feature = "production"))]
244 {
245 for (i, output) in tx.outputs.iter().enumerate() {
246 assert!(i < tx.outputs.len(), "Output index {i} out of bounds");
248 if output.value < 0 || output.value > MAX_MONEY {
252 return Ok(ValidationResult::Invalid(format!(
253 "Invalid output value {} at index {}",
254 output.value, i
255 )));
256 }
257 total_output_value = total_output_value
259 .checked_add(output.value)
260 .ok_or_else(make_output_sum_overflow_error)?;
261 assert!(
263 total_output_value >= 0,
264 "Total output value {total_output_value} must be non-negative after output {i}"
265 );
266 }
267 }
268
269 assert!(
274 total_output_value >= 0,
275 "Total output value {total_output_value} must be non-negative"
276 );
277
278 #[cfg(feature = "production")]
279 {
280 use crate::optimizations::precomputed_constants::MAX_MONEY_U64;
281 let total_u64 = total_output_value as u64;
282 if total_output_value < 0 || total_u64 > MAX_MONEY_U64 {
284 return Ok(ValidationResult::Invalid(format!(
285 "Total output value {total_output_value} is out of valid range [0, {MAX_MONEY}]"
286 )));
287 }
288 assert!(
291 total_u64 <= MAX_MONEY_U64,
292 "Total output value {total_output_value} must not exceed MAX_MONEY"
293 );
294 }
295
296 #[cfg(not(feature = "production"))]
297 {
298 if !(0..=MAX_MONEY).contains(&total_output_value) {
300 return Ok(ValidationResult::Invalid(format!(
301 "Total output value {total_output_value} is out of valid range [0, {MAX_MONEY}]"
302 )));
303 }
304 assert!(
307 total_output_value <= MAX_MONEY,
308 "Total output value {total_output_value} must not exceed MAX_MONEY"
309 );
310 }
311
312 if tx.inputs.len() > MAX_INPUTS {
314 return Ok(ValidationResult::Invalid(format!(
315 "Too many inputs: {}",
316 tx.inputs.len()
317 )));
318 }
319
320 if tx.outputs.len() > MAX_OUTPUTS {
322 return Ok(ValidationResult::Invalid(format!(
323 "Too many outputs: {}",
324 tx.outputs.len()
325 )));
326 }
327
328 use crate::constants::MAX_BLOCK_WEIGHT;
333 const WITNESS_SCALE_FACTOR: usize = 4;
334 let tx_stripped_size = calculate_transaction_size(tx); if tx_stripped_size * WITNESS_SCALE_FACTOR > MAX_BLOCK_WEIGHT {
336 return Ok(ValidationResult::Invalid(format!(
337 "Transaction too large: stripped size {} bytes (weight {} > {})",
338 tx_stripped_size,
339 tx_stripped_size * WITNESS_SCALE_FACTOR,
340 MAX_BLOCK_WEIGHT
341 )));
342 }
343
344 use std::collections::HashSet;
348 let mut seen_prevouts = HashSet::with_capacity(tx.inputs.len());
349 for (i, input) in tx.inputs.iter().enumerate() {
350 assert!(i < tx.inputs.len(), "Input index {i} out of bounds");
352 if !seen_prevouts.insert(&input.prevout) {
353 return Ok(ValidationResult::Invalid(format!(
354 "Duplicate input prevout at index {i}"
355 )));
356 }
357 }
358
359 if is_coinbase(tx) {
362 debug_assert!(
363 !tx.inputs.is_empty(),
364 "Coinbase transaction must have at least one input"
365 );
366 let script_sig_len = tx.inputs[0].script_sig.len();
367 if !(2..=100).contains(&script_sig_len) {
368 return Ok(ValidationResult::Invalid(format!(
369 "Coinbase scriptSig length {script_sig_len} must be between 2 and 100 bytes"
370 )));
371 }
372 }
373
374 Ok(ValidationResult::Valid)
379}
380
381#[spec_locked("5.1")]
391#[cfg_attr(feature = "production", inline(always))]
392#[cfg_attr(not(feature = "production"), inline)]
393#[allow(clippy::overly_complex_bool_expr)] pub fn check_tx_inputs<U: UtxoLookup>(
395 tx: &Transaction,
396 utxo_set: &U,
397 height: Natural,
398) -> Result<(ValidationResult, Integer)> {
399 check_tx_inputs_with_utxos(tx, utxo_set, height, None)
400}
401
402pub fn check_tx_inputs_with_utxos<U: UtxoLookup>(
404 tx: &Transaction,
405 utxo_set: &U,
406 height: Natural,
407 pre_collected_utxos: Option<&[Option<&UTXO>]>,
408) -> Result<(ValidationResult, Integer)> {
409 if let Some(result) = coinbase_or_empty_short_circuit(tx) {
410 return result;
411 }
412 assert!(
413 height <= i64::MAX as u64,
414 "Block height {height} must fit in i64"
415 );
416 assert!(
417 utxo_set.len() <= u32::MAX as usize,
418 "UTXO set size {} exceeds maximum",
419 utxo_set.len()
420 );
421
422 #[cfg(feature = "production")]
426 {
427 use crate::optimizations::constant_folding::is_zero_hash;
428 use crate::optimizations::optimized_access::get_proven_by_;
429 for i in 0..tx.inputs.len() {
430 if let Some(input) = get_proven_by_(&tx.inputs, i) {
431 if is_zero_hash(&input.prevout.hash) && input.prevout.index == 0xffffffff {
432 return Ok((
433 ValidationResult::Invalid(format!(
434 "Non-coinbase input {i} has null prevout"
435 )),
436 0,
437 ));
438 }
439 }
440 }
441 }
442
443 #[cfg(not(feature = "production"))]
444 {
445 for (i, input) in tx.inputs.iter().enumerate() {
446 if input.prevout.hash == [0u8; 32] && input.prevout.index == 0xffffffff {
447 return Ok((
448 ValidationResult::Invalid(format!("Non-coinbase input {i} has null prevout")),
449 0,
450 ));
451 }
452 }
453 }
454
455 #[cfg(feature = "production")]
459 {
460 use crate::optimizations::prefetch;
461 for i in 0..tx.inputs.len().min(8) {
463 if i + 4 < tx.inputs.len() {
464 prefetch::prefetch_ahead(&tx.inputs, i, 4);
465 }
466 }
467 }
468
469 let input_utxos: Vec<(usize, Option<&UTXO>)> = if let Some(pre_utxos) = pre_collected_utxos {
471 pre_utxos
473 .iter()
474 .enumerate()
475 .map(|(i, opt_utxo)| (i, *opt_utxo))
476 .collect()
477 } else {
478 let mut result = Vec::with_capacity(tx.inputs.len());
480 for (i, input) in tx.inputs.iter().enumerate() {
481 result.push((i, utxo_set.get(&input.prevout)));
482 }
483 result
484 };
485
486 let mut total_input_value = 0i64;
487 assert!(
489 total_input_value == 0,
490 "Total input value must start at zero"
491 );
492
493 for (i, opt_utxo) in input_utxos {
494 assert!(i < tx.inputs.len(), "Input index {i} out of bounds");
496
497 if let Some(utxo) = opt_utxo {
499 assert!(
501 utxo.value >= 0,
502 "UTXO value {} must be non-negative at input {}",
503 utxo.value,
504 i
505 );
506 assert!(
507 utxo.value <= MAX_MONEY,
508 "UTXO value {} must not exceed MAX_MONEY at input {}",
509 utxo.value,
510 i
511 );
512
513 if utxo.is_coinbase {
517 use crate::constants::COINBASE_MATURITY;
518 let required_height = utxo.height.saturating_add(COINBASE_MATURITY);
519 assert!(
521 height >= utxo.height,
522 "Current height {} must be >= UTXO creation height {}",
523 height,
524 utxo.height
525 );
526 if height < required_height {
527 return Ok((
528 ValidationResult::Invalid(format!(
529 "Premature spend of coinbase output: input {i} created at height {} cannot be spent until height {} (current: {})",
530 utxo.height, required_height, height
531 )),
532 0,
533 ));
534 }
535 }
536
537 assert!(
540 utxo.value >= 0,
541 "UTXO value {} must be non-negative before addition",
542 utxo.value
543 );
544 total_input_value = total_input_value.checked_add(utxo.value).ok_or_else(|| {
545 ConsensusError::TransactionValidation(
546 format!("Input value overflow at input {i}").into(),
547 )
548 })?;
549 assert!(
551 total_input_value >= 0,
552 "Total input value {total_input_value} must be non-negative after input {i}"
553 );
554 } else {
555 #[cfg(debug_assertions)]
556 {
557 let hash_str: String = tx.inputs[i]
558 .prevout
559 .hash
560 .iter()
561 .map(|b| format!("{b:02x}"))
562 .collect();
563 eprintln!(
564 " ā UTXO NOT FOUND: Input {} prevout {}:{}",
565 i, hash_str, tx.inputs[i].prevout.index
566 );
567 eprintln!(" UTXO set size: {}", utxo_set.len());
568 }
569 return Ok((
570 ValidationResult::Invalid(format!("Input {i} not found in UTXO set")),
571 0,
572 ));
573 }
574 }
575
576 let total_output_value = sum_output_values(&tx.outputs)?;
577
578 assert!(
579 total_output_value >= 0,
580 "Total output value {total_output_value} must be non-negative"
581 );
582 assert!(
583 total_output_value <= MAX_MONEY,
584 "Total output value {total_output_value} must not exceed MAX_MONEY"
585 );
586 if total_output_value > MAX_MONEY {
587 return Ok((
588 ValidationResult::Invalid(format!(
589 "Total output value {total_output_value} exceeds maximum money supply"
590 )),
591 0,
592 ));
593 }
594
595 if total_input_value < total_output_value {
597 return Ok((
598 ValidationResult::Invalid("Insufficient input value".to_string()),
599 0,
600 ));
601 }
602
603 let fee = total_input_value
605 .checked_sub(total_output_value)
606 .ok_or_else(make_fee_calculation_underflow_error)?;
607
608 assert!(fee >= 0, "Fee {fee} must be non-negative");
610 assert!(
611 fee <= total_input_value,
612 "Fee {fee} cannot exceed total input {total_input_value}"
613 );
614 assert!(
615 total_input_value == total_output_value + fee,
616 "Conservation of value: input {total_input_value} must equal output {total_output_value} + fee {fee}"
617 );
618
619 Ok((ValidationResult::Valid, fee))
620}
621
622pub fn check_tx_inputs_with_owned_data(
625 tx: &Transaction,
626 height: Natural,
627 utxo_data: &[Option<(i64, bool, u64)>],
628) -> Result<(ValidationResult, Integer)> {
629 if let Some(result) = coinbase_or_empty_short_circuit(tx) {
630 return result;
631 }
632 if utxo_data.len() != tx.inputs.len() {
633 return Ok((
634 ValidationResult::Invalid("UTXO data length mismatch".to_string()),
635 0,
636 ));
637 }
638 let mut total_input_value = 0i64;
639 for (i, opt) in utxo_data.iter().enumerate() {
640 if let Some((value, is_coinbase, utxo_height)) = opt {
641 if *value < 0 || *value > MAX_MONEY {
642 return Ok((
643 ValidationResult::Invalid(format!(
644 "UTXO value {value} out of bounds at input {i}"
645 )),
646 0,
647 ));
648 }
649 if *is_coinbase {
650 use crate::constants::COINBASE_MATURITY;
651 let required_height = utxo_height.saturating_add(COINBASE_MATURITY);
652 if height < required_height {
653 return Ok((
654 ValidationResult::Invalid(format!(
655 "Premature spend of coinbase output at input {i}"
656 )),
657 0,
658 ));
659 }
660 }
661 total_input_value = total_input_value.checked_add(*value).ok_or_else(|| {
662 ConsensusError::TransactionValidation(
663 format!("Input value overflow at input {i}").into(),
664 )
665 })?;
666 } else {
667 return Ok((
668 ValidationResult::Invalid(format!("Input {i} not found in UTXO set")),
669 0,
670 ));
671 }
672 }
673 let total_output_value = sum_output_values(&tx.outputs)?;
674 if total_output_value > MAX_MONEY {
675 return Ok((
676 ValidationResult::Invalid(format!(
677 "Total output value {total_output_value} exceeds maximum"
678 )),
679 0,
680 ));
681 }
682 if total_input_value < total_output_value {
683 return Ok((
684 ValidationResult::Invalid("Insufficient input value".to_string()),
685 0,
686 ));
687 }
688 let fee = total_input_value
689 .checked_sub(total_output_value)
690 .ok_or_else(make_fee_calculation_underflow_error)?;
691 Ok((ValidationResult::Valid, fee))
692}
693
694#[inline(always)]
699#[spec_locked("6.4")]
700pub fn is_coinbase(tx: &Transaction) -> bool {
701 #[cfg(feature = "production")]
703 {
704 use crate::optimizations::constant_folding::is_zero_hash;
705 tx.inputs.len() == 1
706 && is_zero_hash(&tx.inputs[0].prevout.hash)
707 && tx.inputs[0].prevout.index == 0xffffffff
708 }
709
710 #[cfg(not(feature = "production"))]
711 {
712 tx.inputs.len() == 1
713 && tx.inputs[0].prevout.hash == [0u8; 32]
714 && tx.inputs[0].prevout.index == 0xffffffff
715 }
716}
717
718#[inline(always)]
723#[spec_locked("5.1")]
730pub fn calculate_transaction_size(tx: &Transaction) -> usize {
731 use crate::serialization::transaction::serialize_transaction;
734 serialize_transaction(tx).len()
735}
736
737#[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 pub fn make_tx_single_output(value: i64) -> Transaction {
842 Transaction {
843 version: 1,
844 inputs: vec![TransactionInput {
845 prevout: OutPoint {
846 hash: [0; 32].into(),
847 index: 0,
848 },
849 script_sig: vec![],
850 sequence: 0xffffffff,
851 }]
852 .into(),
853 outputs: vec![TransactionOutput {
854 value,
855 script_pubkey: vec![],
856 }]
857 .into(),
858 lock_time: 0,
859 }
860 }
861}
862
863#[cfg(test)]
864#[allow(unused_doc_comments)]
865mod property_tests {
866 use super::transaction_proptest::{arb_outpoint, arb_transaction, arb_utxo};
867 use super::*;
868 use proptest::prelude::*;
869
870 proptest! {
872 #[test]
873 fn prop_check_transaction_structure(
874 tx in arb_transaction()
875 ) {
876 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 match result {
889 ValidationResult::Valid => {
890 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 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 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 }
908 }
909 }
910 }
911
912 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 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 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 proptest! {
941 #[test]
942 fn prop_is_coinbase_correct(
943 tx in arb_transaction()
944 ) {
945 let is_cb = is_coinbase(&tx);
946
947 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 proptest! {
958 #[test]
959 fn prop_calculate_transaction_size_consistent(
960 tx in arb_transaction()
961 ) {
962 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 prop_assert!(size >= 10, "Transaction size must be at least 10 bytes (version + varints + lock_time)");
977
978 prop_assert!(size <= MAX_TX_SIZE, "Transaction size must not exceed MAX_TX_SIZE ({})", MAX_TX_SIZE);
982
983 let size2 = calculate_transaction_size(&bounded_tx);
985 prop_assert_eq!(size, size2, "Transaction size calculation must be deterministic");
986 }
987 }
988
989 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 fn make_input(hash: [u8; 32], index: u32) -> TransactionInput {
1019 TransactionInput {
1020 prevout: OutPoint { hash, index },
1021 script_sig: vec![],
1022 sequence: 0xffffffff,
1023 }
1024 }
1025
1026 fn make_output(value: i64) -> TransactionOutput {
1027 TransactionOutput {
1028 value,
1029 script_pubkey: vec![].into(),
1030 }
1031 }
1032
1033 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 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 fn make_n_outputs(n: usize, value: i64) -> Vec<TransactionOutput> {
1056 (0..n).map(|_| make_output(value)).collect()
1057 }
1058
1059 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 fn make_coinbase_tx(value: i64) -> Transaction {
1079 Transaction {
1080 version: 1,
1081 inputs: vec![TransactionInput {
1082 prevout: OutPoint {
1083 hash: [0; 32].into(),
1084 index: 0xffffffff,
1085 },
1086 script_sig: vec![],
1087 sequence: 0xffffffff,
1088 }]
1089 .into(),
1090 outputs: vec![make_output(value)].into(),
1091 lock_time: 0,
1092 }
1093 }
1094
1095 #[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 #[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 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 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 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); }
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 )]))); assert!(!is_coinbase(&bare_tx(vec![make_input([0; 32], 0)]))); assert!(!is_coinbase(&bare_tx(vec![
1330 make_input([0; 32], 0xffffffff),
1331 make_input([1; 32], 0),
1332 ]))); assert!(!is_coinbase(&bare_tx(vec![]))); }
1335
1336 #[test]
1337 fn test_calculate_transaction_size() {
1338 let tx = Transaction {
1344 version: 1,
1345 inputs: vec![
1346 TransactionInput {
1347 prevout: OutPoint {
1348 hash: [0; 32].into(),
1349 index: 0,
1350 },
1351 script_sig: vec![1, 2, 3],
1352 sequence: 0xffffffff,
1353 },
1354 TransactionInput {
1355 prevout: OutPoint {
1356 hash: [1; 32],
1357 index: 1,
1358 },
1359 script_sig: vec![4, 5, 6],
1360 sequence: 0xffffffff,
1361 },
1362 ]
1363 .into(),
1364 outputs: vec![
1365 TransactionOutput {
1366 value: 1000,
1367 script_pubkey: vec![7, 8, 9].into(),
1368 },
1369 TransactionOutput {
1370 value: 2000,
1371 script_pubkey: vec![10, 11, 12],
1372 },
1373 ]
1374 .into(),
1375 lock_time: 12345,
1376 };
1377 assert_eq!(calculate_transaction_size(&tx), 122);
1378 }
1379}