cuprate_blockchain/ops/
output.rs

1//! Output functions.
2
3//---------------------------------------------------------------------------------------------------- Import
4use 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//---------------------------------------------------------------------------------------------------- Pre-RCT Outputs
29/// Add a Pre-RCT [`Output`] to the database.
30///
31/// Upon [`Ok`], this function returns the [`PreRctOutputId`] that
32/// can be used to lookup the `Output` in [`get_output()`].
33///
34#[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    // FIXME: this would be much better expressed with a
43    // `btree_map::Entry`-like API, fix `trait DatabaseRw`.
44    let num_outputs = match tables.num_outputs().get(&amount) {
45        // Entry with `amount` already exists.
46        Ok(num_outputs) => num_outputs,
47        // Entry with `amount` didn't exist, this is
48        // the 1st output with this amount.
49        Err(RuntimeError::KeyNotFound) => 0,
50        Err(e) => return Err(e),
51    };
52    // Update the amount of outputs.
53    tables.num_outputs_mut().put(&amount, &(num_outputs + 1))?;
54
55    let pre_rct_output_id = PreRctOutputId {
56        amount,
57        // The new `amount_index` is the length of amount of outputs with same amount.
58        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/// Remove a Pre-RCT [`Output`] from the database.
66#[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    // Decrement the amount index by 1, or delete the entry out-right.
74    // FIXME: this would be much better expressed with a
75    // `btree_map::Entry`-like API, fix `trait DatabaseRw`.
76    tables
77        .num_outputs_mut()
78        .update(&pre_rct_output_id.amount, |num_outputs| {
79            // INVARIANT: Should never be 0.
80            if num_outputs == 1 {
81                None
82            } else {
83                Some(num_outputs - 1)
84            }
85        })?;
86
87    // Delete the output data itself.
88    tables.outputs_mut().delete(pre_rct_output_id)
89}
90
91/// Retrieve a Pre-RCT [`Output`] from the database.
92#[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/// How many pre-RCT [`Output`]s are there?
102///
103/// This returns the amount of pre-RCT outputs currently stored.
104#[doc = doc_error!()]
105#[inline]
106pub fn get_num_outputs(table_outputs: &impl DatabaseRo<Outputs>) -> DbResult<u64> {
107    table_outputs.len()
108}
109
110//---------------------------------------------------------------------------------------------------- RCT Outputs
111/// Add an [`RctOutput`] to the database.
112///
113/// Upon [`Ok`], this function returns the [`AmountIndex`] that
114/// can be used to lookup the `RctOutput` in [`get_rct_output()`].
115#[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/// Remove an [`RctOutput`] from the database.
128#[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/// Retrieve an [`RctOutput`] from the database.
139#[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/// How many [`RctOutput`]s are there?
149///
150/// This returns the amount of RCT outputs currently stored.
151#[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//---------------------------------------------------------------------------------------------------- Mapping functions
158/// Map an [`Output`] to a [`cuprate_types::OutputOnChain`].
159#[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/// Map an [`RctOutput`] to a [`cuprate_types::OutputOnChain`].
209///
210/// # Panics
211/// This function will panic if `rct_output`'s `commitment` fails to decompress
212/// into a valid [`EdwardsPoint`](curve25519_dalek::edwards::EdwardsPoint).
213///
214/// This should normally not happen as commitments that
215/// are stored in the database should always be valid.
216#[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    // INVARIANT: Commitments stored are valid when stored by the database.
226    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/// Map an [`PreRctOutputId`] to an [`OutputOnChain`].
266///
267/// Note that this still support RCT outputs, in that case, [`PreRctOutputId::amount`] should be `0`.
268#[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    // v2 transactions.
275    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        // v1 transactions.
289        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//---------------------------------------------------------------------------------------------------- Tests
305#[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    /// Dummy `Output`.
320    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    /// Dummy `RctOutput`.
328    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    /// Dummy `Amount`
337    const AMOUNT: Amount = 22;
338
339    /// Tests all above output functions when only inputting `Output` data (no Block).
340    ///
341    /// Note that this doesn't test the correctness of values added, as the
342    /// functions have a pre-condition that the caller handles this.
343    ///
344    /// It simply tests if the proper tables are mutated, and if the data
345    /// stored and retrieved is the same.
346    #[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 length is correct.
356        assert_eq!(get_num_outputs(tables.outputs()).unwrap(), 0);
357        assert_eq!(get_rct_num_outputs(tables.rct_outputs()).unwrap(), 0);
358
359        // Add outputs.
360        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        // Assert all reads of the outputs are OK.
372        {
373            // Assert proper tables were added to.
374            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 length is correct.
394            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 value is save after retrieval.
399            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        // Remove the outputs.
411        {
412            remove_output(&pre_rct_output_id, &mut tables).unwrap();
413            remove_rct_output(&amount_index, tables.rct_outputs_mut()).unwrap();
414
415            // Assert value no longer exists.
416            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 length is correct.
426            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}