monero-wallet 0.2.0

Wallet functionality for the Monero protocol, built around monero-oxide
Documentation
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
312
313
314
315
316
317
318
319
320
321
322
323
324
325
326
327
328
329
330
331
332
333
334
335
336
337
338
339
340
341
342
343
344
345
346
347
348
349
350
351
352
353
354
355
356
357
358
359
360
361
362
363
364
365
366
367
368
369
370
371
372
373
374
375
376
377
378
379
380
381
382
383
384
385
386
387
388
389
390
391
392
393
394
395
396
397
398
399
400
401
402
403
404
405
406
407
408
409
410
411
412
413
414
415
416
417
418
419
420
421
422
423
424
425
426
427
428
429
430
431
432
433
434
435
436
437
use std_shims::{
  vec,
  vec::Vec,
  io::{self, Read, Write},
};

use zeroize::{Zeroize, ZeroizeOnDrop};
use subtle::{Choice, ConstantTimeEq as _};

use crate::{
  io::*,
  ed25519::{Scalar, CompressedPoint, Point, Commitment},
  transaction::Timelock,
  address::SubaddressIndex,
  extra::{MAX_ARBITRARY_DATA_SIZE, MAX_EXTRA_SIZE_BY_RELAY_RULE, PaymentId},
};

/// An absolute output ID, defined as its transaction hash and output index.
///
/// This is not the output's key as multiple outputs may share an output key.
#[derive(Clone, Zeroize, ZeroizeOnDrop)]
pub(crate) struct AbsoluteId {
  pub(crate) transaction: [u8; 32],
  pub(crate) index_in_transaction: u64,
}

impl core::fmt::Debug for AbsoluteId {
  fn fmt(&self, fmt: &mut core::fmt::Formatter<'_>) -> Result<(), core::fmt::Error> {
    fmt
      .debug_struct("AbsoluteId")
      .field("transaction", &hex::encode(self.transaction))
      .field("index_in_transaction", &self.index_in_transaction)
      .finish()
  }
}

impl AbsoluteId {
  /// A constant-time `eq`, albeit one not exposed via `ConstantTimeEq`.
  fn ct_eq(&self, other: &Self) -> Choice {
    self.transaction.ct_eq(&other.transaction) &
      self.index_in_transaction.ct_eq(&other.index_in_transaction)
  }

  /// Write the AbsoluteId.
  ///
  /// This is not a Monero protocol defined struct, and this is accordingly not a Monero protocol
  /// defined serialization. This may run in time variable to its value.
  fn write<W: Write>(&self, w: &mut W) -> io::Result<()> {
    w.write_all(&self.transaction)?;
    w.write_all(&self.index_in_transaction.to_le_bytes())
  }

  /// Read an AbsoluteId.
  ///
  /// This is not a Monero protocol defined struct, and this is accordingly not a Monero protocol
  /// defined serialization. This may run in time variable to its value.
  fn read<R: Read>(r: &mut R) -> io::Result<AbsoluteId> {
    Ok(AbsoluteId { transaction: read_bytes(r)?, index_in_transaction: read_u64(r)? })
  }
}

/// An output's relative ID.
///
/// This is defined as the output's index on the blockchain.
#[derive(Clone, Zeroize, ZeroizeOnDrop)]
pub(crate) struct RelativeId {
  pub(crate) index_on_blockchain: u64,
}

impl core::fmt::Debug for RelativeId {
  fn fmt(&self, fmt: &mut core::fmt::Formatter<'_>) -> Result<(), core::fmt::Error> {
    fmt.debug_struct("RelativeId").field("index_on_blockchain", &self.index_on_blockchain).finish()
  }
}

impl RelativeId {
  /// A constant-time `eq`, albeit one not exposed via `ConstantTimeEq`.
  fn ct_eq(&self, other: &Self) -> Choice {
    self.index_on_blockchain.ct_eq(&other.index_on_blockchain)
  }

  /// Write the RelativeId.
  ///
  /// This is not a Monero protocol defined struct, and this is accordingly not a Monero protocol
  /// defined serialization. This may run in time variable to its value.
  fn write<W: Write>(&self, w: &mut W) -> io::Result<()> {
    w.write_all(&self.index_on_blockchain.to_le_bytes())
  }

