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}