1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
194
195
196
197
198
199
200
201
202
203
204
205
206
207
208
209
210
211
212
213
214
215
216
217
218
219
220
221
222
223
224
225
226
227
228
229
230
231
232
233
234
235
236
237
238
239
240
241
242
243
244
245
246
247
248
249
250
251
252
253
254
255
256
257
258
259
260
261
262
263
264
265
266
267
268
269
270
271
272
273
274
275
276
277
278
279
280
281
282
283
284
285
286
287
288
289
290
291
292
293
294
295
296
297
298
299
300
301
302
303
304
305
306
307
308
309
310
311
use std_shims::{io, vec::Vec, string::ToString, collections::HashSet};

use zeroize::{Zeroize, ZeroizeOnDrop};

use rand_core::{RngCore, CryptoRng};
use rand_distr::{Distribution, Gamma};
#[cfg(not(feature = "std"))]
use rand_distr::num_traits::Float;

use curve25519_dalek::{Scalar, EdwardsPoint};

use crate::{
  DEFAULT_LOCK_WINDOW, COINBASE_LOCK_WINDOW, BLOCK_TIME,
  primitives::{Commitment, Decoys},
  rpc::{RpcError, DecoyRpc},
  output::OutputData,
  WalletOutput,
};

const RECENT_WINDOW: usize = 15;
const BLOCKS_PER_YEAR: usize = 365 * 24 * 60 * 60 / BLOCK_TIME;
#[allow(clippy::cast_precision_loss)]
const TIP_APPLICATION: f64 = (DEFAULT_LOCK_WINDOW * BLOCK_TIME) as f64;