  /// Read an RelativeId.
  ///
  /// This is not a Monero protocol defined struct, and this is accordingly not a Monero protocol
  /// defined serialization. This may run in time variable to its value.
  fn read<R: Read>(r: &mut R) -> io::Result<Self> {
    Ok(RelativeId { index_on_blockchain: read_u64(r)? })
  }
}

/// The data within an output, as necessary to spend the output.
#[derive(Clone, Zeroize, ZeroizeOnDrop)]
pub(crate) struct OutputData {
  pub(crate) key: Point,
  pub(crate) key_offset: Scalar,
  pub(crate) commitment: Commitment,
}

impl core::fmt::Debug for OutputData {
  fn fmt(&self, fmt: &mut core::fmt::Formatter<'_>) -> Result<(), core::fmt::Error> {
    fmt
      .debug_struct("OutputData")
      .field("key", &hex::encode(self.key.compress().to_bytes()))
      .field("commitment", &self.commitment)
      .finish_non_exhaustive()
  }
}

impl OutputData {
  /// A constant-time `eq`, albeit one not exposed via `ConstantTimeEq`.
  pub(crate) fn ct_eq(&self, other: &Self) -> Choice {
    self.key.ct_eq(&other.key) &
      self.key_offset.ct_eq(&other.key_offset) &
      self.commitment.ct_eq(&other.commitment)
  }

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

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

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

  /// Write the OutputData.
  ///
  /// This is not a Monero protocol defined struct, and this is accordingly not a Monero protocol
  /// defined serialization. This may run in time variable to its value.
  pub(crate) fn write<W: Write>(&self, w: &mut W) -> io::Result<()> {
    w.write_all(&self.key.compress().to_bytes())?;
    self.key_offset.write(w)?;
    self.commitment.write(w)
  }

  /* Commented as it's unused, due to self being private
  /// Serialize the OutputData to a `Vec<u8>`.
  ///
  /// This is not a Monero protocol defined struct, and this is accordingly not a Monero protocol
  /// defined serialization. This may run in time variable to its value.
  pub fn serialize(&self) -> Vec<u8> {
    let mut res = Vec::with_capacity(32 + 32 + 40);
    self.write(&mut res).expect("write failed but <Vec as io::Write> doesn't fail");
    res
  }
  */

  /// Read an OutputData.
  ///
  /// This is not a Monero protocol defined struct, and this is accordingly not a Monero protocol
  /// defined serialization. This may run in time variable to its value.
  pub(crate) fn read<R: Read>(r: &mut R) -> io::Result<OutputData> {
    Ok(OutputData {
      key: CompressedPoint::read(r)?
        .decompress()
        .ok_or_else(|| io::Error::other("output data included an invalid key"))?,
      key_offset: Scalar::read(r)?,
      commitment: Commitment::read(r)?,
    })
  }
}

/// The metadata for an output.
#[derive(Clone, Zeroize, ZeroizeOnDrop)]
pub(crate) struct Metadata {
  pub(crate) additional_timelock: Timelock,
  pub(crate) subaddress: Option<SubaddressIndex>,
  pub(crate) payment_id: Option<PaymentId>,
  pub(crate) arbitrary_data: Vec<Vec<u8>>,
}

impl core::fmt::Debug for Metadata {
  fn fmt(&self, fmt: &mut core::fmt::Formatter<'_>) -> Result<(), core::fmt::Error> {
    fmt
      .debug_struct("Metadata")
      .field("additional_timelock", &self.additional_timelock)
      .field("subaddress", &self.subaddress)
      .field("payment_id", &self.payment_id)
      .field("arbitrary_data", &self.arbitrary_data.iter().map(hex::encode).collect::<Vec<_>>())
      .finish()
  }
}

impl Metadata {
  fn eq(&self, other: &Self) -> bool {
    (self.additional_timelock == other.additional_timelock) &&
      (self.subaddress == other.subaddress) &&
      (self.payment_id == other.payment_id) &&
      (self.arbitrary_data == other.arbitrary_data)
  }

