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::crypto::compute_zero_commitment;
11use cuprate_helper::map::u64_to_timelock;
12use cuprate_types::OutputOnChain;
13
14use crate::{
15    ops::macros::{doc_add_block_inner_invariant, doc_error},
16    tables::{Outputs, RctOutputs, Tables, TablesMut, TxUnlockTime},
17    types::{Amount, AmountIndex, Output, OutputFlags, PreRctOutputId, RctOutput},
18};
19
20//---------------------------------------------------------------------------------------------------- Pre-RCT Outputs
21/// Add a Pre-RCT [`Output`] to the database.
22///
23/// Upon [`Ok`], this function returns the [`PreRctOutputId`] that
24/// can be used to lookup the `Output` in [`get_output()`].
25///
26#[doc = doc_add_block_inner_invariant!()]
27#[doc = doc_error!()]
28#[inline]
29pub fn add_output(
30    amount: Amount,
31    output: &Output,
32    tables: &mut impl TablesMut,
33) -> DbResult<PreRctOutputId> {
34    // FIXME: this would be much better expressed with a
35    // `btree_map::Entry`-like API, fix `trait DatabaseRw`.
36    let num_outputs = match tables.num_outputs().get(&amount) {
37        // Entry with `amount` already exists.
38        Ok(num_outputs) => num_outputs,
39        // Entry with `amount` didn't exist, this is
40        // the 1st output with this amount.
41        Err(RuntimeError::KeyNotFound) => 0,
42        Err(e) => return Err(e),
43    };
44    // Update the amount of outputs.
45    tables.num_outputs_mut().put(&amount, &(num_outputs + 1))?;
46
47    let pre_rct_output_id = PreRctOutputId {
48        amount,
49        // The new `amount_index` is the length of amount of outputs with same amount.
50        amount_index: num_outputs,
51    };
52
53    tables.outputs_mut().put(&pre_rct_output_id, output)?;
54    Ok(pre_rct_output_id)
55}
56
57/// Remove a Pre-RCT [`Output`] from the database.
58#[doc = doc_add_block_inner_invariant!()]
59#[doc = doc_error!()]
60#[inline]
61pub fn remove_output(
62    pre_rct_output_id: &PreRctOutputId,
63    tables: &mut impl TablesMut,
64) -> DbResult<()> {
65    // Decrement the amount index by 1, or delete the entry out-right.
66    // FIXME: this would be much better expressed with a
67    // `btree_map::Entry`-like API, fix `trait DatabaseRw`.
68    tables
69        .num_outputs_mut()
70        .update(&pre_rct_output_id.amount, |num_outputs| {
71            // INVARIANT: Should never be 0.
72            if num_outputs == 1 {
73                None
74            } else {
75                Some(num_outputs - 1)
76            }
77        })?;
78
79    // Delete the output data itself.
80    tables.outputs_mut().delete(pre_rct_output_id)
81}
82
83/// Retrieve a Pre-RCT [`Output`] from the database.
84#[doc = doc_error!()]
85#[inline]
86pub fn get_output(
87    pre_rct_output_id: &PreRctOutputId,
88    table_outputs: &impl DatabaseRo<Outputs>,
89) -> DbResult<Output> {
90    table_outputs.get(pre_rct_output_id)
91}
92
93/// How many pre-RCT [`Output`]s are there?
94///
95/// This returns the amount of pre-RCT outputs currently stored.
96#[doc = doc_error!()]
97#[inline]
98pub fn get_num_outputs(table_outputs: &impl DatabaseRo<Outputs>) -> DbResult<u64> {
99    table_outputs.len()
100}
101
102//---------------------------------------------------------------------------------------------------- RCT Outputs
103/// Add an [`RctOutput`] to the database.
104///
105/// Upon [`Ok`], this function returns the [`AmountIndex`] that
106/// can be used to lookup the `RctOutput` in [`get_rct_output()`].
107#[doc = doc_add_block_inner_invariant!()]
108#[doc = doc_error!()]
109#[inline]
110pub fn add_rct_output(
111    rct_output: &RctOutput,
112    table_rct_outputs: &mut impl DatabaseRw<RctOutputs>,
113) -> DbResult<AmountIndex> {
114    let amount_index = get_rct_num_outputs(table_rct_outputs)?;
115    table_rct_outputs.put(&amount_index, rct_output)?;
116    Ok(amount_index)
117}
118
119/// Remove an [`RctOutput`] from the database.
120#[doc = doc_add_block_inner_invariant!()]
121#[doc = doc_error!()]
122#[inline]
123pub fn remove_rct_output(
124    amount_index: &AmountIndex,
125    table_rct_outputs: &mut impl DatabaseRw<RctOutputs>,
126) -> DbResult<()> {
127    table_rct_outputs.delete(amount_index)
128}
129
130/// Retrieve an [`RctOutput`] from the database.
131#[doc = doc_error!()]
132#[inline]
133pub fn get_rct_output(
134    amount_index: &AmountIndex,
135    table_rct_outputs: &impl DatabaseRo<RctOutputs>,
136) -> DbResult<RctOutput> {
137    table_rct_outputs.get(amount_index)
138}
139
140/// How many [`RctOutput`]s are there?
141///
142/// This returns the amount of RCT outputs currently stored.
143#[doc = doc_error!()]
144#[inline]
145pub fn get_rct_num_outputs(table_rct_outputs: &impl DatabaseRo<RctOutputs>) -> DbResult<u64> {
146    table_rct_outputs.len()
147}
148
149//---------------------------------------------------------------------------------------------------- Mapping functions
150/// Map an [`Output`] to a [`cuprate_types::OutputOnChain`].
151#[doc = doc_error!()]
152pub fn output_to_output_on_chain(
153    output: &Output,
154    amount: Amount,
155    table_tx_unlock_time: &impl DatabaseRo<TxUnlockTime>,
156) -> DbResult<OutputOnChain> {
157    let commitment = compute_zero_commitment(amount);
158
159    let time_lock = if output
160        .output_flags
161        .contains(OutputFlags::NON_ZERO_UNLOCK_TIME)
162    {
163        u64_to_timelock(table_tx_unlock_time.get(&output.tx_idx)?)
164    } else {
165        Timelock::None
166    };
167
168    let key = CompressedEdwardsY(output.key);
169
170    Ok(OutputOnChain {
171        height: output.height as usize,
172        time_lock,
173        key,
174        commitment,
175    })
176}
177
178/// Map an [`RctOutput`] to a [`cuprate_types::OutputOnChain`].
179///
180/// # Panics
181/// This function will panic if `rct_output`'s `commitment` fails to decompress
182/// into a valid [`EdwardsPoint`](curve25519_dalek::edwards::EdwardsPoint).
183///
184/// This should normally not happen as commitments that
185/// are stored in the database should always be valid.
186#[doc = doc_error!()]
187pub fn rct_output_to_output_on_chain(
188    rct_output: &RctOutput,
189    table_tx_unlock_time: &impl DatabaseRo<TxUnlockTime>,
190) -> DbResult<OutputOnChain> {
191    // INVARIANT: Commitments stored are valid when stored by the database.
192    let commitment = CompressedEdwardsY(rct_output.commitment);
193
194    let time_lock = if rct_output
195        .output_flags
196        .contains(OutputFlags::NON_ZERO_UNLOCK_TIME)
197    {
198        u64_to_timelock(table_tx_unlock_time.get(&rct_output.tx_idx)?)
199    } else {
200        Timelock::None
201    };
202
203    let key = CompressedEdwardsY(rct_output.key);
204
205    Ok(OutputOnChain {
206        height: rct_output.height as usize,
207        time_lock,
208        key,
209        commitment,
210    })
211}
212
213/// Map an [`PreRctOutputId`] to an [`OutputOnChain`].
214///
215/// Note that this still support RCT outputs, in that case, [`PreRctOutputId::amount`] should be `0`.
216#[doc = doc_error!()]
217pub fn id_to_output_on_chain(id: &PreRctOutputId, tables: &impl Tables) -> DbResult<OutputOnChain> {
218    // v2 transactions.
219    if id.amount == 0 {
220        let rct_output = get_rct_output(&id.amount_index, tables.rct_outputs())?;
221        let output_on_chain = rct_output_to_output_on_chain(&rct_output, tables.tx_unlock_time())?;
222
223        Ok(output_on_chain)
224    } else {
225        // v1 transactions.
226        let output = get_output(id, tables.outputs())?;
227        let output_on_chain =
228            output_to_output_on_chain(&output, id.amount, tables.tx_unlock_time())?;
229
230        Ok(output_on_chain)
231    }
232}
233
234//---------------------------------------------------------------------------------------------------- Tests
235#[cfg(test)]
236mod test {
237    use super::*;
238
239    use pretty_assertions::assert_eq;
240
241    use cuprate_database::{Env, EnvInner};
242
243    use crate::{
244        tables::{OpenTables, Tables, TablesMut},
245        tests::{assert_all_tables_are_empty, tmp_concrete_env, AssertTableLen},
246        types::OutputFlags,
247    };
248
249    /// Dummy `Output`.
250    const OUTPUT: Output = Output {
251        key: [44; 32],
252        height: 0,
253        output_flags: OutputFlags::NON_ZERO_UNLOCK_TIME,
254        tx_idx: 0,
255    };
256
257    /// Dummy `RctOutput`.
258    const RCT_OUTPUT: RctOutput = RctOutput {
259        key: [88; 32],
260        height: 1,
261        output_flags: OutputFlags::empty(),
262        tx_idx: 1,
263        commitment: [100; 32],
264    };
265
266    /// Dummy `Amount`
267    const AMOUNT: Amount = 22;
268
269    /// Tests all above output functions when only inputting `Output` data (no Block).
270    ///
271    /// Note that this doesn't test the correctness of values added, as the
272    /// functions have a pre-condition that the caller handles this.
273    ///
274    /// It simply tests if the proper tables are mutated, and if the data
275    /// stored and retrieved is the same.
276    #[test]
277    fn all_output_functions() {
278        let (env, _tmp) = tmp_concrete_env();
279        let env_inner = env.env_inner();
280        assert_all_tables_are_empty(&env);
281
282        let tx_rw = env_inner.tx_rw().unwrap();
283        let mut tables = env_inner.open_tables_mut(&tx_rw).unwrap();
284
285        // Assert length is correct.
286        assert_eq!(get_num_outputs(tables.outputs()).unwrap(), 0);
287        assert_eq!(get_rct_num_outputs(tables.rct_outputs()).unwrap(), 0);
288
289        // Add outputs.
290        let pre_rct_output_id = add_output(AMOUNT, &OUTPUT, &mut tables).unwrap();
291        let amount_index = add_rct_output(&RCT_OUTPUT, tables.rct_outputs_mut()).unwrap();
292
293        assert_eq!(
294            pre_rct_output_id,
295            PreRctOutputId {
296                amount: AMOUNT,
297                amount_index: 0,
298            }
299        );
300
301        // Assert all reads of the outputs are OK.
302        {
303            // Assert proper tables were added to.
304            AssertTableLen {
305                block_infos: 0,
306                block_header_blobs: 0,
307                block_txs_hashes: 0,
308                block_heights: 0,
309                key_images: 0,
310                num_outputs: 1,
311                pruned_tx_blobs: 0,
312                prunable_hashes: 0,
313                outputs: 1,
314                prunable_tx_blobs: 0,
315                rct_outputs: 1,
316                tx_blobs: 0,
317                tx_ids: 0,
318                tx_heights: 0,
319                tx_unlock_time: 0,
320            }
321            .assert(&tables);
322
323            // Assert length is correct.
324            assert_eq!(get_num_outputs(tables.outputs()).unwrap(), 1);
325            assert_eq!(get_rct_num_outputs(tables.rct_outputs()).unwrap(), 1);
326            assert_eq!(1, tables.num_outputs().get(&AMOUNT).unwrap());
327
328            // Assert value is save after retrieval.
329            assert_eq!(
330                OUTPUT,
331                get_output(&pre_rct_output_id, tables.outputs()).unwrap(),
332            );
333
334            assert_eq!(
335                RCT_OUTPUT,
336                get_rct_output(&amount_index, tables.rct_outputs()).unwrap(),
337            );
338        }
339
340        // Remove the outputs.
341        {
342            remove_output(&pre_rct_output_id, &mut tables).unwrap();
343            remove_rct_output(&amount_index, tables.rct_outputs_mut()).unwrap();
344
345            // Assert value no longer exists.
346            assert!(matches!(
347                get_output(&pre_rct_output_id, tables.outputs()),
348                Err(RuntimeError::KeyNotFound)
349            ));
350            assert!(matches!(
351                get_rct_output(&amount_index, tables.rct_outputs()),
352                Err(RuntimeError::KeyNotFound)
353            ));
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
360        assert_all_tables_are_empty(&env);
361    }
362}