async fn select_n(
  rng: &mut (impl RngCore + CryptoRng),
  rpc: &impl DecoyRpc,
  height: usize,
  real_output: u64,
  ring_len: usize,
  fingerprintable_deterministic: bool,
) -> Result<Vec<(u64, [EdwardsPoint; 2])>, RpcError> {
  if height < DEFAULT_LOCK_WINDOW {
    Err(RpcError::InternalError("not enough blocks to select decoys".to_string()))?;
  }
  if height > rpc.get_output_distribution_end_height().await? {
    Err(RpcError::InternalError(
      "decoys being requested from blocks this node doesn't have".to_string(),
    ))?;
  }

  // Get the distribution
  let distribution = rpc.get_output_distribution(.. height).await?;
  if distribution.len() < DEFAULT_LOCK_WINDOW {
    Err(RpcError::InternalError("not enough blocks to select decoys".to_string()))?;
  }
  let highest_output_exclusive_bound = distribution[distribution.len() - DEFAULT_LOCK_WINDOW];
  // This assumes that each miner TX had one output (as sane) and checks we have sufficient
  // outputs even when excluding them (due to their own timelock requirements)
  // Considering this a temporal error for very new chains, it's sufficiently sane to have
  if highest_output_exclusive_bound.saturating_sub(u64::try_from(COINBASE_LOCK_WINDOW).unwrap()) <
    u64::try_from(ring_len).unwrap()
  {
    Err(RpcError::InternalError("not enough decoy candidates".to_string()))?;
  }

  // Determine the outputs per second
  #[allow(clippy::cast_precision_loss)]
  let per_second = {
    let blocks = distribution.len().min(BLOCKS_PER_YEAR);
    let initial = distribution[distribution.len().saturating_sub(blocks + 1)];
    let outputs = distribution[distribution.len() - 1].saturating_sub(initial);
    (outputs as f64) / ((blocks * BLOCK_TIME) as f64)
  };

  // Don't select the real output
  let mut do_not_select = HashSet::new();
  do_not_select.insert(real_output);

  let decoy_count = ring_len - 1;
  let mut res = Vec::with_capacity(decoy_count);

  let mut iters = 0;
  // Iterates until we have enough decoys
  // If an iteration only returns a partial set of decoys, the remainder will be obvious as decoys
  // to the RPC
  // The length of that remainder is expected to be minimal
  while res.len() != decoy_count {
    iters += 1;
    #[cfg(not(test))]
    const MAX_ITERS: usize = 10;
    // When testing on fresh chains, increased iterations can be useful and we don't necessitate
    // reasonable performance
    #[cfg(test)]
    const MAX_ITERS: usize = 100;
    // Ensure this isn't infinitely looping
    // We check both that we aren't at the maximum amount of iterations and that the not-yet
    // selected candidates exceed the amount of candidates necessary to trigger the next iteration
    if (iters == MAX_ITERS) ||
      ((highest_output_exclusive_bound - u64::try_from(do_not_select.len()).unwrap()) <
        u64::try_from(ring_len).unwrap())
    {
      Err(RpcError::InternalError("hit decoy selection round limit".to_string()))?;
    }

    let remaining = decoy_count - res.len();
    let mut candidates = Vec::with_capacity(remaining);
    while candidates.len() != remaining {
      // Use a gamma distribution, as Monero does
      // https://github.com/monero-project/monero/blob/cc73fe71162d564ffda8e549b79a350bca53c45
      //   /src/wallet/wallet2.cpp#L142-L143
      let mut age = Gamma::<f64>::new(19.28, 1.0 / 1.61).unwrap().sample(rng).exp();
      #[allow(clippy::cast_precision_loss)]
      if age > TIP_APPLICATION {
        age -= TIP_APPLICATION;
      } else {
        // f64 does not have try_from available, which is why these are written with `as`
        age = (rng.next_u64() % u64::try_from(RECENT_WINDOW * BLOCK_TIME).unwrap()) as f64;
      }

      #[allow(clippy::cast_sign_loss, clippy::cast_possible_truncation)]
      let o = (age * per_second) as u64;
      if o < highest_output_exclusive_bound {
        // Find which block this points to
        let i = distribution.partition_point(|s| *s < (highest_output_exclusive_bound - 1 - o));
        let prev = i.saturating_sub(1);
        let n = distribution[i] - distribution[prev];
        if n != 0 {
          // Select an output from within this block
          let o = distribution[prev] + (rng.next_u64() % n);
          if !do_not_select.contains(&o) {
            candidates.push(o);
            // This output will either be used or is unusable
            // In either case, we should not try it again
            do_not_select.insert(o);
          }
        }
      }
    }

    // If this is the first time we're requesting these outputs, include the real one as well
    // Prevents the node we're connected to from having a list of known decoys and then seeing a
    // TX which uses all of them, with one additional output (the true spend)
    let real_index = if iters == 0 {
      candidates.push(real_output);
      // Sort candidates so the real spends aren't the ones at the end
      candidates.sort();
      Some(candidates.binary_search(&real_output).unwrap())
    } else {
      None
    };

    for (i, output) in rpc
      .get_unlocked_outputs(&candidates, height, fingerprintable_deterministic)
      .await?
      .iter_mut()
      .enumerate()
    {
      // We could check the returned info is equivalent to our expectations, yet that'd allow the
      // node to malleate the returned info to see if they can cause this error (allowing them to
      // figure out the output being spent)
      //
      // Some degree of this attack (forcing resampling/trying to observe errors) is likely
      // always possible
      if real_index == Some(i) {
        continue;
      }

      // If this is an unlocked output, push it to the result
      if let Some(output) = output.take() {
        res.push((candidates[i], output));
      }
    }
  }

  Ok(res)
}

