monero_wallet/
output.rs

1use std_shims::{
2  vec,
3  vec::Vec,
4  io::{self, Read, Write},
5};
6
7use zeroize::{Zeroize, ZeroizeOnDrop};
8use subtle::{Choice, ConstantTimeEq as _};
9
10use crate::{
11  io::*,
12  ed25519::{Scalar, CompressedPoint, Point, Commitment},
13  transaction::Timelock,
14  address::SubaddressIndex,
15  extra::{MAX_ARBITRARY_DATA_SIZE, MAX_EXTRA_SIZE_BY_RELAY_RULE, PaymentId},
16};
17
18/// An absolute output ID, defined as its transaction hash and output index.
19///
20/// This is not the output's key as multiple outputs may share an output key.
21#[derive(Clone, Zeroize, ZeroizeOnDrop)]
22pub(crate) struct AbsoluteId {
23  pub(crate) transaction: [u8; 32],
24  pub(crate) index_in_transaction: u64,
25}
26
27impl core::fmt::Debug for AbsoluteId {
28  fn fmt(&self, fmt: &mut core::fmt::Formatter<'_>) -> Result<(), core::fmt::Error> {
29    fmt
30      .debug_struct("AbsoluteId")
31      .field("transaction", &hex::encode(self.transaction))
32      .field("index_in_transaction", &self.index_in_transaction)
33      .finish()
34  }
35}
36
37impl AbsoluteId {
38  /// A constant-time `eq`, albeit one not exposed via `ConstantTimeEq`.
39  fn ct_eq(&self, other: &Self) -> Choice {
40    self.transaction.ct_eq(&other.transaction) &
41      self.index_in_transaction.ct_eq(&other.index_in_transaction)
42  }
43
44  /// Write the AbsoluteId.
45  ///
46  /// This is not a Monero protocol defined struct, and this is accordingly not a Monero protocol
47  /// defined serialization. This may run in time variable to its value.
48  fn write<W: Write>(&self, w: &mut W) -> io::Result<()> {
49    w.write_all(&self.transaction)?;
50    w.write_all(&self.index_in_transaction.to_le_bytes())
51  }
52
53  /// Read an AbsoluteId.
54  ///
55  /// This is not a Monero protocol defined struct, and this is accordingly not a Monero protocol
56  /// defined serialization. This may run in time variable to its value.
57  fn read<R: Read>(r: &mut R) -> io::Result<AbsoluteId> {
58    Ok(AbsoluteId { transaction: read_bytes(r)?, index_in_transaction: read_u64(r)? })
59  }
60}
61
62/// An output's relative ID.
63///
64/// This is defined as the output's index on the blockchain.
65#[derive(Clone, Zeroize, ZeroizeOnDrop)]
66pub(crate) struct RelativeId {
67  pub(crate) index_on_blockchain: u64,
68}
69
70impl core::fmt::Debug for RelativeId {
71  fn fmt(&self, fmt: &mut core::fmt::Formatter<'_>) -> Result<(), core::fmt::Error> {
72    fmt.debug_struct("RelativeId").field("index_on_blockchain", &self.index_on_blockchain).finish()
73  }
74}
75
76impl RelativeId {
77  /// A constant-time `eq`, albeit one not exposed via `ConstantTimeEq`.
78  fn ct_eq(&self, other: &Self) -> Choice {
79    self.index_on_blockchain.ct_eq(&other.index_on_blockchain)
80  }
81
82  /// Write the RelativeId.
83  ///
84  /// This is not a Monero protocol defined struct, and this is accordingly not a Monero protocol
85  /// defined serialization. This may run in time variable to its value.
86  fn write<W: Write>(&self, w: &mut W) -> io::Result<()> {
87    w.write_all(&self.index_on_blockchain.to_le_bytes())
88  }
89
90  /// Read an RelativeId.
91  ///
92  /// This is not a Monero protocol defined struct, and this is accordingly not a Monero protocol
93  /// defined serialization. This may run in time variable to its value.
94  fn read<R: Read>(r: &mut R) -> io::Result<Self> {
95    Ok(RelativeId { index_on_blockchain: read_u64(r)? })
96  }
97}
98
99/// The data within an output, as necessary to spend the output.
100#[derive(Clone, Zeroize, ZeroizeOnDrop)]
101pub(crate) struct OutputData {
102  pub(crate) key: Point,
103  pub(crate) key_offset: Scalar,
104  pub(crate) commitment: Commitment,
105}
106
107impl core::fmt::Debug for OutputData {
108  fn fmt(&self, fmt: &mut core::fmt::Formatter<'_>) -> Result<(), core::fmt::Error> {
109    fmt
110      .debug_struct("OutputData")
111      .field("key", &hex::encode(self.key.compress().to_bytes()))
112      .field("commitment", &self.commitment)
113      .finish_non_exhaustive()
114  }
115}
116
117impl OutputData {
118  /// A constant-time `eq`, albeit one not exposed via `ConstantTimeEq`.
119  pub(crate) fn ct_eq(&self, other: &Self) -> Choice {
120    self.key.ct_eq(&other.key) &
121      self.key_offset.ct_eq(&other.key_offset) &
122      self.commitment.ct_eq(&other.commitment)
123  }
124
125  /// The key this output may be spent by.
126  pub(crate) fn key(&self) -> Point {
127    self.key
128  }
129
130  /// The scalar to add to the private spend key for it to be the discrete logarithm of this
131  /// output's key.
132  pub(crate) fn key_offset(&self) -> Scalar {
133    self.key_offset
134  }
135
136  /// The commitment this output created.
137  pub(crate) fn commitment(&self) -> &Commitment {
138    &self.commitment
139  }
140
141  /// Write the OutputData.
142  ///
143  /// This is not a Monero protocol defined struct, and this is accordingly not a Monero protocol
144  /// defined serialization. This may run in time variable to its value.
145  pub(crate) fn write<W: Write>(&self, w: &mut W) -> io::Result<()> {
146    w.write_all(&self.key.compress().to_bytes())?;
147    self.key_offset.write(w)?;
148    self.commitment.write(w)
149  }
150
151  /* Commented as it's unused, due to self being private
152  /// Serialize the OutputData to a `Vec<u8>`.
153  ///
154  /// This is not a Monero protocol defined struct, and this is accordingly not a Monero protocol
155  /// defined serialization. This may run in time variable to its value.
156  pub fn serialize(&self) -> Vec<u8> {
157    let mut res = Vec::with_capacity(32 + 32 + 40);
158    self.write(&mut res).expect("write failed but <Vec as io::Write> doesn't fail");
159    res
160  }
161  */
162
163  /// Read an OutputData.
164  ///
165  /// This is not a Monero protocol defined struct, and this is accordingly not a Monero protocol
166  /// defined serialization. This may run in time variable to its value.
167  pub(crate) fn read<R: Read>(r: &mut R) -> io::Result<OutputData> {
168    Ok(OutputData {
169      key: CompressedPoint::read(r)?
170        .decompress()
171        .ok_or_else(|| io::Error::other("output data included an invalid key"))?,
172      key_offset: Scalar::read(r)?,
173      commitment: Commitment::read(r)?,
174    })
175  }
176}
177
178/// The metadata for an output.
179#[derive(Clone, Zeroize, ZeroizeOnDrop)]
180pub(crate) struct Metadata {
181  pub(crate) additional_timelock: Timelock,
182  pub(crate) subaddress: Option<SubaddressIndex>,
183  pub(crate) payment_id: Option<PaymentId>,
184  pub(crate) arbitrary_data: Vec<Vec<u8>>,
185}
186
187impl core::fmt::Debug for Metadata {
188  fn fmt(&self, fmt: &mut core::fmt::Formatter<'_>) -> Result<(), core::fmt::Error> {
189    fmt
190      .debug_struct("Metadata")
191      .field("additional_timelock", &self.additional_timelock)
192      .field("subaddress", &self.subaddress)
193      .field("payment_id", &self.payment_id)
194      .field("arbitrary_data", &self.arbitrary_data.iter().map(hex::encode).collect::<Vec<_>>())
195      .finish()
196  }
197}
198
199impl Metadata {
200  fn eq(&self, other: &Self) -> bool {
201    (self.additional_timelock == other.additional_timelock) &&
202      (self.subaddress == other.subaddress) &&
203      (self.payment_id == other.payment_id) &&
204      (self.arbitrary_data == other.arbitrary_data)
205  }
206
207  /// Write the Metadata.
208  ///
209  /// This is not a Monero protocol defined struct, and this is accordingly not a Monero protocol
210  /// defined serialization. This may run in time variable to its value.
211  fn write<W: Write>(&self, w: &mut W) -> io::Result<()> {
212    self.additional_timelock.write(w)?;
213
214    if let Some(subaddress) = self.subaddress {
215      w.write_all(&[1])?;
216      w.write_all(&subaddress.account().to_le_bytes())?;
217      w.write_all(&subaddress.address().to_le_bytes())?;
218    } else {
219      w.write_all(&[0])?;
220    }
221
222    if let Some(payment_id) = self.payment_id {
223      w.write_all(&[1])?;
224      payment_id.write(w)?;
225    } else {
226      w.write_all(&[0])?;
227    }
228
229    VarInt::write(&self.arbitrary_data.len(), w)?;
230    for part in &self.arbitrary_data {
231      #[expect(clippy::as_conversions)]
232      const _ASSERT_MAX_ARBITRARY_DATA_SIZE_FITS_WITHIN_U8: [();
233        (u8::MAX as usize) - MAX_ARBITRARY_DATA_SIZE] = [(); _];
234      w.write_all(&[
235        u8::try_from(part.len()).expect("piece of arbitrary data exceeded max length of u8::MAX")
236      ])?;
237      w.write_all(part)?;
238    }
239    Ok(())
240  }
241
242  /// Read a Metadata.
243  ///
244  /// This is not a Monero protocol defined struct, and this is accordingly not a Monero protocol
245  /// defined serialization. This may run in time variable to its value.
246  fn read<R: Read>(r: &mut R) -> io::Result<Metadata> {
247    let additional_timelock = Timelock::read(r)?;
248
249    let subaddress = match read_byte(r)? {
250      0 => None,
251      1 => Some(
252        SubaddressIndex::new(read_u32(r)?, read_u32(r)?)
253          .ok_or_else(|| io::Error::other("invalid subaddress in metadata"))?,
254      ),
255      _ => Err(io::Error::other("invalid subaddress is_some boolean in metadata"))?,
256    };
257
258    Ok(Metadata {
259      additional_timelock,
260      subaddress,
261      payment_id: if read_byte(r)? == 1 { PaymentId::read(r).ok() } else { None },
262      /*
263        This may technically read more `arbitrary_data` than can fit in actual transaction as it
264        only checks the arbitrary data, raw, will fit in an extra, with no other structure/fields.
265      */
266      arbitrary_data: {
267        let chunks = <usize as VarInt>::read(r)?;
268        // Each chunk will use at least one byte to be declared
269        if chunks > MAX_EXTRA_SIZE_BY_RELAY_RULE {
270          Err(io::Error::other(
271            "amount of arbitrary data chunks exceeded amount possible under policy",
272          ))?;
273        }
274
275        let mut data = vec![];
276        let mut total_len = 0usize;
277        for _ in 0 .. chunks {
278          let len = read_byte(r)?;
279          let chunk = read_raw_vec(read_byte, usize::from(len), r)?;
280          total_len = total_len.saturating_add(chunk.len());
281          if total_len > MAX_EXTRA_SIZE_BY_RELAY_RULE {
282            Err(io::Error::other("amount of arbitrary data exceeded amount allowed by policy"))?;
283          }
284          data.push(chunk);
285        }
286        data
287      },
288    })
289  }
290}
291
292/// A scanned output and all associated data.
293///
294/// This struct contains all data necessary to spend this output, or handle it as a payment.
295///
296/// This struct is bound to a specific instance of the blockchain. If the blockchain reorganizes
297/// the block this struct is bound to, it MUST be discarded. If any outputs are mutual to both
298/// blockchains, scanning the new blockchain will yield those outputs again.
299///
300/// The `Debug` implementation may reveal every value within its memory.
301#[derive(Clone, Debug, Zeroize, ZeroizeOnDrop)]
302pub struct WalletOutput {
303  /// The absolute ID for this transaction.
304  pub(crate) absolute_id: AbsoluteId,
305  /// The ID for this transaction, relative to the blockchain.
306  pub(crate) relative_id: RelativeId,
307  /// The output's data.
308  pub(crate) data: OutputData,
309  /// Associated metadata relevant for handling it as a payment.
310  pub(crate) metadata: Metadata,
311}
312
313impl PartialEq for WalletOutput {
314  /// This equality evaluates the entire object, not just the ID.
315  fn eq(&self, other: &Self) -> bool {
316    bool::from(
317      self.absolute_id.ct_eq(&other.absolute_id) &
318        self.relative_id.ct_eq(&other.relative_id) &
319        self.data.ct_eq(&other.data),
320    ) & self.metadata.eq(&other.metadata)
321  }
322}
323impl Eq for WalletOutput {}
324
325impl WalletOutput {
326  /// The hash of the transaction which created this output.
327  pub fn transaction(&self) -> [u8; 32] {
328    self.absolute_id.transaction
329  }
330
331  /// The index of the output within the transaction.
332  pub fn index_in_transaction(&self) -> u64 {
333    self.absolute_id.index_in_transaction
334  }
335
336  /// The index of the output on the blockchain.
337  pub fn index_on_blockchain(&self) -> u64 {
338    self.relative_id.index_on_blockchain
339  }
340
341  /// The key this output may be spent by.
342  pub fn key(&self) -> Point {
343    self.data.key()
344  }
345
346  /// The scalar to add to the private spend key for it to be the discrete logarithm of this
347  /// output's key.
348  pub fn key_offset(&self) -> Scalar {
349    self.data.key_offset()
350  }
351
352  /// The commitment this output created.
353  pub fn commitment(&self) -> &Commitment {
354    self.data.commitment()
355  }
356
357  /// The additional timelock this output is subject to.
358  ///
359  /// All outputs are subject to the '10-block lock', a 10-block window after their inclusion
360  /// on-chain during which they cannot be spent. Outputs may be additionally timelocked. This
361  /// function only returns the additional timelock.
362  pub fn additional_timelock(&self) -> Timelock {
363    self.metadata.additional_timelock
364  }
365
366  /// The index of the subaddress this output was identified as sent to.
367  pub fn subaddress(&self) -> Option<SubaddressIndex> {
368    self.metadata.subaddress
369  }
370
371  /// The payment ID included with this output.
372  ///
373  /// This field may be `Some` even if wallet2 would not return a payment ID. wallet2 will only
374  /// decrypt a payment ID if either:
375  ///
376  /// A) The transaction wasn't made by the wallet (via checking if any key images are recognized)
377  /// B) For the highest-indexed input with a recognized key image, it spends an output with
378  ///    subaddress account `(a, _)` which is distinct from this output's subaddress account
379  ///
380  /// Neither of these cases are handled by `monero-wallet` as scanning doesn't have the context
381  /// of key images.
382  //
383  // Identification of the subaddress account for the highest-indexed input with a recognized key
384  // image:
385  //   https://github.com/monero-project/monero/blob/a1dc85c5373a30f14aaf7dcfdd95f5a7375d3623
386  //     /src/wallet/wallet2.cpp/#L2637-L2670
387  //
388  // Removal of 'transfers' received to this account:
389  //   https://github.com/monero-project/monero/blob/a1dc85c5373a30f14aaf7dcfdd95f5a7375d3623
390  //     /src/wallet/wallet2.cpp/#L2782-L2794
391  //
392  // Payment IDs only being decrypted for the remaining transfers:
393  //   https://github.com/monero-project/monero/blob/a1dc85c5373a30f14aaf7dcfdd95f5a7375d3623
394  //     /src/wallet/wallet2.cpp/#L2796-L2844
395  pub fn payment_id(&self) -> Option<PaymentId> {
396    self.metadata.payment_id
397  }
398
399  /// The arbitrary data from the `extra` field of the transaction which created this output.
400  pub fn arbitrary_data(&self) -> &[Vec<u8>] {
401    &self.metadata.arbitrary_data
402  }
403
404  /// Write the WalletOutput.
405  ///
406  /// This is not a Monero protocol defined struct, and this is accordingly not a Monero protocol
407  /// defined serialization. This may run in time variable to its value.
408  pub fn write<W: Write>(&self, w: &mut W) -> io::Result<()> {
409    self.absolute_id.write(w)?;
410    self.relative_id.write(w)?;
411    self.data.write(w)?;
412    self.metadata.write(w)
413  }
414
415  /// Serialize the WalletOutput to a `Vec<u8>`.
416  ///
417  /// This is not a Monero protocol defined struct, and this is accordingly not a Monero protocol
418  /// defined serialization. This may run in time variable to its value.
419  pub fn serialize(&self) -> Vec<u8> {
420    let mut serialized = Vec::with_capacity(128);
421    self.write(&mut serialized).expect("write failed but <Vec as io::Write> doesn't fail");
422    serialized
423  }
424
425  /// Read a WalletOutput.
426  ///
427  /// This is not a Monero protocol defined struct, and this is accordingly not a Monero protocol
428  /// defined serialization. This may run in time variable to its value.
429  pub fn read<R: Read>(r: &mut R) -> io::Result<WalletOutput> {
430    Ok(WalletOutput {
431      absolute_id: AbsoluteId::read(r)?,
432      relative_id: RelativeId::read(r)?,
433      data: OutputData::read(r)?,
434      metadata: Metadata::read(r)?,
435    })
436  }
437}