From e23176deeb58f65f2847a45989ba9df6754b1996 Mon Sep 17 00:00:00 2001 From: Luke Parker Date: Sat, 14 Sep 2024 04:23:42 -0400 Subject: [PATCH] Change dummy payment ID behavior on 2-output, no change This reduces the ability to fingerprint from any observer of the blockchain to just one of the two recipients. --- networks/monero/wallet/src/send/mod.rs | 9 +++++---- networks/monero/wallet/src/send/tx.rs | 16 ++++++++++++---- 2 files changed, 17 insertions(+), 8 deletions(-) diff --git a/networks/monero/wallet/src/send/mod.rs b/networks/monero/wallet/src/send/mod.rs index 87d98d69..3bd883df 100644 --- a/networks/monero/wallet/src/send/mod.rs +++ b/networks/monero/wallet/src/send/mod.rs @@ -100,10 +100,11 @@ impl Change { /// /// 1) The change in the TX is shunted to the fee (making it fingerprintable). /// - /// 2) If there are two outputs in the TX, Monero would create a payment ID for the non-change - /// output so an observer can't tell apart TXs with a payment ID from TXs without a payment - /// ID. monero-wallet will simply not create a payment ID in this case, revealing it's a - /// monero-wallet TX without change. + /// 2) In two-output transactions, where the payment address doesn't have a payment ID, wallet2 + /// includes an encrypted dummy payment ID for the non-change output in order to not allow + /// differentiating if transactions send to addresses with payment IDs or not. monero-wallet + /// includes a dummy payment ID which at least one recipient will identify as not the expected + /// dummy payment ID, revealing to the recipient(s) the sender is using non-wallet2 software. pub fn fingerprintable(address: Option) -> Change { if let Some(address) = address { Change(Some(ChangeEnum::AddressOnly(address))) diff --git a/networks/monero/wallet/src/send/tx.rs b/networks/monero/wallet/src/send/tx.rs index 65962211..0ebd47f1 100644 --- a/networks/monero/wallet/src/send/tx.rs +++ b/networks/monero/wallet/src/send/tx.rs @@ -76,10 +76,18 @@ impl SignableTransaction { PaymentId::Encrypted(id).write(&mut id_vec).unwrap(); extra.push_nonce(id_vec); } else { - // If there's no payment ID, we push a dummy (as wallet2 does) if there's only one payment - if (self.payments.len() == 2) && - self.payments.iter().any(|payment| matches!(payment, InternalPayment::Change(_))) - { + /* + If there's no payment ID, we push a dummy (as wallet2 does) to the first payment. + + This does cause a random payment ID for the other recipient (a documented fingerprint). + Functionally, random payment IDs should be fine as wallet2 will trigger this same behavior + (a random payment ID being seen by the recipient) with a batch send if one of the recipient + addresses has a payment ID. + + The alternative would be to not include any payment ID, fingerprinting to the entire + blockchain this is non-standard wallet software (instead of just a single recipient). + */ + if self.payments.len() == 2 { let (_, payment_id_xor) = self .payments .iter()