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_hash_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 block_info = tapes
154 .read_entry(&db.block_infos, usize_to_u64(output.height))?
155 .unwrap();
156 let txid = get_tx_hash_from_id(
157 &output.tx_idx,
158 tapes,
159 block_info.mining_tx_index == output.tx_idx,
160 db,
161 )?;
162
163 Some(txid)
164 } else {
165 None
166 };
167
168 Ok(OutputOnChain {
169 height: output.height,
170 time_lock: u64_to_timelock(output.timelock),
171 key,
172 commitment,
173 txid,
174 })
175}
176
177#[inline]
179pub fn rct_output_to_output_on_chain(
180 rct_output: &RctOutput,
181 get_txid: bool,
182 tapes: &tapes::TapesReadTransaction,
183 db: &BlockchainDatabase,
184) -> DbResult<OutputOnChain> {
185 let commitment = CompressedPoint::from(rct_output.commitment);
187
188 let key = CompressedPoint::from(rct_output.key);
189
190 let txid = if get_txid {
191 let block_info = tapes
192 .read_entry(&db.block_infos, usize_to_u64(rct_output.height))?
193 .unwrap();
194 let txid = get_tx_hash_from_id(
195 &rct_output.tx_idx,
196 tapes,
197 block_info.mining_tx_index == rct_output.tx_idx,
198 db,
199 )?;
200
201 Some(txid)
202 } else {
203 None
204 };
205
206 Ok(OutputOnChain {
207 height: rct_output.height,
208 time_lock: u64_to_timelock(rct_output.timelock),
209 key,
210 commitment,
211 txid,
212 })
213}
214
215pub fn id_to_output_on_chain(
219 db: &BlockchainDatabase,
220 id: &PreRctOutputId,
221 get_txid: bool,
222 tx_ro: &fjall::Snapshot,
223 tapes: &tapes::TapesReadTransaction,
224) -> DbResult<OutputOnChain> {
225 if id.amount == 0 {
227 let rct_output = tapes
228 .read_entry(&db.rct_outputs, id.amount_index)?
229 .ok_or(BlockchainError::NotFound)?;
230 let output_on_chain = rct_output_to_output_on_chain(&rct_output, get_txid, tapes, db)?;
231
232 Ok(output_on_chain)
233 } else {
234 let output = get_output(db, id, tx_ro)?;
236 let output_on_chain = output_to_output_on_chain(&output, id.amount, get_txid, tapes, db)?;
237
238 Ok(output_on_chain)
239 }
240}
241
242pub fn unlocked_and_recent_instances(
244 db: &BlockchainDatabase,
245 tx_ro: &fjall::Snapshot,
246 tapes: &tapes::TapesReadTransaction,
247 amount: Amount,
248 total_instances: u64,
249 current_height: u64,
250 recent_cutoff: u64,
251) -> DbResult<(u64, u64)> {
252 let (unlocked, recent) = if amount == 0 {
253 let Some(tip) = current_height.checked_sub(DEFAULT_LOCK_WINDOW as u64) else {
257 return Ok((0, 0));
258 };
259 let unlocked = tapes
260 .read_entry(&db.block_infos, tip)?
261 .map_or(0, |info| info.cumulative_rct_outs);
262
263 let mut recent = 0;
264 if recent_cutoff > 0 {
265 let mut cumulative = unlocked;
266 for height in (0..=tip).rev() {
267 let timestamp =
268 get_block_extended_header_from_height(u64_to_usize(height), tapes, db)?
269 .timestamp;
270 if timestamp < recent_cutoff {
271 break;
272 }
273 let prev = height.checked_sub(1).map_or(DbResult::Ok(0), |h| {
274 Ok(tapes
275 .read_entry(&db.block_infos, h)?
276 .map_or(0, |info| info.cumulative_rct_outs))
277 })?;
278 recent += cumulative - prev;
279 cumulative = prev;
280 }
281 }
282
283 (unlocked, recent)
284 } else {
285 let height_of = |index: u64| -> DbResult<usize> {
290 Ok(get_output(
291 db,
292 &PreRctOutputId {
293 amount,
294 amount_index: index,
295 },
296 tx_ro,
297 )?
298 .height)
299 };
300
301 let mut unlocked = 0;
302 for index in (0..total_instances).rev() {
303 if usize_to_u64(height_of(index)?) + DEFAULT_LOCK_WINDOW as u64 <= current_height {
304 unlocked = index + 1;
305 break;
306 }
307 }
308
309 let mut recent = 0;
310 if recent_cutoff > 0 {
311 for index in (0..unlocked).rev() {
312 let timestamp =
313 get_block_extended_header_from_height(height_of(index)?, tapes, db)?.timestamp;
314 if timestamp < recent_cutoff {
315 break;
316 }
317 recent += 1;
318 }
319 }
320
321 (unlocked, recent)
322 };
323
324 Ok((unlocked, recent))
325}