use crate::constants::*;
use crate::error::{ConsensusError, Result};
use crate::types::*;
use crate::utxo_overlay::UtxoLookup;
use blvm_spec_lock::spec_locked;
use std::borrow::Cow;
#[cold]
fn make_output_sum_overflow_error() -> ConsensusError {
ConsensusError::TransactionValidation("Output value sum overflow".into())
}
#[cold]
fn make_fee_calculation_underflow_error() -> ConsensusError {
ConsensusError::TransactionValidation("Fee calculation underflow".into())
}
#[inline(always)]
#[cfg(feature = "production")]
fn check_transaction_fast_path(tx: &Transaction) -> Option<ValidationResult> {
if tx.inputs.is_empty() || tx.outputs.is_empty() {
return Some(ValidationResult::Invalid("Empty inputs or outputs".into()));
}
if tx.inputs.len() > MAX_INPUTS {
return Some(ValidationResult::Invalid(format!(
"Too many inputs: {}",
tx.inputs.len()
)));
}
if tx.outputs.len() > MAX_OUTPUTS {
return Some(ValidationResult::Invalid(format!(
"Too many outputs: {}",
tx.outputs.len()
)));
}
#[cfg(feature = "production")]
{
use crate::optimizations::precomputed_constants::MAX_MONEY_U64;
for output in &tx.outputs {
let value_u64 = output.value as u64;
if output.value < 0 || value_u64 > MAX_MONEY_U64 {
return Some(ValidationResult::Invalid(format!(
"Invalid output value: {}",
output.value
)));
}
}
}
#[cfg(not(feature = "production"))]
for output in &tx.outputs {
if output.value < 0 || output.value > MAX_MONEY {
return Some(ValidationResult::Invalid(format!(
"Invalid output value: {}",
output.value
)));
}
}
#[cfg(feature = "production")]
let is_coinbase_hash = {
use crate::optimizations::constant_folding::is_zero_hash;
is_zero_hash(&tx.inputs[0].prevout.hash)
};
#[cfg(not(feature = "production"))]
let is_coinbase_hash = tx.inputs[0].prevout.hash == [0u8; 32];
if tx.inputs.len() == 1 && is_coinbase_hash && tx.inputs[0].prevout.index == 0xffffffff {
let script_sig_len = tx.inputs[0].script_sig.len();
if !(2..=100).contains(&script_sig_len) {
return Some(ValidationResult::Invalid(format!(
"Coinbase scriptSig length {script_sig_len} must be between 2 and 100 bytes"
)));
}
}
None
}
#[spec_locked("5.1")]
#[track_caller] #[cfg_attr(feature = "production", inline(always))]
#[cfg_attr(not(feature = "production"), inline)]
pub fn check_transaction(tx: &Transaction) -> Result<ValidationResult> {
if tx.inputs.len() > MAX_INPUTS {
return Ok(ValidationResult::Invalid(format!(
"Input count {} exceeds maximum {}",
tx.inputs.len(),
MAX_INPUTS
)));
}
if tx.outputs.len() > MAX_OUTPUTS {
return Ok(ValidationResult::Invalid(format!(
"Output count {} exceeds maximum {}",
tx.outputs.len(),
MAX_OUTPUTS
)));
}
#[cfg(feature = "production")]
if let Some(result) = check_transaction_fast_path(tx) {
return Ok(result);
}
if tx.inputs.is_empty() {
return Ok(ValidationResult::Invalid(
"Transaction must have inputs unless it's a coinbase".to_string(),
));
}
if tx.outputs.is_empty() {
return Ok(ValidationResult::Invalid(
"Transaction must have at least one output".to_string(),
));
}
let mut total_output_value = 0i64;
assert!(
total_output_value == 0,
"Total output value must start at zero"
);
#[cfg(feature = "production")]
{
use crate::optimizations::optimized_access::get_proven_by_;
use crate::optimizations::precomputed_constants::MAX_MONEY_U64;
for i in 0..tx.outputs.len() {
if let Some(output) = get_proven_by_(&tx.outputs, i) {
let value_u64 = output.value as u64;
if output.value < 0 || value_u64 > MAX_MONEY_U64 {
return Ok(ValidationResult::Invalid(format!(
"Invalid output value {} at index {}",
output.value, i
)));
}
assert!(
output.value >= 0,
"Output value {} must be non-negative at index {}",
output.value,
i
);
total_output_value = total_output_value
.checked_add(output.value)
.ok_or_else(make_output_sum_overflow_error)?;
assert!(
total_output_value >= 0,
"Total output value {total_output_value} must be non-negative after output {i}"
);
}
}
}
#[cfg(not(feature = "production"))]
{
for (i, output) in tx.outputs.iter().enumerate() {
assert!(i < tx.outputs.len(), "Output index {i} out of bounds");
if output.value < 0 || output.value > MAX_MONEY {
return Ok(ValidationResult::Invalid(format!(
"Invalid output value {} at index {}",
output.value, i
)));
}
total_output_value = total_output_value
.checked_add(output.value)
.ok_or_else(make_output_sum_overflow_error)?;
assert!(
total_output_value >= 0,
"Total output value {total_output_value} must be non-negative after output {i}"
);
}
}
assert!(
total_output_value >= 0,
"Total output value {total_output_value} must be non-negative"
);
#[cfg(feature = "production")]
{
use crate::optimizations::precomputed_constants::MAX_MONEY_U64;
let total_u64 = total_output_value as u64;
if total_output_value < 0 || total_u64 > MAX_MONEY_U64 {
return Ok(ValidationResult::Invalid(format!(
"Total output value {total_output_value} is out of valid range [0, {MAX_MONEY}]"
)));
}
assert!(
total_u64 <= MAX_MONEY_U64,
"Total output value {total_output_value} must not exceed MAX_MONEY"
);
}
#[cfg(not(feature = "production"))]
{
if !(0..=MAX_MONEY).contains(&total_output_value) {
return Ok(ValidationResult::Invalid(format!(
"Total output value {total_output_value} is out of valid range [0, {MAX_MONEY}]"
)));
}
assert!(
total_output_value <= MAX_MONEY,
"Total output value {total_output_value} must not exceed MAX_MONEY"
);
}
if tx.inputs.len() > MAX_INPUTS {
return Ok(ValidationResult::Invalid(format!(
"Too many inputs: {}",
tx.inputs.len()
)));
}
if tx.outputs.len() > MAX_OUTPUTS {
return Ok(ValidationResult::Invalid(format!(
"Too many outputs: {}",
tx.outputs.len()
)));
}
use crate::constants::MAX_BLOCK_WEIGHT;
const WITNESS_SCALE_FACTOR: usize = 4;
let tx_stripped_size = calculate_transaction_size(tx); if tx_stripped_size * WITNESS_SCALE_FACTOR > MAX_BLOCK_WEIGHT {
return Ok(ValidationResult::Invalid(format!(
"Transaction too large: stripped size {} bytes (weight {} > {})",
tx_stripped_size,
tx_stripped_size * WITNESS_SCALE_FACTOR,
MAX_BLOCK_WEIGHT
)));
}
use std::collections::HashSet;
let mut seen_prevouts = HashSet::with_capacity(tx.inputs.len());
for (i, input) in tx.inputs.iter().enumerate() {
assert!(i < tx.inputs.len(), "Input index {i} out of bounds");
if !seen_prevouts.insert(&input.prevout) {
return Ok(ValidationResult::Invalid(format!(
"Duplicate input prevout at index {i}"
)));
}
}
if is_coinbase(tx) {
debug_assert!(
!tx.inputs.is_empty(),
"Coinbase transaction must have at least one input"
);
let script_sig_len = tx.inputs[0].script_sig.len();
if !(2..=100).contains(&script_sig_len) {
return Ok(ValidationResult::Invalid(format!(
"Coinbase scriptSig length {script_sig_len} must be between 2 and 100 bytes"
)));
}
}
Ok(ValidationResult::Valid)
}
#[spec_locked("5.1")]
#[cfg_attr(feature = "production", inline(always))]
#[cfg_attr(not(feature = "production"), inline)]
#[allow(clippy::overly_complex_bool_expr)] pub fn check_tx_inputs<U: UtxoLookup>(
tx: &Transaction,
utxo_set: &U,
height: Natural,
) -> Result<(ValidationResult, Integer)> {
check_tx_inputs_with_utxos(tx, utxo_set, height, None)
}
pub fn check_tx_inputs_with_utxos<U: UtxoLookup>(
tx: &Transaction,
utxo_set: &U,
height: Natural,
pre_collected_utxos: Option<&[Option<&UTXO>]>,
) -> Result<(ValidationResult, Integer)> {
if tx.inputs.is_empty() && !is_coinbase(tx) {
return Ok((
ValidationResult::Invalid(
"Transaction must have inputs unless it's a coinbase".to_string(),
),
0,
));
}
assert!(
height <= i64::MAX as u64,
"Block height {height} must fit in i64"
);
assert!(
utxo_set.len() <= u32::MAX as usize,
"UTXO set size {} exceeds maximum",
utxo_set.len()
);
if is_coinbase(tx) {
#[allow(clippy::eq_op)]
{
}
return Ok((ValidationResult::Valid, 0));
}
#[cfg(feature = "production")]
{
use crate::optimizations::constant_folding::is_zero_hash;
use crate::optimizations::optimized_access::get_proven_by_;
for i in 0..tx.inputs.len() {
if let Some(input) = get_proven_by_(&tx.inputs, i) {
if is_zero_hash(&input.prevout.hash) && input.prevout.index == 0xffffffff {
return Ok((
ValidationResult::Invalid(format!(
"Non-coinbase input {i} has null prevout"
)),
0,
));
}
}
}
}
#[cfg(not(feature = "production"))]
{
for (i, input) in tx.inputs.iter().enumerate() {
if input.prevout.hash == [0u8; 32] && input.prevout.index == 0xffffffff {
return Ok((
ValidationResult::Invalid(format!("Non-coinbase input {i} has null prevout")),
0,
));
}
}
}
#[cfg(feature = "production")]
{
use crate::optimizations::prefetch;
for i in 0..tx.inputs.len().min(8) {
if i + 4 < tx.inputs.len() {
prefetch::prefetch_ahead(&tx.inputs, i, 4);
}
}
}
let input_utxos: Vec<(usize, Option<&UTXO>)> = if let Some(pre_utxos) = pre_collected_utxos {
pre_utxos
.iter()
.enumerate()
.map(|(i, opt_utxo)| (i, *opt_utxo))
.collect()
} else {
let mut result = Vec::with_capacity(tx.inputs.len());
for (i, input) in tx.inputs.iter().enumerate() {
result.push((i, utxo_set.get(&input.prevout)));
}
result
};
let mut total_input_value = 0i64;
assert!(
total_input_value == 0,
"Total input value must start at zero"
);
for (i, opt_utxo) in input_utxos {
assert!(i < tx.inputs.len(), "Input index {i} out of bounds");
if let Some(utxo) = opt_utxo {
assert!(
utxo.value >= 0,
"UTXO value {} must be non-negative at input {}",
utxo.value,
i
);
assert!(
utxo.value <= MAX_MONEY,
"UTXO value {} must not exceed MAX_MONEY at input {}",
utxo.value,
i
);
if utxo.is_coinbase {
use crate::constants::COINBASE_MATURITY;
let required_height = utxo.height.saturating_add(COINBASE_MATURITY);
assert!(
height >= utxo.height,
"Current height {} must be >= UTXO creation height {}",
height,
utxo.height
);
if height < required_height {
return Ok((
ValidationResult::Invalid(format!(
"Premature spend of coinbase output: input {i} created at height {} cannot be spent until height {} (current: {})",
utxo.height, required_height, height
)),
0,
));
}
}
assert!(
utxo.value >= 0,
"UTXO value {} must be non-negative before addition",
utxo.value
);
total_input_value = total_input_value.checked_add(utxo.value).ok_or_else(|| {
ConsensusError::TransactionValidation(
format!("Input value overflow at input {i}").into(),
)
})?;
assert!(
total_input_value >= 0,
"Total input value {total_input_value} must be non-negative after input {i}"
);
} else {
#[cfg(debug_assertions)]
{
let hash_str: String = tx.inputs[i]
.prevout
.hash
.iter()
.map(|b| format!("{b:02x}"))
.collect();
eprintln!(
" ❌ UTXO NOT FOUND: Input {} prevout {}:{}",
i, hash_str, tx.inputs[i].prevout.index
);
eprintln!(" UTXO set size: {}", utxo_set.len());
}
return Ok((
ValidationResult::Invalid(format!("Input {i} not found in UTXO set")),
0,
));
}
}
let total_output_value: i64 = tx
.outputs
.iter()
.try_fold(0i64, |acc, output| {
assert!(
output.value >= 0,
"Output value {} must be non-negative",
output.value
);
acc.checked_add(output.value).ok_or_else(|| {
ConsensusError::TransactionValidation("Output value overflow".into())
})
})
.map_err(|e| ConsensusError::TransactionValidation(Cow::Owned(e.to_string())))?;
assert!(
total_output_value >= 0,
"Total output value {total_output_value} must be non-negative"
);
assert!(
total_output_value <= MAX_MONEY,
"Total output value {total_output_value} must not exceed MAX_MONEY"
);
if total_output_value > MAX_MONEY {
return Ok((
ValidationResult::Invalid(format!(
"Total output value {total_output_value} exceeds maximum money supply"
)),
0,
));
}
if total_input_value < total_output_value {
return Ok((
ValidationResult::Invalid("Insufficient input value".to_string()),
0,
));
}
let fee = total_input_value
.checked_sub(total_output_value)
.ok_or_else(make_fee_calculation_underflow_error)?;
assert!(fee >= 0, "Fee {fee} must be non-negative");
assert!(
fee <= total_input_value,
"Fee {fee} cannot exceed total input {total_input_value}"
);
assert!(
total_input_value == total_output_value + fee,
"Conservation of value: input {total_input_value} must equal output {total_output_value} + fee {fee}"
);
Ok((ValidationResult::Valid, fee))
}
pub fn check_tx_inputs_with_owned_data(
tx: &Transaction,
height: Natural,
utxo_data: &[Option<(i64, bool, u64)>],
) -> Result<(ValidationResult, Integer)> {
if tx.inputs.is_empty() && !is_coinbase(tx) {
return Ok((
ValidationResult::Invalid(
"Transaction must have inputs unless it's a coinbase".to_string(),
),
0,
));
}
if is_coinbase(tx) {
return Ok((ValidationResult::Valid, 0));
}
if utxo_data.len() != tx.inputs.len() {
return Ok((
ValidationResult::Invalid("UTXO data length mismatch".to_string()),
0,
));
}
let mut total_input_value = 0i64;
for (i, opt) in utxo_data.iter().enumerate() {
if let Some((value, is_coinbase, utxo_height)) = opt {
if *value < 0 || *value > MAX_MONEY {
return Ok((
ValidationResult::Invalid(format!(
"UTXO value {value} out of bounds at input {i}"
)),
0,
));
}
if *is_coinbase {
use crate::constants::COINBASE_MATURITY;
let required_height = utxo_height.saturating_add(COINBASE_MATURITY);
if height < required_height {
return Ok((
ValidationResult::Invalid(format!(
"Premature spend of coinbase output at input {i}"
)),
0,
));
}
}
total_input_value = total_input_value.checked_add(*value).ok_or_else(|| {
ConsensusError::TransactionValidation(
format!("Input value overflow at input {i}").into(),
)
})?;
} else {
return Ok((
ValidationResult::Invalid(format!("Input {i} not found in UTXO set")),
0,
));
}
}
let total_output_value: i64 = tx
.outputs
.iter()
.try_fold(0i64, |acc, output| {
acc.checked_add(output.value).ok_or_else(|| {
ConsensusError::TransactionValidation("Output value overflow".into())
})
})
.map_err(|e| ConsensusError::TransactionValidation(Cow::Owned(e.to_string())))?;
if total_output_value > MAX_MONEY {
return Ok((
ValidationResult::Invalid(format!(
"Total output value {total_output_value} exceeds maximum"
)),
0,
));
}
if total_input_value < total_output_value {
return Ok((
ValidationResult::Invalid("Insufficient input value".to_string()),
0,
));
}
let fee = total_input_value
.checked_sub(total_output_value)
.ok_or_else(make_fee_calculation_underflow_error)?;
Ok((ValidationResult::Valid, fee))
}
#[inline(always)]
#[spec_locked("6.4")]
pub fn is_coinbase(tx: &Transaction) -> bool {
#[cfg(feature = "production")]
{
use crate::optimizations::constant_folding::is_zero_hash;
tx.inputs.len() == 1
&& is_zero_hash(&tx.inputs[0].prevout.hash)
&& tx.inputs[0].prevout.index == 0xffffffff
}
#[cfg(not(feature = "production"))]
{
tx.inputs.len() == 1
&& tx.inputs[0].prevout.hash == [0u8; 32]
&& tx.inputs[0].prevout.index == 0xffffffff
}
}
#[inline(always)]
#[spec_locked("5.1")]
pub fn calculate_transaction_size(tx: &Transaction) -> usize {
use crate::serialization::transaction::serialize_transaction;
serialize_transaction(tx).len()
}
#[cfg(test)]
pub(crate) mod transaction_proptest {
use super::*;
use proptest::prelude::*;
pub fn arb_transaction() -> BoxedStrategy<Transaction> {
(
any::<u64>(),
prop::collection::vec(
(
any::<[u8; 32]>(),
any::<u64>(),
prop::collection::vec(any::<u8>(), 0..100),
any::<u64>(),
),
0..10,
),
prop::collection::vec(
(any::<i64>(), prop::collection::vec(any::<u8>(), 0..100)),
0..10,
),
any::<u64>(),
)
.prop_map(|(version, inputs, outputs, lock_time)| {
let inputs: Vec<TransactionInput> = inputs
.into_iter()
.map(|(hash, index, script_sig, sequence)| TransactionInput {
prevout: OutPoint {
hash,
index: index as u32,
},
script_sig,
sequence,
})
.collect();
let outputs: Vec<TransactionOutput> = outputs
.into_iter()
.map(|(value, script_pubkey)| TransactionOutput {
value,
script_pubkey,
})
.collect();
Transaction {
version,
#[cfg(feature = "production")]
inputs: inputs.into(),
#[cfg(not(feature = "production"))]
inputs,
#[cfg(feature = "production")]
outputs: outputs.into(),
#[cfg(not(feature = "production"))]
outputs,
lock_time,
}
})
.boxed()
}
pub fn arb_outpoint() -> impl Strategy<Value = OutPoint> {
(any::<[u8; 32]>(), any::<u32>()).prop_map(|(hash, index)| OutPoint { hash, index })
}
pub fn arb_utxo() -> impl Strategy<Value = UTXO> {
(
any::<i64>(),
prop::collection::vec(any::<u8>(), 0..40),
any::<u64>(),
any::<bool>(),
)
.prop_map(|(value, script_pubkey, height, is_coinbase)| UTXO {
value,
script_pubkey: script_pubkey.into(),
height,
is_coinbase,
})
}
}
#[cfg(test)]
#[allow(unused_doc_comments)]
mod property_tests {
use super::transaction_proptest::{arb_outpoint, arb_transaction, arb_utxo};
use super::*;
use proptest::prelude::*;
proptest! {
#[test]
fn prop_check_transaction_structure(
tx in arb_transaction()
) {
let mut bounded_tx = tx;
if bounded_tx.inputs.len() > 10 {
bounded_tx.inputs.truncate(10);
}
if bounded_tx.outputs.len() > 10 {
bounded_tx.outputs.truncate(10);
}
let result = check_transaction(&bounded_tx).unwrap_or_else(|_| ValidationResult::Invalid("Error".to_string()));
match result {
ValidationResult::Valid => {
prop_assert!(!bounded_tx.inputs.is_empty(), "Valid transaction must have inputs");
prop_assert!(!bounded_tx.outputs.is_empty(), "Valid transaction must have outputs");
prop_assert!(bounded_tx.inputs.len() <= MAX_INPUTS, "Valid transaction must respect input limit");
prop_assert!(bounded_tx.outputs.len() <= MAX_OUTPUTS, "Valid transaction must respect output limit");
for output in &bounded_tx.outputs {
prop_assert!(output.value >= 0, "Valid transaction outputs must be non-negative");
prop_assert!(output.value <= MAX_MONEY, "Valid transaction outputs must not exceed max money");
}
},
ValidationResult::Invalid(_) => {
}
}
}
}
proptest! {
#[test]
fn prop_check_tx_inputs_coinbase(
tx in arb_transaction(),
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>()),
height in 0u64..1000u64
) {
let mut bounded_tx = tx;
if bounded_tx.inputs.len() > 5 {
bounded_tx.inputs.truncate(5);
}
if bounded_tx.outputs.len() > 5 {
bounded_tx.outputs.truncate(5);
}
let result = check_tx_inputs(&bounded_tx, &utxo_set, height).unwrap_or((ValidationResult::Invalid("Error".to_string()), 0));
if is_coinbase(&bounded_tx) {
prop_assert!(matches!(result.0, ValidationResult::Valid), "Coinbase transactions must be valid");
prop_assert_eq!(result.1, 0, "Coinbase transactions must have zero fee");
}
}
}
proptest! {
#[test]
fn prop_is_coinbase_correct(
tx in arb_transaction()
) {
let is_cb = is_coinbase(&tx);
if is_cb {
prop_assert_eq!(tx.inputs.len(), 1, "Coinbase must have exactly one input");
prop_assert_eq!(tx.inputs[0].prevout.hash, [0u8; 32], "Coinbase input must have zero hash");
prop_assert_eq!(tx.inputs[0].prevout.index, 0xffffffffu32, "Coinbase input must have max index");
}
}
}
proptest! {
#[test]
fn prop_calculate_transaction_size_consistent(
tx in arb_transaction()
) {
let mut bounded_tx = tx;
if bounded_tx.inputs.len() > 10 {
bounded_tx.inputs.truncate(10);
}
if bounded_tx.outputs.len() > 10 {
bounded_tx.outputs.truncate(10);
}
let size = calculate_transaction_size(&bounded_tx);
prop_assert!(size >= 10, "Transaction size must be at least 10 bytes (version + varints + lock_time)");
prop_assert!(size <= MAX_TX_SIZE, "Transaction size must not exceed MAX_TX_SIZE ({})", MAX_TX_SIZE);
let size2 = calculate_transaction_size(&bounded_tx);
prop_assert_eq!(size, size2, "Transaction size calculation must be deterministic");
}
}
proptest! {
#[test]
fn prop_output_value_bounds(
value in 0i64..(MAX_MONEY + 1000)
) {
let tx = Transaction {
version: 1,
inputs: vec![TransactionInput {
prevout: OutPoint { hash: [0; 32].into(), index: 0 },
script_sig: vec![],
sequence: 0xffffffff,
}].into(),
outputs: vec![TransactionOutput {
value,
script_pubkey: vec![],
}].into(),
lock_time: 0,
};
let result = check_transaction(&tx).unwrap_or(ValidationResult::Invalid("Error".to_string()));
if !(0..=MAX_MONEY).contains(&value) {
prop_assert!(matches!(result, ValidationResult::Invalid(_)),
"Transactions with invalid output values must be invalid");
} else {
if !tx.inputs.is_empty() && !tx.outputs.is_empty() {
prop_assert!(matches!(result, ValidationResult::Valid),
"Transactions with valid output values should be valid");
}
}
}
}
}
#[cfg(test)]
mod tests {
use super::*;
#[test]
fn test_check_transaction_valid() {
let tx = Transaction {
version: 1,
inputs: vec![TransactionInput {
prevout: OutPoint {
hash: [0; 32].into(),
index: 0,
},
script_sig: vec![],
sequence: 0xffffffff,
}]
.into(),
outputs: vec![TransactionOutput {
value: 1000,
script_pubkey: vec![].into(),
}]
.into(),
lock_time: 0,
};
assert_eq!(check_transaction(&tx).unwrap(), ValidationResult::Valid);
}
#[test]
fn test_check_transaction_empty_inputs() {
let tx = Transaction {
version: 1,
inputs: vec![].into(),
outputs: vec![TransactionOutput {
value: 1000,
script_pubkey: vec![].into(),
}]
.into(),
lock_time: 0,
};
assert!(matches!(
check_transaction(&tx).unwrap(),
ValidationResult::Invalid(_)
));
}
#[test]
fn test_check_tx_inputs_coinbase() {
let tx = Transaction {
version: 1,
inputs: vec![TransactionInput {
prevout: OutPoint {
hash: [0; 32].into(),
index: 0xffffffff,
},
script_sig: vec![],
sequence: 0xffffffff,
}]
.into(),
outputs: vec![TransactionOutput {
value: 5000000000, script_pubkey: vec![].into(),
}]
.into(),
lock_time: 0,
};
let utxo_set = UtxoSet::default();
let (result, fee) = check_tx_inputs(&tx, &utxo_set, 0).unwrap();
assert_eq!(result, ValidationResult::Valid);
assert_eq!(fee, 0);
}
#[test]
fn test_check_transaction_empty_outputs() {
let tx = Transaction {
version: 1,
inputs: vec![TransactionInput {
prevout: OutPoint {
hash: [0; 32].into(),
index: 0,
},
script_sig: vec![],
sequence: 0xffffffff,
}]
.into(),
outputs: vec![].into(),
lock_time: 0,
};
assert!(matches!(
check_transaction(&tx).unwrap(),
ValidationResult::Invalid(_)
));
}
#[test]
fn test_check_transaction_invalid_output_value_negative() {
let tx = Transaction {
version: 1,
inputs: vec![TransactionInput {
prevout: OutPoint {
hash: [0; 32].into(),
index: 0,
},
script_sig: vec![],
sequence: 0xffffffff,
}]
.into(),
outputs: vec![TransactionOutput {
value: -1, script_pubkey: vec![].into(),
}]
.into(),
lock_time: 0,
};
assert!(matches!(
check_transaction(&tx).unwrap(),
ValidationResult::Invalid(_)
));
}
#[test]
fn test_check_transaction_invalid_output_value_too_large() {
let tx = Transaction {
version: 1,
inputs: vec![TransactionInput {
prevout: OutPoint {
hash: [0; 32].into(),
index: 0,
},
script_sig: vec![],
sequence: 0xffffffff,
}]
.into(),
outputs: vec![TransactionOutput {
value: MAX_MONEY + 1, script_pubkey: vec![].into(),
}]
.into(),
lock_time: 0,
};
assert!(matches!(
check_transaction(&tx).unwrap(),
ValidationResult::Invalid(_)
));
}
#[test]
fn test_check_transaction_max_output_value() {
let tx = Transaction {
version: 1,
inputs: vec![TransactionInput {
prevout: OutPoint {
hash: [0; 32].into(),
index: 0,
},
script_sig: vec![],
sequence: 0xffffffff,
}]
.into(),
outputs: vec![TransactionOutput {
value: MAX_MONEY, script_pubkey: vec![].into(),
}]
.into(),
lock_time: 0,
};
assert_eq!(check_transaction(&tx).unwrap(), ValidationResult::Valid);
}
#[test]
fn test_check_transaction_too_many_inputs() {
let mut inputs = Vec::new();
for i in 0..=MAX_INPUTS {
inputs.push(TransactionInput {
prevout: OutPoint {
hash: [i as u8; 32],
index: 0,
},
script_sig: vec![],
sequence: 0xffffffff,
});
}
let tx = Transaction {
version: 1,
inputs: inputs.into(),
outputs: vec![TransactionOutput {
value: 1000,
script_pubkey: vec![].into(),
}]
.into(),
lock_time: 0,
};
assert!(matches!(
check_transaction(&tx).unwrap(),
ValidationResult::Invalid(_)
));
}
#[test]
fn test_check_transaction_max_inputs() {
let num_inputs = 20_000;
let mut inputs = Vec::new();
for i in 0..num_inputs {
let mut hash = [0u8; 32];
hash[0] = (i & 0xff) as u8;
hash[1] = ((i >> 8) & 0xff) as u8;
hash[2] = ((i >> 16) & 0xff) as u8;
hash[3] = ((i >> 24) & 0xff) as u8;
inputs.push(TransactionInput {
prevout: OutPoint {
hash,
index: i as u32,
},
script_sig: vec![],
sequence: 0xffffffff,
});
}
let tx = Transaction {
version: 1,
inputs: inputs.into(),
outputs: vec![TransactionOutput {
value: 1000,
script_pubkey: vec![].into(),
}]
.into(),
lock_time: 0,
};
assert_eq!(check_transaction(&tx).unwrap(), ValidationResult::Valid);
}
#[test]
fn test_check_transaction_too_many_outputs() {
let mut outputs = Vec::new();
for _ in 0..=MAX_OUTPUTS {
outputs.push(TransactionOutput {
value: 1000,
script_pubkey: vec![].into(),
});
}
let tx = Transaction {
version: 1,
inputs: vec![TransactionInput {
prevout: OutPoint {
hash: [0; 32].into(),
index: 0,
},
script_sig: vec![],
sequence: 0xffffffff,
}]
.into(),
outputs: outputs.into(),
lock_time: 0,
};
assert!(matches!(
check_transaction(&tx).unwrap(),
ValidationResult::Invalid(_)
));
}
#[test]
fn test_check_transaction_max_outputs() {
let mut outputs = Vec::new();
for _ in 0..MAX_OUTPUTS {
outputs.push(TransactionOutput {
value: 1000,
script_pubkey: vec![].into(),
});
}
let tx = Transaction {
version: 1,
inputs: vec![TransactionInput {
prevout: OutPoint {
hash: [0; 32].into(),
index: 0,
},
script_sig: vec![],
sequence: 0xffffffff,
}]
.into(),
outputs: outputs.into(),
lock_time: 0,
};
assert_eq!(check_transaction(&tx).unwrap(), ValidationResult::Valid);
}
#[test]
fn test_check_transaction_too_large() {
use crate::constants::MAX_INPUTS;
let mut inputs = Vec::new();
for i in 0..MAX_INPUTS {
inputs.push(TransactionInput {
prevout: OutPoint {
hash: [i as u8; 32],
index: 0,
},
script_sig: vec![0u8; 1000], sequence: 0xffffffff,
});
}
let tx = Transaction {
version: 1,
inputs: inputs.into(),
outputs: vec![TransactionOutput {
value: 1000,
script_pubkey: vec![].into(),
}]
.into(),
lock_time: 0,
};
assert!(matches!(
check_transaction(&tx).unwrap(),
ValidationResult::Invalid(_)
));
}
#[test]
fn test_check_tx_inputs_regular_transaction() {
let mut utxo_set = UtxoSet::default();
let outpoint = OutPoint {
hash: [1; 32],
index: 0,
};
let utxo = UTXO {
value: 1000000000, script_pubkey: vec![].into(),
height: 0,
is_coinbase: false,
};
utxo_set.insert(outpoint, std::sync::Arc::new(utxo));
let tx = Transaction {
version: 1,
inputs: vec![TransactionInput {
prevout: OutPoint {
hash: [1; 32].into(),
index: 0,
},
script_sig: vec![],
sequence: 0xffffffff,
}]
.into(),
outputs: vec![TransactionOutput {
value: 900000000, script_pubkey: vec![],
}]
.into(),
lock_time: 0,
};
let (result, fee) = check_tx_inputs(&tx, &utxo_set, 0).unwrap();
assert_eq!(result, ValidationResult::Valid);
assert_eq!(fee, 100000000); }
#[test]
fn test_check_tx_inputs_missing_utxo() {
let utxo_set = UtxoSet::default();
let tx = Transaction {
version: 1,
inputs: vec![TransactionInput {
prevout: OutPoint {
hash: [1; 32].into(),
index: 0,
},
script_sig: vec![],
sequence: 0xffffffff,
}]
.into(),
outputs: vec![TransactionOutput {
value: 100000000,
script_pubkey: vec![],
}]
.into(),
lock_time: 0,
};
let (result, fee) = check_tx_inputs(&tx, &utxo_set, 0).unwrap();
assert!(matches!(result, ValidationResult::Invalid(_)));
assert_eq!(fee, 0);
}
#[test]
fn test_check_tx_inputs_insufficient_funds() {
let mut utxo_set = UtxoSet::default();
let outpoint = OutPoint {
hash: [1; 32],
index: 0,
};
let utxo = UTXO {
value: 100000000, script_pubkey: vec![].into(),
height: 0,
is_coinbase: false,
};
utxo_set.insert(outpoint, std::sync::Arc::new(utxo));
let tx = Transaction {
version: 1,
inputs: vec![TransactionInput {
prevout: OutPoint {
hash: [1; 32].into(),
index: 0,
},
script_sig: vec![],
sequence: 0xffffffff,
}]
.into(),
outputs: vec![TransactionOutput {
value: 200000000, script_pubkey: vec![],
}]
.into(),
lock_time: 0,
};
let (result, fee) = check_tx_inputs(&tx, &utxo_set, 0).unwrap();
assert!(matches!(result, ValidationResult::Invalid(_)));
assert_eq!(fee, 0);
}
#[test]
fn test_check_tx_inputs_multiple_inputs() {
let mut utxo_set = UtxoSet::default();
let outpoint1 = OutPoint {
hash: [1; 32],
index: 0,
};
let utxo1 = UTXO {
value: 500000000, script_pubkey: vec![].into(),
height: 0,
is_coinbase: false,
};
utxo_set.insert(outpoint1, std::sync::Arc::new(utxo1));
let outpoint2 = OutPoint {
hash: [2; 32],
index: 0,
};
let utxo2 = UTXO {
value: 300000000, script_pubkey: vec![].into(),
height: 0,
is_coinbase: false,
};
utxo_set.insert(outpoint2, std::sync::Arc::new(utxo2));
let tx = Transaction {
version: 1,
inputs: vec![
TransactionInput {
prevout: OutPoint {
hash: [1; 32].into(),
index: 0,
},
script_sig: vec![],
sequence: 0xffffffff,
},
TransactionInput {
prevout: OutPoint {
hash: [2; 32],
index: 0,
},
script_sig: vec![],
sequence: 0xffffffff,
},
]
.into(),
outputs: vec![TransactionOutput {
value: 700000000, script_pubkey: vec![].into(),
}]
.into(),
lock_time: 0,
};
let (result, fee) = check_tx_inputs(&tx, &utxo_set, 0).unwrap();
assert_eq!(result, ValidationResult::Valid);
assert_eq!(fee, 100000000); }
#[test]
fn test_is_coinbase_edge_cases() {
let valid_coinbase = Transaction {
version: 1,
inputs: vec![TransactionInput {
prevout: OutPoint {
hash: [0; 32].into(),
index: 0xffffffff,
},
script_sig: vec![],
sequence: 0xffffffff,
}]
.into(),
outputs: vec![].into(),
lock_time: 0,
};
assert!(is_coinbase(&valid_coinbase));
let wrong_hash = Transaction {
version: 1,
inputs: vec![TransactionInput {
prevout: OutPoint {
hash: [1; 32].into(),
index: 0xffffffff,
},
script_sig: vec![],
sequence: 0xffffffff,
}]
.into(),
outputs: vec![].into(),
lock_time: 0,
};
assert!(!is_coinbase(&wrong_hash));
let wrong_index = Transaction {
version: 1,
inputs: vec![TransactionInput {
prevout: OutPoint {
hash: [0; 32].into(),
index: 0,
},
script_sig: vec![],
sequence: 0xffffffff,
}]
.into(),
outputs: vec![].into(),
lock_time: 0,
};
assert!(!is_coinbase(&wrong_index));
let multiple_inputs = Transaction {
version: 1,
inputs: vec![
TransactionInput {
prevout: OutPoint {
hash: [0; 32].into(),
index: 0xffffffff,
},
script_sig: vec![],
sequence: 0xffffffff,
},
TransactionInput {
prevout: OutPoint {
hash: [1; 32],
index: 0,
},
script_sig: vec![],
sequence: 0xffffffff,
},
]
.into(),
outputs: vec![].into(),
lock_time: 0,
};
assert!(!is_coinbase(&multiple_inputs));
let no_inputs = Transaction {
version: 1,
inputs: vec![].into(),
outputs: vec![].into(),
lock_time: 0,
};
assert!(!is_coinbase(&no_inputs));
}
#[test]
fn test_calculate_transaction_size() {
let tx = Transaction {
version: 1,
inputs: vec![
TransactionInput {
prevout: OutPoint {
hash: [0; 32].into(),
index: 0,
},
script_sig: vec![1, 2, 3],
sequence: 0xffffffff,
},
TransactionInput {
prevout: OutPoint {
hash: [1; 32],
index: 1,
},
script_sig: vec![4, 5, 6],
sequence: 0xffffffff,
},
]
.into(),
outputs: vec![
TransactionOutput {
value: 1000,
script_pubkey: vec![7, 8, 9].into(),
},
TransactionOutput {
value: 2000,
script_pubkey: vec![10, 11, 12],
},
]
.into(),
lock_time: 12345,
};
let size = calculate_transaction_size(&tx);
assert_eq!(size, 122);
}
}