mirror of
https://github.com/serai-dex/serai.git
synced 2025-12-08 12:19:24 +00:00
Remove Monero torsion-free requirement and make output keys 32 bytes
Maintains the torsion-free requirement in the one place it's used (key images). In the modern day, the output keys are checked to be points, yet in older protocol versions they were allowed to be arbitrary bytes. Closes https://github.com/serai-dex/serai/issues/23 and https://github.com/serai-dex/serai/issues/25.
This commit is contained in:
@@ -104,11 +104,15 @@ pub(crate) fn read_point<R: io::Read>(r: &mut R) -> io::Result<EdwardsPoint> {
|
|||||||
let bytes = read_bytes(r)?;
|
let bytes = read_bytes(r)?;
|
||||||
CompressedEdwardsY(bytes)
|
CompressedEdwardsY(bytes)
|
||||||
.decompress()
|
.decompress()
|
||||||
// Ban torsioned points, and points which are either unreduced or -0
|
// Ban points which are either unreduced or -0
|
||||||
.filter(|point| point.is_torsion_free() && (point.compress().to_bytes() == bytes))
|
.filter(|point| point.compress().to_bytes() == bytes)
|
||||||
.ok_or_else(|| io::Error::new(io::ErrorKind::Other, "invalid point"))
|
.ok_or_else(|| io::Error::new(io::ErrorKind::Other, "invalid point"))
|
||||||
}
|
}
|
||||||
|
|
||||||
|
pub(crate) fn read_torsion_free_point<R: io::Read>(r: &mut R) -> io::Result<EdwardsPoint> {
|
||||||
|
read_point(r).ok().filter(|point| point.is_torsion_free()).ok_or(io::Error::new(io::ErrorKind::Other, "invalid point"))
|
||||||
|
}
|
||||||
|
|
||||||
pub(crate) fn read_raw_vec<R: io::Read, T, F: Fn(&mut R) -> io::Result<T>>(
|
pub(crate) fn read_raw_vec<R: io::Read, T, F: Fn(&mut R) -> io::Result<T>>(
|
||||||
f: F,
|
f: F,
|
||||||
len: usize,
|
len: usize,
|
||||||
|
|||||||
@@ -2,7 +2,7 @@ use core::cmp::Ordering;
|
|||||||
|
|
||||||
use zeroize::Zeroize;
|
use zeroize::Zeroize;
|
||||||
|
|
||||||
use curve25519_dalek::edwards::EdwardsPoint;
|
use curve25519_dalek::edwards::{EdwardsPoint, CompressedEdwardsY};
|
||||||
|
|
||||||
use crate::{
|
use crate::{
|
||||||
Protocol, hash,
|
Protocol, hash,
|
||||||
@@ -46,7 +46,7 @@ impl Input {
|
|||||||
2 => Input::ToKey {
|
2 => Input::ToKey {
|
||||||
amount: read_varint(r)?,
|
amount: read_varint(r)?,
|
||||||
key_offsets: read_vec(read_varint, r)?,
|
key_offsets: read_vec(read_varint, r)?,
|
||||||
key_image: read_point(r)?,
|
key_image: read_torsion_free_point(r)?,
|
||||||
},
|
},
|
||||||
_ => Err(std::io::Error::new(
|
_ => Err(std::io::Error::new(
|
||||||
std::io::ErrorKind::Other,
|
std::io::ErrorKind::Other,
|
||||||
@@ -60,7 +60,7 @@ impl Input {
|
|||||||
#[derive(Clone, PartialEq, Eq, Debug)]
|
#[derive(Clone, PartialEq, Eq, Debug)]
|
||||||
pub struct Output {
|
pub struct Output {
|
||||||
pub amount: u64,
|
pub amount: u64,
|
||||||
pub key: EdwardsPoint,
|
pub key: CompressedEdwardsY,
|
||||||
pub view_tag: Option<u8>,
|
pub view_tag: Option<u8>,
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -72,7 +72,7 @@ impl Output {
|
|||||||
pub fn serialize<W: std::io::Write>(&self, w: &mut W) -> std::io::Result<()> {
|
pub fn serialize<W: std::io::Write>(&self, w: &mut W) -> std::io::Result<()> {
|
||||||
write_varint(&self.amount, w)?;
|
write_varint(&self.amount, w)?;
|
||||||
w.write_all(&[2 + (if self.view_tag.is_some() { 1 } else { 0 })])?;
|
w.write_all(&[2 + (if self.view_tag.is_some() { 1 } else { 0 })])?;
|
||||||
write_point(&self.key, w)?;
|
w.write_all(&self.key.to_bytes())?;
|
||||||
if let Some(view_tag) = self.view_tag {
|
if let Some(view_tag) = self.view_tag {
|
||||||
w.write_all(&[view_tag])?;
|
w.write_all(&[view_tag])?;
|
||||||
}
|
}
|
||||||
@@ -92,7 +92,7 @@ impl Output {
|
|||||||
|
|
||||||
Ok(Output {
|
Ok(Output {
|
||||||
amount,
|
amount,
|
||||||
key: read_point(r)?,
|
key: CompressedEdwardsY(read_bytes(r)?),
|
||||||
view_tag: if view_tag { Some(read_byte(r)?) } else { None },
|
view_tag: if view_tag { Some(read_byte(r)?) } else { None },
|
||||||
})
|
})
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -65,8 +65,7 @@ pub struct Metadata {
|
|||||||
pub subaddress: (u32, u32),
|
pub subaddress: (u32, u32),
|
||||||
// Can be an Option, as extra doesn't necessarily have a payment ID, yet all Monero TXs should
|
// Can be an Option, as extra doesn't necessarily have a payment ID, yet all Monero TXs should
|
||||||
// have this
|
// have this
|
||||||
// This will be gibberish if the payment ID wasn't intended for the recipient
|
// This will be gibberish if the payment ID wasn't intended for the recipient or wasn't included
|
||||||
// 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
|
// 0xff was chosen as it'd be distinct from [0; 8], enabling atomically incrementing IDs (though
|
||||||
// they should be randomly generated)
|
// they should be randomly generated)
|
||||||
pub payment_id: [u8; 8],
|
pub payment_id: [u8; 8],
|
||||||
@@ -211,14 +210,19 @@ impl Scanner {
|
|||||||
|
|
||||||
let mut res = vec![];
|
let mut res = vec![];
|
||||||
for (o, output) in tx.prefix.outputs.iter().enumerate() {
|
for (o, output) in tx.prefix.outputs.iter().enumerate() {
|
||||||
// https://github.com/serai-dex/serai/issues/102
|
// https://github.com/serai-dex/serai/issues/106
|
||||||
let output_key_compressed = output.key.compress();
|
|
||||||
if let Some(burning_bug) = self.burning_bug.as_ref() {
|
if let Some(burning_bug) = self.burning_bug.as_ref() {
|
||||||
if burning_bug.contains(&output_key_compressed) {
|
if burning_bug.contains(&output.key) {
|
||||||
continue;
|
continue;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
let output_key = output.key.decompress();
|
||||||
|
if output_key.is_none() {
|
||||||
|
continue;
|
||||||
|
}
|
||||||
|
let output_key = output_key.unwrap();
|
||||||
|
|
||||||
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 },
|
||||||
@@ -231,7 +235,7 @@ impl Scanner {
|
|||||||
if let Some(PaymentId::Encrypted(id)) = payment_id.map(|id| id ^ payment_id_xor) {
|
if let Some(PaymentId::Encrypted(id)) = payment_id.map(|id| id ^ payment_id_xor) {
|
||||||
id
|
id
|
||||||
} else {
|
} else {
|
||||||
[0xff; 8]
|
payment_id_xor
|
||||||
};
|
};
|
||||||
|
|
||||||
if let Some(actual_view_tag) = output.view_tag {
|
if let Some(actual_view_tag) = output.view_tag {
|
||||||
@@ -243,15 +247,15 @@ impl Scanner {
|
|||||||
// P - shared == spend
|
// P - shared == spend
|
||||||
let subaddress = self
|
let subaddress = self
|
||||||
.subaddresses
|
.subaddresses
|
||||||
.get(&(output.key - (&shared_key * &ED25519_BASEPOINT_TABLE)).compress());
|
.get(&(output_key - (&shared_key * &ED25519_BASEPOINT_TABLE)).compress());
|
||||||
if subaddress.is_none() {
|
if subaddress.is_none() {
|
||||||
continue;
|
continue;
|
||||||
}
|
}
|
||||||
// If it has torsion, it'll substract the non-torsioned shared key to a torsioned key
|
// If it has torsion, it'll substract the non-torsioned shared key to a torsioned key
|
||||||
// We will not have a torsioned key in our HashMap of keys, so we wouldn't identify it as
|
// We will not have a torsioned key in our HashMap of keys, so we wouldn't identify it as
|
||||||
// ours
|
// ours
|
||||||
// If we did, it'd enable bypassing the included burning bug protection however
|
// If we did though, it'd enable bypassing the included burning bug protection
|
||||||
debug_assert!(output.key.is_torsion_free());
|
debug_assert!(output_key.is_torsion_free());
|
||||||
|
|
||||||
let key_offset = shared_key + self.pair.subaddress(*subaddress.unwrap());
|
let key_offset = shared_key + self.pair.subaddress(*subaddress.unwrap());
|
||||||
// Since we've found an output to us, get its amount
|
// Since we've found an output to us, get its amount
|
||||||
@@ -282,13 +286,13 @@ impl Scanner {
|
|||||||
res.push(ReceivedOutput {
|
res.push(ReceivedOutput {
|
||||||
absolute: AbsoluteId { tx: tx.hash(), o: o.try_into().unwrap() },
|
absolute: AbsoluteId { tx: tx.hash(), o: o.try_into().unwrap() },
|
||||||
|
|
||||||
data: OutputData { key: output.key, key_offset, commitment },
|
data: OutputData { key: output_key, key_offset, commitment },
|
||||||
|
|
||||||
metadata: Metadata { subaddress: (0, 0), payment_id },
|
metadata: Metadata { subaddress: (0, 0), payment_id },
|
||||||
});
|
});
|
||||||
|
|
||||||
if let Some(burning_bug) = self.burning_bug.as_mut() {
|
if let Some(burning_bug) = self.burning_bug.as_mut() {
|
||||||
burning_bug.insert(output_key_compressed);
|
burning_bug.insert(output.key);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
// Break to prevent public keys from being included multiple times, triggering multiple
|
// Break to prevent public keys from being included multiple times, triggering multiple
|
||||||
|
|||||||
@@ -305,7 +305,7 @@ impl SignableTransaction {
|
|||||||
for output in &outputs {
|
for output in &outputs {
|
||||||
tx_outputs.push(Output {
|
tx_outputs.push(Output {
|
||||||
amount: 0,
|
amount: 0,
|
||||||
key: output.dest,
|
key: output.dest.compress(),
|
||||||
view_tag: Some(output.view_tag).filter(|_| matches!(self.protocol, Protocol::v16)),
|
view_tag: Some(output.view_tag).filter(|_| matches!(self.protocol, Protocol::v16)),
|
||||||
});
|
});
|
||||||
ecdh_info.push(output.amount);
|
ecdh_info.push(output.amount);
|
||||||
|
|||||||
@@ -199,7 +199,7 @@ impl Coin for Monero {
|
|||||||
|
|
||||||
Ok((
|
Ok((
|
||||||
tx.hash().to_vec(),
|
tx.hash().to_vec(),
|
||||||
tx.prefix.outputs.iter().map(|output| output.key.compress().to_bytes()).collect(),
|
tx.prefix.outputs.iter().map(|output| output.key.to_bytes()).collect(),
|
||||||
))
|
))
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
Reference in New Issue
Block a user