use core::ops::BitXor; use std_shims::{ vec, vec::Vec, io::{self, Read, BufRead, Write}, }; use zeroize::Zeroize; use curve25519_dalek::edwards::EdwardsPoint; use monero_serai::io::*; pub(crate) const MAX_TX_EXTRA_PADDING_COUNT: usize = 255; const MAX_TX_EXTRA_NONCE_SIZE: usize = 255; const PAYMENT_ID_MARKER: u8 = 0; const ENCRYPTED_PAYMENT_ID_MARKER: u8 = 1; // Used as it's the highest value not interpretable as a continued VarInt pub(crate) const ARBITRARY_DATA_MARKER: u8 = 127; /// The max amount of data which will fit within a blob of arbitrary data. // 1 byte is used for the marker pub const MAX_ARBITRARY_DATA_SIZE: usize = MAX_TX_EXTRA_NONCE_SIZE - 1; /// A Payment ID. /// /// This is a legacy method of identifying why Monero was sent to the receiver. #[derive(Clone, Copy, PartialEq, Eq, Debug, Zeroize)] pub enum PaymentId { /// A deprecated form of payment ID which is no longer supported. Unencrypted([u8; 32]), /// An encrypted payment ID. Encrypted([u8; 8]), } impl BitXor<[u8; 8]> for PaymentId { type Output = PaymentId; fn bitxor(self, bytes: [u8; 8]) -> PaymentId { match self { // Don't perform the xor since this isn't intended to be encrypted with xor PaymentId::Unencrypted(_) => self, PaymentId::Encrypted(id) => { PaymentId::Encrypted((u64::from_le_bytes(id) ^ u64::from_le_bytes(bytes)).to_le_bytes()) } } } } impl PaymentId { /// Write the PaymentId. pub fn write(&self, w: &mut W) -> io::Result<()> { match self { PaymentId::Unencrypted(id) => { w.write_all(&[PAYMENT_ID_MARKER])?; w.write_all(id)?; } PaymentId::Encrypted(id) => { w.write_all(&[ENCRYPTED_PAYMENT_ID_MARKER])?; w.write_all(id)?; } } Ok(()) } /// Serialize the PaymentId to a `Vec`. pub fn serialize(&self) -> Vec { let mut res = Vec::with_capacity(1 + 8); self.write(&mut res).unwrap(); res } /// Read a PaymentId. pub fn read(r: &mut R) -> io::Result { Ok(match read_byte(r)? { 0 => PaymentId::Unencrypted(read_bytes(r)?), 1 => PaymentId::Encrypted(read_bytes(r)?), _ => Err(io::Error::other("unknown payment ID type"))?, }) } } /// A field within the TX extra. #[derive(Clone, PartialEq, Eq, Debug, Zeroize)] pub enum ExtraField { /// Padding. /// /// This is a block of zeroes within the TX extra. Padding(usize), /// The transaction key. /// /// This is a commitment to the randomness used for deriving outputs. PublicKey(EdwardsPoint), /// The nonce field. /// /// This is used for data, such as payment IDs. Nonce(Vec), /// The field for merge-mining. /// /// This is used within miner transactions who are merge-mining Monero to specify the foreign /// block they mined. MergeMining(usize, [u8; 32]), /// The additional transaction keys. /// /// These are the per-output commitments to the randomness used for deriving outputs. PublicKeys(Vec), /// The 'mysterious' Minergate tag. /// /// This was used by a closed source entity without documentation. Support for parsing it was /// added to reduce extra which couldn't be decoded. MysteriousMinergate(Vec), } impl ExtraField { /// Write the ExtraField. pub fn write(&self, w: &mut W) -> io::Result<()> { match self { ExtraField::Padding(size) => { w.write_all(&[0])?; for _ in 1 .. *size { write_byte(&0u8, w)?; } } ExtraField::PublicKey(key) => { w.write_all(&[1])?; w.write_all(&key.compress().to_bytes())?; } ExtraField::Nonce(data) => { w.write_all(&[2])?; write_vec(write_byte, data, w)?; } ExtraField::MergeMining(height, merkle) => { w.write_all(&[3])?; write_varint(&u64::try_from(*height).unwrap(), w)?; w.write_all(merkle)?; } ExtraField::PublicKeys(keys) => { w.write_all(&[4])?; write_vec(write_point, keys, w)?; } ExtraField::MysteriousMinergate(data) => { w.write_all(&[0xDE])?; write_vec(write_byte, data, w)?; } } Ok(()) } /// Serialize the ExtraField to a `Vec`. pub fn serialize(&self) -> Vec { let mut res = Vec::with_capacity(1 + 8); self.write(&mut res).unwrap(); res } /// Read an ExtraField. pub fn read(r: &mut R) -> io::Result { Ok(match read_byte(r)? { 0 => ExtraField::Padding({ // Read until either non-zero, max padding count, or end of buffer let mut size: usize = 1; loop { let buf = r.fill_buf()?; let mut n_consume = 0; for v in buf { if *v != 0u8 { Err(io::Error::other("non-zero value after padding"))? } n_consume += 1; size += 1; if size > MAX_TX_EXTRA_PADDING_COUNT { Err(io::Error::other("padding exceeded max count"))? } } if n_consume == 0 { break; } r.consume(n_consume); } size }), 1 => ExtraField::PublicKey(read_point(r)?), 2 => ExtraField::Nonce({ let nonce = read_vec(read_byte, r)?; if nonce.len() > MAX_TX_EXTRA_NONCE_SIZE { Err(io::Error::other("too long nonce"))?; } nonce }), 3 => ExtraField::MergeMining(read_varint(r)?, read_bytes(r)?), 4 => ExtraField::PublicKeys(read_vec(read_point, r)?), 0xDE => ExtraField::MysteriousMinergate(read_vec(read_byte, r)?), _ => Err(io::Error::other("unknown extra field"))?, }) } } /// The result of decoding a transaction's extra field. #[derive(Clone, PartialEq, Eq, Debug, Zeroize)] pub struct Extra(pub(crate) Vec); impl Extra { /// The keys within this extra. /// /// This returns all keys specified with `PublicKey` and the first set of keys specified with /// `PublicKeys`, so long as they're well-formed. // https://github.com/monero-project/monero/blob/cc73fe71162d564ffda8e549b79a350bca53c45 // /src/wallet/wallet2.cpp#L2290-L2300 // https://github.com/monero-project/monero/blob/cc73fe71162d564ffda8e549b79a350bca53c454 // /src/wallet/wallet2.cpp#L2337-L2340 pub fn keys(&self) -> Option<(Vec, Option>)> { let mut keys = vec![]; let mut additional = None; for field in &self.0 { match field.clone() { ExtraField::PublicKey(this_key) => keys.push(this_key), ExtraField::PublicKeys(these_additional) => { additional = additional.or(Some(these_additional)) } _ => (), } } // Don't return any keys if this was non-standard and didn't include the primary key if keys.is_empty() { None } else { Some((keys, additional)) } } /// The payment ID embedded within this extra. // Monero finds the first nonce field and reads the payment ID from it: // https://github.com/monero-project/monero/blob/ac02af92867590ca80b2779a7bbeafa99ff94dcb/ // src/wallet/wallet2.cpp#L2709-L2752 pub fn payment_id(&self) -> Option { for field in &self.0 { if let ExtraField::Nonce(data) = field { return PaymentId::read::<&[u8]>(&mut data.as_ref()).ok(); } } None } /// The arbitrary data within this extra. /// /// This uses a marker custom to monero-wallet. pub fn data(&self) -> Vec> { let mut res = vec![]; for field in &self.0 { if let ExtraField::Nonce(data) = field { if data[0] == ARBITRARY_DATA_MARKER { res.push(data[1 ..].to_vec()); } } } res } pub(crate) fn new(key: EdwardsPoint, additional: Vec) -> Extra { let mut res = Extra(Vec::with_capacity(3)); // https://github.com/monero-project/monero/blob/cc73fe71162d564ffda8e549b79a350bca53c454 // /src/cryptonote_basic/cryptonote_format_utils.cpp#L627-L633 // We only support pushing nonces which come after these in the sort order res.0.push(ExtraField::PublicKey(key)); if !additional.is_empty() { res.0.push(ExtraField::PublicKeys(additional)); } res } pub(crate) fn push_nonce(&mut self, nonce: Vec) { self.0.push(ExtraField::Nonce(nonce)); } /// Write the Extra. /// /// This is not of deterministic length nor length-prefixed. It should only be written to a /// buffer which will be delimited. pub fn write(&self, w: &mut W) -> io::Result<()> { for field in &self.0 { field.write(w)?; } Ok(()) } /// Serialize the Extra to a `Vec`. pub fn serialize(&self) -> Vec { let mut buf = vec![]; self.write(&mut buf).unwrap(); buf } /// Read an `Extra`. /// /// This is not of deterministic length nor length-prefixed. It should only be read from a buffer /// already delimited. #[allow(clippy::unnecessary_wraps)] pub fn read(r: &mut R) -> io::Result { let mut res = Extra(vec![]); // Extra reads until EOF // We take a BufRead so we can detect when the buffer is empty // `fill_buf` returns the current buffer, filled if empty, only empty if the reader is // exhausted while !r.fill_buf()?.is_empty() { let Ok(field) = ExtraField::read(r) else { break }; res.0.push(field); } Ok(res) } }