cuprate_blockchain/ops/
tx.rs

1//! Transaction functions.
2
3//---------------------------------------------------------------------------------------------------- Import
4use bytemuck::TransparentWrapper;
5use monero_serai::transaction::{Input, Timelock, Transaction};
6
7use cuprate_database::{DatabaseRo, DatabaseRw, DbResult, RuntimeError, StorableVec};
8use cuprate_helper::crypto::compute_zero_commitment;
9
10use crate::{
11    ops::{
12        key_image::{add_key_image, remove_key_image},
13        macros::{doc_add_block_inner_invariant, doc_error},
14        output::{
15            add_output, add_rct_output, get_rct_num_outputs, remove_output, remove_rct_output,
16        },
17    },
18    tables::{TablesMut, TxBlobs, TxIds},
19    types::{BlockHeight, Output, OutputFlags, PreRctOutputId, RctOutput, TxHash, TxId},
20};
21
22//---------------------------------------------------------------------------------------------------- Private
23/// Add a [`Transaction`] (and related data) to the database.
24///
25/// The `block_height` is the block that this `tx` belongs to.
26///
27/// Note that the caller's input is trusted implicitly and no checks
28/// are done (in this function) whether the `block_height` is correct or not.
29///
30#[doc = doc_add_block_inner_invariant!()]
31///
32/// # Notes
33/// This function is different from other sub-functions and slightly more similar to
34/// [`add_block()`](crate::ops::block::add_block) in that it calls other sub-functions.
35///
36/// This function calls:
37/// - [`add_output()`]
38/// - [`add_rct_output()`]
39/// - [`add_key_image()`]
40///
41/// Thus, after [`add_tx`], those values (outputs and key images)
42/// will be added to database tables as well.
43///
44/// # Panics
45/// This function will panic if:
46/// - `block.height > u32::MAX` (not normally possible)
47#[doc = doc_error!()]
48#[inline]
49pub fn add_tx(
50    tx: &Transaction,
51    tx_blob: &Vec<u8>,
52    tx_hash: &TxHash,
53    block_height: &BlockHeight,
54    tables: &mut impl TablesMut,
55) -> DbResult<TxId> {
56    let tx_id = get_num_tx(tables.tx_ids_mut())?;
57
58    //------------------------------------------------------ Transaction data
59    tables.tx_ids_mut().put(tx_hash, &tx_id)?;
60    tables.tx_heights_mut().put(&tx_id, block_height)?;
61    tables
62        .tx_blobs_mut()
63        .put(&tx_id, StorableVec::wrap_ref(tx_blob))?;
64
65    //------------------------------------------------------ Timelocks
66    // Height/time is not differentiated via type, but rather:
67    // "height is any value less than 500_000_000 and timestamp is any value above"
68    // so the `u64/usize` is stored without any tag.
69    //
70    // <https://github.com/Cuprate/cuprate/pull/102#discussion_r1558504285>
71    match tx.prefix().additional_timelock {
72        Timelock::None => (),
73        Timelock::Block(height) => tables.tx_unlock_time_mut().put(&tx_id, &(height as u64))?,
74        Timelock::Time(time) => tables.tx_unlock_time_mut().put(&tx_id, &time)?,
75    }
76
77    //------------------------------------------------------ Pruning
78    // SOMEDAY: implement pruning after `monero-serai` does.
79    // if let PruningSeed::Pruned(decompressed_pruning_seed) = get_blockchain_pruning_seed()? {
80    // SOMEDAY: what to store here? which table?
81    // }
82
83    //------------------------------------------------------
84    let Ok(height) = u32::try_from(*block_height) else {
85        panic!("add_tx(): block_height ({block_height}) > u32::MAX");
86    };
87
88    //------------------------------------------------------ Key Images
89    // Is this a miner transaction?
90    // Which table we add the output data to depends on this.
91    // <https://github.com/monero-project/monero/blob/eac1b86bb2818ac552457380c9dd421fb8935e5b/src/blockchain_db/blockchain_db.cpp#L212-L216>
92    let mut miner_tx = false;
93
94    // Key images.
95    for inputs in &tx.prefix().inputs {
96        match inputs {
97            // Key images.
98            Input::ToKey { key_image, .. } => {
99                add_key_image(key_image.as_bytes(), tables.key_images_mut())?;
100            }
101            // This is a miner transaction, set it for later use.
102            Input::Gen(_) => miner_tx = true,
103        }
104    }
105
106    //------------------------------------------------------ Outputs
107    // Output bit flags.
108    // Set to a non-zero bit value if the unlock time is non-zero.
109    let output_flags = match tx.prefix().additional_timelock {
110        Timelock::None => OutputFlags::empty(),
111        Timelock::Block(_) | Timelock::Time(_) => OutputFlags::NON_ZERO_UNLOCK_TIME,
112    };
113
114    let amount_indices = match &tx {
115        Transaction::V1 { prefix, .. } => prefix
116            .outputs
117            .iter()
118            .map(|output| {
119                // Pre-RingCT outputs.
120                Ok(add_output(
121                    output.amount.unwrap_or(0),
122                    &Output {
123                        key: output.key.0,
124                        height,
125                        output_flags,
126                        tx_idx: tx_id,
127                    },
128                    tables,
129                )?
130                .amount_index)
131            })
132            .collect::<DbResult<Vec<_>>>()?,
133        Transaction::V2 { prefix, proofs } => prefix
134            .outputs
135            .iter()
136            .enumerate()
137            .map(|(i, output)| {
138                // Create commitment.
139
140                let commitment = if miner_tx {
141                    compute_zero_commitment(output.amount.unwrap_or(0))
142                } else {
143                    proofs
144                        .as_ref()
145                        .expect("A V2 transaction with no RCT proofs is a miner tx")
146                        .base
147                        .commitments[i]
148                };
149
150                // Add the RCT output.
151                add_rct_output(
152                    &RctOutput {
153                        key: output.key.0,
154                        height,
155                        output_flags,
156                        tx_idx: tx_id,
157                        commitment: commitment.0,
158                    },
159                    tables.rct_outputs_mut(),
160                )
161            })
162            .collect::<Result<Vec<_>, _>>()?,
163    };
164
165    tables
166        .tx_outputs_mut()
167        .put(&tx_id, &StorableVec(amount_indices))?;
168
169    Ok(tx_id)
170}
171
172/// Remove a transaction from the database with its [`TxHash`].
173///
174/// This returns the [`TxId`] and [`TxBlob`](crate::types::TxBlob) of the removed transaction.
175///
176#[doc = doc_add_block_inner_invariant!()]
177///
178/// # Notes
179/// As mentioned in [`add_tx`], this function will call other sub-functions:
180/// - [`remove_output()`]
181/// - [`remove_rct_output()`]
182/// - [`remove_key_image()`]
183///
184/// Thus, after [`remove_tx`], those values (outputs and key images)
185/// will be remove from database tables as well.
186///
187#[doc = doc_error!()]
188#[inline]
189pub fn remove_tx(tx_hash: &TxHash, tables: &mut impl TablesMut) -> DbResult<(TxId, Transaction)> {
190    //------------------------------------------------------ Transaction data
191    let tx_id = tables.tx_ids_mut().take(tx_hash)?;
192    let tx_blob = tables.tx_blobs_mut().take(&tx_id)?;
193    tables.tx_heights_mut().delete(&tx_id)?;
194    tables.tx_outputs_mut().delete(&tx_id)?;
195
196    //------------------------------------------------------ Pruning
197    // SOMEDAY: implement pruning after `monero-serai` does.
198    // table_prunable_hashes.delete(&tx_id)?;
199    // table_prunable_tx_blobs.delete(&tx_id)?;
200    // if let PruningSeed::Pruned(decompressed_pruning_seed) = get_blockchain_pruning_seed()? {
201    // SOMEDAY: what to remove here? which table?
202    // }
203
204    //------------------------------------------------------ Unlock Time
205    match tables.tx_unlock_time_mut().delete(&tx_id) {
206        Ok(()) | Err(RuntimeError::KeyNotFound) => (),
207        // An actual error occurred, return.
208        Err(e) => return Err(e),
209    }
210
211    //------------------------------------------------------
212    // Refer to the inner transaction type from now on.
213    let tx = Transaction::read(&mut tx_blob.0.as_slice())?;
214
215    //------------------------------------------------------ Key Images
216    // Is this a miner transaction?
217    let mut miner_tx = false;
218    for inputs in &tx.prefix().inputs {
219        match inputs {
220            // Key images.
221            Input::ToKey { key_image, .. } => {
222                remove_key_image(key_image.as_bytes(), tables.key_images_mut())?;
223            }
224            // This is a miner transaction, set it for later use.
225            Input::Gen(_) => miner_tx = true,
226        }
227    } // for each input
228
229    //------------------------------------------------------ Outputs
230    // Remove each output in the transaction.
231    for output in &tx.prefix().outputs {
232        // Outputs with clear amounts.
233        if let Some(amount) = output.amount {
234            // RingCT miner outputs.
235            if miner_tx && tx.version() == 2 {
236                let amount_index = get_rct_num_outputs(tables.rct_outputs())? - 1;
237                remove_rct_output(&amount_index, tables.rct_outputs_mut())?;
238            // Pre-RingCT outputs.
239            } else {
240                let amount_index = tables.num_outputs_mut().get(&amount)? - 1;
241                remove_output(
242                    &PreRctOutputId {
243                        amount,
244                        amount_index,
245                    },
246                    tables,
247                )?;
248            }
249        // RingCT outputs.
250        } else {
251            let amount_index = get_rct_num_outputs(tables.rct_outputs())? - 1;
252            remove_rct_output(&amount_index, tables.rct_outputs_mut())?;
253        }
254    } // for each output
255
256    Ok((tx_id, tx))
257}
258
259//---------------------------------------------------------------------------------------------------- `get_tx_*`
260/// Retrieve a [`Transaction`] from the database with its [`TxHash`].
261#[doc = doc_error!()]
262#[inline]
263pub fn get_tx(
264    tx_hash: &TxHash,
265    table_tx_ids: &impl DatabaseRo<TxIds>,
266    table_tx_blobs: &impl DatabaseRo<TxBlobs>,
267) -> DbResult<Transaction> {
268    get_tx_from_id(&table_tx_ids.get(tx_hash)?, table_tx_blobs)
269}
270
271/// Retrieve a [`Transaction`] from the database with its [`TxId`].
272#[doc = doc_error!()]
273#[inline]
274pub fn get_tx_from_id(
275    tx_id: &TxId,
276    table_tx_blobs: &impl DatabaseRo<TxBlobs>,
277) -> DbResult<Transaction> {
278    let tx_blob = table_tx_blobs.get(tx_id)?.0;
279    Ok(Transaction::read(&mut tx_blob.as_slice())?)
280}
281
282//----------------------------------------------------------------------------------------------------
283/// How many [`Transaction`]s are there?
284///
285/// This returns the amount of transactions currently stored.
286///
287/// For example:
288/// - 0 transactions exist => returns 0
289/// - 1 transactions exist => returns 1
290/// - 5 transactions exist => returns 5
291/// - etc
292#[doc = doc_error!()]
293#[inline]
294pub fn get_num_tx(table_tx_ids: &impl DatabaseRo<TxIds>) -> DbResult<u64> {
295    table_tx_ids.len()
296}
297
298//----------------------------------------------------------------------------------------------------
299/// Check if a transaction exists in the database.
300///
301/// Returns `true` if it does, else `false`.
302#[doc = doc_error!()]
303#[inline]
304pub fn tx_exists(tx_hash: &TxHash, table_tx_ids: &impl DatabaseRo<TxIds>) -> DbResult<bool> {
305    table_tx_ids.contains(tx_hash)
306}
307
308//---------------------------------------------------------------------------------------------------- Tests
309#[cfg(test)]
310mod test {
311    use super::*;
312
313    use pretty_assertions::assert_eq;
314
315    use cuprate_database::{Env, EnvInner, TxRw};
316    use cuprate_test_utils::data::{TX_V1_SIG0, TX_V1_SIG2, TX_V2_RCT3};
317
318    use crate::{
319        tables::{OpenTables, Tables},
320        tests::{assert_all_tables_are_empty, tmp_concrete_env, AssertTableLen},
321    };
322
323    /// Tests all above tx functions when only inputting `Transaction` data (no Block).
324    #[test]
325    fn all_tx_functions() {
326        let (env, _tmp) = tmp_concrete_env();
327        let env_inner = env.env_inner();
328        assert_all_tables_are_empty(&env);
329
330        // Monero `Transaction`, not database tx.
331        let txs = [&*TX_V1_SIG0, &*TX_V1_SIG2, &*TX_V2_RCT3];
332
333        // Add transactions.
334        let tx_ids = {
335            let tx_rw = env_inner.tx_rw().unwrap();
336            let mut tables = env_inner.open_tables_mut(&tx_rw).unwrap();
337
338            let tx_ids = txs
339                .iter()
340                .map(|tx| {
341                    println!("add_tx(): {tx:#?}");
342                    add_tx(&tx.tx, &tx.tx_blob, &tx.tx_hash, &0, &mut tables).unwrap()
343                })
344                .collect::<Vec<TxId>>();
345
346            drop(tables);
347            TxRw::commit(tx_rw).unwrap();
348
349            tx_ids
350        };
351
352        // Assert all reads of the transactions are OK.
353        let tx_hashes = {
354            let tx_ro = env_inner.tx_ro().unwrap();
355            let tables = env_inner.open_tables(&tx_ro).unwrap();
356
357            // Assert only the proper tables were added to.
358            AssertTableLen {
359                block_infos: 0,
360                block_header_blobs: 0,
361                block_txs_hashes: 0,
362                block_heights: 0,
363                key_images: 4, // added to key images
364                pruned_tx_blobs: 0,
365                prunable_hashes: 0,
366                num_outputs: 9,
367                outputs: 10, // added to outputs
368                prunable_tx_blobs: 0,
369                rct_outputs: 2,
370                tx_blobs: 3,
371                tx_ids: 3,
372                tx_heights: 3,
373                tx_unlock_time: 1, // only 1 has a timelock
374            }
375            .assert(&tables);
376
377            // Both from ID and hash should result in getting the same transaction.
378            let mut tx_hashes = vec![];
379            for (i, tx_id) in tx_ids.iter().enumerate() {
380                println!("tx_ids.iter(): i: {i}, tx_id: {tx_id}");
381
382                let tx_get_from_id = get_tx_from_id(tx_id, tables.tx_blobs()).unwrap();
383                let tx_hash = tx_get_from_id.hash();
384                let tx_get = get_tx(&tx_hash, tables.tx_ids(), tables.tx_blobs()).unwrap();
385
386                println!("tx_ids.iter(): tx_get_from_id: {tx_get_from_id:#?}, tx_get: {tx_get:#?}");
387
388                assert_eq!(tx_get_from_id.hash(), tx_get.hash());
389                assert_eq!(tx_get_from_id.hash(), txs[i].tx_hash);
390                assert_eq!(tx_get_from_id, tx_get);
391                assert_eq!(tx_get, txs[i].tx);
392                assert!(tx_exists(&tx_hash, tables.tx_ids()).unwrap());
393
394                tx_hashes.push(tx_hash);
395            }
396
397            tx_hashes
398        };
399
400        // Remove the transactions.
401        {
402            let tx_rw = env_inner.tx_rw().unwrap();
403            let mut tables = env_inner.open_tables_mut(&tx_rw).unwrap();
404
405            for tx_hash in tx_hashes {
406                println!("remove_tx(): tx_hash: {tx_hash:?}");
407
408                let (tx_id, _) = remove_tx(&tx_hash, &mut tables).unwrap();
409                assert!(matches!(
410                    get_tx_from_id(&tx_id, tables.tx_blobs()),
411                    Err(RuntimeError::KeyNotFound)
412                ));
413            }
414
415            drop(tables);
416            TxRw::commit(tx_rw).unwrap();
417        }
418
419        assert_all_tables_are_empty(&env);
420    }
421}