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(always)]
29#[cfg(feature = "production")]
30fn check_transaction_fast_path(tx: &Transaction) -> Option<ValidationResult> {
31 if tx.inputs.is_empty() || tx.outputs.is_empty() {
33 return Some(ValidationResult::Invalid("Empty inputs or outputs".into()));
34 }
35
36 if tx.inputs.len() > MAX_INPUTS {
38 return Some(ValidationResult::Invalid(format!(
39 "Too many inputs: {}",
40 tx.inputs.len()
41 )));
42 }
43 if tx.outputs.len() > MAX_OUTPUTS {
44 return Some(ValidationResult::Invalid(format!(
45 "Too many outputs: {}",
46 tx.outputs.len()
47 )));
48 }
49
50 #[cfg(feature = "production")]
54 {
55 use crate::optimizations::precomputed_constants::MAX_MONEY_U64;
56 for output in &tx.outputs {
57 let value_u64 = output.value as u64;
58 if output.value < 0 || value_u64 > MAX_MONEY_U64 {
59 return Some(ValidationResult::Invalid(format!(
60 "Invalid output value: {}",
61 output.value
62 )));
63 }
64 }
65 }
66
67 #[cfg(not(feature = "production"))]
68 for output in &tx.outputs {
69 if output.value < 0 || output.value > MAX_MONEY {
70 return Some(ValidationResult::Invalid(format!(
71 "Invalid output value: {}",
72 output.value
73 )));
74 }
75 }
76
77 #[cfg(feature = "production")]
80 let is_coinbase_hash = {
81 use crate::optimizations::constant_folding::is_zero_hash;
82 is_zero_hash(&tx.inputs[0].prevout.hash)
83 };
84
85 #[cfg(not(feature = "production"))]
86 let is_coinbase_hash = tx.inputs[0].prevout.hash == [0u8; 32];
87
88 if tx.inputs.len() == 1 && is_coinbase_hash && tx.inputs[0].prevout.index == 0xffffffff {
89 let script_sig_len = tx.inputs[0].script_sig.len();
90 if !(2..=100).contains(&script_sig_len) {
91 return Some(ValidationResult::Invalid(format!(
92 "Coinbase scriptSig length {script_sig_len} must be between 2 and 100 bytes"
93 )));
94 }
95 }
96
97 None
99}
100
101#[spec_locked("5.1")]
115#[track_caller] #[cfg_attr(feature = "production", inline(always))]
117#[cfg_attr(not(feature = "production"), inline)]
118pub fn check_transaction(tx: &Transaction) -> Result<ValidationResult> {
119 if tx.inputs.len() > MAX_INPUTS {
123 return Ok(ValidationResult::Invalid(format!(
124 "Input count {} exceeds maximum {}",
125 tx.inputs.len(),
126 MAX_INPUTS
127 )));
128 }
129 if tx.outputs.len() > MAX_OUTPUTS {
130 return Ok(ValidationResult::Invalid(format!(
131 "Output count {} exceeds maximum {}",
132 tx.outputs.len(),
133 MAX_OUTPUTS
134 )));
135 }
136
137 #[cfg(feature = "production")]
139 if let Some(result) = check_transaction_fast_path(tx) {
140 return Ok(result);
141 }
142
143 if tx.inputs.is_empty() {
147 return Ok(ValidationResult::Invalid(
149 "Transaction must have inputs unless it's a coinbase".to_string(),
150 ));
151 }
152 if tx.outputs.is_empty() {
153 return Ok(ValidationResult::Invalid(
154 "Transaction must have at least one output".to_string(),
155 ));
156 }
157
158 let mut total_output_value = 0i64;
162 assert!(
164 total_output_value == 0,
165 "Total output value must start at zero"
166 );
167 #[cfg(feature = "production")]
168 {
169 use crate::optimizations::optimized_access::get_proven_by_;
170 use crate::optimizations::precomputed_constants::MAX_MONEY_U64;
171 for i in 0..tx.outputs.len() {
172 if let Some(output) = get_proven_by_(&tx.outputs, i) {
173 let value_u64 = output.value as u64;
174 if output.value < 0 || value_u64 > MAX_MONEY_U64 {
175 return Ok(ValidationResult::Invalid(format!(
176 "Invalid output value {} at index {}",
177 output.value, i
178 )));
179 }
180 assert!(
183 output.value >= 0,
184 "Output value {} must be non-negative at index {}",
185 output.value,
186 i
187 );
188 total_output_value = total_output_value
189 .checked_add(output.value)
190 .ok_or_else(make_output_sum_overflow_error)?;
191 assert!(
193 total_output_value >= 0,
194 "Total output value {total_output_value} must be non-negative after output {i}"
195 );
196 }
197 }
198 }
199
200 #[cfg(not(feature = "production"))]
201 {
202 for (i, output) in tx.outputs.iter().enumerate() {
203 assert!(i < tx.outputs.len(), "Output index {i} out of bounds");
205 if output.value < 0 || output.value > MAX_MONEY {
209 return Ok(ValidationResult::Invalid(format!(
210 "Invalid output value {} at index {}",
211 output.value, i
212 )));
213 }
214 total_output_value = total_output_value
216 .checked_add(output.value)
217 .ok_or_else(make_output_sum_overflow_error)?;
218 assert!(
220 total_output_value >= 0,
221 "Total output value {total_output_value} must be non-negative after output {i}"
222 );
223 }
224 }
225
226 assert!(
231 total_output_value >= 0,
232 "Total output value {total_output_value} must be non-negative"
233 );
234
235 #[cfg(feature = "production")]
236 {
237 use crate::optimizations::precomputed_constants::MAX_MONEY_U64;
238 let total_u64 = total_output_value as u64;
239 if total_output_value < 0 || total_u64 > MAX_MONEY_U64 {
241 return Ok(ValidationResult::Invalid(format!(
242 "Total output value {total_output_value} is out of valid range [0, {MAX_MONEY}]"
243 )));
244 }
245 assert!(
248 total_u64 <= MAX_MONEY_U64,
249 "Total output value {total_output_value} must not exceed MAX_MONEY"
250 );
251 }
252
253 #[cfg(not(feature = "production"))]
254 {
255 if !(0..=MAX_MONEY).contains(&total_output_value) {
257 return Ok(ValidationResult::Invalid(format!(
258 "Total output value {total_output_value} is out of valid range [0, {MAX_MONEY}]"
259 )));
260 }
261 assert!(
264 total_output_value <= MAX_MONEY,
265 "Total output value {total_output_value} must not exceed MAX_MONEY"
266 );
267 }
268
269 if tx.inputs.len() > MAX_INPUTS {
271 return Ok(ValidationResult::Invalid(format!(
272 "Too many inputs: {}",
273 tx.inputs.len()
274 )));
275 }
276
277 if tx.outputs.len() > MAX_OUTPUTS {
279 return Ok(ValidationResult::Invalid(format!(
280 "Too many outputs: {}",
281 tx.outputs.len()
282 )));
283 }
284
285 use crate::constants::MAX_BLOCK_WEIGHT;
290 const WITNESS_SCALE_FACTOR: usize = 4;
291 let tx_stripped_size = calculate_transaction_size(tx); if tx_stripped_size * WITNESS_SCALE_FACTOR > MAX_BLOCK_WEIGHT {
293 return Ok(ValidationResult::Invalid(format!(
294 "Transaction too large: stripped size {} bytes (weight {} > {})",
295 tx_stripped_size,
296 tx_stripped_size * WITNESS_SCALE_FACTOR,
297 MAX_BLOCK_WEIGHT
298 )));
299 }
300
301 use std::collections::HashSet;
305 let mut seen_prevouts = HashSet::with_capacity(tx.inputs.len());
306 for (i, input) in tx.inputs.iter().enumerate() {
307 assert!(i < tx.inputs.len(), "Input index {i} out of bounds");
309 if !seen_prevouts.insert(&input.prevout) {
310 return Ok(ValidationResult::Invalid(format!(
311 "Duplicate input prevout at index {i}"
312 )));
313 }
314 }
315
316 if is_coinbase(tx) {
319 debug_assert!(
320 !tx.inputs.is_empty(),
321 "Coinbase transaction must have at least one input"
322 );
323 let script_sig_len = tx.inputs[0].script_sig.len();
324 if !(2..=100).contains(&script_sig_len) {
325 return Ok(ValidationResult::Invalid(format!(
326 "Coinbase scriptSig length {script_sig_len} must be between 2 and 100 bytes"
327 )));
328 }
329 }
330
331 Ok(ValidationResult::Valid)
336}
337
338#[spec_locked("5.1")]
348#[cfg_attr(feature = "production", inline(always))]
349#[cfg_attr(not(feature = "production"), inline)]
350#[allow(clippy::overly_complex_bool_expr)] pub fn check_tx_inputs<U: UtxoLookup>(
352 tx: &Transaction,
353 utxo_set: &U,
354 height: Natural,
355) -> Result<(ValidationResult, Integer)> {
356 check_tx_inputs_with_utxos(tx, utxo_set, height, None)
357}
358
359pub fn check_tx_inputs_with_utxos<U: UtxoLookup>(
361 tx: &Transaction,
362 utxo_set: &U,
363 height: Natural,
364 pre_collected_utxos: Option<&[Option<&UTXO>]>,
365) -> Result<(ValidationResult, Integer)> {
366 if tx.inputs.is_empty() && !is_coinbase(tx) {
370 return Ok((
371 ValidationResult::Invalid(
372 "Transaction must have inputs unless it's a coinbase".to_string(),
373 ),
374 0,
375 ));
376 }
377 assert!(
378 height <= i64::MAX as u64,
379 "Block height {height} must fit in i64"
380 );
381 assert!(
382 utxo_set.len() <= u32::MAX as usize,
383 "UTXO set size {} exceeds maximum",
384 utxo_set.len()
385 );
386
387 if is_coinbase(tx) {
389 #[allow(clippy::eq_op)]
391 {
392 }
394 return Ok((ValidationResult::Valid, 0));
395 }
396
397 #[cfg(feature = "production")]
401 {
402 use crate::optimizations::constant_folding::is_zero_hash;
403 use crate::optimizations::optimized_access::get_proven_by_;
404 for i in 0..tx.inputs.len() {
405 if let Some(input) = get_proven_by_(&tx.inputs, i) {
406 if is_zero_hash(&input.prevout.hash) && input.prevout.index == 0xffffffff {
407 return Ok((
408 ValidationResult::Invalid(format!(
409 "Non-coinbase input {i} has null prevout"
410 )),
411 0,
412 ));
413 }
414 }
415 }
416 }
417
418 #[cfg(not(feature = "production"))]
419 {
420 for (i, input) in tx.inputs.iter().enumerate() {
421 if input.prevout.hash == [0u8; 32] && input.prevout.index == 0xffffffff {
422 return Ok((
423 ValidationResult::Invalid(format!("Non-coinbase input {i} has null prevout")),
424 0,
425 ));
426 }
427 }
428 }
429
430 #[cfg(feature = "production")]
434 {
435 use crate::optimizations::prefetch;
436 for i in 0..tx.inputs.len().min(8) {
438 if i + 4 < tx.inputs.len() {
439 prefetch::prefetch_ahead(&tx.inputs, i, 4);
440 }
441 }
442 }
443
444 let input_utxos: Vec<(usize, Option<&UTXO>)> = if let Some(pre_utxos) = pre_collected_utxos {
446 pre_utxos
448 .iter()
449 .enumerate()
450 .map(|(i, opt_utxo)| (i, *opt_utxo))
451 .collect()
452 } else {
453 let mut result = Vec::with_capacity(tx.inputs.len());
455 for (i, input) in tx.inputs.iter().enumerate() {
456 result.push((i, utxo_set.get(&input.prevout)));
457 }
458 result
459 };
460
461 let mut total_input_value = 0i64;
462 assert!(
464 total_input_value == 0,
465 "Total input value must start at zero"
466 );
467
468 for (i, opt_utxo) in input_utxos {
469 assert!(i < tx.inputs.len(), "Input index {i} out of bounds");
471
472 if let Some(utxo) = opt_utxo {
474 assert!(
476 utxo.value >= 0,
477 "UTXO value {} must be non-negative at input {}",
478 utxo.value,
479 i
480 );
481 assert!(
482 utxo.value <= MAX_MONEY,
483 "UTXO value {} must not exceed MAX_MONEY at input {}",
484 utxo.value,
485 i
486 );
487
488 if utxo.is_coinbase {
492 use crate::constants::COINBASE_MATURITY;
493 let required_height = utxo.height.saturating_add(COINBASE_MATURITY);
494 assert!(
496 height >= utxo.height,
497 "Current height {} must be >= UTXO creation height {}",
498 height,
499 utxo.height
500 );
501 if height < required_height {
502 return Ok((
503 ValidationResult::Invalid(format!(
504 "Premature spend of coinbase output: input {i} created at height {} cannot be spent until height {} (current: {})",
505 utxo.height, required_height, height
506 )),
507 0,
508 ));
509 }
510 }
511
512 assert!(
515 utxo.value >= 0,
516 "UTXO value {} must be non-negative before addition",
517 utxo.value
518 );
519 total_input_value = total_input_value.checked_add(utxo.value).ok_or_else(|| {
520 ConsensusError::TransactionValidation(
521 format!("Input value overflow at input {i}").into(),
522 )
523 })?;
524 assert!(
526 total_input_value >= 0,
527 "Total input value {total_input_value} must be non-negative after input {i}"
528 );
529 } else {
530 #[cfg(debug_assertions)]
531 {
532 let hash_str: String = tx.inputs[i]
533 .prevout
534 .hash
535 .iter()
536 .map(|b| format!("{b:02x}"))
537 .collect();
538 eprintln!(
539 " ā UTXO NOT FOUND: Input {} prevout {}:{}",
540 i, hash_str, tx.inputs[i].prevout.index
541 );
542 eprintln!(" UTXO set size: {}", utxo_set.len());
543 }
544 return Ok((
545 ValidationResult::Invalid(format!("Input {i} not found in UTXO set")),
546 0,
547 ));
548 }
549 }
550
551 let total_output_value: i64 = tx
553 .outputs
554 .iter()
555 .try_fold(0i64, |acc, output| {
556 assert!(
558 output.value >= 0,
559 "Output value {} must be non-negative",
560 output.value
561 );
562 acc.checked_add(output.value).ok_or_else(|| {
563 ConsensusError::TransactionValidation("Output value overflow".into())
564 })
565 })
566 .map_err(|e| ConsensusError::TransactionValidation(Cow::Owned(e.to_string())))?;
567
568 assert!(
570 total_output_value >= 0,
571 "Total output value {total_output_value} must be non-negative"
572 );
573 assert!(
575 total_output_value <= MAX_MONEY,
576 "Total output value {total_output_value} must not exceed MAX_MONEY"
577 );
578 if total_output_value > MAX_MONEY {
579 return Ok((
580 ValidationResult::Invalid(format!(
581 "Total output value {total_output_value} exceeds maximum money supply"
582 )),
583 0,
584 ));
585 }
586
587 if total_input_value < total_output_value {
589 return Ok((
590 ValidationResult::Invalid("Insufficient input value".to_string()),
591 0,
592 ));
593 }
594
595 let fee = total_input_value
597 .checked_sub(total_output_value)
598 .ok_or_else(make_fee_calculation_underflow_error)?;
599
600 assert!(fee >= 0, "Fee {fee} must be non-negative");
602 assert!(
603 fee <= total_input_value,
604 "Fee {fee} cannot exceed total input {total_input_value}"
605 );
606 assert!(
607 total_input_value == total_output_value + fee,
608 "Conservation of value: input {total_input_value} must equal output {total_output_value} + fee {fee}"
609 );
610
611 Ok((ValidationResult::Valid, fee))
612}
613
614pub fn check_tx_inputs_with_owned_data(
617 tx: &Transaction,
618 height: Natural,
619 utxo_data: &[Option<(i64, bool, u64)>],
620) -> Result<(ValidationResult, Integer)> {
621 if tx.inputs.is_empty() && !is_coinbase(tx) {
622 return Ok((
623 ValidationResult::Invalid(
624 "Transaction must have inputs unless it's a coinbase".to_string(),
625 ),
626 0,
627 ));
628 }
629 if is_coinbase(tx) {
630 return Ok((ValidationResult::Valid, 0));
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: i64 = tx
674 .outputs
675 .iter()
676 .try_fold(0i64, |acc, output| {
677 acc.checked_add(output.value).ok_or_else(|| {
678 ConsensusError::TransactionValidation("Output value overflow".into())
679 })
680 })
681 .map_err(|e| ConsensusError::TransactionValidation(Cow::Owned(e.to_string())))?;
682 if total_output_value > MAX_MONEY {
683 return Ok((
684 ValidationResult::Invalid(format!(
685 "Total output value {total_output_value} exceeds maximum"
686 )),
687 0,
688 ));
689 }
690 if total_input_value < total_output_value {
691 return Ok((
692 ValidationResult::Invalid("Insufficient input value".to_string()),
693 0,
694 ));
695 }
696 let fee = total_input_value
697 .checked_sub(total_output_value)
698 .ok_or_else(make_fee_calculation_underflow_error)?;
699 Ok((ValidationResult::Valid, fee))
700}
701
702#[inline(always)]
707#[spec_locked("6.4")]
708pub fn is_coinbase(tx: &Transaction) -> bool {
709 #[cfg(feature = "production")]
711 {
712 use crate::optimizations::constant_folding::is_zero_hash;
713 tx.inputs.len() == 1
714 && is_zero_hash(&tx.inputs[0].prevout.hash)
715 && tx.inputs[0].prevout.index == 0xffffffff
716 }
717
718 #[cfg(not(feature = "production"))]
719 {
720 tx.inputs.len() == 1
721 && tx.inputs[0].prevout.hash == [0u8; 32]
722 && tx.inputs[0].prevout.index == 0xffffffff
723 }
724}
725
726#[inline(always)]
731#[spec_locked("5.1")]
738pub fn calculate_transaction_size(tx: &Transaction) -> usize {
739 use crate::serialization::transaction::serialize_transaction;
742 serialize_transaction(tx).len()
743}
744
745#[cfg(test)]
771pub(crate) mod transaction_proptest {
772 use super::*;
773 use proptest::prelude::*;
774
775 pub fn arb_transaction() -> BoxedStrategy<Transaction> {
776 (
777 any::<u64>(),
778 prop::collection::vec(
779 (
780 any::<[u8; 32]>(),
781 any::<u64>(),
782 prop::collection::vec(any::<u8>(), 0..100),
783 any::<u64>(),
784 ),
785 0..10,
786 ),
787 prop::collection::vec(
788 (any::<i64>(), prop::collection::vec(any::<u8>(), 0..100)),
789 0..10,
790 ),
791 any::<u64>(),
792 )
793 .prop_map(|(version, inputs, outputs, lock_time)| {
794 let inputs: Vec<TransactionInput> = inputs
795 .into_iter()
796 .map(|(hash, index, script_sig, sequence)| TransactionInput {
797 prevout: OutPoint {
798 hash,
799 index: index as u32,
800 },
801 script_sig,
802 sequence,
803 })
804 .collect();
805 let outputs: Vec<TransactionOutput> = outputs
806 .into_iter()
807 .map(|(value, script_pubkey)| TransactionOutput {
808 value,
809 script_pubkey,
810 })
811 .collect();
812 Transaction {
813 version,
814 #[cfg(feature = "production")]
815 inputs: inputs.into(),
816 #[cfg(not(feature = "production"))]
817 inputs,
818 #[cfg(feature = "production")]
819 outputs: outputs.into(),
820 #[cfg(not(feature = "production"))]
821 outputs,
822 lock_time,
823 }
824 })
825 .boxed()
826 }
827
828 pub fn arb_outpoint() -> impl Strategy<Value = OutPoint> {
829 (any::<[u8; 32]>(), any::<u32>()).prop_map(|(hash, index)| OutPoint { hash, index })
830 }
831
832 pub fn arb_utxo() -> impl Strategy<Value = UTXO> {
833 (
834 any::<i64>(),
835 prop::collection::vec(any::<u8>(), 0..40),
836 any::<u64>(),
837 any::<bool>(),
838 )
839 .prop_map(|(value, script_pubkey, height, is_coinbase)| UTXO {
840 value,
841 script_pubkey: script_pubkey.into(),
842 height,
843 is_coinbase,
844 })
845 }
846}
847
848#[cfg(test)]
849#[allow(unused_doc_comments)]
850mod property_tests {
851 use super::transaction_proptest::{arb_outpoint, arb_transaction, arb_utxo};
852 use super::*;
853 use proptest::prelude::*;
854
855 proptest! {
857 #[test]
858 fn prop_check_transaction_structure(
859 tx in arb_transaction()
860 ) {
861 let mut bounded_tx = tx;
863 if bounded_tx.inputs.len() > 10 {
864 bounded_tx.inputs.truncate(10);
865 }
866 if bounded_tx.outputs.len() > 10 {
867 bounded_tx.outputs.truncate(10);
868 }
869
870 let result = check_transaction(&bounded_tx).unwrap_or_else(|_| ValidationResult::Invalid("Error".to_string()));
871
872 match result {
874 ValidationResult::Valid => {
875 prop_assert!(!bounded_tx.inputs.is_empty(), "Valid transaction must have inputs");
877 prop_assert!(!bounded_tx.outputs.is_empty(), "Valid transaction must have outputs");
878
879 prop_assert!(bounded_tx.inputs.len() <= MAX_INPUTS, "Valid transaction must respect input limit");
881 prop_assert!(bounded_tx.outputs.len() <= MAX_OUTPUTS, "Valid transaction must respect output limit");
882
883 for output in &bounded_tx.outputs {
885 prop_assert!(output.value >= 0, "Valid transaction outputs must be non-negative");
886 prop_assert!(output.value <= MAX_MONEY, "Valid transaction outputs must not exceed max money");
887 }
888 },
889 ValidationResult::Invalid(_) => {
890 }
893 }
894 }
895 }
896
897 proptest! {
899 #[test]
900 fn prop_check_tx_inputs_coinbase(
901 tx in arb_transaction(),
902 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>()),
903 height in 0u64..1000u64
904 ) {
905 let mut bounded_tx = tx;
907 if bounded_tx.inputs.len() > 5 {
908 bounded_tx.inputs.truncate(5);
909 }
910 if bounded_tx.outputs.len() > 5 {
911 bounded_tx.outputs.truncate(5);
912 }
913
914 let result = check_tx_inputs(&bounded_tx, &utxo_set, height).unwrap_or((ValidationResult::Invalid("Error".to_string()), 0));
915
916 if is_coinbase(&bounded_tx) {
918 prop_assert!(matches!(result.0, ValidationResult::Valid), "Coinbase transactions must be valid");
919 prop_assert_eq!(result.1, 0, "Coinbase transactions must have zero fee");
920 }
921 }
922 }
923
924 proptest! {
926 #[test]
927 fn prop_is_coinbase_correct(
928 tx in arb_transaction()
929 ) {
930 let is_cb = is_coinbase(&tx);
931
932 if is_cb {
934 prop_assert_eq!(tx.inputs.len(), 1, "Coinbase must have exactly one input");
935 prop_assert_eq!(tx.inputs[0].prevout.hash, [0u8; 32], "Coinbase input must have zero hash");
936 prop_assert_eq!(tx.inputs[0].prevout.index, 0xffffffffu32, "Coinbase input must have max index");
937 }
938 }
939 }
940
941 proptest! {
943 #[test]
944 fn prop_calculate_transaction_size_consistent(
945 tx in arb_transaction()
946 ) {
947 let mut bounded_tx = tx;
949 if bounded_tx.inputs.len() > 10 {
950 bounded_tx.inputs.truncate(10);
951 }
952 if bounded_tx.outputs.len() > 10 {
953 bounded_tx.outputs.truncate(10);
954 }
955
956 let size = calculate_transaction_size(&bounded_tx);
957
958 prop_assert!(size >= 10, "Transaction size must be at least 10 bytes (version + varints + lock_time)");
962
963 prop_assert!(size <= MAX_TX_SIZE, "Transaction size must not exceed MAX_TX_SIZE ({})", MAX_TX_SIZE);
967
968 let size2 = calculate_transaction_size(&bounded_tx);
970 prop_assert_eq!(size, size2, "Transaction size calculation must be deterministic");
971 }
972 }
973
974 proptest! {
976 #[test]
977 fn prop_output_value_bounds(
978 value in 0i64..(MAX_MONEY + 1000)
979 ) {
980 let tx = Transaction {
981 version: 1,
982 inputs: vec![TransactionInput {
983 prevout: OutPoint { hash: [0; 32].into(), index: 0 },
984 script_sig: vec![],
985 sequence: 0xffffffff,
986 }].into(),
987 outputs: vec![TransactionOutput {
988 value,
989 script_pubkey: vec![],
990 }].into(),
991 lock_time: 0,
992 };
993
994 let result = check_transaction(&tx).unwrap_or(ValidationResult::Invalid("Error".to_string()));
995
996 if !(0..=MAX_MONEY).contains(&value) {
998 prop_assert!(matches!(result, ValidationResult::Invalid(_)),
999 "Transactions with invalid output values must be invalid");
1000 } else {
1001 if !tx.inputs.is_empty() && !tx.outputs.is_empty() {
1003 prop_assert!(matches!(result, ValidationResult::Valid),
1004 "Transactions with valid output values should be valid");
1005 }
1006 }
1007 }
1008 }
1009}
1010
1011#[cfg(test)]
1012mod tests {
1013 use super::*;
1014
1015 #[test]
1016 fn test_check_transaction_valid() {
1017 let tx = Transaction {
1018 version: 1,
1019 inputs: vec![TransactionInput {
1020 prevout: OutPoint {
1021 hash: [0; 32].into(),
1022 index: 0,
1023 },
1024 script_sig: vec![],
1025 sequence: 0xffffffff,
1026 }]
1027 .into(),
1028 outputs: vec![TransactionOutput {
1029 value: 1000,
1030 script_pubkey: vec![].into(),
1031 }]
1032 .into(),
1033 lock_time: 0,
1034 };
1035
1036 assert_eq!(check_transaction(&tx).unwrap(), ValidationResult::Valid);
1037 }
1038
1039 #[test]
1040 fn test_check_transaction_empty_inputs() {
1041 let tx = Transaction {
1042 version: 1,
1043 inputs: vec![].into(),
1044 outputs: vec![TransactionOutput {
1045 value: 1000,
1046 script_pubkey: vec![].into(),
1047 }]
1048 .into(),
1049 lock_time: 0,
1050 };
1051
1052 assert!(matches!(
1053 check_transaction(&tx).unwrap(),
1054 ValidationResult::Invalid(_)
1055 ));
1056 }
1057
1058 #[test]
1059 fn test_check_tx_inputs_coinbase() {
1060 let tx = Transaction {
1061 version: 1,
1062 inputs: vec![TransactionInput {
1063 prevout: OutPoint {
1064 hash: [0; 32].into(),
1065 index: 0xffffffff,
1066 },
1067 script_sig: vec![],
1068 sequence: 0xffffffff,
1069 }]
1070 .into(),
1071 outputs: vec![TransactionOutput {
1072 value: 5000000000, script_pubkey: vec![].into(),
1074 }]
1075 .into(),
1076 lock_time: 0,
1077 };
1078
1079 let utxo_set = UtxoSet::default();
1080 let (result, fee) = check_tx_inputs(&tx, &utxo_set, 0).unwrap();
1081
1082 assert_eq!(result, ValidationResult::Valid);
1083 assert_eq!(fee, 0);
1084 }
1085
1086 #[test]
1091 fn test_check_transaction_empty_outputs() {
1092 let tx = Transaction {
1093 version: 1,
1094 inputs: vec![TransactionInput {
1095 prevout: OutPoint {
1096 hash: [0; 32].into(),
1097 index: 0,
1098 },
1099 script_sig: vec![],
1100 sequence: 0xffffffff,
1101 }]
1102 .into(),
1103 outputs: vec![].into(),
1104 lock_time: 0,
1105 };
1106
1107 assert!(matches!(
1108 check_transaction(&tx).unwrap(),
1109 ValidationResult::Invalid(_)
1110 ));
1111 }
1112
1113 #[test]
1114 fn test_check_transaction_invalid_output_value_negative() {
1115 let tx = Transaction {
1116 version: 1,
1117 inputs: vec![TransactionInput {
1118 prevout: OutPoint {
1119 hash: [0; 32].into(),
1120 index: 0,
1121 },
1122 script_sig: vec![],
1123 sequence: 0xffffffff,
1124 }]
1125 .into(),
1126 outputs: vec![TransactionOutput {
1127 value: -1, script_pubkey: vec![].into(),
1129 }]
1130 .into(),
1131 lock_time: 0,
1132 };
1133
1134 assert!(matches!(
1135 check_transaction(&tx).unwrap(),
1136 ValidationResult::Invalid(_)
1137 ));
1138 }
1139
1140 #[test]
1141 fn test_check_transaction_invalid_output_value_too_large() {
1142 let tx = Transaction {
1143 version: 1,
1144 inputs: vec![TransactionInput {
1145 prevout: OutPoint {
1146 hash: [0; 32].into(),
1147 index: 0,
1148 },
1149 script_sig: vec![],
1150 sequence: 0xffffffff,
1151 }]
1152 .into(),
1153 outputs: vec![TransactionOutput {
1154 value: MAX_MONEY + 1, script_pubkey: vec![].into(),
1156 }]
1157 .into(),
1158 lock_time: 0,
1159 };
1160
1161 assert!(matches!(
1162 check_transaction(&tx).unwrap(),
1163 ValidationResult::Invalid(_)
1164 ));
1165 }
1166
1167 #[test]
1168 fn test_check_transaction_max_output_value() {
1169 let tx = Transaction {
1170 version: 1,
1171 inputs: vec![TransactionInput {
1172 prevout: OutPoint {
1173 hash: [0; 32].into(),
1174 index: 0,
1175 },
1176 script_sig: vec![],
1177 sequence: 0xffffffff,
1178 }]
1179 .into(),
1180 outputs: vec![TransactionOutput {
1181 value: MAX_MONEY, script_pubkey: vec![].into(),
1183 }]
1184 .into(),
1185 lock_time: 0,
1186 };
1187
1188 assert_eq!(check_transaction(&tx).unwrap(), ValidationResult::Valid);
1189 }
1190
1191 #[test]
1192 fn test_check_transaction_too_many_inputs() {
1193 let mut inputs = Vec::new();
1194 for i in 0..=MAX_INPUTS {
1195 inputs.push(TransactionInput {
1196 prevout: OutPoint {
1197 hash: [i as u8; 32],
1198 index: 0,
1199 },
1200 script_sig: vec![],
1201 sequence: 0xffffffff,
1202 });
1203 }
1204
1205 let tx = Transaction {
1206 version: 1,
1207 inputs: inputs.into(),
1208 outputs: vec![TransactionOutput {
1209 value: 1000,
1210 script_pubkey: vec![].into(),
1211 }]
1212 .into(),
1213 lock_time: 0,
1214 };
1215
1216 assert!(matches!(
1217 check_transaction(&tx).unwrap(),
1218 ValidationResult::Invalid(_)
1219 ));
1220 }
1221
1222 #[test]
1223 fn test_check_transaction_max_inputs() {
1224 let num_inputs = 20_000;
1228 let mut inputs = Vec::new();
1229 for i in 0..num_inputs {
1230 let mut hash = [0u8; 32];
1231 hash[0] = (i & 0xff) as u8;
1233 hash[1] = ((i >> 8) & 0xff) as u8;
1234 hash[2] = ((i >> 16) & 0xff) as u8;
1235 hash[3] = ((i >> 24) & 0xff) as u8;
1236 inputs.push(TransactionInput {
1237 prevout: OutPoint {
1238 hash,
1239 index: i as u32,
1240 },
1241 script_sig: vec![],
1242 sequence: 0xffffffff,
1243 });
1244 }
1245
1246 let tx = Transaction {
1247 version: 1,
1248 inputs: inputs.into(),
1249 outputs: vec![TransactionOutput {
1250 value: 1000,
1251 script_pubkey: vec![].into(),
1252 }]
1253 .into(),
1254 lock_time: 0,
1255 };
1256
1257 assert_eq!(check_transaction(&tx).unwrap(), ValidationResult::Valid);
1258 }
1259
1260 #[test]
1261 fn test_check_transaction_too_many_outputs() {
1262 let mut outputs = Vec::new();
1263 for _ in 0..=MAX_OUTPUTS {
1264 outputs.push(TransactionOutput {
1265 value: 1000,
1266 script_pubkey: vec![].into(),
1267 });
1268 }
1269
1270 let tx = Transaction {
1271 version: 1,
1272 inputs: vec![TransactionInput {
1273 prevout: OutPoint {
1274 hash: [0; 32].into(),
1275 index: 0,
1276 },
1277 script_sig: vec![],
1278 sequence: 0xffffffff,
1279 }]
1280 .into(),
1281 outputs: outputs.into(),
1282 lock_time: 0,
1283 };
1284
1285 assert!(matches!(
1286 check_transaction(&tx).unwrap(),
1287 ValidationResult::Invalid(_)
1288 ));
1289 }
1290
1291 #[test]
1292 fn test_check_transaction_max_outputs() {
1293 let mut outputs = Vec::new();
1294 for _ in 0..MAX_OUTPUTS {
1295 outputs.push(TransactionOutput {
1296 value: 1000,
1297 script_pubkey: vec![].into(),
1298 });
1299 }
1300
1301 let tx = Transaction {
1302 version: 1,
1303 inputs: vec![TransactionInput {
1304 prevout: OutPoint {
1305 hash: [0; 32].into(),
1306 index: 0,
1307 },
1308 script_sig: vec![],
1309 sequence: 0xffffffff,
1310 }]
1311 .into(),
1312 outputs: outputs.into(),
1313 lock_time: 0,
1314 };
1315
1316 assert_eq!(check_transaction(&tx).unwrap(), ValidationResult::Valid);
1317 }
1318
1319 #[test]
1320 fn test_check_transaction_too_large() {
1321 use crate::constants::MAX_INPUTS;
1326 let mut inputs = Vec::new();
1327 for i in 0..MAX_INPUTS {
1331 inputs.push(TransactionInput {
1332 prevout: OutPoint {
1333 hash: [i as u8; 32],
1334 index: 0,
1335 },
1336 script_sig: vec![0u8; 1000], sequence: 0xffffffff,
1338 });
1339 }
1340
1341 let tx = Transaction {
1342 version: 1,
1343 inputs: inputs.into(),
1344 outputs: vec![TransactionOutput {
1345 value: 1000,
1346 script_pubkey: vec![].into(),
1347 }]
1348 .into(),
1349 lock_time: 0,
1350 };
1351
1352 assert!(matches!(
1353 check_transaction(&tx).unwrap(),
1354 ValidationResult::Invalid(_)
1355 ));
1356 }
1357
1358 #[test]
1359 fn test_check_tx_inputs_regular_transaction() {
1360 let mut utxo_set = UtxoSet::default();
1361
1362 let outpoint = OutPoint {
1364 hash: [1; 32],
1365 index: 0,
1366 };
1367 let utxo = UTXO {
1368 value: 1000000000, script_pubkey: vec![].into(),
1370 height: 0,
1371 is_coinbase: false,
1372 };
1373 utxo_set.insert(outpoint, std::sync::Arc::new(utxo));
1374
1375 let tx = Transaction {
1376 version: 1,
1377 inputs: vec![TransactionInput {
1378 prevout: OutPoint {
1379 hash: [1; 32].into(),
1380 index: 0,
1381 },
1382 script_sig: vec![],
1383 sequence: 0xffffffff,
1384 }]
1385 .into(),
1386 outputs: vec![TransactionOutput {
1387 value: 900000000, script_pubkey: vec![],
1389 }]
1390 .into(),
1391 lock_time: 0,
1392 };
1393
1394 let (result, fee) = check_tx_inputs(&tx, &utxo_set, 0).unwrap();
1395
1396 assert_eq!(result, ValidationResult::Valid);
1397 assert_eq!(fee, 100000000); }
1399
1400 #[test]
1401 fn test_check_tx_inputs_missing_utxo() {
1402 let utxo_set = UtxoSet::default(); let tx = Transaction {
1405 version: 1,
1406 inputs: vec![TransactionInput {
1407 prevout: OutPoint {
1408 hash: [1; 32].into(),
1409 index: 0,
1410 },
1411 script_sig: vec![],
1412 sequence: 0xffffffff,
1413 }]
1414 .into(),
1415 outputs: vec![TransactionOutput {
1416 value: 100000000,
1417 script_pubkey: vec![],
1418 }]
1419 .into(),
1420 lock_time: 0,
1421 };
1422
1423 let (result, fee) = check_tx_inputs(&tx, &utxo_set, 0).unwrap();
1424
1425 assert!(matches!(result, ValidationResult::Invalid(_)));
1426 assert_eq!(fee, 0);
1427 }
1428
1429 #[test]
1430 fn test_check_tx_inputs_insufficient_funds() {
1431 let mut utxo_set = UtxoSet::default();
1432
1433 let outpoint = OutPoint {
1435 hash: [1; 32],
1436 index: 0,
1437 };
1438 let utxo = UTXO {
1439 value: 100000000, script_pubkey: vec![].into(),
1441 height: 0,
1442 is_coinbase: false,
1443 };
1444 utxo_set.insert(outpoint, std::sync::Arc::new(utxo));
1445
1446 let tx = Transaction {
1447 version: 1,
1448 inputs: vec![TransactionInput {
1449 prevout: OutPoint {
1450 hash: [1; 32].into(),
1451 index: 0,
1452 },
1453 script_sig: vec![],
1454 sequence: 0xffffffff,
1455 }]
1456 .into(),
1457 outputs: vec![TransactionOutput {
1458 value: 200000000, script_pubkey: vec![],
1460 }]
1461 .into(),
1462 lock_time: 0,
1463 };
1464
1465 let (result, fee) = check_tx_inputs(&tx, &utxo_set, 0).unwrap();
1466
1467 assert!(matches!(result, ValidationResult::Invalid(_)));
1468 assert_eq!(fee, 0);
1469 }
1470
1471 #[test]
1472 fn test_check_tx_inputs_multiple_inputs() {
1473 let mut utxo_set = UtxoSet::default();
1474
1475 let outpoint1 = OutPoint {
1477 hash: [1; 32],
1478 index: 0,
1479 };
1480 let utxo1 = UTXO {
1481 value: 500000000, script_pubkey: vec![].into(),
1483 height: 0,
1484 is_coinbase: false,
1485 };
1486 utxo_set.insert(outpoint1, std::sync::Arc::new(utxo1));
1487
1488 let outpoint2 = OutPoint {
1489 hash: [2; 32],
1490 index: 0,
1491 };
1492 let utxo2 = UTXO {
1493 value: 300000000, script_pubkey: vec![].into(),
1495 height: 0,
1496 is_coinbase: false,
1497 };
1498 utxo_set.insert(outpoint2, std::sync::Arc::new(utxo2));
1499
1500 let tx = Transaction {
1501 version: 1,
1502 inputs: vec![
1503 TransactionInput {
1504 prevout: OutPoint {
1505 hash: [1; 32].into(),
1506 index: 0,
1507 },
1508 script_sig: vec![],
1509 sequence: 0xffffffff,
1510 },
1511 TransactionInput {
1512 prevout: OutPoint {
1513 hash: [2; 32],
1514 index: 0,
1515 },
1516 script_sig: vec![],
1517 sequence: 0xffffffff,
1518 },
1519 ]
1520 .into(),
1521 outputs: vec![TransactionOutput {
1522 value: 700000000, script_pubkey: vec![].into(),
1524 }]
1525 .into(),
1526 lock_time: 0,
1527 };
1528
1529 let (result, fee) = check_tx_inputs(&tx, &utxo_set, 0).unwrap();
1530
1531 assert_eq!(result, ValidationResult::Valid);
1532 assert_eq!(fee, 100000000); }
1534
1535 #[test]
1536 fn test_is_coinbase_edge_cases() {
1537 let valid_coinbase = Transaction {
1539 version: 1,
1540 inputs: vec![TransactionInput {
1541 prevout: OutPoint {
1542 hash: [0; 32].into(),
1543 index: 0xffffffff,
1544 },
1545 script_sig: vec![],
1546 sequence: 0xffffffff,
1547 }]
1548 .into(),
1549 outputs: vec![].into(),
1550 lock_time: 0,
1551 };
1552 assert!(is_coinbase(&valid_coinbase));
1553
1554 let wrong_hash = Transaction {
1556 version: 1,
1557 inputs: vec![TransactionInput {
1558 prevout: OutPoint {
1559 hash: [1; 32].into(),
1560 index: 0xffffffff,
1561 },
1562 script_sig: vec![],
1563 sequence: 0xffffffff,
1564 }]
1565 .into(),
1566 outputs: vec![].into(),
1567 lock_time: 0,
1568 };
1569 assert!(!is_coinbase(&wrong_hash));
1570
1571 let wrong_index = Transaction {
1573 version: 1,
1574 inputs: vec![TransactionInput {
1575 prevout: OutPoint {
1576 hash: [0; 32].into(),
1577 index: 0,
1578 },
1579 script_sig: vec![],
1580 sequence: 0xffffffff,
1581 }]
1582 .into(),
1583 outputs: vec![].into(),
1584 lock_time: 0,
1585 };
1586 assert!(!is_coinbase(&wrong_index));
1587
1588 let multiple_inputs = Transaction {
1590 version: 1,
1591 inputs: vec![
1592 TransactionInput {
1593 prevout: OutPoint {
1594 hash: [0; 32].into(),
1595 index: 0xffffffff,
1596 },
1597 script_sig: vec![],
1598 sequence: 0xffffffff,
1599 },
1600 TransactionInput {
1601 prevout: OutPoint {
1602 hash: [1; 32],
1603 index: 0,
1604 },
1605 script_sig: vec![],
1606 sequence: 0xffffffff,
1607 },
1608 ]
1609 .into(),
1610 outputs: vec![].into(),
1611 lock_time: 0,
1612 };
1613 assert!(!is_coinbase(&multiple_inputs));
1614
1615 let no_inputs = Transaction {
1617 version: 1,
1618 inputs: vec![].into(),
1619 outputs: vec![].into(),
1620 lock_time: 0,
1621 };
1622 assert!(!is_coinbase(&no_inputs));
1623 }
1624
1625 #[test]
1626 fn test_calculate_transaction_size() {
1627 let tx = Transaction {
1628 version: 1,
1629 inputs: vec![
1630 TransactionInput {
1631 prevout: OutPoint {
1632 hash: [0; 32].into(),
1633 index: 0,
1634 },
1635 script_sig: vec![1, 2, 3],
1636 sequence: 0xffffffff,
1637 },
1638 TransactionInput {
1639 prevout: OutPoint {
1640 hash: [1; 32],
1641 index: 1,
1642 },
1643 script_sig: vec![4, 5, 6],
1644 sequence: 0xffffffff,
1645 },
1646 ]
1647 .into(),
1648 outputs: vec![
1649 TransactionOutput {
1650 value: 1000,
1651 script_pubkey: vec![7, 8, 9].into(),
1652 },
1653 TransactionOutput {
1654 value: 2000,
1655 script_pubkey: vec![10, 11, 12],
1656 },
1657 ]
1658 .into(),
1659 lock_time: 12345,
1660 };
1661
1662 let size = calculate_transaction_size(&tx);
1663 assert_eq!(size, 122);
1671 }
1672}