diff --git a/coins/monero/wallet/address/Cargo.toml b/coins/monero/wallet/address/Cargo.toml index c823f61f..bb5baee0 100644 --- a/coins/monero/wallet/address/Cargo.toml +++ b/coins/monero/wallet/address/Cargo.toml @@ -1,7 +1,7 @@ [package] name = "monero-address" version = "0.1.0" -description = "Monero addresses and associated functionality" +description = "Rust implementation of Monero addresses" license = "MIT" repository = "https://github.com/serai-dex/serai/tree/develop/coins/monero/wallet/address" authors = ["Luke Parker "] @@ -21,7 +21,6 @@ std-shims = { path = "../../../../common/std-shims", version = "^0.1.1", default thiserror = { version = "1", default-features = false, optional = true } zeroize = { version = "^1.5", default-features = false, features = ["zeroize_derive"] } -hex = { version = "0.4", default-features = false, features = ["alloc"] } curve25519-dalek = { version = "4", default-features = false, features = ["alloc", "zeroize"] } @@ -29,8 +28,11 @@ monero-io = { path = "../../io", default-features = false } monero-primitives = { path = "../../primitives", default-features = false } [dev-dependencies] -hex-literal = { version = "0.4", default-features = false } rand_core = { version = "0.6", default-features = false, features = ["std"] } + +hex-literal = { version = "0.4", default-features = false } +hex = { version = "0.4", default-features = false, features = ["alloc"] } + serde = { version = "1", default-features = false, features = ["derive", "alloc"] } serde_json = { version = "1", default-features = false, features = ["alloc"] } diff --git a/coins/monero/wallet/address/README.md b/coins/monero/wallet/address/README.md index 4869bba0..8fe3b77d 100644 --- a/coins/monero/wallet/address/README.md +++ b/coins/monero/wallet/address/README.md @@ -1,6 +1,6 @@ -# Polyseed +# Monero Address -Rust implementation of [Polyseed](https://github.com/tevador/polyseed). +Rust implementation of Monero addresses. This library is usable under no-std when the `std` feature (on by default) is disabled. diff --git a/coins/monero/wallet/address/src/lib.rs b/coins/monero/wallet/address/src/lib.rs index 42635ca8..9b224f54 100644 --- a/coins/monero/wallet/address/src/lib.rs +++ b/coins/monero/wallet/address/src/lib.rs @@ -1,16 +1,16 @@ #![cfg_attr(docsrs, feature(doc_auto_cfg))] #![doc = include_str!("../README.md")] -// #![deny(missing_docs)] // TODO +#![deny(missing_docs)] #![cfg_attr(not(feature = "std"), no_std)] -use core::{marker::PhantomData, fmt}; +use core::fmt::{self, Write}; use std_shims::string::ToString; use zeroize::Zeroize; use curve25519_dalek::edwards::EdwardsPoint; -use monero_io::decompress_point; +use monero_io::*; mod base58check; use base58check::{encode_check, decode_check}; @@ -18,65 +18,53 @@ use base58check::{encode_check, decode_check}; #[cfg(test)] mod tests; -/// The network this address is for. -#[derive(Clone, Copy, PartialEq, Eq, Debug, Zeroize)] -pub enum Network { - Mainnet, - Testnet, - Stagenet, -} - -/// The address type, supporting the officially documented addresses, along with +/// The address type. +/// +/// The officially specified addresses are supported, along with /// [Featured Addresses](https://gist.github.com/kayabaNerve/01c50bbc35441e0bbdcee63a9d823789). #[derive(Clone, Copy, PartialEq, Eq, Debug, Zeroize)] pub enum AddressType { - Standard, - Integrated([u8; 8]), + /// A legacy address type. + Legacy, + /// A legacy address with a payment ID embedded. + LegacyIntegrated([u8; 8]), + /// A subaddress. + /// + /// This is what SHOULD be used if specific functionality isn't needed. Subaddress, - Featured { subaddress: bool, payment_id: Option<[u8; 8]>, guaranteed: bool }, -} - -#[derive(Clone, Copy, PartialEq, Eq, Debug, Zeroize)] -pub struct SubaddressIndex { - pub(crate) account: u32, - pub(crate) address: u32, -} - -impl SubaddressIndex { - pub const fn new(account: u32, address: u32) -> Option { - if (account == 0) && (address == 0) { - return None; - } - Some(SubaddressIndex { account, address }) - } - - pub fn account(&self) -> u32 { - self.account - } - - pub fn address(&self) -> u32 { - self.address - } -} - -/// Address specification. Used internally to create addresses. -#[derive(Clone, Copy, PartialEq, Eq, Debug, Zeroize)] -pub enum AddressSpec { - Standard, - Integrated([u8; 8]), - Subaddress(SubaddressIndex), - Featured { subaddress: Option, payment_id: Option<[u8; 8]>, guaranteed: bool }, + /// A featured address. + /// + /// Featured Addresses are an unofficial address specification which is meant to be extensible + /// and support a variety of functionality. This functionality includes being a subaddresses AND + /// having a payment ID, along with being immune to the burning bug. + /// + /// At this time, support for featured addresses is limited to this crate. There should be no + /// expectation of interoperability. + Featured { + /// If this address is a subaddress. + subaddress: bool, + /// The payment ID associated with this address. + payment_id: Option<[u8; 8]>, + /// If this address is guaranteed. + /// + /// A guaranteed address is one where any outputs scanned to it are guaranteed to be spendable + /// under the hardness of various cryptographic problems (which are assumed hard). This is via + /// a modified shared-key derivation which eliminates the burning bug. + guaranteed: bool, + }, } impl AddressType { + /// If this address is a subaddress. pub fn is_subaddress(&self) -> bool { matches!(self, AddressType::Subaddress) || matches!(self, AddressType::Featured { subaddress: true, .. }) } + /// The payment ID within this address. // TODO: wallet-core PaymentId? TX extra crate imported here? pub fn payment_id(&self) -> Option<[u8; 8]> { - if let AddressType::Integrated(id) = self { + if let AddressType::LegacyIntegrated(id) = self { Some(*id) } else if let AddressType::Featured { payment_id, .. } = self { *payment_id @@ -85,251 +73,461 @@ impl AddressType { } } + /// If this address is guaranteed. + /// + /// A guaranteed address is one where any outputs scanned to it are guaranteed to be spendable + /// under the hardness of various cryptographic problems (which are assumed hard). This is via + /// a modified shared-key derivation which eliminates the burning bug. pub fn is_guaranteed(&self) -> bool { matches!(self, AddressType::Featured { guaranteed: true, .. }) } } -/// A type which returns the byte for a given address. -pub trait AddressBytes: Clone + Copy + PartialEq + Eq + fmt::Debug { - fn network_bytes(network: Network) -> (u8, u8, u8, u8); +/// A subaddress index. +/// +/// Subaddresses are derived from a root using a `(account, address)` tuple as an index. +#[derive(Clone, Copy, PartialEq, Eq, Debug, Zeroize)] +pub struct SubaddressIndex { + account: u32, + address: u32, } -/// Address bytes for Monero. -#[derive(Clone, Copy, PartialEq, Eq, Debug)] -pub struct MoneroAddressBytes; -impl AddressBytes for MoneroAddressBytes { - fn network_bytes(network: Network) -> (u8, u8, u8, u8) { - match network { - Network::Mainnet => (18, 19, 42, 70), - Network::Testnet => (53, 54, 63, 111), - Network::Stagenet => (24, 25, 36, 86), +impl SubaddressIndex { + /// Create a new SubaddressIndex. + pub const fn new(account: u32, address: u32) -> Option { + if (account == 0) && (address == 0) { + return None; } + Some(SubaddressIndex { account, address }) + } + + /// Get the account this subaddress index is under. + pub const fn account(&self) -> u32 { + self.account + } + + /// Get the address this subaddress index is for, within its account. + pub const fn address(&self) -> u32 { + self.address } } -/// Address metadata. -#[derive(Clone, Copy, PartialEq, Eq, Debug)] -pub struct AddressMeta { - _bytes: PhantomData, - pub network: Network, - pub kind: AddressType, +/// A specification for an address to be derived. +/// +/// This contains all the information an address will embed once derived. +#[derive(Clone, Copy, PartialEq, Eq, Debug, Zeroize)] +pub enum AddressSpec { + /// A legacy address type. + Legacy, + /// A legacy address with a payment ID embedded. + LegacyIntegrated([u8; 8]), + /// A subaddress. + /// + /// This is what SHOULD be used if specific functionality isn't needed. + Subaddress(SubaddressIndex), + /// A featured address. + /// + /// Featured Addresses are an unofficial address specification which is meant to be extensible + /// and support a variety of functionality. This functionality includes being a subaddresses AND + /// having a payment ID, along with being immune to the burning bug. + /// + /// At this time, support for featured addresses is limited to this crate. There should be no + /// expectation of interoperability. + Featured { + /// The subaddress index to derive this address with. + /// + /// If None, no subaddress derivation occurs. + subaddress: Option, + /// The payment ID to embed in this address. + payment_id: Option<[u8; 8]>, + /// If this address should be guaranteed. + /// + /// A guaranteed address is one where any outputs scanned to it are guaranteed to be spendable + /// under the hardness of various cryptographic problems (which are assumed hard). This is via + /// a modified shared-key derivation which eliminates the burning bug. + guaranteed: bool, + }, } -impl Zeroize for AddressMeta { - fn zeroize(&mut self) { - self.network.zeroize(); - self.kind.zeroize(); +/// Bytes used as prefixes when encoding addresses. +/// +/// These distinguish the address's type. +#[derive(Clone, Copy, PartialEq, Eq, Debug, Zeroize)] +pub struct AddressBytes { + legacy: u8, + legacy_integrated: u8, + subaddress: u8, + featured: u8, +} + +impl AddressBytes { + /// Create a new set of address bytes, one for each address type. + pub const fn new( + legacy: u8, + legacy_integrated: u8, + subaddress: u8, + featured: u8, + ) -> Option { + if (legacy == legacy_integrated) || (legacy == subaddress) || (legacy == featured) { + return None; + } + if (legacy_integrated == subaddress) || (legacy_integrated == featured) { + return None; + } + if subaddress == featured { + return None; + } + Some(AddressBytes { legacy, legacy_integrated, subaddress, featured }) } + + const fn to_const_generic(self) -> u32 { + ((self.legacy as u32) << 24) + + ((self.legacy_integrated as u32) << 16) + + ((self.subaddress as u32) << 8) + + (self.featured as u32) + } + + #[allow(clippy::cast_possible_truncation)] + const fn from_const_generic(const_generic: u32) -> Self { + let legacy = (const_generic >> 24) as u8; + let legacy_integrated = ((const_generic >> 16) & (u8::MAX as u32)) as u8; + let subaddress = ((const_generic >> 8) & (u8::MAX as u32)) as u8; + let featured = (const_generic & (u8::MAX as u32)) as u8; + + AddressBytes { legacy, legacy_integrated, subaddress, featured } + } +} + +// TODO: Cite origin +const MONERO_MAINNET_BYTES: AddressBytes = match AddressBytes::new(18, 19, 42, 70) { + Some(bytes) => bytes, + None => panic!("mainnet byte constants conflicted"), +}; +const MONERO_STAGENET_BYTES: AddressBytes = match AddressBytes::new(53, 54, 63, 111) { + Some(bytes) => bytes, + None => panic!("stagenet byte constants conflicted"), +}; +const MONERO_TESTNET_BYTES: AddressBytes = match AddressBytes::new(24, 25, 36, 86) { + Some(bytes) => bytes, + None => panic!("testnet byte constants conflicted"), +}; + +/// The network this address is for. +#[derive(Clone, Copy, PartialEq, Eq, Debug, Zeroize)] +pub enum Network { + /// A mainnet address. + Mainnet, + /// A stagenet address. + /// + /// Stagenet maintains parity with mainnet and is useful for testing integrations accordingly. + Stagenet, + /// A testnet address. + /// + /// Testnet is used to test new consensus rules and functionality. + Testnet, } /// Error when decoding an address. #[derive(Clone, Copy, PartialEq, Eq, Debug)] #[cfg_attr(feature = "std", derive(thiserror::Error))] pub enum AddressError { - #[cfg_attr(feature = "std", error("invalid address byte"))] - InvalidByte, + /// The address had an invalid (network, type) byte. + #[cfg_attr(feature = "std", error("invalid byte for the address's network/type ({0})"))] + InvalidTypeByte(u8), + /// The address wasn't a valid Base58Check (as defined by Monero) string. #[cfg_attr(feature = "std", error("invalid address encoding"))] InvalidEncoding, + /// The data encoded wasn't the proper length. #[cfg_attr(feature = "std", error("invalid length"))] InvalidLength, + /// The address had an invalid key. #[cfg_attr(feature = "std", error("invalid key"))] InvalidKey, + /// The address was featured with unrecognized features. #[cfg_attr(feature = "std", error("unknown features"))] - UnknownFeatures, - #[cfg_attr(feature = "std", error("different network than expected"))] - DifferentNetwork, + UnknownFeatures(u64), + /// The network was for a different network than expected. + #[cfg_attr( + feature = "std", + error("different network ({actual:?}) than expected ({expected:?})") + )] + DifferentNetwork { + /// The Network expected. + expected: Network, + /// The Network embedded within the Address. + actual: Network, + }, } -impl AddressMeta { - #[allow(clippy::wrong_self_convention)] - fn to_byte(&self) -> u8 { - let bytes = B::network_bytes(self.network); - match self.kind { - AddressType::Standard => bytes.0, - AddressType::Integrated(_) => bytes.1, - AddressType::Subaddress => bytes.2, - AddressType::Featured { .. } => bytes.3, +/// Bytes used as prefixes when encoding addresses, variable to the network instance. +/// +/// These distinguish the address's network and type. +#[derive(Clone, Copy, PartialEq, Eq, Debug, Zeroize)] +pub struct NetworkedAddressBytes { + mainnet: AddressBytes, + stagenet: AddressBytes, + testnet: AddressBytes, +} + +impl NetworkedAddressBytes { + /// Create a new set of address bytes, one for each network. + pub const fn new( + mainnet: AddressBytes, + stagenet: AddressBytes, + testnet: AddressBytes, + ) -> Option { + let res = NetworkedAddressBytes { mainnet, stagenet, testnet }; + let all_bytes = res.to_const_generic(); + + let mut i = 0; + while i < 12 { + let this_byte = (all_bytes >> (32 + (i * 8))) & (u8::MAX as u128); + + let mut j = 0; + while j < 12 { + if i == j { + j += 1; + continue; + } + let other_byte = (all_bytes >> (32 + (j * 8))) & (u8::MAX as u128); + if this_byte == other_byte { + return None; + } + + j += 1; + } + + i += 1; + } + + Some(res) + } + + /// Convert this set of address bytes to its representation as a u128. + /// + /// We cannot use this struct directly as a const generic unfortunately. + pub const fn to_const_generic(self) -> u128 { + ((self.mainnet.to_const_generic() as u128) << 96) + + ((self.stagenet.to_const_generic() as u128) << 64) + + ((self.testnet.to_const_generic() as u128) << 32) + } + + #[allow(clippy::cast_possible_truncation)] + const fn from_const_generic(const_generic: u128) -> Self { + let mainnet = AddressBytes::from_const_generic((const_generic >> 96) as u32); + let stagenet = + AddressBytes::from_const_generic(((const_generic >> 64) & (u32::MAX as u128)) as u32); + let testnet = + AddressBytes::from_const_generic(((const_generic >> 32) & (u32::MAX as u128)) as u32); + + NetworkedAddressBytes { mainnet, stagenet, testnet } + } + + fn network(&self, network: Network) -> &AddressBytes { + match network { + Network::Mainnet => &self.mainnet, + Network::Stagenet => &self.stagenet, + Network::Testnet => &self.testnet, } } - /// Create an address's metadata. - pub fn new(network: Network, kind: AddressType) -> Self { - AddressMeta { _bytes: PhantomData, network, kind } + fn byte(&self, network: Network, kind: AddressType) -> u8 { + let address_bytes = self.network(network); + + match kind { + AddressType::Legacy => address_bytes.legacy, + AddressType::LegacyIntegrated(_) => address_bytes.legacy_integrated, + AddressType::Subaddress => address_bytes.subaddress, + AddressType::Featured { .. } => address_bytes.featured, + } } - // Returns an incomplete instantiation in the case of Integrated/Featured addresses - fn from_byte(byte: u8) -> Result { + // This will return an incomplete AddressType for LegacyIntegrated/Featured. + fn metadata_from_byte(&self, byte: u8) -> Result<(Network, AddressType), AddressError> { let mut meta = None; for network in [Network::Mainnet, Network::Testnet, Network::Stagenet] { - let (standard, integrated, subaddress, featured) = B::network_bytes(network); + let address_bytes = self.network(network); if let Some(kind) = match byte { - _ if byte == standard => Some(AddressType::Standard), - _ if byte == integrated => Some(AddressType::Integrated([0; 8])), - _ if byte == subaddress => Some(AddressType::Subaddress), - _ if byte == featured => { + _ if byte == address_bytes.legacy => Some(AddressType::Legacy), + _ if byte == address_bytes.legacy_integrated => Some(AddressType::LegacyIntegrated([0; 8])), + _ if byte == address_bytes.subaddress => Some(AddressType::Subaddress), + _ if byte == address_bytes.featured => { Some(AddressType::Featured { subaddress: false, payment_id: None, guaranteed: false }) } _ => None, } { - meta = Some(AddressMeta::new(network, kind)); + meta = Some((network, kind)); break; } } - meta.ok_or(AddressError::InvalidByte) - } - - pub fn is_subaddress(&self) -> bool { - self.kind.is_subaddress() - } - - pub fn payment_id(&self) -> Option<[u8; 8]> { - self.kind.payment_id() - } - - pub fn is_guaranteed(&self) -> bool { - self.kind.is_guaranteed() + meta.ok_or(AddressError::InvalidTypeByte(byte)) } } -/// A Monero address, composed of metadata and a spend/view key. -#[derive(Clone, Copy, PartialEq, Eq)] -pub struct Address { - pub meta: AddressMeta, - pub spend: EdwardsPoint, - pub view: EdwardsPoint, +/// The bytes used for distinguishing Monero addresses. +pub const MONERO_BYTES: NetworkedAddressBytes = match NetworkedAddressBytes::new( + MONERO_MAINNET_BYTES, + MONERO_STAGENET_BYTES, + MONERO_TESTNET_BYTES, +) { + Some(bytes) => bytes, + None => panic!("Monero network byte constants conflicted"), +}; + +/// A Monero address. +#[derive(Clone, Copy, PartialEq, Eq, Zeroize)] +pub struct Address { + network: Network, + kind: AddressType, + spend: EdwardsPoint, + view: EdwardsPoint, } -impl fmt::Debug for Address { +impl fmt::Debug for Address { fn fmt(&self, fmt: &mut fmt::Formatter<'_>) -> Result<(), fmt::Error> { + let hex = |bytes: &[u8]| -> String { + let mut res = String::with_capacity(2 + (2 * bytes.len())); + res.push_str("0x"); + for b in bytes { + write!(&mut res, "{b:02x}").unwrap(); + } + res + }; + fmt .debug_struct("Address") - .field("meta", &self.meta) - .field("spend", &hex::encode(self.spend.compress().0)) - .field("view", &hex::encode(self.view.compress().0)) + .field("network", &self.network) + .field("kind", &self.kind) + .field("spend", &hex(&self.spend.compress().to_bytes())) + .field("view", &hex(&self.view.compress().to_bytes())) // This is not a real field yet is the most valuable thing to know when debugging .field("(address)", &self.to_string()) .finish() } } -impl Zeroize for Address { - fn zeroize(&mut self) { - self.meta.zeroize(); - self.spend.zeroize(); - self.view.zeroize(); - } -} - -impl fmt::Display for Address { +impl fmt::Display for Address { fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result { - let mut data = vec![self.meta.to_byte()]; + let address_bytes: NetworkedAddressBytes = + NetworkedAddressBytes::from_const_generic(ADDRESS_BYTES); + + let mut data = vec![address_bytes.byte(self.network, self.kind)]; data.extend(self.spend.compress().to_bytes()); data.extend(self.view.compress().to_bytes()); - if let AddressType::Featured { subaddress, payment_id, guaranteed } = self.meta.kind { - // Technically should be a VarInt, yet we don't have enough features it's needed - data.push( - u8::from(subaddress) + (u8::from(payment_id.is_some()) << 1) + (u8::from(guaranteed) << 2), - ); + if let AddressType::Featured { subaddress, payment_id, guaranteed } = self.kind { + let features_uint = + (u8::from(guaranteed) << 2) + (u8::from(payment_id.is_some()) << 1) + u8::from(subaddress); + write_varint(&features_uint, &mut data).unwrap(); } - if let Some(id) = self.meta.kind.payment_id() { + if let Some(id) = self.kind.payment_id() { data.extend(id); } write!(f, "{}", encode_check(data)) } } -impl Address { - pub fn new(meta: AddressMeta, spend: EdwardsPoint, view: EdwardsPoint) -> Self { - Address { meta, spend, view } +impl Address { + /// Create a new address. + pub fn new(network: Network, kind: AddressType, spend: EdwardsPoint, view: EdwardsPoint) -> Self { + Address { network, kind, spend, view } } - pub fn from_str_raw(s: &str) -> Result { + /// Parse an address from a String, accepting any network it is. + pub fn from_str_with_unchecked_network(s: &str) -> Result { let raw = decode_check(s).ok_or(AddressError::InvalidEncoding)?; - if raw.len() < (1 + 32 + 32) { - Err(AddressError::InvalidLength)?; - } + let mut raw = raw.as_slice(); - let mut meta = AddressMeta::from_byte(raw[0])?; - let spend = - decompress_point(raw[1 .. 33].try_into().unwrap()).ok_or(AddressError::InvalidKey)?; - let view = - decompress_point(raw[33 .. 65].try_into().unwrap()).ok_or(AddressError::InvalidKey)?; - let mut read = 65; + let address_bytes: NetworkedAddressBytes = + NetworkedAddressBytes::from_const_generic(ADDRESS_BYTES); + let (network, mut kind) = address_bytes + .metadata_from_byte(read_byte(&mut raw).map_err(|_| AddressError::InvalidLength)?)?; + let spend = read_point(&mut raw).map_err(|_| AddressError::InvalidKey)?; + let view = read_point(&mut raw).map_err(|_| AddressError::InvalidKey)?; - if matches!(meta.kind, AddressType::Featured { .. }) { - if raw[read] >= (2 << 3) { - Err(AddressError::UnknownFeatures)?; + if matches!(kind, AddressType::Featured { .. }) { + let features = read_varint::<_, u64>(&mut raw).map_err(|_| AddressError::InvalidLength)?; + if (features >> 3) != 0 { + Err(AddressError::UnknownFeatures(features))?; } - let subaddress = (raw[read] & 1) == 1; - let integrated = ((raw[read] >> 1) & 1) == 1; - let guaranteed = ((raw[read] >> 2) & 1) == 1; + let subaddress = (features & 1) == 1; + let integrated = ((features >> 1) & 1) == 1; + let guaranteed = ((features >> 2) & 1) == 1; - meta.kind = AddressType::Featured { - subaddress, - payment_id: Some([0; 8]).filter(|_| integrated), - guaranteed, - }; - read += 1; + kind = + AddressType::Featured { subaddress, payment_id: integrated.then_some([0; 8]), guaranteed }; } - // Update read early so we can verify the length - if meta.kind.payment_id().is_some() { - read += 8; - } - if raw.len() != read { + // Read the payment ID, if there should be one + match kind { + AddressType::LegacyIntegrated(ref mut id) | + AddressType::Featured { payment_id: Some(ref mut id), .. } => { + *id = read_bytes(&mut raw).map_err(|_| AddressError::InvalidLength)?; + } + _ => {} + }; + + if !raw.is_empty() { Err(AddressError::InvalidLength)?; } - if let AddressType::Integrated(ref mut id) = meta.kind { - id.copy_from_slice(&raw[(read - 8) .. read]); - } - if let AddressType::Featured { payment_id: Some(ref mut id), .. } = meta.kind { - id.copy_from_slice(&raw[(read - 8) .. read]); - } - - Ok(Address { meta, spend, view }) + Ok(Address { network, kind, spend, view }) } + /// Create a new address from a `&str`. + /// + /// This takes in an argument for the expected network, erroring if a distinct network was used. + /// It also errors if the address is invalid (as expected). pub fn from_str(network: Network, s: &str) -> Result { - Self::from_str_raw(s).and_then(|addr| { - if addr.meta.network == network { + Self::from_str_with_unchecked_network(s).and_then(|addr| { + if addr.network == network { Ok(addr) } else { - Err(AddressError::DifferentNetwork)? + Err(AddressError::DifferentNetwork { actual: addr.network, expected: network })? } }) } + /// The network this address is intended for use on. pub fn network(&self) -> Network { - self.meta.network + self.network } + /// The type of address this is. + pub fn kind(&self) -> &AddressType { + &self.kind + } + + /// If this is a subaddress. pub fn is_subaddress(&self) -> bool { - self.meta.is_subaddress() + self.kind.is_subaddress() } + /// The payment ID for this address. pub fn payment_id(&self) -> Option<[u8; 8]> { - self.meta.payment_id() + self.kind.payment_id() } + /// If this address is guaranteed. + /// + /// A guaranteed address is one where any outputs scanned to it are guaranteed to be spendable + /// under the hardness of various cryptographic problems (which are assumed hard). This is via + /// a modified shared-key derivation which eliminates the burning bug. pub fn is_guaranteed(&self) -> bool { - self.meta.is_guaranteed() + self.kind.is_guaranteed() + } + + /// The public spend key for this address. + pub fn spend(&self) -> EdwardsPoint { + self.spend + } + + /// The public view key for this address. + pub fn view(&self) -> EdwardsPoint { + self.view } } /// Instantiation of the Address type with Monero's network bytes. -pub type MoneroAddress = Address; -// Allow re-interpreting of an arbitrary address as a monero address so it can be used with the -// rest of this library. Doesn't use From as it was conflicting with From for T. -impl MoneroAddress { - pub fn from(address: Address) -> MoneroAddress { - MoneroAddress::new( - AddressMeta::new(address.meta.network, address.meta.kind), - address.spend, - address.view, - ) - } -} +pub type MoneroAddress = Address<{ MONERO_BYTES.to_const_generic() }>; diff --git a/coins/monero/wallet/address/src/tests.rs b/coins/monero/wallet/address/src/tests.rs index 0f386d19..2804832a 100644 --- a/coins/monero/wallet/address/src/tests.rs +++ b/coins/monero/wallet/address/src/tests.rs @@ -6,7 +6,7 @@ use curve25519_dalek::{constants::ED25519_BASEPOINT_TABLE, scalar::Scalar}; use monero_io::decompress_point; -use crate::{Network, AddressType, AddressMeta, MoneroAddress}; +use crate::{Network, AddressType, MoneroAddress}; const SPEND: [u8; 32] = hex!("f8631661f6ab4e6fda310c797330d86e23a682f20d5bc8cc27b18051191f16d7"); const VIEW: [u8; 32] = hex!("4a1535063ad1fee2dabbf909d4fd9a873e29541b401f0944754e17c9a41820ce"); @@ -65,11 +65,11 @@ fn base58check() { #[test] fn standard_address() { let addr = MoneroAddress::from_str(Network::Mainnet, STANDARD).unwrap(); - assert_eq!(addr.meta.network, Network::Mainnet); - assert_eq!(addr.meta.kind, AddressType::Standard); - assert!(!addr.meta.kind.is_subaddress()); - assert_eq!(addr.meta.kind.payment_id(), None); - assert!(!addr.meta.kind.is_guaranteed()); + assert_eq!(addr.network(), Network::Mainnet); + assert_eq!(addr.kind(), &AddressType::Legacy); + assert!(!addr.is_subaddress()); + assert_eq!(addr.payment_id(), None); + assert!(!addr.is_guaranteed()); assert_eq!(addr.spend.compress().to_bytes(), SPEND); assert_eq!(addr.view.compress().to_bytes(), VIEW); assert_eq!(addr.to_string(), STANDARD); @@ -78,11 +78,11 @@ fn standard_address() { #[test] fn integrated_address() { let addr = MoneroAddress::from_str(Network::Mainnet, INTEGRATED).unwrap(); - assert_eq!(addr.meta.network, Network::Mainnet); - assert_eq!(addr.meta.kind, AddressType::Integrated(PAYMENT_ID)); - assert!(!addr.meta.kind.is_subaddress()); - assert_eq!(addr.meta.kind.payment_id(), Some(PAYMENT_ID)); - assert!(!addr.meta.kind.is_guaranteed()); + assert_eq!(addr.network(), Network::Mainnet); + assert_eq!(addr.kind(), &AddressType::LegacyIntegrated(PAYMENT_ID)); + assert!(!addr.is_subaddress()); + assert_eq!(addr.payment_id(), Some(PAYMENT_ID)); + assert!(!addr.is_guaranteed()); assert_eq!(addr.spend.compress().to_bytes(), SPEND); assert_eq!(addr.view.compress().to_bytes(), VIEW); assert_eq!(addr.to_string(), INTEGRATED); @@ -91,11 +91,11 @@ fn integrated_address() { #[test] fn subaddress() { let addr = MoneroAddress::from_str(Network::Mainnet, SUBADDRESS).unwrap(); - assert_eq!(addr.meta.network, Network::Mainnet); - assert_eq!(addr.meta.kind, AddressType::Subaddress); - assert!(addr.meta.kind.is_subaddress()); - assert_eq!(addr.meta.kind.payment_id(), None); - assert!(!addr.meta.kind.is_guaranteed()); + assert_eq!(addr.network(), Network::Mainnet); + assert_eq!(addr.kind(), &AddressType::Subaddress); + assert!(addr.is_subaddress()); + assert_eq!(addr.payment_id(), None); + assert!(!addr.is_guaranteed()); assert_eq!(addr.spend.compress().to_bytes(), SUB_SPEND); assert_eq!(addr.view.compress().to_bytes(), SUB_VIEW); assert_eq!(addr.to_string(), SUBADDRESS); @@ -125,8 +125,7 @@ fn featured() { let guaranteed = (features & GUARANTEED_FEATURE_BIT) == GUARANTEED_FEATURE_BIT; let kind = AddressType::Featured { subaddress, payment_id, guaranteed }; - let meta = AddressMeta::new(network, kind); - let addr = MoneroAddress::new(meta, spend, view); + let addr = MoneroAddress::new(network, kind, spend, view); assert_eq!(addr.to_string().chars().next().unwrap(), first); assert_eq!(MoneroAddress::from_str(network, &addr.to_string()).unwrap(), addr); @@ -190,14 +189,12 @@ fn featured_vectors() { assert_eq!( MoneroAddress::new( - AddressMeta::new( - network, - AddressType::Featured { - subaddress: vector.subaddress, - payment_id: vector.payment_id, - guaranteed: vector.guaranteed - } - ), + network, + AddressType::Featured { + subaddress: vector.subaddress, + payment_id: vector.payment_id, + guaranteed: vector.guaranteed + }, spend, view ) diff --git a/coins/monero/wallet/src/lib.rs b/coins/monero/wallet/src/lib.rs index 42bae1b8..7e9ccd5f 100644 --- a/coins/monero/wallet/src/lib.rs +++ b/coins/monero/wallet/src/lib.rs @@ -24,7 +24,7 @@ pub mod extra; pub(crate) use extra::{PaymentId, Extra}; pub use monero_address as address; -use address::{Network, AddressType, SubaddressIndex, AddressSpec, AddressMeta, MoneroAddress}; +use address::{Network, AddressType, SubaddressIndex, AddressSpec, MoneroAddress}; pub mod scan; @@ -88,28 +88,23 @@ impl ViewPair { let mut spend = self.spend; let mut view: EdwardsPoint = self.view.deref() * ED25519_BASEPOINT_TABLE; - // construct the address meta - let meta = match spec { - AddressSpec::Standard => AddressMeta::new(network, AddressType::Standard), - AddressSpec::Integrated(payment_id) => { - AddressMeta::new(network, AddressType::Integrated(payment_id)) - } + // construct the address type + let kind = match spec { + AddressSpec::Legacy => AddressType::Legacy, + AddressSpec::LegacyIntegrated(payment_id) => AddressType::LegacyIntegrated(payment_id), AddressSpec::Subaddress(index) => { (spend, view) = self.subaddress_keys(index); - AddressMeta::new(network, AddressType::Subaddress) + AddressType::Subaddress } AddressSpec::Featured { subaddress, payment_id, guaranteed } => { if let Some(index) = subaddress { (spend, view) = self.subaddress_keys(index); } - AddressMeta::new( - network, - AddressType::Featured { subaddress: subaddress.is_some(), payment_id, guaranteed }, - ) + AddressType::Featured { subaddress: subaddress.is_some(), payment_id, guaranteed } } }; - MoneroAddress::new(meta, spend, view) + MoneroAddress::new(network, kind, spend, view) } } diff --git a/coins/monero/wallet/src/send/mod.rs b/coins/monero/wallet/src/send/mod.rs index 32ae248c..eec719cc 100644 --- a/coins/monero/wallet/src/send/mod.rs +++ b/coins/monero/wallet/src/send/mod.rs @@ -75,7 +75,7 @@ impl Change { // Which network doesn't matter as the derivations will all be the same Network::Mainnet, if !guaranteed { - AddressSpec::Standard + AddressSpec::Legacy } else { AddressSpec::Featured { subaddress: None, payment_id: None, guaranteed: true } }, @@ -390,7 +390,7 @@ impl SignableTransaction { fn read_address(r: &mut R) -> io::Result { String::from_utf8(read_vec(read_byte, r)?) .ok() - .and_then(|str| MoneroAddress::from_str_raw(&str).ok()) + .and_then(|str| MoneroAddress::from_str_with_unchecked_network(&str).ok()) .ok_or_else(|| io::Error::other("invalid address")) } diff --git a/coins/monero/wallet/src/send/tx.rs b/coins/monero/wallet/src/send/tx.rs index c0f37832..b86cd3b7 100644 --- a/coins/monero/wallet/src/send/tx.rs +++ b/coins/monero/wallet/src/send/tx.rs @@ -42,7 +42,7 @@ impl SignableTransaction { let mut res = Vec::with_capacity(self.payments.len()); for (payment, shared_key_derivations) in self.payments.iter().zip(&shared_key_derivations) { let key = - (&shared_key_derivations.shared_key * ED25519_BASEPOINT_TABLE) + payment.address().spend; + (&shared_key_derivations.shared_key * ED25519_BASEPOINT_TABLE) + payment.address().spend(); res.push(Output { key: key.compress(), amount: None, diff --git a/coins/monero/wallet/src/send/tx_keys.rs b/coins/monero/wallet/src/send/tx_keys.rs index f8eac0e4..458bf796 100644 --- a/coins/monero/wallet/src/send/tx_keys.rs +++ b/coins/monero/wallet/src/send/tx_keys.rs @@ -106,7 +106,7 @@ impl SignableTransaction { let ecdh = match payment { // If we don't have the view key, use the key dedicated for this address (r A) InternalPayment::Payment(_, _) | InternalPayment::Change(_, None) => { - Zeroizing::new(key_to_use.deref() * addr.view) + Zeroizing::new(key_to_use.deref() * addr.view()) } // If we do have the view key, use the commitment to the key (a R) InternalPayment::Change(_, Some(view)) => Zeroizing::new(view.deref() * tx_key_pub), @@ -173,7 +173,7 @@ impl SignableTransaction { // TODO: Support subaddresses as change? debug_assert!(addr.is_subaddress()); - return (tx_key.deref() * addr.spend, vec![]); + return (tx_key.deref() * addr.spend(), vec![]); } if should_use_additional_keys { @@ -182,7 +182,7 @@ impl SignableTransaction { let addr = payment.address(); // TODO: Double check this against wallet2 if addr.is_subaddress() { - additional_keys_pub.push(additional_key.deref() * addr.spend); + additional_keys_pub.push(additional_key.deref() * addr.spend()); } else { additional_keys_pub.push(additional_key.deref() * ED25519_BASEPOINT_TABLE) } diff --git a/coins/monero/wallet/tests/eventuality.rs b/coins/monero/wallet/tests/eventuality.rs index 37b968f8..40ace81c 100644 --- a/coins/monero/wallet/tests/eventuality.rs +++ b/coins/monero/wallet/tests/eventuality.rs @@ -3,7 +3,7 @@ use curve25519_dalek::constants::ED25519_BASEPOINT_POINT; use monero_serai::transaction::Transaction; use monero_wallet::{ rpc::Rpc, - address::{AddressType, AddressMeta, MoneroAddress}, + address::{AddressType, MoneroAddress}, send::Eventuality, }; @@ -17,7 +17,8 @@ test!( // Each have their own slight implications to eventualities builder.add_payment( MoneroAddress::new( - AddressMeta::new(Network::Mainnet, AddressType::Standard), + Network::Mainnet, + AddressType::Legacy, ED25519_BASEPOINT_POINT, ED25519_BASEPOINT_POINT, ), @@ -25,7 +26,8 @@ test!( ); builder.add_payment( MoneroAddress::new( - AddressMeta::new(Network::Mainnet, AddressType::Integrated([0xaa; 8])), + Network::Mainnet, + AddressType::LegacyIntegrated([0xaa; 8]), ED25519_BASEPOINT_POINT, ED25519_BASEPOINT_POINT, ), @@ -33,7 +35,8 @@ test!( ); builder.add_payment( MoneroAddress::new( - AddressMeta::new(Network::Mainnet, AddressType::Subaddress), + Network::Mainnet, + AddressType::Subaddress, ED25519_BASEPOINT_POINT, ED25519_BASEPOINT_POINT, ), @@ -41,10 +44,8 @@ test!( ); builder.add_payment( MoneroAddress::new( - AddressMeta::new( - Network::Mainnet, - AddressType::Featured { subaddress: false, payment_id: None, guaranteed: true }, - ), + Network::Mainnet, + AddressType::Featured { subaddress: false, payment_id: None, guaranteed: true }, ED25519_BASEPOINT_POINT, ED25519_BASEPOINT_POINT, ), diff --git a/coins/monero/wallet/tests/runner/mod.rs b/coins/monero/wallet/tests/runner/mod.rs index 978f91aa..1a91dd3f 100644 --- a/coins/monero/wallet/tests/runner/mod.rs +++ b/coins/monero/wallet/tests/runner/mod.rs @@ -14,7 +14,7 @@ use monero_wallet::{ transaction::Transaction, rpc::{Rpc, FeeRate}, ViewPair, - address::{Network, AddressType, AddressSpec, AddressMeta, MoneroAddress}, + address::{Network, AddressType, AddressSpec, MoneroAddress}, scan::{SpendableOutput, Scanner}, }; @@ -36,11 +36,12 @@ pub fn random_address() -> (Scalar, ViewPair, MoneroAddress) { ( spend, ViewPair::new(spend_pub, view.clone()), - MoneroAddress { - meta: AddressMeta::new(Network::Mainnet, AddressType::Standard), - spend: spend_pub, - view: view.deref() * ED25519_BASEPOINT_TABLE, - }, + MoneroAddress::new( + Network::Mainnet, + AddressType::Legacy, + spend_pub, + view.deref() * ED25519_BASEPOINT_TABLE, + ), ) } @@ -82,7 +83,7 @@ pub async fn get_miner_tx_output(rpc: &SimpleRequestRpc, view: &ViewPair) -> Spe // Mine 60 blocks to unlock a miner TX let start = rpc.get_height().await.unwrap(); rpc - .generate_blocks(&view.address(Network::Mainnet, AddressSpec::Standard).to_string(), 60) + .generate_blocks(&view.address(Network::Mainnet, AddressSpec::Legacy).to_string(), 60) .await .unwrap(); @@ -112,11 +113,12 @@ pub async fn rpc() -> SimpleRequestRpc { return rpc; } - let addr = MoneroAddress { - meta: AddressMeta::new(Network::Mainnet, AddressType::Standard), - spend: &Scalar::random(&mut OsRng) * ED25519_BASEPOINT_TABLE, - view: &Scalar::random(&mut OsRng) * ED25519_BASEPOINT_TABLE, - } + let addr = MoneroAddress::new( + Network::Mainnet, + AddressType::Legacy, + &Scalar::random(&mut OsRng) * ED25519_BASEPOINT_TABLE, + &Scalar::random(&mut OsRng) * ED25519_BASEPOINT_TABLE, + ) .to_string(); // Mine 40 blocks to ensure decoy availability @@ -222,7 +224,7 @@ macro_rules! test { let view_priv = Zeroizing::new(Scalar::random(&mut OsRng)); let view = ViewPair::new(spend_pub, view_priv.clone()); - let addr = view.address(Network::Mainnet, AddressSpec::Standard); + let addr = view.address(Network::Mainnet, AddressSpec::Legacy); let miner_tx = get_miner_tx_output(&rpc, &view).await; diff --git a/coins/monero/wallet/tests/scan.rs b/coins/monero/wallet/tests/scan.rs index b4d24eb8..e0025976 100644 --- a/coins/monero/wallet/tests/scan.rs +++ b/coins/monero/wallet/tests/scan.rs @@ -11,7 +11,7 @@ test!( |_, mut builder: Builder, _| async move { let view = runner::random_address().1; let scanner = Scanner::from_view(view.clone(), Some(HashSet::new())); - builder.add_payment(view.address(Network::Mainnet, AddressSpec::Standard), 5); + builder.add_payment(view.address(Network::Mainnet, AddressSpec::Legacy), 5); (builder.build().unwrap(), scanner) }, |_, tx: Transaction, _, mut state: Scanner| async move { @@ -54,7 +54,8 @@ test!( let mut payment_id = [0u8; 8]; OsRng.fill_bytes(&mut payment_id); - builder.add_payment(view.address(Network::Mainnet, AddressSpec::Integrated(payment_id)), 5); + builder + .add_payment(view.address(Network::Mainnet, AddressSpec::LegacyIntegrated(payment_id)), 5); (builder.build().unwrap(), (scanner, payment_id)) }, |_, tx: Transaction, _, mut state: (Scanner, [u8; 8])| async move { diff --git a/coins/monero/wallet/tests/wallet2_compatibility.rs b/coins/monero/wallet/tests/wallet2_compatibility.rs index a3321260..f4c54927 100644 --- a/coins/monero/wallet/tests/wallet2_compatibility.rs +++ b/coins/monero/wallet/tests/wallet2_compatibility.rs @@ -117,11 +117,11 @@ async fn from_wallet_rpc_to_self(spec: AddressSpec) { assert_eq!(output.metadata.subaddress, Some(index)); assert_eq!(output.metadata.payment_id, Some(PaymentId::Encrypted([0u8; 8]))); } - AddressSpec::Integrated(payment_id) => { + AddressSpec::LegacyIntegrated(payment_id) => { assert_eq!(output.metadata.payment_id, Some(PaymentId::Encrypted(payment_id))); assert_eq!(output.metadata.subaddress, None); } - AddressSpec::Standard | AddressSpec::Featured { .. } => { + AddressSpec::Legacy | AddressSpec::Featured { .. } => { assert_eq!(output.metadata.subaddress, None); assert_eq!(output.metadata.payment_id, Some(PaymentId::Encrypted([0u8; 8]))); } @@ -131,7 +131,7 @@ async fn from_wallet_rpc_to_self(spec: AddressSpec) { async_sequential!( async fn receipt_of_wallet_rpc_tx_standard() { - from_wallet_rpc_to_self(AddressSpec::Standard).await; + from_wallet_rpc_to_self(AddressSpec::Legacy).await; } async fn receipt_of_wallet_rpc_tx_subaddress() { @@ -141,7 +141,7 @@ async_sequential!( async fn receipt_of_wallet_rpc_tx_integrated() { let mut payment_id = [0u8; 8]; OsRng.fill_bytes(&mut payment_id); - from_wallet_rpc_to_self(AddressSpec::Integrated(payment_id)).await; + from_wallet_rpc_to_self(AddressSpec::LegacyIntegrated(payment_id)).await; } ); diff --git a/substrate/client/src/networks/monero.rs b/substrate/client/src/networks/monero.rs index 7585f5ef..bd5e0a15 100644 --- a/substrate/client/src/networks/monero.rs +++ b/substrate/client/src/networks/monero.rs @@ -4,7 +4,7 @@ use scale::{Encode, Decode}; use ciphersuite::{Ciphersuite, Ed25519}; -use monero_wallet::address::{AddressError, Network, AddressType, AddressMeta, MoneroAddress}; +use monero_wallet::address::{AddressError, Network, AddressType, MoneroAddress}; #[derive(Clone, PartialEq, Eq, Debug)] pub struct Address(MoneroAddress); @@ -33,7 +33,7 @@ impl fmt::Display for Address { // SCALE-encoded variant of Monero addresses. #[derive(Clone, PartialEq, Eq, Debug, Encode, Decode)] enum EncodedAddressType { - Standard, + Legacy, Subaddress, Featured(u8), } @@ -52,22 +52,20 @@ impl TryFrom> for Address { let addr = EncodedAddress::decode(&mut data.as_ref()).map_err(|_| ())?; // Convert over Ok(Address(MoneroAddress::new( - AddressMeta::new( - Network::Mainnet, - match addr.kind { - EncodedAddressType::Standard => AddressType::Standard, - EncodedAddressType::Subaddress => AddressType::Subaddress, - EncodedAddressType::Featured(flags) => { - let subaddress = (flags & 1) != 0; - let integrated = (flags & (1 << 1)) != 0; - let guaranteed = (flags & (1 << 2)) != 0; - if integrated { - Err(())?; - } - AddressType::Featured { subaddress, payment_id: None, guaranteed } + Network::Mainnet, + match addr.kind { + EncodedAddressType::Legacy => AddressType::Legacy, + EncodedAddressType::Subaddress => AddressType::Subaddress, + EncodedAddressType::Featured(flags) => { + let subaddress = (flags & 1) != 0; + let integrated = (flags & (1 << 1)) != 0; + let guaranteed = (flags & (1 << 2)) != 0; + if integrated { + Err(())?; } - }, - ), + AddressType::Featured { subaddress, payment_id: None, guaranteed } + } + }, Ed25519::read_G::<&[u8]>(&mut addr.spend.as_ref()).map_err(|_| ())?.0, Ed25519::read_G::<&[u8]>(&mut addr.view.as_ref()).map_err(|_| ())?.0, ))) @@ -85,16 +83,19 @@ impl Into for Address { impl Into> for Address { fn into(self) -> Vec { EncodedAddress { - kind: match self.0.meta.kind { - AddressType::Standard => EncodedAddressType::Standard, + kind: match self.0.kind() { + AddressType::Legacy => EncodedAddressType::Legacy, + AddressType::LegacyIntegrated(_) => { + panic!("integrated address became Serai Monero address") + } AddressType::Subaddress => EncodedAddressType::Subaddress, - AddressType::Integrated(_) => panic!("integrated address became Serai Monero address"), - AddressType::Featured { subaddress, payment_id: _, guaranteed } => { - EncodedAddressType::Featured(u8::from(subaddress) + (u8::from(guaranteed) << 2)) + AddressType::Featured { subaddress, payment_id, guaranteed } => { + debug_assert!(payment_id.is_none()); + EncodedAddressType::Featured(u8::from(*subaddress) + (u8::from(*guaranteed) << 2)) } }, - spend: self.0.spend.compress().0, - view: self.0.view.compress().0, + spend: self.0.spend().compress().0, + view: self.0.view().compress().0, } .encode() }