1use 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#[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 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 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
100pub 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 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 Input::ToKey { key_image, .. } => {
128 w.insert(&db.key_images, key_image.to_bytes(), []);
129 }
130 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 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#[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 for output in &tx.prefix().outputs {
207 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#[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#[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#[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#[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
306pub(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
344pub 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#[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#[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#[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 tapes.read_bytes(prunable_table, tx_info.prunable_blob_idx, buf)?;
425 } else {
426 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}