mirror of
https://github.com/serai-dex/serai.git
synced 2025-12-12 14:09:25 +00:00
311 lines
9.3 KiB
Rust
311 lines
9.3 KiB
Rust
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<W: 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<u8>`.
|
|
pub fn serialize(&self) -> Vec<u8> {
|
|
let mut res = Vec::with_capacity(1 + 8);
|
|
self.write(&mut res).unwrap();
|
|
res
|
|
}
|
|
|
|
/// Read a PaymentId.
|
|
pub fn read<R: Read>(r: &mut R) -> io::Result<PaymentId> {
|
|
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<u8>),
|
|
/// 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<EdwardsPoint>),
|
|
/// 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<u8>),
|
|
}
|
|
|
|
impl ExtraField {
|
|
/// Write the ExtraField.
|
|
pub fn write<W: 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<u8>`.
|
|
pub fn serialize(&self) -> Vec<u8> {
|
|
let mut res = Vec::with_capacity(1 + 8);
|
|
self.write(&mut res).unwrap();
|
|
res
|
|
}
|
|
|
|
/// Read an ExtraField.
|
|
pub fn read<R: BufRead>(r: &mut R) -> io::Result<ExtraField> {
|
|
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<ExtraField>);
|
|
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<EdwardsPoint>, Option<Vec<EdwardsPoint>>)> {
|
|
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<PaymentId> {
|
|
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<Vec<u8>> {
|
|
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<EdwardsPoint>) -> 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<u8>) {
|
|
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<W: Write>(&self, w: &mut W) -> io::Result<()> {
|
|
for field in &self.0 {
|
|
field.write(w)?;
|
|
}
|
|
Ok(())
|
|
}
|
|
|
|
/// Serialize the Extra to a `Vec<u8>`.
|
|
pub fn serialize(&self) -> Vec<u8> {
|
|
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: BufRead>(r: &mut R) -> io::Result<Extra> {
|
|
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)
|
|
}
|
|
}
|