cuprate_blockchain/ops/
output.rs

1//! Output functions.
2
3//---------------------------------------------------------------------------------------------------- Import
4use 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//---------------------------------------------------------------------------------------------------- Pre-RCT Outputs
23/// Add a Pre-RCT [`Output`] to the database.
24///
25/// Upon [`Ok`], this function returns the [`PreRctOutputId`] that
26/// can be used to lookup the `Output` in [`get_output()`].
27///
28#[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    // FIXME: this would be much better expressed with a
37    // `btree_map::Entry`-like API, fix `trait DatabaseRw`.
38    let num_outputs = match tables.num_outputs().get(&amount) {
39        // Entry with `amount` already exists.
40        Ok(num_outputs) => num_outputs,
41        // Entry with `amount` didn't exist, this is
42        // the 1st output with this amount.
43        Err(RuntimeError::KeyNotFound) => 0,
44        Err(e) => return Err(e),
45    };
46    // Update the amount of outputs.
47    tables.num_outputs_mut().put(&amount, &(num_outputs + 1))?;
48
49    let pre_rct_output_id = PreRctOutputId {
50        amount,
51        // The new `amount_index` is the length of amount of outputs with same amount.
52        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/// Remove a Pre-RCT [`Output`] from the database.
60#[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    // Decrement the amount index by 1, or delete the entry out-right.
68    // FIXME: this would be much better expressed with a
69    // `btree_map::Entry`-like API, fix `trait DatabaseRw`.
70    tables
71        .num_outputs_mut()
72        .update(&pre_rct_output_id.amount, |num_outputs| {
73            // INVARIANT: Should never be 0.
74            if num_outputs == 1 {
75                None
76            } else {
77                Some(num_outputs - 1)
78            }
79        })?;
80
81    // Delete the output data itself.
82    tables.outputs_mut().delete(pre_rct_output_id)
83}
84
85/// Retrieve a Pre-RCT [`Output`] from the database.
86#[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/// How many pre-RCT [`Output`]s are there?
96///
97/// This returns the amount of pre-RCT outputs currently stored.
98#[doc = doc_error!()]
99#[inline]
100pub fn get_num_outputs(table_outputs: &impl DatabaseRo<Outputs>) -> DbResult<u64> {
101    table_outputs.len()
102}
103
104//---------------------------------------------------------------------------------------------------- RCT Outputs
105/// Add an [`RctOutput`] to the database.
106///
107/// Upon [`Ok`], this function returns the [`AmountIndex`] that
108/// can be used to lookup the `RctOutput` in [`get_rct_output()`].
109#[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/// Remove an [`RctOutput`] from the database.
122#[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/// Retrieve an [`RctOutput`] from the database.
133#[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/// How many [`RctOutput`]s are there?
143///
144/// This returns the amount of RCT outputs currently stored.
145#[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//---------------------------------------------------------------------------------------------------- Mapping functions
152/// Map an [`Output`] to a [`cuprate_types::OutputOnChain`].
153#[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/// Map an [`RctOutput`] to a [`cuprate_types::OutputOnChain`].
201///
202/// # Panics
203/// This function will panic if `rct_output`'s `commitment` fails to decompress
204/// into a valid [`EdwardsPoint`](curve25519_dalek::edwards::EdwardsPoint).
205///
206/// This should normally not happen as commitments that
207/// are stored in the database should always be valid.
208#[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    // INVARIANT: Commitments stored are valid when stored by the database.
218    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/// Map an [`PreRctOutputId`] to an [`OutputOnChain`].
259///
260/// Note that this still support RCT outputs, in that case, [`PreRctOutputId::amount`] should be `0`.
261#[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    // v2 transactions.
268    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        // v1 transactions.
282        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//---------------------------------------------------------------------------------------------------- Tests
298#[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    /// Dummy `Output`.
313    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    /// Dummy `RctOutput`.
321    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    /// Dummy `Amount`
330    const AMOUNT: Amount = 22;
331
332    /// Tests all above output functions when only inputting `Output` data (no Block).
333    ///
334    /// Note that this doesn't test the correctness of values added, as the
335    /// functions have a pre-condition that the caller handles this.
336    ///
337    /// It simply tests if the proper tables are mutated, and if the data
338    /// stored and retrieved is the same.
339    #[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 length is correct.
349        assert_eq!(get_num_outputs(tables.outputs()).unwrap(), 0);
350        assert_eq!(get_rct_num_outputs(tables.rct_outputs()).unwrap(), 0);
351
352        // Add outputs.
353        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        // Assert all reads of the outputs are OK.
365        {
366            // Assert proper tables were added to.
367            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 length is correct.
387            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 value is save after retrieval.
392            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        // Remove the outputs.
404        {
405            remove_output(&pre_rct_output_id, &mut tables).unwrap();
406            remove_rct_output(&amount_index, tables.rct_outputs_mut()).unwrap();
407
408            // Assert value no longer exists.
409            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 length is correct.
419            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}