async fn select_decoys<R: RngCore + CryptoRng>(
  rng: &mut R,
  rpc: &impl DecoyRpc,
  ring_len: usize,
  height: usize,
  input: &WalletOutput,
  fingerprintable_deterministic: bool,
) -> Result<Decoys, RpcError> {
  // Select all decoys for this transaction, assuming we generate a sane transaction
  // We should almost never naturally generate an insane transaction, hence why this doesn't
  // bother with an overage
  let decoys = select_n(
    rng,
    rpc,
    height,
    input.relative_id.index_on_blockchain,
    ring_len,
    fingerprintable_deterministic,
  )
  .await?;

  // Form the complete ring
  let mut ring = decoys;
  ring.push((input.relative_id.index_on_blockchain, [input.key(), input.commitment().calculate()]));
  ring.sort_by(|a, b| a.0.cmp(&b.0));

  /*
    Monero does have sanity checks which it applies to the selected ring.

    They're statistically unlikely to be hit and only occur when the transaction is published over
    the RPC (so they are not a relay rule). The RPC allows disabling them, which monero-rpc does to
    ensure they don't pose a problem.

    They aren't worth the complexity to implement here, especially since they're non-deterministic.
  */

  // We need to convert our positional indexes to offset indexes
  let mut offsets = Vec::with_capacity(ring.len());
  {
    offsets.push(ring[0].0);
    for m in 1 .. ring.len() {
      offsets.push(ring[m].0 - ring[m - 1].0);
    }
  }

  Ok(
    Decoys::new(
      offsets,
      // Binary searches for the real spend since we don't know where it sorted to
      u8::try_from(ring.partition_point(|x| x.0 < input.relative_id.index_on_blockchain)).unwrap(),
      ring.into_iter().map(|output| output.1).collect(),
    )
    .unwrap(),
  )
}

/// An output with decoys selected.
#[derive(Clone, PartialEq, Eq, Debug, Zeroize, ZeroizeOnDrop)]
pub struct OutputWithDecoys {
  output: OutputData,
  decoys: Decoys,
}

impl OutputWithDecoys {
  /// Select decoys for this output.
  pub async fn new(
    rng: &mut (impl Send + Sync + RngCore + CryptoRng),
    rpc: &impl DecoyRpc,
    ring_len: usize,
    height: usize,
    output: WalletOutput,
  ) -> Result<OutputWithDecoys, RpcError> {
    let decoys = select_decoys(rng, rpc, ring_len, height, &output, false).await?;
    Ok(OutputWithDecoys { output: output.data.clone(), decoys })
  }

  /// Select a set of decoys for this output with a deterministic process.
  ///
  /// This function will always output the same set of decoys when called with the same arguments.
  /// This makes it very useful in multisignature contexts, where instead of having one participant
  /// select the decoys, everyone can locally select the decoys while coming to the same result.
  ///
  /// The set of decoys selected may be fingerprintable as having been produced by this
  /// methodology.
  pub async fn fingerprintable_deterministic_new(
    rng: &mut (impl Send + Sync + RngCore + CryptoRng),
    rpc: &impl DecoyRpc,
    ring_len: usize,
    height: usize,
    output: WalletOutput,
  ) -> Result<OutputWithDecoys, RpcError> {
    let decoys = select_decoys(rng, rpc, ring_len, height, &output, true).await?;
    Ok(OutputWithDecoys { output: output.data.clone(), decoys })
  }

  /// The key this output may be spent by.
  pub fn key(&self) -> EdwardsPoint {
    self.output.key()
  }

  /// The scalar to add to the private spend key for it to be the discrete logarithm of this
  /// output's key.
  pub fn key_offset(&self) -> Scalar {
    self.output.key_offset
  }

  /// The commitment this output created.
  pub fn commitment(&self) -> &Commitment {
    &self.output.commitment
  }

  /// The decoys this output selected.
  pub fn decoys(&self) -> &Decoys {
    &self.decoys
  }

  /// Write the OutputWithDecoys.
  ///
  /// This is not a Monero protocol defined struct, and this is accordingly not a Monero protocol
  /// defined serialization.
  pub fn write<W: io::Write>(&self, w: &mut W) -> io::Result<()> {
    self.output.write(w)?;
    self.decoys.write(w)
  }

  /// Serialize the OutputWithDecoys to a `Vec<u8>`.
  ///
  /// This is not a Monero protocol defined struct, and this is accordingly not a Monero protocol
  /// defined serialization.
  pub fn serialize(&self) -> Vec<u8> {
    let mut serialized = Vec::with_capacity(128);
    self.write(&mut serialized).unwrap();
    serialized
  }

  /// Read an OutputWithDecoys.
  ///
  /// This is not a Monero protocol defined struct, and this is accordingly not a Monero protocol
  /// defined serialization.
  pub fn read<R: io::Read>(r: &mut R) -> io::Result<Self> {
    Ok(Self { output: OutputData::read(r)?, decoys: Decoys::read(r)? })
  }
}