1use std_shims::{io, vec::Vec, string::ToString, collections::HashSet};
2
3use zeroize::{Zeroize, ZeroizeOnDrop};
4
5use rand_core::{RngCore, CryptoRng};
6use rand_distr::{Distribution, Gamma};
7#[cfg(not(feature = "std"))]
8use rand_distr::num_traits::Float;
9
10use curve25519_dalek::{Scalar, EdwardsPoint};
11
12use crate::{
13 DEFAULT_LOCK_WINDOW, COINBASE_LOCK_WINDOW, BLOCK_TIME,
14 primitives::{Commitment, Decoys},
15 rpc::{RpcError, DecoyRpc},
16 output::OutputData,
17 WalletOutput,
18};
19
20const RECENT_WINDOW: u64 = 15;
21const BLOCKS_PER_YEAR: usize = (365 * 24 * 60 * 60) / BLOCK_TIME;
22#[allow(clippy::cast_precision_loss)]
23const TIP_APPLICATION: f64 = (DEFAULT_LOCK_WINDOW * BLOCK_TIME) as f64;
24
25async fn select_n(
26 rng: &mut (impl RngCore + CryptoRng),
27 rpc: &impl DecoyRpc,
28 height: usize,
29 real_output: u64,
30 ring_len: u8,
31 fingerprintable_deterministic: bool,
32) -> Result<Vec<(u64, [EdwardsPoint; 2])>, RpcError> {
33 if height < DEFAULT_LOCK_WINDOW {
34 Err(RpcError::InternalError("not enough blocks to select decoys".to_string()))?;
35 }
36 if height > rpc.get_output_distribution_end_height().await? {
37 Err(RpcError::InternalError(
38 "decoys being requested from blocks this node doesn't have".to_string(),
39 ))?;
40 }
41
42 let distribution = rpc.get_output_distribution(.. height).await?;
44 if distribution.len() < DEFAULT_LOCK_WINDOW {
45 Err(RpcError::InternalError("not enough blocks to select decoys".to_string()))?;
46 }
47 let highest_output_exclusive_bound = distribution[distribution.len() - DEFAULT_LOCK_WINDOW];
48 if highest_output_exclusive_bound.saturating_sub(
52 u64::try_from(COINBASE_LOCK_WINDOW).expect("coinbase lock window exceeds 2^{64}"),
53 ) < u64::from(ring_len)
54 {
55 Err(RpcError::InternalError("not enough decoy candidates".to_string()))?;
56 }
57
58 #[allow(clippy::cast_precision_loss)]
60 let per_second = {
61 let blocks = distribution.len().min(BLOCKS_PER_YEAR);
62 let initial = distribution[distribution.len().saturating_sub(blocks + 1)];
63 let outputs = distribution[distribution.len() - 1].saturating_sub(initial);
64 (outputs as f64) / ((blocks * BLOCK_TIME) as f64)
65 };
66
67 let mut do_not_select = HashSet::new();
69 do_not_select.insert(real_output);
70
71 let decoy_count = usize::from(ring_len - 1);
72 let mut res = Vec::with_capacity(decoy_count);
73
74 let mut iters = 0;
75 while res.len() != decoy_count {
80 iters += 1;
81 #[cfg(not(test))]
82 const MAX_ITERS: usize = 10;
83 #[cfg(test)]
86 const MAX_ITERS: usize = 100;
87 if (iters == MAX_ITERS) ||
91 ((highest_output_exclusive_bound -
92 u64::try_from(do_not_select.len()).expect("amount of ignored decoys exceeds 2^{64}")) <
93 u64::from(ring_len))
94 {
95 Err(RpcError::InternalError("hit decoy selection round limit".to_string()))?;
96 }
97
98 let remaining = decoy_count - res.len();
99 let mut candidates = Vec::with_capacity(remaining);
100 while candidates.len() != remaining {
101 let mut age = Gamma::<f64>::new(19.28, 1.0 / 1.61)
105 .expect("constant Gamma distribution could no longer be created")
106 .sample(rng)
107 .exp();
108 #[allow(clippy::cast_precision_loss)]
109 if age > TIP_APPLICATION {
110 age -= TIP_APPLICATION;
111 } else {
112 age = (rng.next_u64() %
114 (RECENT_WINDOW * u64::try_from(BLOCK_TIME).expect("BLOCK_TIME exceeded u64::MAX")))
115 as f64;
116 }
117
118 #[allow(clippy::cast_sign_loss, clippy::cast_possible_truncation)]
119 let o = (age * per_second) as u64;
120 if o < highest_output_exclusive_bound {
121 let i = distribution.partition_point(|s| *s < (highest_output_exclusive_bound - 1 - o));
123 let prev = i.saturating_sub(1);
124 let n = distribution[i].checked_sub(distribution[prev]).ok_or_else(|| {
125 RpcError::InternalError("RPC returned non-monotonic distribution".to_string())
126 })?;
127 if n != 0 {
128 let o = distribution[prev] + (rng.next_u64() % n);
130 if !do_not_select.contains(&o) {
131 candidates.push(o);
132 do_not_select.insert(o);
135 }
136 }
137 }
138 }
139
140 let real_index = if iters == 0 {
144 candidates.push(real_output);
145 candidates.sort();
147 Some(
148 candidates
149 .binary_search(&real_output)
150 .expect("selected a ring which didn't include the real spend"),
151 )
152 } else {
153 None
154 };
155
156 for (i, output) in rpc
157 .get_unlocked_outputs(&candidates, height, fingerprintable_deterministic)
158 .await?
159 .iter_mut()
160 .enumerate()
161 {
162 if real_index == Some(i) {
169 continue;
170 }
171
172 if let Some(output) = output.take() {
174 {
178 let [key, commitment] = output;
179 if !(key.is_torsion_free() && commitment.is_torsion_free()) {
180 continue;
181 }
182 }
183 res.push((candidates[i], output));
184 }
185 }
186 }
187
188 Ok(res)
189}
190
191async fn select_decoys<R: RngCore + CryptoRng>(
192 rng: &mut R,
193 rpc: &impl DecoyRpc,
194 ring_len: u8,
195 height: usize,
196 input: &WalletOutput,
197 fingerprintable_deterministic: bool,
198) -> Result<Decoys, RpcError> {
199 if ring_len == 0 {
200 Err(RpcError::InternalError("requesting a ring of length 0".to_string()))?;
201 }
202
203 let decoys = select_n(
207 rng,
208 rpc,
209 height,
210 input.relative_id.index_on_blockchain,
211 ring_len,
212 fingerprintable_deterministic,
213 )
214 .await?;
215
216 let mut ring = decoys;
218 ring.push((input.relative_id.index_on_blockchain, [input.key(), input.commitment().calculate()]));
219 ring.sort_by(|a, b| a.0.cmp(&b.0));
220
221 let mut offsets = Vec::with_capacity(ring.len());
233 {
234 offsets.push(ring[0].0);
235 for m in 1 .. ring.len() {
236 offsets.push(ring[m].0 - ring[m - 1].0);
237 }
238 }
239
240 Ok(
241 Decoys::new(
242 offsets,
243 u8::try_from(ring.partition_point(|x| x.0 < input.relative_id.index_on_blockchain))
247 .expect("ring of size <= u8::MAX had an index exceeding u8::MAX"),
248 ring.into_iter().map(|output| output.1).collect(),
249 )
250 .expect("selected a syntactically-invalid set of Decoys"),
251 )
252}
253
254#[derive(Clone, PartialEq, Eq, Debug, Zeroize, ZeroizeOnDrop)]
256pub struct OutputWithDecoys {
257 output: OutputData,
258 decoys: Decoys,
259}
260
261impl OutputWithDecoys {
262 pub async fn new(
264 rng: &mut (impl Send + Sync + RngCore + CryptoRng),
265 rpc: &impl DecoyRpc,
266 ring_len: u8,
267 height: usize,
268 output: WalletOutput,
269 ) -> Result<OutputWithDecoys, RpcError> {
270 let decoys = select_decoys(rng, rpc, ring_len, height, &output, false).await?;
271 Ok(OutputWithDecoys { output: output.data.clone(), decoys })
272 }
273
274 pub async fn fingerprintable_deterministic_new(
283 rng: &mut (impl Send + Sync + RngCore + CryptoRng),
284 rpc: &impl DecoyRpc,
285 ring_len: u8,
286 height: usize,
287 output: WalletOutput,
288 ) -> Result<OutputWithDecoys, RpcError> {
289 let decoys = select_decoys(rng, rpc, ring_len, height, &output, true).await?;
290 Ok(OutputWithDecoys { output: output.data.clone(), decoys })
291 }
292
293 pub fn key(&self) -> EdwardsPoint {
295 self.output.key()
296 }
297
298 pub fn key_offset(&self) -> Scalar {
301 self.output.key_offset
302 }
303
304 pub fn commitment(&self) -> &Commitment {
306 &self.output.commitment
307 }
308
309 pub fn decoys(&self) -> &Decoys {
311 &self.decoys
312 }
313
314 pub fn write<W: io::Write>(&self, w: &mut W) -> io::Result<()> {
319 self.output.write(w)?;
320 self.decoys.write(w)
321 }
322
323 pub fn serialize(&self) -> Vec<u8> {
328 let mut serialized = Vec::with_capacity(128);
329 self.write(&mut serialized).expect("write failed but <Vec as io::Write> doesn't fail");
330 serialized
331 }
332
333 pub fn read<R: io::Read>(r: &mut R) -> io::Result<Self> {
338 Ok(Self { output: OutputData::read(r)?, decoys: Decoys::read(r)? })
339 }
340}