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 output_being_spent: &WalletOutput,
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 output_being_spent_index = output_being_spent.relative_id.index_on_blockchain;
68
69 let mut do_not_select = HashSet::new();
71 do_not_select.insert(output_being_spent_index);
72
73 let decoy_count = usize::from(ring_len - 1);
74 let mut res = Vec::with_capacity(decoy_count);
75
76 let mut first_iter = true;
77 let mut iters = 0;
78 while res.len() != decoy_count {
83 {
84 iters += 1;
85 #[cfg(not(test))]
86 const MAX_ITERS: usize = 10;
87 #[cfg(test)]
90 const MAX_ITERS: usize = 100;
91 if (iters == MAX_ITERS) ||
95 ((highest_output_exclusive_bound -
96 u64::try_from(do_not_select.len())
97 .expect("amount of ignored decoys exceeds 2^{64}")) <
98 u64::from(ring_len))
99 {
100 Err(RpcError::InternalError("hit decoy selection round limit".to_string()))?;
101 }
102 }
103
104 let remaining = decoy_count - res.len();
105 let mut candidates = Vec::with_capacity(remaining);
106 while candidates.len() != remaining {
107 let mut age = Gamma::<f64>::new(19.28, 1.0 / 1.61)
111 .expect("constant Gamma distribution could no longer be created")
112 .sample(rng)
113 .exp();
114 #[allow(clippy::cast_precision_loss)]
115 if age > TIP_APPLICATION {
116 age -= TIP_APPLICATION;
117 } else {
118 age = (rng.next_u64() %
120 (RECENT_WINDOW * u64::try_from(BLOCK_TIME).expect("BLOCK_TIME exceeded u64::MAX")))
121 as f64;
122 }
123
124 #[allow(clippy::cast_sign_loss, clippy::cast_possible_truncation)]
125 let o = (age * per_second) as u64;
126 if o < highest_output_exclusive_bound {
127 let i = distribution.partition_point(|s| *s < (highest_output_exclusive_bound - 1 - o));
129 let prev = i.saturating_sub(1);
130 let n = distribution[i].checked_sub(distribution[prev]).ok_or_else(|| {
131 RpcError::InternalError("RPC returned non-monotonic distribution".to_string())
132 })?;
133 if n != 0 {
134 let o = distribution[prev] + (rng.next_u64() % n);
136 if !do_not_select.contains(&o) {
137 candidates.push(o);
138 do_not_select.insert(o);
141 }
142 }
143 }
144 }
145
146 let real_index = if first_iter {
150 first_iter = false;
151
152 candidates.push(output_being_spent_index);
153 candidates.sort();
155 Some(
156 candidates
157 .binary_search(&output_being_spent_index)
158 .expect("selected a ring which didn't include the real spend"),
159 )
160 } else {
161 None
162 };
163
164 for (i, output) in rpc
165 .get_unlocked_outputs(&candidates, height, fingerprintable_deterministic)
166 .await?
167 .iter_mut()
168 .enumerate()
169 {
170 if real_index == Some(i) {
172 if (Some(output_being_spent.key()) != output.map(|[key, _commitment]| key)) ||
173 (Some(output_being_spent.commitment().calculate()) !=
174 output.map(|[_key, commitment]| commitment))
175 {
176 Err(RpcError::InvalidNode(
177 "node presented different view of output we're trying to spend".to_string(),
178 ))?;
179 }
180
181 continue;
182 }
183
184 if let Some(output) = output.take() {
186 {
190 let [key, commitment] = output;
191 if !(key.is_torsion_free() && commitment.is_torsion_free()) {
192 continue;
193 }
194 }
195 res.push((candidates[i], output));
196 }
197 }
198 }
199
200 Ok(res)
201}
202
203async fn select_decoys<R: RngCore + CryptoRng>(
204 rng: &mut R,
205 rpc: &impl DecoyRpc,
206 ring_len: u8,
207 height: usize,
208 input: &WalletOutput,
209 fingerprintable_deterministic: bool,
210) -> Result<Decoys, RpcError> {
211 if ring_len == 0 {
212 Err(RpcError::InternalError("requesting a ring of length 0".to_string()))?;
213 }
214
215 let decoys = select_n(rng, rpc, height, input, ring_len, fingerprintable_deterministic).await?;
219
220 let mut ring = decoys;
222 ring.push((input.relative_id.index_on_blockchain, [input.key(), input.commitment().calculate()]));
223 ring.sort_by(|a, b| a.0.cmp(&b.0));
224
225 let mut offsets = Vec::with_capacity(ring.len());
237 {
238 offsets.push(ring[0].0);
239 for m in 1 .. ring.len() {
240 offsets.push(ring[m].0 - ring[m - 1].0);
241 }
242 }
243
244 Ok(
245 Decoys::new(
246 offsets,
247 u8::try_from(ring.partition_point(|x| x.0 < input.relative_id.index_on_blockchain))
249 .expect("ring of size <= u8::MAX had an index exceeding u8::MAX"),
250 ring.into_iter().map(|output| output.1).collect(),
251 )
252 .expect("selected a syntactically-invalid set of Decoys"),
253 )
254}
255
256#[derive(Clone, PartialEq, Eq, Debug, Zeroize, ZeroizeOnDrop)]
258pub struct OutputWithDecoys {
259 output: OutputData,
260 decoys: Decoys,
261}
262
263impl OutputWithDecoys {
264 pub async fn new(
272 rng: &mut (impl Send + Sync + RngCore + CryptoRng),
273 rpc: &impl DecoyRpc,
274 ring_len: u8,
275 height: usize,
276 output: WalletOutput,
277 ) -> Result<OutputWithDecoys, RpcError> {
278 let decoys = select_decoys(rng, rpc, ring_len, height, &output, false).await?;
279 Ok(OutputWithDecoys { output: output.data.clone(), decoys })
280 }
281
282 pub async fn fingerprintable_deterministic_new(
297 rng: &mut (impl Send + Sync + RngCore + CryptoRng),
298 rpc: &impl DecoyRpc,
299 ring_len: u8,
300 height: usize,
301 output: WalletOutput,
302 ) -> Result<OutputWithDecoys, RpcError> {
303 let decoys = select_decoys(rng, rpc, ring_len, height, &output, true).await?;
304 Ok(OutputWithDecoys { output: output.data.clone(), decoys })
305 }
306
307 pub fn key(&self) -> EdwardsPoint {
309 self.output.key()
310 }
311
312 pub fn key_offset(&self) -> Scalar {
315 self.output.key_offset
316 }
317
318 pub fn commitment(&self) -> &Commitment {
320 &self.output.commitment
321 }
322
323 pub fn decoys(&self) -> &Decoys {
325 &self.decoys
326 }
327
328 pub fn write<W: io::Write>(&self, w: &mut W) -> io::Result<()> {
333 self.output.write(w)?;
334 self.decoys.write(w)
335 }
336
337 pub fn serialize(&self) -> Vec<u8> {
342 let mut serialized = Vec::with_capacity(128);
343 self.write(&mut serialized).expect("write failed but <Vec as io::Write> doesn't fail");
344 serialized
345 }
346
347 pub fn read<R: io::Read>(r: &mut R) -> io::Result<Self> {
352 Ok(Self { output: OutputData::read(r)?, decoys: Decoys::read(r)? })
353 }
354}