  /// Write the Metadata.
  ///
  /// This is not a Monero protocol defined struct, and this is accordingly not a Monero protocol
  /// defined serialization. This may run in time variable to its value.
  fn write<W: Write>(&self, w: &mut W) -> io::Result<()> {
    self.additional_timelock.write(w)?;

    if let Some(subaddress) = self.subaddress {
      w.write_all(&[1])?;
      w.write_all(&subaddress.account().to_le_bytes())?;
      w.write_all(&subaddress.address().to_le_bytes())?;
    } else {
      w.write_all(&[0])?;
    }

    if let Some(payment_id) = self.payment_id {
      w.write_all(&[1])?;
      payment_id.write(w)?;
    } else {
      w.write_all(&[0])?;
    }

    VarInt::write(&self.arbitrary_data.len(), w)?;
    for part in &self.arbitrary_data {
      #[expect(clippy::as_conversions)]
      const _ASSERT_MAX_ARBITRARY_DATA_SIZE_FITS_WITHIN_U8: [();
        (u8::MAX as usize) - MAX_ARBITRARY_DATA_SIZE] = [(); _];
      w.write_all(&[
        u8::try_from(part.len()).expect("piece of arbitrary data exceeded max length of u8::MAX")
      ])?;
      w.write_all(part)?;
    }
    Ok(())
  }

  /// Read a Metadata.
  ///
  /// This is not a Monero protocol defined struct, and this is accordingly not a Monero protocol
  /// defined serialization. This may run in time variable to its value.
  fn read<R: Read>(r: &mut R) -> io::Result<Metadata> {
    let additional_timelock = Timelock::read(r)?;

    let subaddress = match read_byte(r)? {
      0 => None,
      1 => Some(
        SubaddressIndex::new(read_u32(r)?, read_u32(r)?)
          .ok_or_else(|| io::Error::other("invalid subaddress in metadata"))?,
      ),
      _ => Err(io::Error::other("invalid subaddress is_some boolean in metadata"))?,
    };

    Ok(Metadata {
      additional_timelock,
      subaddress,
      payment_id: if read_byte(r)? == 1 { PaymentId::read(r).ok() } else { None },
      /*
        This may technically read more `arbitrary_data` than can fit in actual transaction as it
        only checks the arbitrary data, raw, will fit in an extra, with no other structure/fields.
      */
      arbitrary_data: {
        let chunks = <usize as VarInt>::read(r)?;
        // Each chunk will use at least one byte to be declared
        if chunks > MAX_EXTRA_SIZE_BY_RELAY_RULE {
          Err(io::Error::other(
            "amount of arbitrary data chunks exceeded amount possible under policy",
          ))?;
        }

        let mut data = vec![];
        let mut total_len = 0usize;
        for _ in 0 .. chunks {
          let len = read_byte(r)?;
          let chunk = read_raw_vec(read_byte, usize::from(len), r)?;
          total_len = total_len.saturating_add(chunk.len());
          if total_len > MAX_EXTRA_SIZE_BY_RELAY_RULE {
            Err(io::Error::other("amount of arbitrary data exceeded amount allowed by policy"))?;
          }
          data.push(chunk);
        }
        data
      },
    })
  }
}

/// A scanned output and all associated data.
///
/// This struct contains all data necessary to spend this output, or handle it as a payment.
///
/// This struct is bound to a specific instance of the blockchain. If the blockchain reorganizes
/// the block this struct is bound to, it MUST be discarded. If any outputs are mutual to both
/// blockchains, scanning the new blockchain will yield those outputs again.
///
/// The `Debug` implementation may reveal every value within its memory.
#[derive(Clone, Debug, Zeroize, ZeroizeOnDrop)]
pub struct WalletOutput {
  /// The absolute ID for this transaction.
  pub(crate) absolute_id: AbsoluteId,
  /// The ID for this transaction, relative to the blockchain.
  pub(crate) relative_id: RelativeId,
  /// The output's data.
  pub(crate) data: OutputData,
  /// Associated metadata relevant for handling it as a payment.
  pub(crate) metadata: Metadata,
}

impl PartialEq for WalletOutput {
  /// This equality evaluates the entire object, not just the ID.
  fn eq(&self, other: &Self) -> bool {
    bool::from(
      self.absolute_id.ct_eq(&other.absolute_id) &
        self.relative_id.ct_eq(&other.relative_id) &
        self.data.ct_eq(&other.data),
    ) & self.metadata.eq(&other.metadata)
  }
}
impl Eq for WalletOutput {}

