diff --git a/coins/monero/src/serialize.rs b/coins/monero/src/serialize.rs index 47cbaadc..ee4c715f 100644 --- a/coins/monero/src/serialize.rs +++ b/coins/monero/src/serialize.rs @@ -71,6 +71,10 @@ pub(crate) fn read_u64(r: &mut R) -> io::Result { read_bytes(r).map(u64::from_le_bytes) } +pub(crate) fn read_u32(r: &mut R) -> io::Result { + read_bytes(r).map(u32::from_le_bytes) +} + pub(crate) fn read_varint(r: &mut R) -> io::Result { let mut bits = 0; let mut res = 0; diff --git a/coins/monero/src/wallet/extra.rs b/coins/monero/src/wallet/extra.rs index 6766fff6..97ca033a 100644 --- a/coins/monero/src/wallet/extra.rs +++ b/coins/monero/src/wallet/extra.rs @@ -1,3 +1,4 @@ +use core::ops::BitXor; use std::io::{self, Read, Write}; use zeroize::Zeroize; @@ -15,6 +16,19 @@ pub(crate) enum PaymentId { Encrypted([u8; 8]), } +impl BitXor<[u8; 8]> for PaymentId { + type Output = PaymentId; + + fn bitxor(self, bytes: [u8; 8]) -> PaymentId { + match self { + PaymentId::Unencrypted(_) => self, + PaymentId::Encrypted(id) => { + PaymentId::Encrypted((u64::from_le_bytes(id) ^ u64::from_le_bytes(bytes)).to_le_bytes()) + } + } + } +} + impl PaymentId { fn serialize(&self, w: &mut W) -> io::Result<()> { match self { @@ -115,6 +129,15 @@ impl Extra { keys } + pub(crate) fn payment_id(&self) -> Option { + for field in &self.0 { + if let ExtraField::PaymentId(id) = field { + return Some(*id); + } + } + None + } + pub(crate) fn data(&self) -> Option> { for field in &self.0 { if let ExtraField::Padding(data) = field { diff --git a/coins/monero/src/wallet/scan.rs b/coins/monero/src/wallet/scan.rs index b2aee606..6005dda1 100644 --- a/coins/monero/src/wallet/scan.rs +++ b/coins/monero/src/wallet/scan.rs @@ -6,18 +6,31 @@ use curve25519_dalek::{constants::ED25519_BASEPOINT_TABLE, scalar::Scalar, edwar use crate::{ Commitment, - serialize::{read_byte, read_u64, read_bytes, read_scalar, read_point}, + serialize::{read_byte, read_u32, read_u64, read_bytes, read_scalar, read_point}, transaction::{Timelock, Transaction}, - wallet::{ViewPair, Extra, uniqueness, shared_key, amount_decryption, commitment_mask}, + wallet::{ + ViewPair, PaymentId, Extra, uniqueness, shared_key, amount_decryption, commitment_mask, + }, }; #[derive(Clone, PartialEq, Eq, Debug, Zeroize, ZeroizeOnDrop)] pub struct SpendableOutput { pub tx: [u8; 32], pub o: u8, + pub key: EdwardsPoint, pub key_offset: Scalar, pub commitment: Commitment, + + // Does not have to be an Option since the 0 subaddress is the main address + pub subaddress: (u32, u32), + // Can be an Option, as extra doesn't necessarily have a payment ID, yet all Monero TXs should + // have this + // This will be gibberish if the payment ID wasn't intended for the recipient + // This will be [0xff; 8] if the transaction didn't have a payment ID + // 0xff was chosen as it'd be distinct from [0; 8], enabling atomically incrementing IDs (though + // they should be randomly generated) + pub payment_id: [u8; 8], } #[derive(Zeroize, ZeroizeOnDrop)] @@ -48,12 +61,19 @@ impl Timelocked { impl SpendableOutput { pub fn serialize(&self) -> Vec { let mut res = Vec::with_capacity(32 + 1 + 32 + 32 + 40); + res.extend(&self.tx); res.push(self.o); + res.extend(self.key.compress().to_bytes()); res.extend(self.key_offset.to_bytes()); res.extend(self.commitment.mask.to_bytes()); res.extend(self.commitment.amount.to_le_bytes()); + + res.extend(self.subaddress.0.to_le_bytes()); + res.extend(self.subaddress.1.to_le_bytes()); + res.extend(self.payment_id); + res } @@ -61,9 +81,13 @@ impl SpendableOutput { Ok(SpendableOutput { tx: read_bytes(r)?, o: read_byte(r)?, + key: read_point(r)?, key_offset: read_scalar(r)?, commitment: Commitment::new(read_scalar(r)?, read_u64(r)?), + + subaddress: (read_u32(r)?, read_u32(r)?), + payment_id: read_bytes(r)?, }) } } @@ -72,22 +96,31 @@ impl Transaction { pub fn scan(&self, view: &ViewPair, guaranteed: bool) -> Timelocked { let extra = Extra::deserialize(&mut Cursor::new(&self.prefix.extra)); let keys; - if let Ok(extra) = extra { + let extra = if let Ok(extra) = extra { keys = extra.keys(); + extra } else { return Timelocked(self.prefix.timelock, vec![]); }; + let payment_id = extra.payment_id(); let mut res = vec![]; for (o, output) in self.prefix.outputs.iter().enumerate() { for key in &keys { - let (view_tag, key_offset, _) = shared_key( + let (view_tag, key_offset, payment_id_xor) = shared_key( Some(uniqueness(&self.prefix.inputs)).filter(|_| guaranteed), &view.view, key, o, ); + let payment_id = + if let Some(PaymentId::Encrypted(id)) = payment_id.map(|id| id ^ payment_id_xor) { + id + } else { + [0xff; 8] + }; + if let Some(actual_view_tag) = output.view_tag { if actual_view_tag != view_tag { continue; @@ -127,9 +160,13 @@ impl Transaction { res.push(SpendableOutput { tx: self.hash(), o: o.try_into().unwrap(), + key: output.key, key_offset, commitment, + + subaddress: (0, 0), + payment_id, }); } // Break to prevent public keys from being included multiple times, triggering multiple