1use 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#[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 let num_outputs = match tables.num_outputs().get(&amount) {
37 Ok(num_outputs) => num_outputs,
39 Err(RuntimeError::KeyNotFound) => 0,
42 Err(e) => return Err(e),
43 };
44 tables.num_outputs_mut().put(&amount, &(num_outputs + 1))?;
46
47 let pre_rct_output_id = PreRctOutputId {
48 amount,
49 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#[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 tables
69 .num_outputs_mut()
70 .update(&pre_rct_output_id.amount, |num_outputs| {
71 if num_outputs == 1 {
73 None
74 } else {
75 Some(num_outputs - 1)
76 }
77 })?;
78
79 tables.outputs_mut().delete(pre_rct_output_id)
81}
82
83#[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#[doc = doc_error!()]
97#[inline]
98pub fn get_num_outputs(table_outputs: &impl DatabaseRo<Outputs>) -> DbResult<u64> {
99 table_outputs.len()
100}
101
102#[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#[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#[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#[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#[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#[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 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#[doc = doc_error!()]
217pub fn id_to_output_on_chain(id: &PreRctOutputId, tables: &impl Tables) -> DbResult<OutputOnChain> {
218 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 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#[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 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 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 const AMOUNT: Amount = 22;
268
269 #[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_eq!(get_num_outputs(tables.outputs()).unwrap(), 0);
287 assert_eq!(get_rct_num_outputs(tables.rct_outputs()).unwrap(), 0);
288
289 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 {
303 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_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_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 {
342 remove_output(&pre_rct_output_id, &mut tables).unwrap();
343 remove_rct_output(&amount_index, tables.rct_outputs_mut()).unwrap();
344
345 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_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}