monero_wallet/
decoys.rs

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  // Get the distribution
43  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  // This assumes that each miner TX had one output (as sane) and checks we have sufficient
49  // outputs even when excluding them (due to their own timelock requirements)
50  // Considering this a temporal error for very new chains, it's sufficiently sane to have
51  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  // Determine the outputs per second
59  #[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  // Don't select the real output
68  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  // Iterates until we have enough decoys
76  // If an iteration only returns a partial set of decoys, the remainder will be obvious as decoys
77  // to the RPC
78  // The length of that remainder is expected to be minimal
79  while res.len() != decoy_count {
80    iters += 1;
81    #[cfg(not(test))]
82    const MAX_ITERS: usize = 10;
83    // When testing on fresh chains, increased iterations can be useful and we don't necessitate
84    // reasonable performance
85    #[cfg(test)]
86    const MAX_ITERS: usize = 100;
87    // Ensure this isn't infinitely looping
88    // We check both that we aren't at the maximum amount of iterations and that the not-yet
89    // selected candidates exceed the amount of candidates necessary to trigger the next iteration
90    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      // Use a gamma distribution, as Monero does
102      // https://github.com/monero-project/monero/blob/cc73fe71162d564ffda8e549b79a350bca53c45
103      //   /src/wallet/wallet2.cpp#L142-L143
104      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        // f64 does not have try_from available, which is why these are written with `as`
113        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        // Find which block this points to
122        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          // Select an output from within this block
129          let o = distribution[prev] + (rng.next_u64() % n);
130          if !do_not_select.contains(&o) {
131            candidates.push(o);
132            // This output will either be used or is unusable
133            // In either case, we should not try it again
134            do_not_select.insert(o);
135          }
136        }
137      }
138    }
139
140    // If this is the first time we're requesting these outputs, include the real one as well
141    // Prevents the node we're connected to from having a list of known decoys and then seeing a
142    // TX which uses all of them, with one additional output (the true spend)
143    let real_index = if iters == 0 {
144      candidates.push(real_output);
145      // Sort candidates so the real spends aren't the ones at the end
146      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      // We could check the returned info is equivalent to our expectations, yet that'd allow the
163      // node to malleate the returned info to see if they can cause this error (allowing them to
164      // figure out the output being spent)
165      //
166      // Some degree of this attack (forcing resampling/trying to observe errors) is likely
167      // always possible
168      if real_index == Some(i) {
169        continue;
170      }
171
172      // If this is an unlocked output, push it to the result
173      if let Some(output) = output.take() {
174        // Unless torsion is present
175        // https://github.com/monero-project/monero/blob/893916ad091a92e765ce3241b94e706ad012b62a
176        //   /src/wallet/wallet2.cpp#L9050-L9060
177        {
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  // Select all decoys for this transaction, assuming we generate a sane transaction
204  // We should almost never naturally generate an insane transaction, hence why this doesn't
205  // bother with an overage
206  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  // Form the complete ring
217  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  /*
222    Monero does have sanity checks which it applies to the selected ring.
223
224    They're statistically unlikely to be hit and only occur when the transaction is published over
225    the RPC (so they are not a relay rule). The RPC allows disabling them, which monero-rpc does to
226    ensure they don't pose a problem.
227
228    They aren't worth the complexity to implement here, especially since they're non-deterministic.
229  */
230
231  // We need to convert our positional indexes to offset indexes
232  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      // Binary searches for the real spend since we don't know where it sorted to
244      // TODO: Define our own collection whose `len` function returns `u8` to ensure this bound
245      // with types
246      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/// An output with decoys selected.
255#[derive(Clone, PartialEq, Eq, Debug, Zeroize, ZeroizeOnDrop)]
256pub struct OutputWithDecoys {
257  output: OutputData,
258  decoys: Decoys,
259}
260
261impl OutputWithDecoys {
262  /// Select decoys for this output.
263  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  /// Select a set of decoys for this output with a deterministic process.
275  ///
276  /// This function will always output the same set of decoys when called with the same arguments.
277  /// This makes it very useful in multisignature contexts, where instead of having one participant
278  /// select the decoys, everyone can locally select the decoys while coming to the same result.
279  ///
280  /// The set of decoys selected may be fingerprintable as having been produced by this
281  /// methodology.
282  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  /// The key this output may be spent by.
294  pub fn key(&self) -> EdwardsPoint {
295    self.output.key()
296  }
297
298  /// The scalar to add to the private spend key for it to be the discrete logarithm of this
299  /// output's key.
300  pub fn key_offset(&self) -> Scalar {
301    self.output.key_offset
302  }
303
304  /// The commitment this output created.
305  pub fn commitment(&self) -> &Commitment {
306    &self.output.commitment
307  }
308
309  /// The decoys this output selected.
310  pub fn decoys(&self) -> &Decoys {
311    &self.decoys
312  }
313
314  /// Write the OutputWithDecoys.
315  ///
316  /// This is not a Monero protocol defined struct, and this is accordingly not a Monero protocol
317  /// defined serialization.
318  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  /// Serialize the OutputWithDecoys to a `Vec<u8>`.
324  ///
325  /// This is not a Monero protocol defined struct, and this is accordingly not a Monero protocol
326  /// defined serialization.
327  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  /// Read an OutputWithDecoys.
334  ///
335  /// This is not a Monero protocol defined struct, and this is accordingly not a Monero protocol
336  /// defined serialization.
337  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}