use crate::bip_validation::Bip30Index;
use crate::constants::MAX_MONEY;
use crate::error::Result;
use crate::reorganization::UndoEntry;
use crate::transaction::is_coinbase;
use crate::types::{Hash, Natural, OutPoint, Transaction, UtxoSet, UTXO};
use blvm_spec_lock::spec_locked;
#[spec_locked("5.3.1")]
#[track_caller]
pub fn apply_transaction(
tx: &Transaction,
utxo_set: UtxoSet,
height: Natural,
) -> Result<(UtxoSet, Vec<UndoEntry>)> {
let tx_id = calculate_tx_id(tx);
let mut no_index = None;
apply_transaction_with_id(tx, tx_id, utxo_set, height, &mut no_index, true)
}
#[spec_locked("5.3.1")]
pub(crate) fn apply_transaction_with_id(
tx: &Transaction,
tx_id: Hash,
mut utxo_set: UtxoSet,
height: Natural,
bip30_index: &mut Option<&mut Bip30Index>,
collect_undo: bool,
) -> Result<(UtxoSet, Vec<UndoEntry>)> {
assert!(
!tx.inputs.is_empty() || is_coinbase(tx),
"Transaction must have inputs unless it's a coinbase"
);
assert!(
!tx.outputs.is_empty(),
"Transaction must have at least one output"
);
assert!(
height <= i64::MAX as u64,
"Block height {height} must fit in i64"
);
let mut undo_entries = if collect_undo {
Vec::with_capacity(tx.inputs.len().saturating_add(tx.outputs.len()))
} else {
Vec::new()
};
let initial_utxo_count = utxo_set.len();
#[cfg(feature = "production")]
{
let estimated_new_size = utxo_set
.len()
.saturating_add(tx.outputs.len())
.saturating_sub(if is_coinbase(tx) { 0 } else { tx.inputs.len() });
if estimated_new_size > utxo_set.capacity() {
utxo_set.reserve(estimated_new_size.saturating_sub(utxo_set.len()));
}
}
if !is_coinbase(tx) {
assert!(
!tx.inputs.is_empty(),
"Non-coinbase transaction must have inputs"
);
for input in &tx.inputs {
assert!(
input.prevout.hash != [0u8; 32] || input.prevout.index != 0xffffffff,
"Prevout must be valid for non-coinbase input"
);
if let Some(arc) = utxo_set.remove(&input.prevout) {
let previous_utxo = arc.as_ref();
if let Some(idx) = bip30_index.as_deref_mut() {
if previous_utxo.is_coinbase {
if let std::collections::hash_map::Entry::Occupied(mut o) =
idx.entry(input.prevout.hash)
{
*o.get_mut() = o.get().saturating_sub(1);
if *o.get() == 0 {
o.remove();
}
}
}
}
assert!(
previous_utxo.value >= 0,
"Previous UTXO value {} must be non-negative",
previous_utxo.value
);
assert!(
previous_utxo.value <= MAX_MONEY,
"Previous UTXO value {} must not exceed MAX_MONEY",
previous_utxo.value
);
if collect_undo {
undo_entries.push(UndoEntry {
outpoint: input.prevout,
previous_utxo: Some(std::sync::Arc::clone(&arc)),
new_utxo: None,
});
assert!(
undo_entries.len() <= tx.inputs.len() + tx.outputs.len(),
"Undo entry count {} must be reasonable",
undo_entries.len()
);
}
}
}
}
for (i, output) in tx.outputs.iter().enumerate() {
assert!(
i < tx.outputs.len(),
"Output index {} out of bounds (transaction has {} outputs)",
i,
tx.outputs.len()
);
assert!(
output.value >= 0,
"Output value {} must be non-negative",
output.value
);
assert!(
output.value <= MAX_MONEY,
"Output value {} must not exceed MAX_MONEY",
output.value
);
let outpoint = OutPoint {
hash: tx_id,
index: i as u32,
};
assert!(
i <= u32::MAX as usize,
"Output index {i} must fit in Natural"
);
let utxo = UTXO {
value: output.value,
script_pubkey: output.script_pubkey.as_slice().into(),
height,
is_coinbase: is_coinbase(tx),
};
assert!(
utxo.value == output.value,
"UTXO value {} must match output value {}",
utxo.value,
output.value
);
let utxo_arc = std::sync::Arc::new(utxo);
if collect_undo {
undo_entries.push(UndoEntry {
outpoint,
previous_utxo: None,
new_utxo: Some(std::sync::Arc::clone(&utxo_arc)),
});
assert!(
undo_entries.len() <= tx.outputs.len() + tx.inputs.len(),
"Undo entry count {} must be reasonable",
undo_entries.len()
);
}
utxo_set.insert(outpoint, utxo_arc);
if let Some(idx) = bip30_index.as_deref_mut() {
if is_coinbase(tx) {
*idx.entry(tx_id).or_insert(0) += 1;
}
}
}
if !is_coinbase(tx) {
let current_count = utxo_set.len();
let expected_count = initial_utxo_count
.saturating_sub(tx.inputs.len())
.saturating_add(tx.outputs.len());
if current_count < expected_count {
for (j, output) in tx.outputs.iter().enumerate() {
let op = OutPoint {
hash: tx_id,
index: j as u32,
};
utxo_set.entry(op).or_insert_with(|| {
let utxo = UTXO {
value: output.value,
script_pubkey: output.script_pubkey.as_slice().into(),
height,
is_coinbase: false,
};
std::sync::Arc::new(utxo)
});
}
}
}
let final_utxo_count = utxo_set.len();
if is_coinbase(tx) {
assert!(
final_utxo_count >= initial_utxo_count,
"UTXO set size {final_utxo_count} must not decrease after coinbase (was {initial_utxo_count})"
);
assert!(
final_utxo_count <= initial_utxo_count + tx.outputs.len(),
"UTXO set size {} must not exceed initial {} + outputs {}",
final_utxo_count,
initial_utxo_count,
tx.outputs.len()
);
} else {
let expected_change = tx.outputs.len() as i64 - tx.inputs.len() as i64;
let actual_change = final_utxo_count as i64 - initial_utxo_count as i64;
let lower = -(tx.inputs.len() as i64);
debug_assert!(
actual_change >= lower,
"UTXO set size change {actual_change} must be reasonable (expected ~{expected_change})"
);
}
assert!(
utxo_set.len() <= u32::MAX as usize,
"UTXO set size {} must not exceed maximum",
utxo_set.len()
);
Ok((utxo_set, undo_entries))
}
#[inline(always)]
#[spec_locked("5.1")]
pub fn calculate_tx_id(tx: &Transaction) -> Hash {
use crate::crypto::OptimizedSha256;
use crate::serialization::transaction::serialize_transaction;
let serialized = serialize_transaction(tx);
OptimizedSha256::new().hash256(&serialized)
}