Skip to main content

cuprate_blockchain/ops/
tx.rs

1//! Transaction functions.
2use std::collections::HashMap;
3
4use fjall::Readable;
5use monero_oxide::transaction::{Input, Pruned, Timelock, Transaction};
6use tapes::{TapesAppend, TapesRead};
7
8use cuprate_helper::{cast::usize_to_u64, crypto::compute_zero_commitment};
9use cuprate_pruning::CRYPTONOTE_PRUNING_LOG_STRIPES;
10
11use crate::{
12    database::PrunableTables,
13    error::{BlockchainError, DbResult},
14    ops::output::{add_output, remove_output},
15    types::{Amount, BlockHeight, Output, RctOutput, TxHash, TxId, TxInfo},
16    BlockchainDatabase,
17};
18
19/// Adds the tx info and related data to the tapes, this does not add the tx blob to the tapes.
20#[expect(clippy::too_many_arguments)]
21pub fn add_tx_info_to_tapes(
22    tx: &Transaction<Pruned>,
23    pruned_blob_idx: u64,
24    prunable_blob_idx: u64,
25    pruned_size: usize,
26    prunable_size: usize,
27    height: &BlockHeight,
28    numb_rct_outputs: &mut u64,
29    append_tx: &mut tapes::TapesAppendTransaction,
30    db: &BlockchainDatabase,
31) -> DbResult<TxId> {
32    let tx_id = append_tx
33        .fixed_sized_tape_len(&db.tx_infos)
34        .expect("Required tape was not open.");
35
36    append_tx.append_entries(
37        &db.tx_infos,
38        &[TxInfo {
39            height: *height,
40            pruned_blob_idx,
41            prunable_blob_idx,
42            pruned_size,
43            prunable_size,
44            rct_output_start_idx: if tx.version() == 1 {
45                u64::MAX
46            } else {
47                *numb_rct_outputs
48            },
49            numb_rct_outputs: tx.prefix().outputs.len(),
50        }],
51    )?;
52
53    if let PrunableTables::Pruned { prunable_tip, .. } = &db.prunable_tables {
54        append_tx.append_entries(prunable_tip, &[prunable_blob_idx])?;
55    }
56
57    let timelock = match tx.prefix().additional_timelock {
58        Timelock::None => 0,
59        Timelock::Block(height) => usize_to_u64(height),
60        Timelock::Time(time) => time,
61    };
62
63    //------------------------------------------------------ Key Images
64    // Is this a miner transaction?
65    // Which table we add the output data to depends on this.
66    // <https://github.com/monero-project/monero/blob/eac1b86bb2818ac552457380c9dd421fb8935e5b/src/blockchain_db/blockchain_db.cpp#L212-L216>
67    let miner_tx = matches!(tx.prefix().inputs.as_slice(), &[Input::Gen(_)]);
68
69    if let Transaction::V2 { prefix, proofs } = &tx {
70        for (i, output) in prefix.outputs.iter().enumerate() {
71            // Create commitment.
72            let commitment = if miner_tx {
73                compute_zero_commitment(output.amount.unwrap_or(0))
74            } else {
75                proofs
76                    .as_ref()
77                    .expect("A V2 transaction with no RCT proofs is a miner tx")
78                    .base
79                    .commitments[i]
80            };
81
82            append_tx.append_entries(
83                &db.rct_outputs,
84                &[RctOutput {
85                    key: output.key.to_bytes(),
86                    height: *height,
87                    timelock,
88                    tx_idx: tx_id,
89                    commitment: commitment.to_bytes(),
90                }],
91            )?;
92
93            *numb_rct_outputs += 1;
94        }
95    }
96
97    Ok(tx_id)
98}
99
100/// Adds the tx info and related data to the dynamic tables.
101pub fn add_tx_info_to_dynamic_tables(
102    db: &BlockchainDatabase,
103    tx: &Transaction<Pruned>,
104    tx_id: TxId,
105    tx_hash: &TxHash,
106    height: &BlockHeight,
107    w: &mut fjall::OwnedWriteBatch,
108    pre_rct_numb_outputs_cache: &mut HashMap<Amount, u64>,
109) -> DbResult<()> {
110    w.insert(&db.tx_ids, tx_hash, tx_id.to_le_bytes());
111
112    //------------------------------------------------------ Timelocks
113    // Height/time is not differentiated via type, but rather:
114    // "height is any value less than 500_000_000 and timestamp is any value above"
115    // so the `u64/usize` is stored without any tag.
116    //
117    // <https://github.com/Cuprate/cuprate/pull/102#discussion_r1558504285>
118    let timelock = match tx.prefix().additional_timelock {
119        Timelock::None => 0,
120        Timelock::Block(height) => usize_to_u64(height),
121        Timelock::Time(time) => time,
122    };
123
124    for inputs in &tx.prefix().inputs {
125        match inputs {
126            // Key images.
127            Input::ToKey { key_image, .. } => {
128                w.insert(&db.key_images, key_image.to_bytes(), []);
129            }
130            // This is a miner transaction.
131            Input::Gen(_) => (),
132        }
133    }
134
135    match &tx {
136        Transaction::V1 { prefix, .. } => {
137            let amount_indices = prefix
138                .outputs
139                .iter()
140                .map(|output| {
141                    // Pre-RingCT outputs.
142                    Ok(add_output(
143                        db,
144                        output.amount.unwrap_or(0),
145                        &Output {
146                            key: output.key.to_bytes(),
147                            height: *height,
148                            timelock,
149                            tx_idx: tx_id,
150                        },
151                        w,
152                        pre_rct_numb_outputs_cache,
153                    )?
154                    .amount_index)
155                })
156                .collect::<DbResult<Vec<_>>>()?;
157
158            w.insert(
159                &db.v1_tx_outputs,
160                tx_id.to_le_bytes(),
161                bytemuck::cast_slice::<_, u8>(&amount_indices),
162            );
163        }
164        Transaction::V2 { .. } => {}
165    }
166
167    Ok(())
168}
169
170/// Removes a transaction from the dynamic tables.
171#[inline]
172pub fn remove_tx_from_dynamic_tables(
173    db: &BlockchainDatabase,
174    tx_hash: &TxHash,
175    tx_rw: &mut fjall::OwnedWriteBatch,
176    tapes: &tapes::TapesTruncateTransaction,
177) -> DbResult<(TxId, Transaction<Pruned>)> {
178    let tx_id = u64::from_le_bytes(
179        db.tx_ids
180            .get(tx_hash)?
181            .ok_or(BlockchainError::NotFound)?
182            .as_ref()
183            .try_into()
184            .unwrap(),
185    );
186
187    tx_rw.remove(&db.tx_ids, tx_hash);
188
189    let tx = get_pruned_tx_from_id(&tx_id, tapes, db)?;
190
191    for inputs in &tx.prefix().inputs {
192        match inputs {
193            Input::ToKey { key_image, .. } => {
194                tx_rw.remove(&db.key_images, key_image.to_bytes());
195            }
196            Input::Gen(_) => (),
197        }
198    }
199
200    if tx.version() != 1 {
201        return Ok((tx_id, tx));
202    }
203
204    // Remove each v1 output in the transaction.
205
206    for output in &tx.prefix().outputs {
207        // Outputs with clear amounts.
208        if let Some(amount) = output.amount {
209            remove_output(db, amount, tx_rw)?;
210        }
211    }
212
213    tx_rw.remove(&db.v1_tx_outputs, tx_id.to_le_bytes());
214
215    Ok((tx_id, tx))
216}
217
218//---------------------------------------------------------------------------------------------------- `get_tx_*`
219/// Retrieve a [`Transaction`] from the database with its [`TxHash`].
220#[inline]
221pub fn get_tx(
222    db: &BlockchainDatabase,
223    tx_hash: &TxHash,
224    tx_ro: &fjall::Snapshot,
225    tapes: &impl TapesRead,
226) -> DbResult<Transaction> {
227    let tx_id = tx_ro
228        .get(&db.tx_ids, tx_hash)?
229        .ok_or(BlockchainError::NotFound)?;
230
231    get_tx_from_id(
232        &u64::from_le_bytes(tx_id.as_ref().try_into().unwrap()),
233        tapes,
234        db,
235    )
236}
237
238/// Retrieve a [`Transaction`]'s hash from the database with its [`TxId`].
239///
240/// This is expensive.
241#[inline]
242pub fn get_tx_hash_from_id(
243    tx_id: &TxId,
244    tapes: &impl TapesRead,
245    is_miner_tx: bool,
246    db: &BlockchainDatabase,
247) -> DbResult<[u8; 32]> {
248    let tx_info = tapes
249        .read_entry(&db.tx_infos, *tx_id)?
250        .ok_or(BlockchainError::NotFound)?;
251
252    if tx_info.is_v1_tx() {
253        return Ok(get_tx_from_id(tx_id, tapes, db)?.hash());
254    }
255
256    let pruned_len = if is_miner_tx {
257        tx_info.pruned_size
258    } else {
259        tx_info.pruned_size + 32
260    };
261
262    let mut blob = vec![0; pruned_len];
263
264    tapes.read_bytes(&db.pruned_blobs, tx_info.pruned_blob_idx, &mut blob)?;
265
266    let tx = Transaction::<Pruned>::read(&mut &blob[..tx_info.pruned_size])?;
267
268    let prunable_hash = if is_miner_tx {
269        [0; 32]
270    } else {
271        blob[tx_info.pruned_size..].try_into().unwrap()
272    };
273
274    Ok(tx.hash_with_prunable_hash(prunable_hash).unwrap())
275}
276
277/// Retrieve a [`Transaction`] from the database with its [`TxId`].
278#[inline]
279pub fn get_tx_from_id(
280    tx_id: &TxId,
281    tapes: &impl TapesRead,
282    db: &BlockchainDatabase,
283) -> DbResult<Transaction> {
284    let blob = get_tx_blob_from_id(tx_id, tapes, db)?;
285    let tx = Transaction::read(&mut blob.as_slice())?;
286
287    Ok(tx)
288}
289
290/// Retrieve a pruned transaction from the database with its [`TxId`].
291#[inline]
292pub fn get_pruned_tx_from_id(
293    tx_id: &TxId,
294    tapes: &impl TapesRead,
295    db: &BlockchainDatabase,
296) -> DbResult<Transaction<Pruned>> {
297    let tx_info = tapes
298        .read_entry(&db.tx_infos, *tx_id)?
299        .ok_or(BlockchainError::NotFound)?;
300    let mut blob = vec![0; tx_info.pruned_size];
301    tapes.read_bytes(&db.pruned_blobs, tx_info.pruned_blob_idx, &mut blob)?;
302
303    Ok(Transaction::read(&mut blob.as_slice())?)
304}
305
306/// Returns a transaction's split blobs and prunable hash from its [`TxInfo`].
307pub(crate) fn get_split_tx_blobs(
308    tx_id: TxId,
309    tx_info: &TxInfo,
310    is_miner_tx: bool,
311    tapes: &impl TapesRead,
312    db: &BlockchainDatabase,
313) -> DbResult<(Vec<u8>, Vec<u8>, [u8; 32])> {
314    let pruned_len = if is_miner_tx {
315        tx_info.pruned_size
316    } else {
317        tx_info.pruned_size + 32
318    };
319    let mut pruned_blob = vec![0; pruned_len];
320    tapes.read_bytes(&db.pruned_blobs, tx_info.pruned_blob_idx, &mut pruned_blob)?;
321
322    let prunable_hash = if is_miner_tx {
323        [0; 32]
324    } else {
325        pruned_blob[tx_info.pruned_size..].try_into().unwrap()
326    };
327    pruned_blob.truncate(tx_info.pruned_size);
328
329    let prunable_blob = if tx_info.prunable_size == 0 {
330        vec![]
331    } else {
332        let mut prunable_blob = vec![0; tx_info.prunable_size];
333
334        match read_prunable_tape(&tx_id, tx_info, &mut prunable_blob, tapes, db) {
335            Ok(()) => prunable_blob,
336            Err(BlockchainError::NotFound) => vec![],
337            Err(e) => return Err(e),
338        }
339    };
340
341    Ok((pruned_blob, prunable_blob, prunable_hash))
342}
343
344/// Returns the tx-blob from a tx id.
345pub fn get_tx_blob_from_id(
346    tx_id: &TxId,
347    tapes: &impl TapesRead,
348    db: &BlockchainDatabase,
349) -> DbResult<Vec<u8>> {
350    let tx_info = tapes
351        .read_entry(&db.tx_infos, *tx_id)?
352        .ok_or(BlockchainError::NotFound)?;
353
354    let mut blob = vec![0; tx_info.pruned_size + tx_info.prunable_size];
355
356    tapes.read_bytes(
357        &db.pruned_blobs,
358        tx_info.pruned_blob_idx,
359        &mut blob[..tx_info.pruned_size],
360    )?;
361
362    if tx_info.prunable_size != 0 {
363        read_prunable_tape(tx_id, &tx_info, &mut blob[tx_info.pruned_size..], tapes, db)?;
364    }
365
366    Ok(blob)
367}
368
369//----------------------------------------------------------------------------------------------------
370/// How many [`Transaction`]s are there?
371///
372/// This returns the amount of transactions currently stored.
373///
374/// For example:
375/// - 0 transactions exist => returns 0
376/// - 1 transactions exist => returns 1
377/// - 5 transactions exist => returns 5
378/// - etc
379#[inline]
380pub fn get_num_tx(db: &BlockchainDatabase, tx_ro: &fjall::Snapshot) -> DbResult<u64> {
381    Ok(usize_to_u64(tx_ro.len(&db.tx_ids)?))
382}
383
384//----------------------------------------------------------------------------------------------------
385/// Check if a transaction exists in the database.
386///
387/// Returns `true` if it does, else `false`.
388#[inline]
389pub fn tx_exists(
390    db: &BlockchainDatabase,
391    tx_hash: &TxHash,
392    tx_ro: &fjall::Snapshot,
393) -> DbResult<bool> {
394    Ok(tx_ro.contains_key(&db.tx_ids, tx_hash)?)
395}
396
397/// Gets a transaction by reading the `prunable_tape` if it is Some or
398/// otherwise, checking the tip block if the tx is there
399///
400/// If it's neither where, it errors.
401#[inline]
402pub fn read_prunable_tape(
403    tx_id: &TxId,
404    tx_info: &TxInfo,
405    buf: &mut [u8],
406    tapes: &impl TapesRead,
407    db: &BlockchainDatabase,
408) -> Result<(), BlockchainError> {
409    if tx_info.is_v1_tx() {
410        tapes.read_bytes(&db.v1_prunable_blobs, tx_info.prunable_blob_idx, buf)?;
411
412        return Ok(());
413    }
414
415    let stripe = cuprate_pruning::get_block_pruning_stripe(
416        tx_info.height,
417        usize::MAX,
418        CRYPTONOTE_PRUNING_LOG_STRIPES,
419    )
420    .unwrap();
421
422    if let Some(prunable_table) = db.prunable_tables.try_get_prunable_tape(stripe) {
423        // We have the tx unpruned.
424        tapes.read_bytes(prunable_table, tx_info.prunable_blob_idx, buf)?;
425    } else {
426        // We have the tx pruned - check the tip blocks.
427        let PrunableTables::Pruned {
428            prunable_tip_blobs,
429            prunable_tip,
430            ..
431        } = &db.prunable_tables
432        else {
433            unreachable!()
434        };
435
436        let prunable_blob_idx = tapes.read_entry(prunable_tip, *tx_id)?;
437
438        if let Some(prunable_blob_idx) = prunable_blob_idx {
439            tapes.read_bytes(prunable_tip_blobs, prunable_blob_idx, buf)?;
440        } else {
441            return Err(BlockchainError::NotFound);
442        }
443    }
444
445    Ok(())
446}