Skip to main content

cuprate_blockchain/ops/
output.rs

1//! Output functions.
2use std::{
3    collections::HashMap,
4    ops::{AddAssign, SubAssign},
5};
6
7use fjall::Readable;
8use monero_oxide::ed25519::CompressedPoint;
9use monero_oxide::DEFAULT_LOCK_WINDOW;
10use tapes::TapesRead;
11
12use cuprate_helper::cast::{u64_to_usize, usize_to_u64};
13use cuprate_helper::{crypto::compute_zero_commitment, map::u64_to_timelock};
14use cuprate_types::OutputOnChain;
15
16use crate::{
17    error::{BlockchainError, DbResult},
18    ops::{block::get_block_extended_header_from_height, tx::get_tx_from_id},
19    types::{Amount, Output, PreRctOutputId, RctOutput},
20    BlockchainDatabase,
21};
22
23/// Add a Pre-RCT [`Output`] to the database.
24///
25#[inline]
26pub fn add_output(
27    db: &BlockchainDatabase,
28    amount: Amount,
29    output: &Output,
30    w: &mut fjall::OwnedWriteBatch,
31    pre_rct_numb_outputs_cache: &mut HashMap<Amount, u64>,
32) -> DbResult<PreRctOutputId> {
33    let mut err = None;
34    let num_outputs = pre_rct_numb_outputs_cache.entry(amount).or_insert_with(|| {
35        let last_out = db.pre_rct_outputs.prefix(amount.to_be_bytes()).next_back();
36
37        match last_out.map(fjall::Guard::key) {
38            None => 0,
39            Some(Ok(o)) => u64::from_be_bytes(o[8..].try_into().unwrap()) + 1,
40            Some(Err(e)) => {
41                err = Some(e);
42                0
43            }
44        }
45    });
46
47    if let Some(e) = err {
48        return Err(e.into());
49    }
50
51    let pre_rct_output_id = PreRctOutputId {
52        amount,
53        // The new `amount_index` is the length of amount of outputs with same amount.
54        amount_index: *num_outputs,
55    };
56
57    w.insert(
58        &db.pre_rct_outputs,
59        pre_rct_output_id.to_bytes(),
60        bytemuck::bytes_of(output),
61    );
62
63    num_outputs.add_assign(1);
64
65    Ok(pre_rct_output_id)
66}
67
68/// Remove a Pre-RCT [`Output`] from the database.
69#[inline]
70pub fn remove_output(
71    db: &BlockchainDatabase,
72    amount: Amount,
73    tx_rw: &mut fjall::OwnedWriteBatch,
74) -> DbResult<()> {
75    let mut pre_rct_numb_outputs_cache = db.pre_rct_numb_outputs_cache.lock().unwrap();
76
77    let mut err = None;
78    let num_outputs = pre_rct_numb_outputs_cache.entry(amount).or_insert_with(|| {
79        let last_out = db.pre_rct_outputs.prefix(amount.to_be_bytes()).next_back();
80
81        match last_out.map(fjall::Guard::key) {
82            Some(Ok(o)) => u64::from_be_bytes(o[8..].try_into().unwrap()) + 1,
83            Some(Err(e)) => {
84                err = Some(e.into());
85                0
86            }
87            None => {
88                err = Some(BlockchainError::NotFound);
89                0
90            }
91        }
92    });
93
94    if let Some(e) = err {
95        return Err(e);
96    }
97
98    let pre_rct_output_id = PreRctOutputId {
99        amount,
100        // Remove the last output with same amount.
101        amount_index: *num_outputs - 1,
102    };
103
104    tx_rw.remove(&db.pre_rct_outputs, pre_rct_output_id.to_bytes());
105
106    num_outputs.sub_assign(1);
107    Ok(())
108}
109
110/// Retrieve a Pre-RCT [`Output`] from the database.
111#[inline]
112pub fn get_output(
113    db: &BlockchainDatabase,
114    pre_rct_output_id: &PreRctOutputId,
115    tx_ro: &fjall::Snapshot,
116) -> DbResult<Output> {
117    let output = tx_ro
118        .get(&db.pre_rct_outputs, pre_rct_output_id.to_bytes())?
119        .ok_or(BlockchainError::NotFound)?;
120
121    Ok(bytemuck::pod_read_unaligned(output.as_ref()))
122}
123
124/// Get the number of outputs with a given amount.
125#[inline]
126pub fn get_num_outputs_with_amount(
127    db: &BlockchainDatabase,
128    tx_ro: &fjall::Snapshot,
129    amount: Amount,
130) -> DbResult<u64> {
131    let last_out = tx_ro
132        .prefix(&db.pre_rct_outputs, amount.to_be_bytes())
133        .next_back();
134
135    last_out.map_or(Ok(0), |o| {
136        Ok(u64::from_be_bytes(o.key()?[8..].try_into().unwrap()) + 1)
137    })
138}
139
140/// Map an [`Output`] to a [`cuprate_types::OutputOnChain`].
141pub fn output_to_output_on_chain(
142    output: &Output,
143    amount: Amount,
144    get_txid: bool,
145    tapes: &tapes::TapesReadTransaction,
146    db: &BlockchainDatabase,
147) -> DbResult<OutputOnChain> {
148    let commitment = compute_zero_commitment(amount);
149
150    let key = CompressedPoint::from(output.key);
151
152    let txid = if get_txid {
153        let txid = get_tx_from_id(&output.tx_idx, tapes, db)?.hash();
154
155        Some(txid)
156    } else {
157        None
158    };
159
160    Ok(OutputOnChain {
161        height: output.height,
162        time_lock: u64_to_timelock(output.timelock),
163        key,
164        commitment,
165        txid,
166    })
167}
168
169/// Map a [`RctOutput`] to a [`cuprate_types::OutputOnChain`].
170#[inline]
171pub fn rct_output_to_output_on_chain(
172    rct_output: &RctOutput,
173    get_txid: bool,
174    tapes: &tapes::TapesReadTransaction,
175    db: &BlockchainDatabase,
176) -> DbResult<OutputOnChain> {
177    // INVARIANT: Commitments stored are valid when stored by the database.
178    let commitment = CompressedPoint::from(rct_output.commitment);
179
180    let key = CompressedPoint::from(rct_output.key);
181
182    let txid = if get_txid {
183        let txid = get_tx_from_id(&rct_output.tx_idx, tapes, db)?.hash();
184
185        Some(txid)
186    } else {
187        None
188    };
189
190    Ok(OutputOnChain {
191        height: rct_output.height,
192        time_lock: u64_to_timelock(rct_output.timelock),
193        key,
194        commitment,
195        txid,
196    })
197}
198
199/// Map an [`PreRctOutputId`] to an [`OutputOnChain`].
200///
201/// Note that this still support RCT outputs, in that case, [`PreRctOutputId::amount`] should be `0`.
202pub fn id_to_output_on_chain(
203    db: &BlockchainDatabase,
204    id: &PreRctOutputId,
205    get_txid: bool,
206    tx_ro: &fjall::Snapshot,
207    tapes: &tapes::TapesReadTransaction,
208) -> DbResult<OutputOnChain> {
209    // v2 transactions.
210    if id.amount == 0 {
211        let rct_output = tapes
212            .read_entry(&db.rct_outputs, id.amount_index)?
213            .ok_or(BlockchainError::NotFound)?;
214        let output_on_chain = rct_output_to_output_on_chain(&rct_output, get_txid, tapes, db)?;
215
216        Ok(output_on_chain)
217    } else {
218        // v1 transactions.
219        let output = get_output(db, id, tx_ro)?;
220        let output_on_chain = output_to_output_on_chain(&output, id.amount, get_txid, tapes, db)?;
221
222        Ok(output_on_chain)
223    }
224}
225
226/// Compute `(unlocked_instances, recent_instances)` for an output amount.
227pub fn unlocked_and_recent_instances(
228    db: &BlockchainDatabase,
229    tx_ro: &fjall::Snapshot,
230    tapes: &tapes::TapesReadTransaction,
231    amount: Amount,
232    total_instances: u64,
233    current_height: u64,
234    recent_cutoff: u64,
235) -> DbResult<(u64, u64)> {
236    let (unlocked, recent) = if amount == 0 {
237        // For RCT outputs we can use the `cumulative_rct_outs` in the block_infos to calculate the
238        // number of locked and recent outputs.
239
240        let Some(tip) = current_height.checked_sub(DEFAULT_LOCK_WINDOW as u64) else {
241            return Ok((0, 0));
242        };
243        let unlocked = tapes
244            .read_entry(&db.block_infos, tip)?
245            .map_or(0, |info| info.cumulative_rct_outs);
246
247        let mut recent = 0;
248        if recent_cutoff > 0 {
249            let mut cumulative = unlocked;
250            for height in (0..=tip).rev() {
251                let timestamp =
252                    get_block_extended_header_from_height(u64_to_usize(height), tapes, db)?
253                        .timestamp;
254                if timestamp < recent_cutoff {
255                    break;
256                }
257                let prev = height.checked_sub(1).map_or(DbResult::Ok(0), |h| {
258                    Ok(tapes
259                        .read_entry(&db.block_infos, h)?
260                        .map_or(0, |info| info.cumulative_rct_outs))
261                })?;
262                recent += cumulative - prev;
263                cumulative = prev;
264            }
265        }
266
267        (unlocked, recent)
268    } else {
269        // For pre-RCT we need to work back from each output with this amount.
270
271        // Closure that returns the block height of the given output index. (output amount is the one
272        // given to this function).
273        let height_of = |index: u64| -> DbResult<usize> {
274            Ok(get_output(
275                db,
276                &PreRctOutputId {
277                    amount,
278                    amount_index: index,
279                },
280                tx_ro,
281            )?
282            .height)
283        };
284
285        let mut unlocked = 0;
286        for index in (0..total_instances).rev() {
287            if usize_to_u64(height_of(index)?) + DEFAULT_LOCK_WINDOW as u64 <= current_height {
288                unlocked = index + 1;
289                break;
290            }
291        }
292
293        let mut recent = 0;
294        if recent_cutoff > 0 {
295            for index in (0..unlocked).rev() {
296                let timestamp =
297                    get_block_extended_header_from_height(height_of(index)?, tapes, db)?.timestamp;
298                if timestamp < recent_cutoff {
299                    break;
300                }
301                recent += 1;
302            }
303        }
304
305        (unlocked, recent)
306    };
307
308    Ok((unlocked, recent))
309}