1#![expect(clippy::as_conversions, clippy::float_arithmetic)]
2
3use std_shims::{prelude::*, io, vec::Vec, collections::HashSet};
4
5use zeroize::{Zeroize, ZeroizeOnDrop};
6
7use rand_core::{RngCore, CryptoRng};
8use rand_distr::{Distribution as _, Gamma};
9#[cfg(not(feature = "std"))]
10use rand_distr::num_traits::Float;
11
12use crate::{
13 DEFAULT_LOCK_WINDOW, COINBASE_LOCK_WINDOW, BLOCK_TIME,
14 ed25519::{Scalar, Point, Commitment},
15 ringct::clsag::Decoys,
16 interface::{InterfaceError, TransactionsError, EvaluateUnlocked, ProvidesDecoys},
17 output::OutputData,
18 WalletOutput,
19};
20
21const RECENT_WINDOW: u64 = 15;
22const BLOCKS_PER_YEAR: usize = (365 * 24 * 60 * 60) / BLOCK_TIME;
23#[expect(clippy::cast_precision_loss)]
24const TIP_APPLICATION: f64 = (DEFAULT_LOCK_WINDOW * BLOCK_TIME) as f64;
25
26async fn select_n(
27 rng: &mut (impl RngCore + CryptoRng),
28 rpc: &impl ProvidesDecoys,
29 block_number: usize,
30 output_being_spent: &WalletOutput,
31 ring_len: u8,
32 fingerprintable_deterministic: bool,
33) -> Result<Vec<(u64, [Point; 2])>, TransactionsError> {
34 if block_number <= DEFAULT_LOCK_WINDOW {
35 Err(InterfaceError::InternalError("not enough blocks to select decoys".to_owned()))?;
36 }
37 if block_number > rpc.latest_block_number().await? {
38 Err(InterfaceError::InternalError(
39 "decoys being requested from blocks this node doesn't have".to_owned(),
40 ))?;
41 }
42
43 let distribution = rpc.ringct_output_distribution(..= block_number).await?;
45 if distribution.len() < DEFAULT_LOCK_WINDOW {
46 Err(InterfaceError::InternalError("not enough blocks to select decoys".to_owned()))?;
47 }
48 let highest_output_exclusive_bound = distribution[distribution.len() - DEFAULT_LOCK_WINDOW];
49 if highest_output_exclusive_bound.saturating_sub(
53 u64::try_from(COINBASE_LOCK_WINDOW).expect("coinbase lock window exceeds 2^{64}"),
54 ) < u64::from(ring_len)
55 {
56 Err(InterfaceError::InternalError("not enough decoy candidates".to_owned()))?;
57 }
58
59 #[expect(clippy::cast_precision_loss)]
61 let per_second = {
62 let blocks = distribution.len().min(BLOCKS_PER_YEAR);
63 let initial = distribution[distribution.len().saturating_sub(blocks + 1)];
64 let outputs = distribution[distribution.len() - 1].saturating_sub(initial);
65 (outputs as f64) / ((blocks * BLOCK_TIME) as f64)
66 };
67
68 let output_being_spent_index = output_being_spent.relative_id.index_on_blockchain;
69
70 let mut do_not_select = HashSet::new();
72 do_not_select.insert(output_being_spent_index);
73
74 let decoy_count = usize::from(ring_len - 1);
75 let mut res = Vec::with_capacity(decoy_count);
76
77 let mut first_iter = true;
78 let mut iters = 0;
79 while res.len() != decoy_count {
84 {
85 iters += 1;
86 const MAX_ITERS: usize = {
87 #[cfg_attr(test, expect(unused))]
88 let max_iters = 10;
89 #[cfg(test)]
92 let max_iters = 1000;
93 max_iters
94 };
95 if (iters == MAX_ITERS) ||
99 ((highest_output_exclusive_bound -
100 u64::try_from(do_not_select.len())
101 .expect("amount of ignored decoys exceeds 2^{64}")) <
102 u64::from(ring_len))
103 {
104 Err(InterfaceError::InternalError("hit decoy selection round limit".to_owned()))?;
105 }
106 }
107
108 let remaining = decoy_count - res.len();
109 let mut candidates = Vec::with_capacity(remaining);
110 while candidates.len() != remaining {
111 let mut age = Gamma::<f64>::new(19.28, 1.0 / 1.61)
115 .expect("constant Gamma distribution could no longer be created")
116 .sample(rng)
117 .exp();
118 #[expect(clippy::cast_precision_loss)]
119 if age > TIP_APPLICATION {
120 age -= TIP_APPLICATION;
121 } else {
122 age = (rng.next_u64() %
124 (RECENT_WINDOW * u64::try_from(BLOCK_TIME).expect("BLOCK_TIME exceeded u64::MAX")))
125 as f64;
126 }
127
128 #[expect(clippy::cast_sign_loss, clippy::cast_possible_truncation)]
129 let o = (age * per_second) as u64;
130 if o < highest_output_exclusive_bound {
131 let i = distribution.partition_point(|s| *s < (highest_output_exclusive_bound - 1 - o));
133 let prev = i.saturating_sub(1);
134 let n = distribution[i].checked_sub(distribution[prev]).ok_or_else(|| {
135 InterfaceError::InternalError("RPC returned non-monotonic distribution".to_owned())
136 })?;
137 if n != 0 {
138 let o = distribution[prev] + (rng.next_u64() % n);
140 if !do_not_select.contains(&o) {
141 candidates.push(o);
142 do_not_select.insert(o);
145 }
146 }
147 }
148 }
149
150 let real_index = first_iter.then(|| {
154 first_iter = false;
155
156 candidates.push(output_being_spent_index);
157 candidates.sort_unstable();
159 candidates
160 .binary_search(&output_being_spent_index)
161 .expect("selected a ring which didn't include the real spend")
162 });
163
164 for (i, output) in rpc
165 .unlocked_ringct_outputs(
166 &candidates,
167 if fingerprintable_deterministic {
168 EvaluateUnlocked::FingerprintableDeterministic { block_number }
169 } else {
170 EvaluateUnlocked::Normal
171 },
172 )
173 .await?
174 .iter_mut()
175 .enumerate()
176 {
177 if real_index == Some(i) {
179 if (Some(output_being_spent.key()) != output.map(|[key, _commitment]| key)) ||
180 (Some(output_being_spent.commitment().commit()) !=
181 output.map(|[_key, commitment]| commitment))
182 {
183 Err(InterfaceError::InvalidInterface(
184 "node presented different view of output we're trying to spend".to_owned(),
185 ))?;
186 }
187
188 continue;
189 }
190
191 if let Some(output) = output.take() {
193 {
194 let [key, commitment] = output;
195 if !(key.into().is_torsion_free() && commitment.into().is_torsion_free()) {
199 continue;
200 }
201 use curve25519_dalek::traits::IsIdentity as _;
208 if key.into().is_identity() {
209 continue;
210 }
211 }
212 res.push((candidates[i], output));
213 }
214 }
215 }
216
217 Ok(res)
218}
219
220async fn select_decoys<R: RngCore + CryptoRng>(
221 rng: &mut R,
222 rpc: &impl ProvidesDecoys,
223 ring_len: u8,
224 block_number: usize,
225 input: &WalletOutput,
226 fingerprintable_deterministic: bool,
227) -> Result<Decoys, TransactionsError> {
228 if ring_len == 0 {
229 Err(InterfaceError::InternalError("requesting a ring of length 0".to_owned()))?;
230 }
231
232 let decoys =
236 select_n(rng, rpc, block_number, input, ring_len, fingerprintable_deterministic).await?;
237
238 let mut ring = decoys;
240 ring.push((input.relative_id.index_on_blockchain, [input.key(), input.commitment().commit()]));
241 ring.sort_by(|a, b| a.0.cmp(&b.0));
242
243 let mut offsets = Vec::with_capacity(ring.len());
255 {
256 offsets.push(ring[0].0);
257 for m in 1 .. ring.len() {
258 offsets.push(ring[m].0 - ring[m - 1].0);
259 }
260 }
261
262 Ok(
263 Decoys::new(
264 offsets,
265 u8::try_from(ring.partition_point(|x| x.0 < input.relative_id.index_on_blockchain))
267 .expect("ring of size <= u8::MAX had an index exceeding u8::MAX"),
268 ring.into_iter().map(|output| output.1).collect(),
269 )
270 .expect("selected a syntactically-invalid set of Decoys"),
271 )
272}
273
274#[derive(Clone, Debug, Zeroize, ZeroizeOnDrop)]
278pub struct OutputWithDecoys {
279 output: OutputData,
280 decoys: Decoys,
281}
282
283impl PartialEq for OutputWithDecoys {
284 fn eq(&self, other: &Self) -> bool {
285 bool::from(self.output.ct_eq(&other.output) & self.decoys.ct_eq(&other.decoys))
286 }
287}
288impl Eq for OutputWithDecoys {}
289
290impl OutputWithDecoys {
291 pub async fn new(
299 rng: &mut (impl Send + Sync + RngCore + CryptoRng),
300 rpc: &impl ProvidesDecoys,
301 ring_len: u8,
302 block_number: usize,
303 output: WalletOutput,
304 ) -> Result<OutputWithDecoys, TransactionsError> {
305 let decoys = select_decoys(rng, rpc, ring_len, block_number, &output, false).await?;
306 Ok(OutputWithDecoys { output: output.data.clone(), decoys })
307 }
308
309 pub async fn fingerprintable_deterministic_new(
324 rng: &mut (impl Send + Sync + RngCore + CryptoRng),
325 rpc: &impl ProvidesDecoys,
326 ring_len: u8,
327 block_number: usize,
328 output: WalletOutput,
329 ) -> Result<OutputWithDecoys, TransactionsError> {
330 let decoys = select_decoys(rng, rpc, ring_len, block_number, &output, true).await?;
331 Ok(OutputWithDecoys { output: output.data.clone(), decoys })
332 }
333
334 pub fn key(&self) -> Point {
336 self.output.key()
337 }
338
339 pub fn key_offset(&self) -> Scalar {
342 self.output.key_offset
343 }
344
345 pub fn commitment(&self) -> &Commitment {
347 &self.output.commitment
348 }
349
350 pub fn decoys(&self) -> &Decoys {
352 &self.decoys
353 }
354
355 pub fn write<W: io::Write>(&self, w: &mut W) -> io::Result<()> {
360 self.output.write(w)?;
361 self.decoys.write(w)
362 }
363
364 pub fn serialize(&self) -> Vec<u8> {
369 let mut serialized = Vec::with_capacity(128);
370 self.write(&mut serialized).expect("write failed but <Vec as io::Write> doesn't fail");
371 serialized
372 }
373
374 pub fn read<R: io::Read>(r: &mut R) -> io::Result<Self> {
379 Ok(Self { output: OutputData::read(r)?, decoys: Decoys::read(r)? })
380 }
381}