monero_wallet/
extra.rs

1use core::{ops::BitXor, num::NonZero};
2use std_shims::{
3  vec,
4  vec::Vec,
5  io::{self, Read, BufRead, Write},
6};
7
8use zeroize::Zeroize;
9
10use monero_oxide::{
11  io::*,
12  ed25519::{CompressedPoint, Point},
13};
14
15const MAX_TX_EXTRA_NONCE_SIZE: usize = 255;
16
17const PAYMENT_ID_MARKER: u8 = 0;
18const ENCRYPTED_PAYMENT_ID_MARKER: u8 = 1;
19// Used as it's the highest value not interpretable as a continued VarInt
20pub(crate) const ARBITRARY_DATA_MARKER: u8 = 127;
21
22/// The max amount of data which will fit within a blob of arbitrary data.
23// 1 byte is used for the marker
24pub const MAX_ARBITRARY_DATA_SIZE: usize = MAX_TX_EXTRA_NONCE_SIZE - 1;
25
26/// The maximum length for a transaction's extra under current relay rules.
27// https://github.com/monero-project/monero
28//  /blob/8d4c625713e3419573dfcc7119c8848f47cabbaa/src/cryptonote_config.h#L217
29pub const MAX_EXTRA_SIZE_BY_RELAY_RULE: usize = 1060;
30
31/// A Payment ID.
32///
33/// This is a legacy method of identifying why Monero was sent to the receiver.
34#[derive(Clone, Copy, PartialEq, Eq, Debug, Zeroize)]
35pub enum PaymentId {
36  /// A deprecated form of payment ID which is no longer supported.
37  Unencrypted([u8; 32]),
38  /// An encrypted payment ID.
39  Encrypted([u8; 8]),
40}
41
42impl BitXor<[u8; 8]> for PaymentId {
43  type Output = PaymentId;
44
45  fn bitxor(self, bytes: [u8; 8]) -> PaymentId {
46    match self {
47      // Don't perform the xor since this isn't intended to be encrypted with xor
48      PaymentId::Unencrypted(_) => self,
49      PaymentId::Encrypted(id) => {
50        PaymentId::Encrypted((u64::from_le_bytes(id) ^ u64::from_le_bytes(bytes)).to_le_bytes())
51      }
52    }
53  }
54}
55
56impl PaymentId {
57  /// Write the PaymentId.
58  pub fn write<W: Write>(&self, w: &mut W) -> io::Result<()> {
59    match self {
60      PaymentId::Unencrypted(id) => {
61        w.write_all(&[PAYMENT_ID_MARKER])?;
62        w.write_all(id)?;
63      }
64      PaymentId::Encrypted(id) => {
65        w.write_all(&[ENCRYPTED_PAYMENT_ID_MARKER])?;
66        w.write_all(id)?;
67      }
68    }
69    Ok(())
70  }
71
72  /// Serialize the PaymentId to a `Vec<u8>`.
73  pub fn serialize(&self) -> Vec<u8> {
74    let mut res = Vec::with_capacity(1 + 8);
75    self.write(&mut res).expect("write failed but <Vec as io::Write> doesn't fail");
76    res
77  }
78
79  /// Read a PaymentId.
80  pub fn read<R: Read>(r: &mut R) -> io::Result<PaymentId> {
81    Ok(match read_byte(r)? {
82      0 => PaymentId::Unencrypted(read_bytes(r)?),
83      1 => PaymentId::Encrypted(read_bytes(r)?),
84      _ => Err(io::Error::other("unknown payment ID type"))?,
85    })
86  }
87}
88
89/// A field within the TX extra.
90#[derive(Clone, PartialEq, Eq, Debug, Zeroize)]
91pub enum ExtraField {
92  /// Padding.
93  ///
94  /// This is a block of zeroes within the TX extra.
95  Padding(NonZero<u8>),
96  /// The transaction key.
97  ///
98  /// This is a commitment to the randomness used for deriving outputs.
99  PublicKey(CompressedPoint),
100  /// The nonce field.
101  ///
102  /// This is used for data, such as payment IDs.
103  ///
104  /// When read, this is bounded by a maximum size. As we directly expose the field here (without a
105  /// constructor asserting its validity), this means it's possible to create an
106  /// `ExtraField::Nonce` which can be written but not read. Please be careful accordingly.
107  Nonce(Vec<u8>),
108  /// The field for merge-mining.
109  ///
110  /// This is used within miner transactions who are merge-mining Monero to specify the foreign
111  /// block they mined.
112  MergeMining(u64, [u8; 32]),
113  /// The additional transaction keys.
114  ///
115  /// These are the per-output commitments to the randomness used for deriving outputs.
116  PublicKeys(Vec<CompressedPoint>),
117  /// The 'mysterious' Minergate tag.
118  ///
119  /// This was used by a closed source entity without documentation. Support for parsing it was
120  /// added to reduce extra which couldn't be decoded.
121  MysteriousMinergate(Vec<u8>),
122}
123
124impl ExtraField {
125  /// Write the ExtraField.
126  pub fn write<W: Write>(&self, w: &mut W) -> io::Result<()> {
127    match self {
128      ExtraField::Padding(size) => {
129        w.write_all(&[0])?;
130        for _ in 1 .. u8::from(*size) {
131          write_byte(&0u8, w)?;
132        }
133      }
134      ExtraField::PublicKey(key) => {
135        w.write_all(&[1])?;
136        key.write(w)?;
137      }
138      ExtraField::Nonce(data) => {
139        w.write_all(&[2])?;
140        /*
141          This uses `write_vec`, where the `Vec` will be length-prefixed with a VarInt, which
142          differs from Monero's `add_extra_nonce_to_tx_extra`:
143
144          https://github.com/monero-project/monero/blob/02357fe53fbcab3f5102183f0837feed68cf5355
145            /src/cryptonote_basic/cryptonote_format_utils.cpp#L726
146
147          This is because the definition in `tx_extra.h` is followed which does consider this a
148          `VarInt`-length-prefixed container:
149
150          https://github.com/monero-project/monero/blob/02357fe53fbcab3f5102183f0837feed68cf5355
151            /src/cryptonote_basic/tx_extra.h#L112-L115
152
153          The former is considered faulty. See https://github.com/monero-project/monero/pull/10220.
154        */
155        write_vec(write_byte, data, w)?;
156      }
157      ExtraField::MergeMining(depth, merkle_root) => {
158        w.write_all(&[3])?;
159        VarInt::write(&(depth.varint_len() + merkle_root.len()), w)?;
160        VarInt::write(depth, w)?;
161        w.write_all(merkle_root)?;
162      }
163      ExtraField::PublicKeys(keys) => {
164        w.write_all(&[4])?;
165        write_vec(CompressedPoint::write, keys, w)?;
166      }
167      ExtraField::MysteriousMinergate(data) => {
168        w.write_all(&[0xDE])?;
169        write_vec(write_byte, data, w)?;
170      }
171    }
172    Ok(())
173  }
174
175  /// Serialize the ExtraField to a `Vec<u8>`.
176  pub fn serialize(&self) -> Vec<u8> {
177    let mut res = Vec::with_capacity(1 + 8);
178    self.write(&mut res).expect("write failed but <Vec as io::Write> doesn't fail");
179    res
180  }
181
182  /// Read an ExtraField.
183  ///
184  /// This may be lossy in that `ExtraField::read(&mut buf.as_slice()).serialize() == buf` is not
185  /// guaranteed to hold true.
186  pub fn read<R: BufRead>(r: &mut R) -> io::Result<ExtraField> {
187    Ok(match read_byte(r)? {
188      0 => ExtraField::Padding({
189        // Read until either non-zero, max padding count, or end of buffer
190        let mut size = 1u8;
191        loop {
192          let buf = r.fill_buf()?;
193          let mut n_consume = 0;
194          for v in buf {
195            if *v != 0u8 {
196              Err(io::Error::other("non-zero value after padding"))?;
197            }
198            n_consume += 1;
199            // https://github.com/monero-project/monero
200            //   /blob/02357fe53fbcab3f5102183f0837feed68cf5355/src/cryptonote_basic/tx_extra.h#L43
201            if size == u8::MAX {
202              Err(io::Error::other("padding exceeded max count"))?;
203            }
204            size += 1;
205          }
206          if n_consume == 0 {
207            break;
208          }
209          r.consume(n_consume);
210        }
211        NonZero::new(size).expect("size started at 1 but incremented to 0?")
212      }),
213      1 => ExtraField::PublicKey(CompressedPoint::read(r)?),
214      2 => ExtraField::Nonce(read_vec(read_byte, Some(MAX_TX_EXTRA_NONCE_SIZE), r)?),
215      3 => {
216        let field_len = <usize as VarInt>::read(r)?;
217        let depth = <u64 as VarInt>::read(r)?;
218        let merkle_root = read_bytes(r)?;
219
220        match field_len.checked_sub(depth.varint_len() + merkle_root.len()) {
221          Some(remaining) => {
222            for _ in 0 .. remaining {
223              read_byte(r)?;
224            }
225          }
226          None => Err(io::Error::other("`MergeMining` tag had a length smaller than its fields"))?,
227        }
228        ExtraField::MergeMining(depth, merkle_root)
229      }
230      4 => ExtraField::PublicKeys(read_vec(CompressedPoint::read, None, r)?),
231      0xDE => ExtraField::MysteriousMinergate(read_vec(read_byte, None, r)?),
232      _ => Err(io::Error::other("unknown extra field"))?,
233    })
234  }
235}
236
237/// The result of decoding a transaction's extra field.
238///
239/// Note that the Monero protocol defines a transaction's `extra` field as a byte vector. This is a
240/// parsed view of such a byte vector, yet the Monero protocol does not require the `extra` field
241/// be parseable. The parsing is also lossy in that
242/// `Extra::read(&mut buf.as_slice()).serialize() == buf` is not guaranteed to hold true.
243#[derive(Clone, PartialEq, Eq, Debug, Zeroize)]
244pub struct Extra(pub(crate) Vec<ExtraField>);
245impl Extra {
246  /// The keys within this extra.
247  ///
248  /// This returns all keys specified with `PublicKey` and the first set of keys specified with
249  /// `PublicKeys`. If any are improperly encoded, identity will be yielded in place, intending to
250  /// cause an ECDH of the identity point, as Monero uses upon improperly-encoded points.
251  // https://github.com/monero-project/monero/blob/cc73fe71162d564ffda8e549b79a350bca53c45
252  //   /src/wallet/wallet2.cpp#L2290-L2300 (use all transaction keys)
253  // https://github.com/monero-project/monero/blob/cc73fe71162d564ffda8e549b79a350bca53c454
254  //   /src/wallet/wallet2.cpp#L2337-L2340 (use only the first set of additional keys)
255  // https://github.com/monero-project/monero/blob/6bb36309d69e7157b459e957a9a2d64c67e5892e
256  //   /src/wallet/wallet2.cpp#L2368-L2373 (public key was improperly encoded)
257  // https://github.com/monero-project/monero/blob/6bb36309d69e7157b459e957a9a2d64c67e5892e
258  //   /src/wallet/wallet2.cpp#L2383-L2387 (additional key was improperly encoded)
259  pub fn keys(&self) -> Option<(Vec<Point>, Option<Vec<Point>>)> {
260    let identity = {
261      use curve25519_dalek::{traits::Identity as _, EdwardsPoint};
262      Point::from(EdwardsPoint::identity())
263    };
264
265    let mut keys = vec![];
266    let mut additional = None;
267    for field in &self.0 {
268      match field.clone() {
269        ExtraField::PublicKey(key) => keys.push(key.decompress().unwrap_or(identity)),
270        ExtraField::PublicKeys(keys) => {
271          additional = additional
272            .or(Some(keys.into_iter().map(|key| key.decompress().unwrap_or(identity)).collect()));
273        }
274        ExtraField::Padding(_) |
275        ExtraField::Nonce(_) |
276        ExtraField::MergeMining(_, _) |
277        ExtraField::MysteriousMinergate(_) => (),
278      }
279    }
280    // Don't return any keys if this was non-standard and didn't include the primary key
281    // https://github.com/monero-project/monero/blob/6bb36309d69e7157b459e957a9a2d64c67e5892e
282    //   /src/wallet/wallet2.cpp#L2338-L2346
283    if keys.is_empty() {
284      None
285    } else {
286      Some((keys, additional))
287    }
288  }
289
290  /// The payment ID embedded within this extra.
291  // Monero finds the first nonce field and reads the payment ID from it:
292  // https://github.com/monero-project/monero/blob/ac02af92867590ca80b2779a7bbeafa99ff94dcb/
293  //   src/wallet/wallet2.cpp#L2709-L2752
294  pub fn payment_id(&self) -> Option<PaymentId> {
295    for field in &self.0 {
296      if let ExtraField::Nonce(data) = field {
297        let mut reader = data.as_slice();
298        let res = PaymentId::read(&mut reader).ok();
299        // https://github.com/monero-project/monero/blob/8d4c625713e3419573dfcc7119c8848f47cabbaa
300        //   /src/cryptonote_basic/cryptonote_format_utils.cpp#L801
301        //
302        //   /src/cryptonote_basic/cryptonote_format_utils.cpp#L811
303        if !reader.is_empty() {
304          None?;
305        }
306        return res;
307      }
308    }
309    None
310  }
311
312  /// The arbitrary data within this extra.
313  ///
314  /// This looks for all instances of `ExtraField::Nonce` with a marker byte of 0b0111_1111. This
315  /// is the largest possible value not interpretable as a VarInt, ensuring it's able to be
316  /// interpreted as a VarInt without issue, and that it's the most unlikely value to be used by
317  /// the Monero wallet protocol itself (which itself has assigned marker bytes incrementally). As
318  /// Monero itself does not support including arbitrary data with its wallet however, this was
319  /// first introduced by `monero-wallet` (under the monero-oxide project) and may be bespoke to
320  /// the ecosystem of monero-oxide and dependents of it.
321  ///
322  /// The data is stored without any padding or encryption applied. Applications MUST consider this
323  /// themselves. As Monero does not reserve any space for arbitrary data, the inclusion of _any_
324  /// arbitrary data will _always_ be a fingerprint even before considering what the data is.
325  /// Applications SHOULD include arbitrary data indistinguishable from random, of a popular length
326  /// (such as padded to the next power of two or the maximum length per chunk) IF arbitrary data
327  /// is included at all.
328  ///
329  /// For applications where indistinguishability from 'regular' Monero transactions is required,
330  /// steganography should be considered. Steganography is somewhat-frowned upon however due to it
331  /// bloating the Monero blockchain however and efficient methods are likely specific to
332  /// individual hard forks. They may also have their own privacy implications, which is why no
333  /// methods of stegnography are supported outright by `monero-wallet`.
334  pub fn arbitrary_data(&self) -> Vec<Vec<u8>> {
335    // Only parse arbitrary data from the amount of extra data accepted under the relay rule
336    let serialized = self.serialize();
337    let bounded_extra =
338      Self::read(&mut &serialized[.. serialized.len().min(MAX_EXTRA_SIZE_BY_RELAY_RULE)])
339        .expect("`Extra::read` only fails if the IO fails and `&[u8]` won't");
340
341    let mut res = vec![];
342    for field in &bounded_extra.0 {
343      if let ExtraField::Nonce(data) = field {
344        if data.first() == Some(&ARBITRARY_DATA_MARKER) {
345          res.push(data[1 ..].to_vec());
346        }
347      }
348    }
349    res
350  }
351
352  pub(crate) fn new(key: CompressedPoint, additional: Vec<CompressedPoint>) -> Extra {
353    let mut res = Extra(Vec::with_capacity(3));
354    // https://github.com/monero-project/monero/blob/cc73fe71162d564ffda8e549b79a350bca53c454
355    //   /src/cryptonote_basic/cryptonote_format_utils.cpp#L627-L633
356    // We only support pushing nonces which come after these in the sort order
357    res.0.push(ExtraField::PublicKey(key));
358    if !additional.is_empty() {
359      res.0.push(ExtraField::PublicKeys(additional));
360    }
361    res
362  }
363
364  // TODO: This allows pushing a nonce of size greater than allowed. That's likely fine as it's
365  // internal, yet should be better?
366  pub(crate) fn push_nonce(&mut self, nonce: Vec<u8>) {
367    self.0.push(ExtraField::Nonce(nonce));
368  }
369
370  /// Write the Extra.
371  ///
372  /// This will write the value in a sorted fashion.
373  ///
374  /// This is not of deterministic length nor length-prefixed. It should only be written to a
375  /// buffer which will be delimited.
376  pub fn write<W: Write>(&self, w: &mut W) -> io::Result<()> {
377    #[cfg(debug_assertions)]
378    let mut written = 0;
379    // https://github.com/monero-project/monero/blob/02357fe53fbcab3f5102183f0837feed68cf5355
380    //   /src/cryptonote_basic/cryptonote_format_utils.cpp#L618-L624
381    const SORT_ORDER: [fn(&ExtraField) -> bool; 6] = [
382      |field: &ExtraField| matches!(field, ExtraField::PublicKey(_)),
383      |field: &ExtraField| matches!(field, ExtraField::PublicKeys(_)),
384      |field: &ExtraField| matches!(field, ExtraField::Nonce(_)),
385      |field: &ExtraField| matches!(field, ExtraField::MergeMining(_, _)),
386      |field: &ExtraField| matches!(field, ExtraField::MysteriousMinergate(_)),
387      |field: &ExtraField| matches!(field, ExtraField::Padding(_)),
388    ];
389    // Ensure the length of the `SORT_ORDER` array corresponds to the amount of variants
390    #[cfg(monero_oxide_rust_nightly)]
391    const _SORT_LEN: [(); 0 - core::mem::variant_count::<ExtraField>().abs_diff(SORT_ORDER.len())] =
392      [(); _];
393    for selection in SORT_ORDER {
394      for field in &self.0 {
395        if selection(field) {
396          field.write(w)?;
397          #[cfg(debug_assertions)]
398          {
399            written += 1;
400          }
401        }
402      }
403    }
404    #[cfg(debug_assertions)]
405    debug_assert_eq!(written, self.0.len());
406    Ok(())
407  }
408
409  /// Serialize the Extra to a `Vec<u8>`.
410  ///
411  /// This will write the value in a sorted fashion.
412  pub fn serialize(&self) -> Vec<u8> {
413    let mut buf = vec![];
414    self.write(&mut buf).expect("write failed but <Vec as io::Write> doesn't fail");
415    buf
416  }
417
418  /// Read an `Extra`.
419  ///
420  /// This is not of deterministic length nor length-prefixed. It should only be read from a buffer
421  /// already delimited.
422  pub fn read<R: BufRead>(r: &mut R) -> io::Result<Extra> {
423    let mut res = Extra(vec![]);
424    // Extra reads until EOF
425    // We take a BufRead so we can detect when the buffer is empty
426    // `fill_buf` returns the current buffer, filled if empty, only empty if the reader is
427    // exhausted
428    while !r.fill_buf()?.is_empty() {
429      let Ok(field) = ExtraField::read(r) else { break };
430      res.0.push(field);
431    }
432    Ok(res)
433  }
434}