Skip to main content

blvm_consensus/block/
apply.rs

1//! Apply block effects: apply_transaction, apply_transaction_with_id, calculate_tx_id.
2//!
3//! Clear "apply block effects" API; used by connect_block and external callers.
4
5use 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/// ApplyTransaction (Orange Paper 5.3.2)
14///
15/// For transaction tx and UTXO set us:
16/// 1. If tx is coinbase: us' = us ∪ {(tx.id, i) ↦ tx.outputs\[i\] : i ∈ \[0, |tx.outputs|)}
17/// 2. Otherwise: us' = (us \ {i.prevout : i ∈ tx.inputs}) ∪ {(tx.id, i) ↦ tx.outputs\[i\] : i ∈ \[0, |tx.outputs|)}
18/// 3. Return us'
19///
20/// This function computes the transaction ID internally.
21/// For batch operations, use `apply_transaction_with_id` instead.
22///
23/// Returns both the new UTXO set and undo entries for all UTXO changes.
24#[spec_locked("5.3.2", "ApplyTransaction")]
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/// ApplyTransaction with pre-computed transaction ID
37///
38/// Same as `apply_transaction` but accepts a pre-computed transaction ID
39/// to avoid redundant computation when transaction IDs are batch-computed.
40///
41/// Returns both the new UTXO set and undo entries for all UTXO changes.
42/// When `bip30_index` is Some, updates it for coinbase add/remove (O(1) BIP30 checks).
43#[spec_locked("5.3.2", "ApplyTransaction")]
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/// Calculate transaction ID using proper Bitcoin double SHA256
256///
257/// Transaction ID is SHA256(SHA256(serialized_tx)) where serialized_tx
258/// is the transaction in Bitcoin wire format.
259///
260/// For batch operations, use serialize_transaction + batch_double_sha256 instead.
261#[inline(always)]
262#[spec_locked("5.1", "CalculateTxId")]
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}