1use 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#[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 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 { .. } => return Ok(()),
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)> {
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 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]
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
251const 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
260pub(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
304pub 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#[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#[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}