Skip to main content

cuprate_blockchain/ops/
tx.rs

1//! Transaction functions.
2use std::collections::HashMap;
3
4use cuprate_helper::{cast::usize_to_u64, crypto::compute_zero_commitment};
5use cuprate_pruning::CRYPTONOTE_PRUNING_LOG_STRIPES;
6
7use fjall::Readable;
8use monero_oxide::transaction::{Input, Pruned, Timelock, Transaction};
9use tapes::{TapesAppend, TapesRead};
10
11use crate::{
12    error::{BlockchainError, DbResult},
13    ops::output::{add_output, remove_output},
14    types::{Amount, BlockHeight, Output, RctOutput, TxHash, TxId, TxInfo},
15    BlockchainDatabase,
16};
17
18const EMPTY_PRUNABLE_BLOB_HASH: [u8; 32] = [
19    0xc5, 0xd2, 0x46, 0x01, 0x86, 0xf7, 0x23, 0x3c, 0x92, 0x7e, 0x7d, 0xb2, 0xdc, 0xc7, 0x03, 0xc0,
20    0xe5, 0x00, 0xb6, 0x53, 0xca, 0x82, 0x27, 0x3b, 0x7b, 0xfa, 0xd8, 0x04, 0x5d, 0x85, 0xa4, 0x70,
21];
22
23/// Adds the tx info and related data to the tapes, this does not add the tx blob to the tapes.
24#[expect(clippy::too_many_arguments)]
25pub fn add_tx_info_to_tapes(
26    tx: &Transaction<Pruned>,
27    pruned_blob_idx: u64,
28    prunable_blob_idx: u64,
29    pruned_size: usize,
30    prunable_size: usize,
31    height: &BlockHeight,
32    numb_rct_outputs: &mut u64,
33    append_tx: &mut tapes::TapesAppendTransaction,
34    db: &BlockchainDatabase,
35) -> DbResult<TxId> {
36    let tx_id = append_tx
37        .fixed_sized_tape_len(&db.tx_infos)
38        .expect("Required tape was not open.");
39
40    append_tx.append_entries(
41        &db.tx_infos,
42        &[TxInfo {
43            height: *height,
44            pruned_blob_idx,
45            prunable_blob_idx,
46            pruned_size,
47            prunable_size,
48            rct_output_start_idx: if tx.version() == 1 {
49                u64::MAX
50            } else {
51                *numb_rct_outputs
52            },
53            numb_rct_outputs: tx.prefix().outputs.len(),
54        }],
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 { .. } => return Ok(()),
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)> {
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_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`] from the database with its [`TxId`].
239#[inline]
240pub fn get_tx_from_id(
241    tx_id: &TxId,
242    tapes: &impl TapesRead,
243    db: &BlockchainDatabase,
244) -> DbResult<Transaction> {
245    let blob = get_tx_blob_from_id(tx_id, tapes, db)?;
246    let tx = Transaction::read(&mut blob.as_slice())?;
247
248    Ok(tx)
249}
250
251/// Returns the prunable hash for a miner transaction.
252const fn miner_tx_prunable_hash(tx_info: &TxInfo) -> [u8; 32] {
253    if tx_info.is_v1_tx() {
254        [0; 32]
255    } else {
256        EMPTY_PRUNABLE_BLOB_HASH
257    }
258}
259
260/// Returns a transaction's split blobs and prunable hash from its [`TxInfo`].
261pub(crate) fn get_split_tx_blobs(
262    tx_info: &TxInfo,
263    is_miner_tx: bool,
264    tapes: &impl TapesRead,
265    db: &BlockchainDatabase,
266) -> DbResult<(Vec<u8>, Vec<u8>, [u8; 32])> {
267    let pruned_len = if is_miner_tx {
268        tx_info.pruned_size
269    } else {
270        tx_info.pruned_size + 32
271    };
272    let mut pruned_blob = vec![0; pruned_len];
273    tapes.read_bytes(&db.pruned_blobs, tx_info.pruned_blob_idx, &mut pruned_blob)?;
274
275    let prunable_hash = if is_miner_tx {
276        miner_tx_prunable_hash(tx_info)
277    } else {
278        pruned_blob[tx_info.pruned_size..].try_into().unwrap()
279    };
280    pruned_blob.truncate(tx_info.pruned_size);
281
282    let mut prunable_blob = vec![0; tx_info.prunable_size];
283    if !prunable_blob.is_empty() {
284        let prunable_tape = if tx_info.is_v1_tx() {
285            &db.v1_prunable_blobs
286        } else {
287            let stripe = cuprate_pruning::get_block_pruning_stripe(
288                tx_info.height,
289                usize::MAX,
290                CRYPTONOTE_PRUNING_LOG_STRIPES,
291            )
292            .unwrap();
293
294            &db.prunable_blobs
295                [usize::try_from(stripe).expect("stripe will not exceed usize::MAX") - 1]
296        };
297
298        tapes.read_bytes(prunable_tape, tx_info.prunable_blob_idx, &mut prunable_blob)?;
299    }
300
301    Ok((pruned_blob, prunable_blob, prunable_hash))
302}
303
304/// Returns the tx-blob from a tx id.
305pub fn get_tx_blob_from_id(
306    tx_id: &TxId,
307    tapes: &impl TapesRead,
308    db: &BlockchainDatabase,
309) -> DbResult<Vec<u8>> {
310    let tx_info = tapes
311        .read_entry(&db.tx_infos, *tx_id)?
312        .ok_or(BlockchainError::NotFound)?;
313
314    let mut blob = vec![0; tx_info.pruned_size + tx_info.prunable_size];
315
316    tapes.read_bytes(
317        &db.pruned_blobs,
318        tx_info.pruned_blob_idx,
319        &mut blob[..tx_info.pruned_size],
320    )?;
321
322    let prunable_tape = if tx_info.is_v1_tx() {
323        &db.v1_prunable_blobs
324    } else {
325        let stripe = cuprate_pruning::get_block_pruning_stripe(
326            tx_info.height,
327            usize::MAX,
328            CRYPTONOTE_PRUNING_LOG_STRIPES,
329        )
330        .unwrap();
331        &db.prunable_blobs[usize::try_from(stripe).expect("stripe will not exceed usize::MAX") - 1]
332    };
333
334    tapes.read_bytes(
335        prunable_tape,
336        tx_info.prunable_blob_idx,
337        &mut blob[tx_info.pruned_size..],
338    )?;
339
340    Ok(blob)
341}
342
343//----------------------------------------------------------------------------------------------------
344/// How many [`Transaction`]s are there?
345///
346/// This returns the amount of transactions currently stored.
347///
348/// For example:
349/// - 0 transactions exist => returns 0
350/// - 1 transactions exist => returns 1
351/// - 5 transactions exist => returns 5
352/// - etc
353#[inline]
354pub fn get_num_tx(db: &BlockchainDatabase, tx_ro: &fjall::Snapshot) -> DbResult<u64> {
355    Ok(usize_to_u64(tx_ro.len(&db.tx_ids)?))
356}
357
358//----------------------------------------------------------------------------------------------------
359/// Check if a transaction exists in the database.
360///
361/// Returns `true` if it does, else `false`.
362#[inline]
363pub fn tx_exists(
364    db: &BlockchainDatabase,
365    tx_hash: &TxHash,
366    tx_ro: &fjall::Snapshot,
367) -> DbResult<bool> {
368    Ok(tx_ro.contains_key(&db.tx_ids, tx_hash)?)
369}
370
371#[cfg(test)]
372mod tests {
373    use super::EMPTY_PRUNABLE_BLOB_HASH;
374
375    #[test]
376    fn empty_prunable_blob_hash_correct() {
377        assert_eq!(
378            EMPTY_PRUNABLE_BLOB_HASH,
379            monero_oxide::primitives::keccak256([])
380        );
381    }
382}