mirror of
https://github.com/serai-dex/serai.git
synced 2025-12-08 12:19:24 +00:00
Use Monero-compatible additional TX keys
This still sends a fingerprinting flare up if you send to a subaddress which needs to be fixed. Despite that, Monero no should no longer fail to scan TXs from monero-serai regarding additional keys. Previously it failed becuase we supplied one key as THE key, and n-1 as additional. Monero expects n for additional. This does correctly select when to use THE key versus when to use the additional key when sending. That removes the ability for recipients to fingerprint monero-serai by receiving to a standard address yet needing to use an additional key.
This commit is contained in:
@@ -33,9 +33,9 @@ fn standard_address() {
|
|||||||
let addr = MoneroAddress::from_str(Network::Mainnet, STANDARD).unwrap();
|
let addr = MoneroAddress::from_str(Network::Mainnet, STANDARD).unwrap();
|
||||||
assert_eq!(addr.meta.network, Network::Mainnet);
|
assert_eq!(addr.meta.network, Network::Mainnet);
|
||||||
assert_eq!(addr.meta.kind, AddressType::Standard);
|
assert_eq!(addr.meta.kind, AddressType::Standard);
|
||||||
assert!(!addr.meta.kind.subaddress());
|
assert!(!addr.meta.kind.is_subaddress());
|
||||||
assert_eq!(addr.meta.kind.payment_id(), None);
|
assert_eq!(addr.meta.kind.payment_id(), None);
|
||||||
assert!(!addr.meta.kind.guaranteed());
|
assert!(!addr.meta.kind.is_guaranteed());
|
||||||
assert_eq!(addr.spend.compress().to_bytes(), SPEND);
|
assert_eq!(addr.spend.compress().to_bytes(), SPEND);
|
||||||
assert_eq!(addr.view.compress().to_bytes(), VIEW);
|
assert_eq!(addr.view.compress().to_bytes(), VIEW);
|
||||||
assert_eq!(addr.to_string(), STANDARD);
|
assert_eq!(addr.to_string(), STANDARD);
|
||||||
@@ -46,9 +46,9 @@ fn integrated_address() {
|
|||||||
let addr = MoneroAddress::from_str(Network::Mainnet, INTEGRATED).unwrap();
|
let addr = MoneroAddress::from_str(Network::Mainnet, INTEGRATED).unwrap();
|
||||||
assert_eq!(addr.meta.network, Network::Mainnet);
|
assert_eq!(addr.meta.network, Network::Mainnet);
|
||||||
assert_eq!(addr.meta.kind, AddressType::Integrated(PAYMENT_ID));
|
assert_eq!(addr.meta.kind, AddressType::Integrated(PAYMENT_ID));
|
||||||
assert!(!addr.meta.kind.subaddress());
|
assert!(!addr.meta.kind.is_subaddress());
|
||||||
assert_eq!(addr.meta.kind.payment_id(), Some(PAYMENT_ID));
|
assert_eq!(addr.meta.kind.payment_id(), Some(PAYMENT_ID));
|
||||||
assert!(!addr.meta.kind.guaranteed());
|
assert!(!addr.meta.kind.is_guaranteed());
|
||||||
assert_eq!(addr.spend.compress().to_bytes(), SPEND);
|
assert_eq!(addr.spend.compress().to_bytes(), SPEND);
|
||||||
assert_eq!(addr.view.compress().to_bytes(), VIEW);
|
assert_eq!(addr.view.compress().to_bytes(), VIEW);
|
||||||
assert_eq!(addr.to_string(), INTEGRATED);
|
assert_eq!(addr.to_string(), INTEGRATED);
|
||||||
@@ -59,9 +59,9 @@ fn subaddress() {
|
|||||||
let addr = MoneroAddress::from_str(Network::Mainnet, SUBADDRESS).unwrap();
|
let addr = MoneroAddress::from_str(Network::Mainnet, SUBADDRESS).unwrap();
|
||||||
assert_eq!(addr.meta.network, Network::Mainnet);
|
assert_eq!(addr.meta.network, Network::Mainnet);
|
||||||
assert_eq!(addr.meta.kind, AddressType::Subaddress);
|
assert_eq!(addr.meta.kind, AddressType::Subaddress);
|
||||||
assert!(addr.meta.kind.subaddress());
|
assert!(addr.meta.kind.is_subaddress());
|
||||||
assert_eq!(addr.meta.kind.payment_id(), None);
|
assert_eq!(addr.meta.kind.payment_id(), None);
|
||||||
assert!(!addr.meta.kind.guaranteed());
|
assert!(!addr.meta.kind.is_guaranteed());
|
||||||
assert_eq!(addr.spend.compress().to_bytes(), SUB_SPEND);
|
assert_eq!(addr.spend.compress().to_bytes(), SUB_SPEND);
|
||||||
assert_eq!(addr.view.compress().to_bytes(), SUB_VIEW);
|
assert_eq!(addr.view.compress().to_bytes(), SUB_VIEW);
|
||||||
assert_eq!(addr.to_string(), SUBADDRESS);
|
assert_eq!(addr.to_string(), SUBADDRESS);
|
||||||
@@ -100,9 +100,9 @@ fn featured() {
|
|||||||
assert_eq!(addr.spend, spend);
|
assert_eq!(addr.spend, spend);
|
||||||
assert_eq!(addr.view, view);
|
assert_eq!(addr.view, view);
|
||||||
|
|
||||||
assert_eq!(addr.subaddress(), subaddress);
|
assert_eq!(addr.is_subaddress(), subaddress);
|
||||||
assert_eq!(addr.payment_id(), payment_id);
|
assert_eq!(addr.payment_id(), payment_id);
|
||||||
assert_eq!(addr.guaranteed(), guaranteed);
|
assert_eq!(addr.is_guaranteed(), guaranteed);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@@ -151,10 +151,10 @@ fn featured_vectors() {
|
|||||||
assert_eq!(addr.spend, spend);
|
assert_eq!(addr.spend, spend);
|
||||||
assert_eq!(addr.view, view);
|
assert_eq!(addr.view, view);
|
||||||
|
|
||||||
assert_eq!(addr.subaddress(), vector.subaddress);
|
assert_eq!(addr.is_subaddress(), vector.subaddress);
|
||||||
assert_eq!(vector.integrated, vector.payment_id.is_some());
|
assert_eq!(vector.integrated, vector.payment_id.is_some());
|
||||||
assert_eq!(addr.payment_id(), vector.payment_id);
|
assert_eq!(addr.payment_id(), vector.payment_id);
|
||||||
assert_eq!(addr.guaranteed(), vector.guaranteed);
|
assert_eq!(addr.is_guaranteed(), vector.guaranteed);
|
||||||
|
|
||||||
assert_eq!(
|
assert_eq!(
|
||||||
MoneroAddress::new(
|
MoneroAddress::new(
|
||||||
|
|||||||
@@ -60,7 +60,7 @@ pub enum AddressSpec {
|
|||||||
}
|
}
|
||||||
|
|
||||||
impl AddressType {
|
impl AddressType {
|
||||||
pub fn subaddress(&self) -> bool {
|
pub fn is_subaddress(&self) -> bool {
|
||||||
matches!(self, AddressType::Subaddress) ||
|
matches!(self, AddressType::Subaddress) ||
|
||||||
matches!(self, AddressType::Featured { subaddress: true, .. })
|
matches!(self, AddressType::Featured { subaddress: true, .. })
|
||||||
}
|
}
|
||||||
@@ -75,7 +75,7 @@ impl AddressType {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
pub fn guaranteed(&self) -> bool {
|
pub fn is_guaranteed(&self) -> bool {
|
||||||
matches!(self, AddressType::Featured { guaranteed: true, .. })
|
matches!(self, AddressType::Featured { guaranteed: true, .. })
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@@ -169,16 +169,16 @@ impl<B: AddressBytes> AddressMeta<B> {
|
|||||||
meta.ok_or(AddressError::InvalidByte)
|
meta.ok_or(AddressError::InvalidByte)
|
||||||
}
|
}
|
||||||
|
|
||||||
pub fn subaddress(&self) -> bool {
|
pub fn is_subaddress(&self) -> bool {
|
||||||
self.kind.subaddress()
|
self.kind.is_subaddress()
|
||||||
}
|
}
|
||||||
|
|
||||||
pub fn payment_id(&self) -> Option<[u8; 8]> {
|
pub fn payment_id(&self) -> Option<[u8; 8]> {
|
||||||
self.kind.payment_id()
|
self.kind.payment_id()
|
||||||
}
|
}
|
||||||
|
|
||||||
pub fn guaranteed(&self) -> bool {
|
pub fn is_guaranteed(&self) -> bool {
|
||||||
self.kind.guaranteed()
|
self.kind.is_guaranteed()
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -285,16 +285,16 @@ impl<B: AddressBytes> Address<B> {
|
|||||||
self.meta.network
|
self.meta.network
|
||||||
}
|
}
|
||||||
|
|
||||||
pub fn subaddress(&self) -> bool {
|
pub fn is_subaddress(&self) -> bool {
|
||||||
self.meta.subaddress()
|
self.meta.is_subaddress()
|
||||||
}
|
}
|
||||||
|
|
||||||
pub fn payment_id(&self) -> Option<[u8; 8]> {
|
pub fn payment_id(&self) -> Option<[u8; 8]> {
|
||||||
self.meta.payment_id()
|
self.meta.payment_id()
|
||||||
}
|
}
|
||||||
|
|
||||||
pub fn guaranteed(&self) -> bool {
|
pub fn is_guaranteed(&self) -> bool {
|
||||||
self.meta.guaranteed()
|
self.meta.is_guaranteed()
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
@@ -149,13 +149,11 @@ impl Extra {
|
|||||||
res
|
res
|
||||||
}
|
}
|
||||||
|
|
||||||
pub(crate) fn new(mut keys: Vec<EdwardsPoint>) -> Extra {
|
pub(crate) fn new(key: EdwardsPoint, additional: Vec<EdwardsPoint>) -> Extra {
|
||||||
let mut res = Extra(Vec::with_capacity(3));
|
let mut res = Extra(Vec::with_capacity(3));
|
||||||
if !keys.is_empty() {
|
res.push(ExtraField::PublicKey(key));
|
||||||
res.push(ExtraField::PublicKey(keys[0]));
|
if !additional.is_empty() {
|
||||||
}
|
res.push(ExtraField::PublicKeys(additional));
|
||||||
if keys.len() > 1 {
|
|
||||||
res.push(ExtraField::PublicKeys(keys.drain(1 ..).collect()));
|
|
||||||
}
|
}
|
||||||
res
|
res
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -54,12 +54,12 @@ pub(crate) fn uniqueness(inputs: &[Input]) -> [u8; 32] {
|
|||||||
#[allow(non_snake_case)]
|
#[allow(non_snake_case)]
|
||||||
pub(crate) fn shared_key(
|
pub(crate) fn shared_key(
|
||||||
uniqueness: Option<[u8; 32]>,
|
uniqueness: Option<[u8; 32]>,
|
||||||
s: &Scalar,
|
s: &Zeroizing<Scalar>,
|
||||||
P: &EdwardsPoint,
|
P: &EdwardsPoint,
|
||||||
o: usize,
|
o: usize,
|
||||||
) -> (u8, Scalar, [u8; 8]) {
|
) -> (u8, Scalar, [u8; 8]) {
|
||||||
// 8Ra
|
// 8Ra
|
||||||
let mut output_derivation = (s * P).mul_by_cofactor().compress().to_bytes().to_vec();
|
let mut output_derivation = (s.deref() * P).mul_by_cofactor().compress().to_bytes().to_vec();
|
||||||
|
|
||||||
let mut payment_id_xor = [0; 8];
|
let mut payment_id_xor = [0; 8];
|
||||||
payment_id_xor
|
payment_id_xor
|
||||||
|
|||||||
@@ -296,6 +296,7 @@ impl Scanner {
|
|||||||
}
|
}
|
||||||
let output_key = output_key.unwrap();
|
let output_key = output_key.unwrap();
|
||||||
|
|
||||||
|
// TODO: Only use THE key or the matching additional key. Not any key
|
||||||
for key in &keys {
|
for key in &keys {
|
||||||
let (view_tag, shared_key, payment_id_xor) = shared_key(
|
let (view_tag, shared_key, payment_id_xor) = shared_key(
|
||||||
if self.burning_bug.is_none() { Some(uniqueness(&tx.prefix.inputs)) } else { None },
|
if self.burning_bug.is_none() { Some(uniqueness(&tx.prefix.inputs)) } else { None },
|
||||||
|
|||||||
@@ -7,7 +7,9 @@ use rand::seq::SliceRandom;
|
|||||||
|
|
||||||
use zeroize::{Zeroize, ZeroizeOnDrop, Zeroizing};
|
use zeroize::{Zeroize, ZeroizeOnDrop, Zeroizing};
|
||||||
|
|
||||||
|
use group::Group;
|
||||||
use curve25519_dalek::{constants::ED25519_BASEPOINT_TABLE, scalar::Scalar, edwards::EdwardsPoint};
|
use curve25519_dalek::{constants::ED25519_BASEPOINT_TABLE, scalar::Scalar, edwards::EdwardsPoint};
|
||||||
|
use dalek_ff_group as dfg;
|
||||||
|
|
||||||
#[cfg(feature = "multisig")]
|
#[cfg(feature = "multisig")]
|
||||||
use frost::FrostError;
|
use frost::FrostError;
|
||||||
@@ -47,24 +49,23 @@ struct SendOutput {
|
|||||||
}
|
}
|
||||||
|
|
||||||
impl SendOutput {
|
impl SendOutput {
|
||||||
fn new<R: RngCore + CryptoRng>(
|
fn new(
|
||||||
rng: &mut R,
|
r: &Zeroizing<Scalar>,
|
||||||
unique: [u8; 32],
|
unique: [u8; 32],
|
||||||
output: (usize, (MoneroAddress, u64)),
|
output: (usize, (MoneroAddress, u64)),
|
||||||
) -> (SendOutput, Option<[u8; 8]>) {
|
) -> (SendOutput, Option<[u8; 8]>) {
|
||||||
let o = output.0;
|
let o = output.0;
|
||||||
let output = output.1;
|
let output = output.1;
|
||||||
|
|
||||||
let r = random_scalar(rng);
|
|
||||||
let (view_tag, shared_key, payment_id_xor) =
|
let (view_tag, shared_key, payment_id_xor) =
|
||||||
shared_key(Some(unique).filter(|_| output.0.meta.kind.guaranteed()), &r, &output.0.view, o);
|
shared_key(Some(unique).filter(|_| output.0.is_guaranteed()), r, &output.0.view, o);
|
||||||
|
|
||||||
(
|
(
|
||||||
SendOutput {
|
SendOutput {
|
||||||
R: if !output.0.meta.kind.subaddress() {
|
R: if !output.0.is_subaddress() {
|
||||||
&r * &ED25519_BASEPOINT_TABLE
|
r.deref() * &ED25519_BASEPOINT_TABLE
|
||||||
} else {
|
} else {
|
||||||
r * output.0.spend
|
r.deref() * output.0.spend
|
||||||
},
|
},
|
||||||
view_tag,
|
view_tag,
|
||||||
dest: ((&shared_key * &ED25519_BASEPOINT_TABLE) + output.0.spend),
|
dest: ((&shared_key * &ED25519_BASEPOINT_TABLE) + output.0.spend),
|
||||||
@@ -281,11 +282,26 @@ impl SignableTransaction {
|
|||||||
// Shuffle the payments
|
// Shuffle the payments
|
||||||
self.payments.shuffle(rng);
|
self.payments.shuffle(rng);
|
||||||
|
|
||||||
|
// Used for all non-subaddress outputs, or if there's only one subaddress output and a change
|
||||||
|
let tx_key = Zeroizing::new(random_scalar(rng));
|
||||||
|
// TODO: Support not needing additional when one subaddress and non-subaddress change
|
||||||
|
let additional = self.payments.iter().filter(|payment| payment.0.is_subaddress()).count() != 0;
|
||||||
|
|
||||||
// Actually create the outputs
|
// Actually create the outputs
|
||||||
let mut outputs = Vec::with_capacity(self.payments.len());
|
let mut outputs = Vec::with_capacity(self.payments.len());
|
||||||
let mut id = None;
|
let mut id = None;
|
||||||
for payment in self.payments.drain(..).enumerate() {
|
for payment in self.payments.drain(..).enumerate() {
|
||||||
let (output, payment_id) = SendOutput::new(rng, uniqueness, payment);
|
// If this is a subaddress, generate a dedicated r. Else, reuse the TX key
|
||||||
|
let dedicated = Zeroizing::new(random_scalar(&mut *rng));
|
||||||
|
let use_dedicated = additional && payment.1 .0.is_subaddress();
|
||||||
|
let r = if use_dedicated { &dedicated } else { &tx_key };
|
||||||
|
|
||||||
|
let (mut output, payment_id) = SendOutput::new(r, uniqueness, payment);
|
||||||
|
// If this used the tx_key, randomize its R
|
||||||
|
if !use_dedicated {
|
||||||
|
output.R = dfg::EdwardsPoint::random(&mut *rng).0;
|
||||||
|
}
|
||||||
|
|
||||||
outputs.push(output);
|
outputs.push(output);
|
||||||
id = id.or(payment_id);
|
id = id.or(payment_id);
|
||||||
}
|
}
|
||||||
@@ -308,7 +324,10 @@ impl SignableTransaction {
|
|||||||
|
|
||||||
// Create the TX extra
|
// Create the TX extra
|
||||||
let extra = {
|
let extra = {
|
||||||
let mut extra = Extra::new(outputs.iter().map(|output| output.R).collect());
|
let mut extra = Extra::new(
|
||||||
|
tx_key.deref() * &ED25519_BASEPOINT_TABLE,
|
||||||
|
if additional { outputs.iter().map(|output| output.R).collect() } else { vec![] },
|
||||||
|
);
|
||||||
|
|
||||||
let mut id_vec = Vec::with_capacity(1 + 8);
|
let mut id_vec = Vec::with_capacity(1 + 8);
|
||||||
PaymentId::Encrypted(id).write(&mut id_vec).unwrap();
|
PaymentId::Encrypted(id).write(&mut id_vec).unwrap();
|
||||||
|
|||||||
Reference in New Issue
Block a user