1use monero_oxide::{io::CompressedPoint, transaction::Timelock};
5
6use cuprate_database::{
7 DbResult, RuntimeError, {DatabaseRo, DatabaseRw},
8};
9use cuprate_helper::{
10 cast::{u32_to_usize, u64_to_usize},
11 crypto::compute_zero_commitment,
12 map::u64_to_timelock,
13};
14use cuprate_types::OutputOnChain;
15
16use crate::{
17 ops::{
18 macros::{doc_add_block_inner_invariant, doc_error},
19 tx::get_tx_from_id,
20 },
21 tables::{
22 BlockInfos, BlockTxsHashes, Outputs, RctOutputs, Tables, TablesMut, TxBlobs, TxUnlockTime,
23 },
24 types::{Amount, AmountIndex, Output, OutputFlags, PreRctOutputId, RctOutput},
25};
26
27#[doc = doc_add_block_inner_invariant!()]
34#[doc = doc_error!()]
35#[inline]
36pub fn add_output(
37 amount: Amount,
38 output: &Output,
39 tables: &mut impl TablesMut,
40) -> DbResult<PreRctOutputId> {
41 let num_outputs = match tables.num_outputs().get(&amount) {
44 Ok(num_outputs) => num_outputs,
46 Err(RuntimeError::KeyNotFound) => 0,
49 Err(e) => return Err(e),
50 };
51 tables.num_outputs_mut().put(&amount, &(num_outputs + 1))?;
53
54 let pre_rct_output_id = PreRctOutputId {
55 amount,
56 amount_index: num_outputs,
58 };
59
60 tables.outputs_mut().put(&pre_rct_output_id, output)?;
61 Ok(pre_rct_output_id)
62}
63
64#[doc = doc_add_block_inner_invariant!()]
66#[doc = doc_error!()]
67#[inline]
68pub fn remove_output(
69 pre_rct_output_id: &PreRctOutputId,
70 tables: &mut impl TablesMut,
71) -> DbResult<()> {
72 tables
76 .num_outputs_mut()
77 .update(&pre_rct_output_id.amount, |num_outputs| {
78 if num_outputs == 1 {
80 None
81 } else {
82 Some(num_outputs - 1)
83 }
84 })?;
85
86 tables.outputs_mut().delete(pre_rct_output_id)
88}
89
90#[doc = doc_error!()]
92#[inline]
93pub fn get_output(
94 pre_rct_output_id: &PreRctOutputId,
95 table_outputs: &impl DatabaseRo<Outputs>,
96) -> DbResult<Output> {
97 table_outputs.get(pre_rct_output_id)
98}
99
100#[doc = doc_error!()]
104#[inline]
105pub fn get_num_outputs(table_outputs: &impl DatabaseRo<Outputs>) -> DbResult<u64> {
106 table_outputs.len()
107}
108
109#[doc = doc_add_block_inner_invariant!()]
115#[doc = doc_error!()]
116#[inline]
117pub fn add_rct_output(
118 rct_output: &RctOutput,
119 table_rct_outputs: &mut impl DatabaseRw<RctOutputs>,
120) -> DbResult<AmountIndex> {
121 let amount_index = get_rct_num_outputs(table_rct_outputs)?;
122 table_rct_outputs.put(&amount_index, rct_output)?;
123 Ok(amount_index)
124}
125
126#[doc = doc_add_block_inner_invariant!()]
128#[doc = doc_error!()]
129#[inline]
130pub fn remove_rct_output(
131 amount_index: &AmountIndex,
132 table_rct_outputs: &mut impl DatabaseRw<RctOutputs>,
133) -> DbResult<()> {
134 table_rct_outputs.delete(amount_index)
135}
136
137#[doc = doc_error!()]
139#[inline]
140pub fn get_rct_output(
141 amount_index: &AmountIndex,
142 table_rct_outputs: &impl DatabaseRo<RctOutputs>,
143) -> DbResult<RctOutput> {
144 table_rct_outputs.get(amount_index)
145}
146
147#[doc = doc_error!()]
151#[inline]
152pub fn get_rct_num_outputs(table_rct_outputs: &impl DatabaseRo<RctOutputs>) -> DbResult<u64> {
153 table_rct_outputs.len()
154}
155
156#[doc = doc_error!()]
159pub fn output_to_output_on_chain(
160 output: &Output,
161 amount: Amount,
162 get_txid: bool,
163 table_tx_unlock_time: &impl DatabaseRo<TxUnlockTime>,
164 table_block_txs_hashes: &impl DatabaseRo<BlockTxsHashes>,
165 table_block_infos: &impl DatabaseRo<BlockInfos>,
166 table_tx_blobs: &impl DatabaseRo<TxBlobs>,
167) -> DbResult<OutputOnChain> {
168 let commitment = compute_zero_commitment(amount);
169
170 let time_lock = if output
171 .output_flags
172 .contains(OutputFlags::NON_ZERO_UNLOCK_TIME)
173 {
174 u64_to_timelock(table_tx_unlock_time.get(&output.tx_idx)?)
175 } else {
176 Timelock::None
177 };
178
179 let key = CompressedPoint(output.key);
180
181 let txid = if get_txid {
182 let height = u32_to_usize(output.height);
183
184 let miner_tx_id = table_block_infos.get(&height)?.mining_tx_index;
185
186 let txid = if miner_tx_id == output.tx_idx {
187 get_tx_from_id(&miner_tx_id, table_tx_blobs)?.hash()
188 } else {
189 let idx = u64_to_usize(output.tx_idx - miner_tx_id - 1);
190 table_block_txs_hashes.get(&height)?[idx]
191 };
192
193 Some(txid)
194 } else {
195 None
196 };
197
198 Ok(OutputOnChain {
199 height: u32_to_usize(output.height),
200 time_lock,
201 key,
202 commitment,
203 txid,
204 })
205}
206
207#[doc = doc_error!()]
216pub fn rct_output_to_output_on_chain(
217 rct_output: &RctOutput,
218 get_txid: bool,
219 table_tx_unlock_time: &impl DatabaseRo<TxUnlockTime>,
220 table_block_txs_hashes: &impl DatabaseRo<BlockTxsHashes>,
221 table_block_infos: &impl DatabaseRo<BlockInfos>,
222 table_tx_blobs: &impl DatabaseRo<TxBlobs>,
223) -> DbResult<OutputOnChain> {
224 let commitment = CompressedPoint(rct_output.commitment);
226
227 let time_lock = if rct_output
228 .output_flags
229 .contains(OutputFlags::NON_ZERO_UNLOCK_TIME)
230 {
231 u64_to_timelock(table_tx_unlock_time.get(&rct_output.tx_idx)?)
232 } else {
233 Timelock::None
234 };
235
236 let key = CompressedPoint(rct_output.key);
237
238 let txid = if get_txid {
239 let height = u32_to_usize(rct_output.height);
240
241 let miner_tx_id = table_block_infos.get(&height)?.mining_tx_index;
242
243 let txid = if miner_tx_id == rct_output.tx_idx {
244 get_tx_from_id(&miner_tx_id, table_tx_blobs)?.hash()
245 } else {
246 let idx = u64_to_usize(rct_output.tx_idx - miner_tx_id - 1);
247 table_block_txs_hashes.get(&height)?[idx]
248 };
249
250 Some(txid)
251 } else {
252 None
253 };
254
255 Ok(OutputOnChain {
256 height: u32_to_usize(rct_output.height),
257 time_lock,
258 key,
259 commitment,
260 txid,
261 })
262}
263
264#[doc = doc_error!()]
268pub fn id_to_output_on_chain(
269 id: &PreRctOutputId,
270 get_txid: bool,
271 tables: &impl Tables,
272) -> DbResult<OutputOnChain> {
273 if id.amount == 0 {
275 let rct_output = get_rct_output(&id.amount_index, tables.rct_outputs())?;
276 let output_on_chain = rct_output_to_output_on_chain(
277 &rct_output,
278 get_txid,
279 tables.tx_unlock_time(),
280 tables.block_txs_hashes(),
281 tables.block_infos(),
282 tables.tx_blobs(),
283 )?;
284
285 Ok(output_on_chain)
286 } else {
287 let output = get_output(id, tables.outputs())?;
289 let output_on_chain = output_to_output_on_chain(
290 &output,
291 id.amount,
292 get_txid,
293 tables.tx_unlock_time(),
294 tables.block_txs_hashes(),
295 tables.block_infos(),
296 tables.tx_blobs(),
297 )?;
298
299 Ok(output_on_chain)
300 }
301}
302
303#[cfg(test)]
305mod test {
306 use super::*;
307
308 use pretty_assertions::assert_eq;
309
310 use cuprate_database::{Env, EnvInner};
311
312 use crate::{
313 tables::{OpenTables, Tables, TablesMut},
314 tests::{assert_all_tables_are_empty, tmp_concrete_env, AssertTableLen},
315 types::OutputFlags,
316 };
317
318 const OUTPUT: Output = Output {
320 key: [44; 32],
321 height: 0,
322 output_flags: OutputFlags::NON_ZERO_UNLOCK_TIME,
323 tx_idx: 0,
324 };
325
326 const RCT_OUTPUT: RctOutput = RctOutput {
328 key: [88; 32],
329 height: 1,
330 output_flags: OutputFlags::empty(),
331 tx_idx: 1,
332 commitment: [100; 32],
333 };
334
335 const AMOUNT: Amount = 22;
337
338 #[test]
346 fn all_output_functions() {
347 let (env, _tmp) = tmp_concrete_env();
348 let env_inner = env.env_inner();
349 assert_all_tables_are_empty(&env);
350
351 let tx_rw = env_inner.tx_rw().unwrap();
352 let mut tables = env_inner.open_tables_mut(&tx_rw).unwrap();
353
354 assert_eq!(get_num_outputs(tables.outputs()).unwrap(), 0);
356 assert_eq!(get_rct_num_outputs(tables.rct_outputs()).unwrap(), 0);
357
358 let pre_rct_output_id = add_output(AMOUNT, &OUTPUT, &mut tables).unwrap();
360 let amount_index = add_rct_output(&RCT_OUTPUT, tables.rct_outputs_mut()).unwrap();
361
362 assert_eq!(
363 pre_rct_output_id,
364 PreRctOutputId {
365 amount: AMOUNT,
366 amount_index: 0,
367 }
368 );
369
370 {
372 AssertTableLen {
374 block_infos: 0,
375 block_header_blobs: 0,
376 block_txs_hashes: 0,
377 block_heights: 0,
378 key_images: 0,
379 num_outputs: 1,
380 pruned_tx_blobs: 0,
381 prunable_hashes: 0,
382 outputs: 1,
383 prunable_tx_blobs: 0,
384 rct_outputs: 1,
385 tx_blobs: 0,
386 tx_ids: 0,
387 tx_heights: 0,
388 tx_unlock_time: 0,
389 }
390 .assert(&tables);
391
392 assert_eq!(get_num_outputs(tables.outputs()).unwrap(), 1);
394 assert_eq!(get_rct_num_outputs(tables.rct_outputs()).unwrap(), 1);
395 assert_eq!(1, tables.num_outputs().get(&AMOUNT).unwrap());
396
397 assert_eq!(
399 OUTPUT,
400 get_output(&pre_rct_output_id, tables.outputs()).unwrap(),
401 );
402
403 assert_eq!(
404 RCT_OUTPUT,
405 get_rct_output(&amount_index, tables.rct_outputs()).unwrap(),
406 );
407 }
408
409 {
411 remove_output(&pre_rct_output_id, &mut tables).unwrap();
412 remove_rct_output(&amount_index, tables.rct_outputs_mut()).unwrap();
413
414 assert!(matches!(
416 get_output(&pre_rct_output_id, tables.outputs()),
417 Err(RuntimeError::KeyNotFound)
418 ));
419 assert!(matches!(
420 get_rct_output(&amount_index, tables.rct_outputs()),
421 Err(RuntimeError::KeyNotFound)
422 ));
423
424 assert_eq!(get_num_outputs(tables.outputs()).unwrap(), 0);
426 assert_eq!(get_rct_num_outputs(tables.rct_outputs()).unwrap(), 0);
427 }
428
429 assert_all_tables_are_empty(&env);
430 }
431}