cuprate_blockchain/ops/
output.rs

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