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