blvm_consensus/block/
apply.rs1use crate::bip_validation::Bip30Index;
6use crate::constants::MAX_MONEY;
7use crate::error::Result;
8use crate::reorganization::UndoEntry;
9use crate::transaction::is_coinbase;
10use crate::types::{Hash, Natural, OutPoint, Transaction, UtxoSet, UTXO};
11use blvm_spec_lock::spec_locked;
12
13#[spec_locked("5.3.2")]
25#[track_caller]
26pub fn apply_transaction(
27 tx: &Transaction,
28 utxo_set: UtxoSet,
29 height: Natural,
30) -> Result<(UtxoSet, Vec<UndoEntry>)> {
31 let tx_id = calculate_tx_id(tx);
32 let mut no_index = None;
33 apply_transaction_with_id(tx, tx_id, utxo_set, height, &mut no_index, true)
34}
35
36#[spec_locked("5.3.2")]
44pub(crate) fn apply_transaction_with_id(
45 tx: &Transaction,
46 tx_id: Hash,
47 mut utxo_set: UtxoSet,
48 height: Natural,
49 bip30_index: &mut Option<&mut Bip30Index>,
50 collect_undo: bool,
51) -> Result<(UtxoSet, Vec<UndoEntry>)> {
52 assert!(
53 !tx.inputs.is_empty() || is_coinbase(tx),
54 "Transaction must have inputs unless it's a coinbase"
55 );
56 assert!(
57 !tx.outputs.is_empty(),
58 "Transaction must have at least one output"
59 );
60 assert!(
61 height <= i64::MAX as u64,
62 "Block height {height} must fit in i64"
63 );
64
65 let mut undo_entries = if collect_undo {
66 Vec::with_capacity(tx.inputs.len().saturating_add(tx.outputs.len()))
67 } else {
68 Vec::new()
69 };
70 let initial_utxo_count = utxo_set.len();
71
72 #[cfg(feature = "production")]
73 {
74 let estimated_new_size = utxo_set
75 .len()
76 .saturating_add(tx.outputs.len())
77 .saturating_sub(if is_coinbase(tx) { 0 } else { tx.inputs.len() });
78 if estimated_new_size > utxo_set.capacity() {
79 utxo_set.reserve(estimated_new_size.saturating_sub(utxo_set.len()));
80 }
81 }
82
83 if !is_coinbase(tx) {
84 assert!(
85 !tx.inputs.is_empty(),
86 "Non-coinbase transaction must have inputs"
87 );
88
89 for input in &tx.inputs {
90 assert!(
91 input.prevout.hash != [0u8; 32] || input.prevout.index != 0xffffffff,
92 "Prevout must be valid for non-coinbase input"
93 );
94
95 if let Some(arc) = utxo_set.remove(&input.prevout) {
96 let previous_utxo = arc.as_ref();
97 if let Some(idx) = bip30_index.as_deref_mut() {
98 if previous_utxo.is_coinbase {
99 if let std::collections::hash_map::Entry::Occupied(mut o) =
100 idx.entry(input.prevout.hash)
101 {
102 *o.get_mut() = o.get().saturating_sub(1);
103 if *o.get() == 0 {
104 o.remove();
105 }
106 }
107 }
108 }
109
110 assert!(
111 previous_utxo.value >= 0,
112 "Previous UTXO value {} must be non-negative",
113 previous_utxo.value
114 );
115 assert!(
116 previous_utxo.value <= MAX_MONEY,
117 "Previous UTXO value {} must not exceed MAX_MONEY",
118 previous_utxo.value
119 );
120
121 if collect_undo {
122 undo_entries.push(UndoEntry {
123 outpoint: input.prevout,
124 previous_utxo: Some(std::sync::Arc::clone(&arc)),
125 new_utxo: None,
126 });
127 assert!(
128 undo_entries.len() <= tx.inputs.len() + tx.outputs.len(),
129 "Undo entry count {} must be reasonable",
130 undo_entries.len()
131 );
132 }
133 }
134 }
135 }
136
137 for (i, output) in tx.outputs.iter().enumerate() {
138 assert!(
139 i < tx.outputs.len(),
140 "Output index {} out of bounds (transaction has {} outputs)",
141 i,
142 tx.outputs.len()
143 );
144 assert!(
145 output.value >= 0,
146 "Output value {} must be non-negative",
147 output.value
148 );
149 assert!(
150 output.value <= MAX_MONEY,
151 "Output value {} must not exceed MAX_MONEY",
152 output.value
153 );
154
155 let outpoint = OutPoint {
156 hash: tx_id,
157 index: i as u32,
158 };
159 assert!(
160 i <= u32::MAX as usize,
161 "Output index {i} must fit in Natural"
162 );
163
164 let utxo = UTXO {
165 value: output.value,
166 script_pubkey: output.script_pubkey.as_slice().into(),
167 height,
168 is_coinbase: is_coinbase(tx),
169 };
170 assert!(
171 utxo.value == output.value,
172 "UTXO value {} must match output value {}",
173 utxo.value,
174 output.value
175 );
176
177 let utxo_arc = std::sync::Arc::new(utxo);
178 if collect_undo {
179 undo_entries.push(UndoEntry {
180 outpoint,
181 previous_utxo: None,
182 new_utxo: Some(std::sync::Arc::clone(&utxo_arc)),
183 });
184 assert!(
185 undo_entries.len() <= tx.outputs.len() + tx.inputs.len(),
186 "Undo entry count {} must be reasonable",
187 undo_entries.len()
188 );
189 }
190
191 utxo_set.insert(outpoint, utxo_arc);
192
193 if let Some(idx) = bip30_index.as_deref_mut() {
194 if is_coinbase(tx) {
195 *idx.entry(tx_id).or_insert(0) += 1;
196 }
197 }
198 }
199
200 if !is_coinbase(tx) {
201 let current_count = utxo_set.len();
202 let expected_count = initial_utxo_count
203 .saturating_sub(tx.inputs.len())
204 .saturating_add(tx.outputs.len());
205 if current_count < expected_count {
206 for (j, output) in tx.outputs.iter().enumerate() {
207 let op = OutPoint {
208 hash: tx_id,
209 index: j as u32,
210 };
211 utxo_set.entry(op).or_insert_with(|| {
212 let utxo = UTXO {
213 value: output.value,
214 script_pubkey: output.script_pubkey.as_slice().into(),
215 height,
216 is_coinbase: false,
217 };
218 std::sync::Arc::new(utxo)
219 });
220 }
221 }
222 }
223
224 let final_utxo_count = utxo_set.len();
225 if is_coinbase(tx) {
226 assert!(
227 final_utxo_count >= initial_utxo_count,
228 "UTXO set size {final_utxo_count} must not decrease after coinbase (was {initial_utxo_count})"
229 );
230 assert!(
231 final_utxo_count <= initial_utxo_count + tx.outputs.len(),
232 "UTXO set size {} must not exceed initial {} + outputs {}",
233 final_utxo_count,
234 initial_utxo_count,
235 tx.outputs.len()
236 );
237 } else {
238 let expected_change = tx.outputs.len() as i64 - tx.inputs.len() as i64;
239 let actual_change = final_utxo_count as i64 - initial_utxo_count as i64;
240 let lower = -(tx.inputs.len() as i64);
241 debug_assert!(
242 actual_change >= lower,
243 "UTXO set size change {actual_change} must be reasonable (expected ~{expected_change})"
244 );
245 }
246 assert!(
247 utxo_set.len() <= u32::MAX as usize,
248 "UTXO set size {} must not exceed maximum",
249 utxo_set.len()
250 );
251
252 Ok((utxo_set, undo_entries))
253}
254
255#[inline(always)]
262#[spec_locked("5.1")]
263pub fn calculate_tx_id(tx: &Transaction) -> Hash {
264 use crate::crypto::OptimizedSha256;
265 use crate::serialization::transaction::serialize_transaction;
266
267 let serialized = serialize_transaction(tx);
268 OptimizedSha256::new().hash256(&serialized)
269}