1use curve25519_dalek::edwards::CompressedEdwardsY;
5use monero_serai::transaction::{Timelock, Transaction};
6
7use cuprate_database::{
8 DbResult, RuntimeError, {DatabaseRo, DatabaseRw},
9};
10use cuprate_helper::{cast::u32_to_usize, crypto::compute_zero_commitment};
11use cuprate_helper::{cast::u64_to_usize, map::u64_to_timelock};
12use cuprate_types::OutputOnChain;
13
14use crate::{
15 ops::macros::{doc_add_block_inner_invariant, doc_error},
16 tables::{
17 BlockInfos, BlockTxsHashes, Outputs, RctOutputs, Tables, TablesMut, TxBlobs, TxUnlockTime,
18 },
19 types::{Amount, AmountIndex, Output, OutputFlags, PreRctOutputId, RctOutput},
20};
21
22#[doc = doc_add_block_inner_invariant!()]
29#[doc = doc_error!()]
30#[inline]
31pub fn add_output(
32 amount: Amount,
33 output: &Output,
34 tables: &mut impl TablesMut,
35) -> DbResult<PreRctOutputId> {
36 let num_outputs = match tables.num_outputs().get(&amount) {
39 Ok(num_outputs) => num_outputs,
41 Err(RuntimeError::KeyNotFound) => 0,
44 Err(e) => return Err(e),
45 };
46 tables.num_outputs_mut().put(&amount, &(num_outputs + 1))?;
48
49 let pre_rct_output_id = PreRctOutputId {
50 amount,
51 amount_index: num_outputs,
53 };
54
55 tables.outputs_mut().put(&pre_rct_output_id, output)?;
56 Ok(pre_rct_output_id)
57}
58
59#[doc = doc_add_block_inner_invariant!()]
61#[doc = doc_error!()]
62#[inline]
63pub fn remove_output(
64 pre_rct_output_id: &PreRctOutputId,
65 tables: &mut impl TablesMut,
66) -> DbResult<()> {
67 tables
71 .num_outputs_mut()
72 .update(&pre_rct_output_id.amount, |num_outputs| {
73 if num_outputs == 1 {
75 None
76 } else {
77 Some(num_outputs - 1)
78 }
79 })?;
80
81 tables.outputs_mut().delete(pre_rct_output_id)
83}
84
85#[doc = doc_error!()]
87#[inline]
88pub fn get_output(
89 pre_rct_output_id: &PreRctOutputId,
90 table_outputs: &impl DatabaseRo<Outputs>,
91) -> DbResult<Output> {
92 table_outputs.get(pre_rct_output_id)
93}
94
95#[doc = doc_error!()]
99#[inline]
100pub fn get_num_outputs(table_outputs: &impl DatabaseRo<Outputs>) -> DbResult<u64> {
101 table_outputs.len()
102}
103
104#[doc = doc_add_block_inner_invariant!()]
110#[doc = doc_error!()]
111#[inline]
112pub fn add_rct_output(
113 rct_output: &RctOutput,
114 table_rct_outputs: &mut impl DatabaseRw<RctOutputs>,
115) -> DbResult<AmountIndex> {
116 let amount_index = get_rct_num_outputs(table_rct_outputs)?;
117 table_rct_outputs.put(&amount_index, rct_output)?;
118 Ok(amount_index)
119}
120
121#[doc = doc_add_block_inner_invariant!()]
123#[doc = doc_error!()]
124#[inline]
125pub fn remove_rct_output(
126 amount_index: &AmountIndex,
127 table_rct_outputs: &mut impl DatabaseRw<RctOutputs>,
128) -> DbResult<()> {
129 table_rct_outputs.delete(amount_index)
130}
131
132#[doc = doc_error!()]
134#[inline]
135pub fn get_rct_output(
136 amount_index: &AmountIndex,
137 table_rct_outputs: &impl DatabaseRo<RctOutputs>,
138) -> DbResult<RctOutput> {
139 table_rct_outputs.get(amount_index)
140}
141
142#[doc = doc_error!()]
146#[inline]
147pub fn get_rct_num_outputs(table_rct_outputs: &impl DatabaseRo<RctOutputs>) -> DbResult<u64> {
148 table_rct_outputs.len()
149}
150
151#[doc = doc_error!()]
154pub fn output_to_output_on_chain(
155 output: &Output,
156 amount: Amount,
157 get_txid: bool,
158 table_tx_unlock_time: &impl DatabaseRo<TxUnlockTime>,
159 table_block_txs_hashes: &impl DatabaseRo<BlockTxsHashes>,
160 table_block_infos: &impl DatabaseRo<BlockInfos>,
161 table_tx_blobs: &impl DatabaseRo<TxBlobs>,
162) -> DbResult<OutputOnChain> {
163 let commitment = compute_zero_commitment(amount);
164
165 let time_lock = if output
166 .output_flags
167 .contains(OutputFlags::NON_ZERO_UNLOCK_TIME)
168 {
169 u64_to_timelock(table_tx_unlock_time.get(&output.tx_idx)?)
170 } else {
171 Timelock::None
172 };
173
174 let key = CompressedEdwardsY(output.key);
175
176 let txid = if get_txid {
177 let height = u32_to_usize(output.height);
178 let tx_idx = u64_to_usize(output.tx_idx);
179 let txid = if let Some(hash) = table_block_txs_hashes.get(&height)?.get(tx_idx) {
180 *hash
181 } else {
182 let miner_tx_id = table_block_infos.get(&height)?.mining_tx_index;
183 let tx_blob = table_tx_blobs.get(&miner_tx_id)?;
184 Transaction::read(&mut tx_blob.0.as_slice())?.hash()
185 };
186 Some(txid)
187 } else {
188 None
189 };
190
191 Ok(OutputOnChain {
192 height: u32_to_usize(output.height),
193 time_lock,
194 key,
195 commitment,
196 txid,
197 })
198}
199
200#[doc = doc_error!()]
209pub fn rct_output_to_output_on_chain(
210 rct_output: &RctOutput,
211 get_txid: bool,
212 table_tx_unlock_time: &impl DatabaseRo<TxUnlockTime>,
213 table_block_txs_hashes: &impl DatabaseRo<BlockTxsHashes>,
214 table_block_infos: &impl DatabaseRo<BlockInfos>,
215 table_tx_blobs: &impl DatabaseRo<TxBlobs>,
216) -> DbResult<OutputOnChain> {
217 let commitment = CompressedEdwardsY(rct_output.commitment);
219
220 let time_lock = if rct_output
221 .output_flags
222 .contains(OutputFlags::NON_ZERO_UNLOCK_TIME)
223 {
224 u64_to_timelock(table_tx_unlock_time.get(&rct_output.tx_idx)?)
225 } else {
226 Timelock::None
227 };
228
229 let key = CompressedEdwardsY(rct_output.key);
230
231 let txid = if get_txid {
232 let height = u32_to_usize(rct_output.height);
233
234 let miner_tx_id = table_block_infos.get(&height)?.mining_tx_index;
235
236 let txid = if miner_tx_id == rct_output.tx_idx {
237 let tx_blob = table_tx_blobs.get(&miner_tx_id)?;
238 Transaction::read(&mut tx_blob.0.as_slice())?.hash()
239 } else {
240 let idx = u64_to_usize(rct_output.tx_idx - miner_tx_id - 1);
241 table_block_txs_hashes.get(&height)?[idx]
242 };
243
244 Some(txid)
245 } else {
246 None
247 };
248
249 Ok(OutputOnChain {
250 height: u32_to_usize(rct_output.height),
251 time_lock,
252 key,
253 commitment,
254 txid,
255 })
256}
257
258#[doc = doc_error!()]
262pub fn id_to_output_on_chain(
263 id: &PreRctOutputId,
264 get_txid: bool,
265 tables: &impl Tables,
266) -> DbResult<OutputOnChain> {
267 if id.amount == 0 {
269 let rct_output = get_rct_output(&id.amount_index, tables.rct_outputs())?;
270 let output_on_chain = rct_output_to_output_on_chain(
271 &rct_output,
272 get_txid,
273 tables.tx_unlock_time(),
274 tables.block_txs_hashes(),
275 tables.block_infos(),
276 tables.tx_blobs(),
277 )?;
278
279 Ok(output_on_chain)
280 } else {
281 let output = get_output(id, tables.outputs())?;
283 let output_on_chain = output_to_output_on_chain(
284 &output,
285 id.amount,
286 get_txid,
287 tables.tx_unlock_time(),
288 tables.block_txs_hashes(),
289 tables.block_infos(),
290 tables.tx_blobs(),
291 )?;
292
293 Ok(output_on_chain)
294 }
295}
296
297#[cfg(test)]
299mod test {
300 use super::*;
301
302 use pretty_assertions::assert_eq;
303
304 use cuprate_database::{Env, EnvInner};
305
306 use crate::{
307 tables::{OpenTables, Tables, TablesMut},
308 tests::{assert_all_tables_are_empty, tmp_concrete_env, AssertTableLen},
309 types::OutputFlags,
310 };
311
312 const OUTPUT: Output = Output {
314 key: [44; 32],
315 height: 0,
316 output_flags: OutputFlags::NON_ZERO_UNLOCK_TIME,
317 tx_idx: 0,
318 };
319
320 const RCT_OUTPUT: RctOutput = RctOutput {
322 key: [88; 32],
323 height: 1,
324 output_flags: OutputFlags::empty(),
325 tx_idx: 1,
326 commitment: [100; 32],
327 };
328
329 const AMOUNT: Amount = 22;
331
332 #[test]
340 fn all_output_functions() {
341 let (env, _tmp) = tmp_concrete_env();
342 let env_inner = env.env_inner();
343 assert_all_tables_are_empty(&env);
344
345 let tx_rw = env_inner.tx_rw().unwrap();
346 let mut tables = env_inner.open_tables_mut(&tx_rw).unwrap();
347
348 assert_eq!(get_num_outputs(tables.outputs()).unwrap(), 0);
350 assert_eq!(get_rct_num_outputs(tables.rct_outputs()).unwrap(), 0);
351
352 let pre_rct_output_id = add_output(AMOUNT, &OUTPUT, &mut tables).unwrap();
354 let amount_index = add_rct_output(&RCT_OUTPUT, tables.rct_outputs_mut()).unwrap();
355
356 assert_eq!(
357 pre_rct_output_id,
358 PreRctOutputId {
359 amount: AMOUNT,
360 amount_index: 0,
361 }
362 );
363
364 {
366 AssertTableLen {
368 block_infos: 0,
369 block_header_blobs: 0,
370 block_txs_hashes: 0,
371 block_heights: 0,
372 key_images: 0,
373 num_outputs: 1,
374 pruned_tx_blobs: 0,
375 prunable_hashes: 0,
376 outputs: 1,
377 prunable_tx_blobs: 0,
378 rct_outputs: 1,
379 tx_blobs: 0,
380 tx_ids: 0,
381 tx_heights: 0,
382 tx_unlock_time: 0,
383 }
384 .assert(&tables);
385
386 assert_eq!(get_num_outputs(tables.outputs()).unwrap(), 1);
388 assert_eq!(get_rct_num_outputs(tables.rct_outputs()).unwrap(), 1);
389 assert_eq!(1, tables.num_outputs().get(&AMOUNT).unwrap());
390
391 assert_eq!(
393 OUTPUT,
394 get_output(&pre_rct_output_id, tables.outputs()).unwrap(),
395 );
396
397 assert_eq!(
398 RCT_OUTPUT,
399 get_rct_output(&amount_index, tables.rct_outputs()).unwrap(),
400 );
401 }
402
403 {
405 remove_output(&pre_rct_output_id, &mut tables).unwrap();
406 remove_rct_output(&amount_index, tables.rct_outputs_mut()).unwrap();
407
408 assert!(matches!(
410 get_output(&pre_rct_output_id, tables.outputs()),
411 Err(RuntimeError::KeyNotFound)
412 ));
413 assert!(matches!(
414 get_rct_output(&amount_index, tables.rct_outputs()),
415 Err(RuntimeError::KeyNotFound)
416 ));
417
418 assert_eq!(get_num_outputs(tables.outputs()).unwrap(), 0);
420 assert_eq!(get_rct_num_outputs(tables.rct_outputs()).unwrap(), 0);
421 }
422
423 assert_all_tables_are_empty(&env);
424 }
425}