1use 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#[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 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#[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 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#[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#[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
140pub 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#[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 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
199pub 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 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 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
226pub 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 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 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}