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;
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],
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![],
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],
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],
1349 index: 0,
1350 },
1351 script_sig: vec![1, 2, 3],
1352 sequence: 0xffffffff,
1353 },
1354 TransactionInput {
1355 prevout: OutPoint {
1356 hash: [1; 32],
1357 index: 1,
1358 },
1359 script_sig: vec![4, 5, 6],
1360 sequence: 0xffffffff,
1361 },
1362 ]
1363 .into(),
1364 outputs: vec![
1365 TransactionOutput {
1366 value: 1000,
1367 script_pubkey: vec![7, 8, 9],
1368 },
1369 TransactionOutput {
1370 value: 2000,
1371 script_pubkey: vec![10, 11, 12],
1372 },
1373 ]
1374 .into(),
1375 lock_time: 12345,
1376 };
1377 assert_eq!(calculate_transaction_size(&tx), 122);
1378 }
1379}