impl WalletOutput {
  /// The hash of the transaction which created this output.
  pub fn transaction(&self) -> [u8; 32] {
    self.absolute_id.transaction
  }

  /// The index of the output within the transaction.
  pub fn index_in_transaction(&self) -> u64 {
    self.absolute_id.index_in_transaction
  }

  /// The index of the output on the blockchain.
  pub fn index_on_blockchain(&self) -> u64 {
    self.relative_id.index_on_blockchain
  }

  /// The key this output may be spent by.
  pub fn key(&self) -> Point {
    self.data.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.data.key_offset()
  }

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

  /// The additional timelock this output is subject to.
  ///
  /// All outputs are subject to the '10-block lock', a 10-block window after their inclusion
  /// on-chain during which they cannot be spent. Outputs may be additionally timelocked. This
  /// function only returns the additional timelock.
  pub fn additional_timelock(&self) -> Timelock {
    self.metadata.additional_timelock
  }

  /// The index of the subaddress this output was identified as sent to.
  pub fn subaddress(&self) -> Option<SubaddressIndex> {
    self.metadata.subaddress
  }

  /// The payment ID included with this output.
  ///
  /// This field may be `Some` even if wallet2 would not return a payment ID. wallet2 will only
  /// decrypt a payment ID if either:
  ///
  /// A) The transaction wasn't made by the wallet (via checking if any key images are recognized)
  /// B) For the highest-indexed input with a recognized key image, it spends an output with
  ///    subaddress account `(a, _)` which is distinct from this output's subaddress account
  ///
  /// Neither of these cases are handled by `monero-wallet` as scanning doesn't have the context
  /// of key images.
  //
  // Identification of the subaddress account for the highest-indexed input with a recognized key
  // image:
  //   https://github.com/monero-project/monero/blob/a1dc85c5373a30f14aaf7dcfdd95f5a7375d3623
  //     /src/wallet/wallet2.cpp/#L2637-L2670
  //
  // Removal of 'transfers' received to this account:
  //   https://github.com/monero-project/monero/blob/a1dc85c5373a30f14aaf7dcfdd95f5a7375d3623
  //     /src/wallet/wallet2.cpp/#L2782-L2794
  //
  // Payment IDs only being decrypted for the remaining transfers:
  //   https://github.com/monero-project/monero/blob/a1dc85c5373a30f14aaf7dcfdd95f5a7375d3623
  //     /src/wallet/wallet2.cpp/#L2796-L2844
  pub fn payment_id(&self) -> Option<PaymentId> {
    self.metadata.payment_id
  }

  /// The arbitrary data from the `extra` field of the transaction which created this output.
  pub fn arbitrary_data(&self) -> &[Vec<u8>] {
    &self.metadata.arbitrary_data
  }

  /// Write the WalletOutput.
  ///
  /// This is not a Monero protocol defined struct, and this is accordingly not a Monero protocol
  /// defined serialization. This may run in time variable to its value.
  pub fn write<W: Write>(&self, w: &mut W) -> io::Result<()> {
    self.absolute_id.write(w)?;
    self.relative_id.write(w)?;
    self.data.write(w)?;
    self.metadata.write(w)
  }

  /// Serialize the WalletOutput to a `Vec<u8>`.
  ///
  /// This is not a Monero protocol defined struct, and this is accordingly not a Monero protocol
  /// defined serialization. This may run in time variable to its value.
  pub fn serialize(&self) -> Vec<u8> {
    let mut serialized = Vec::with_capacity(128);
    self.write(&mut serialized).expect("write failed but <Vec as io::Write> doesn't fail");
    serialized
  }

  /// Read a WalletOutput.
  ///
  /// This is not a Monero protocol defined struct, and this is accordingly not a Monero protocol
  /// defined serialization. This may run in time variable to its value.
  pub fn read<R: Read>(r: &mut R) -> io::Result<WalletOutput> {
    Ok(WalletOutput {
      absolute_id: AbsoluteId::read(r)?,
      relative_id: RelativeId::read(r)?,
      data: OutputData::read(r)?,
      metadata: Metadata::read(r)?,
    })
  }
}