#![cfg_attr(docsrs, feature(doc_auto_cfg))] #![doc = include_str!("../README.md")] #![deny(missing_docs)] #![cfg_attr(not(feature = "std"), no_std)] use core::fmt::{self, Write}; use std_shims::{ vec, string::{String, ToString}, }; use zeroize::Zeroize; use curve25519_dalek::EdwardsPoint; use monero_io::*; mod base58check; use base58check::{encode_check, decode_check}; #[cfg(test)] mod tests; /// 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 { /// 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, /// 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. pub fn payment_id(&self) -> Option<[u8; 8]> { if let AddressType::LegacyIntegrated(id) = self { Some(*id) } else if let AddressType::Featured { payment_id, .. } = self { *payment_id } else { None } } /// 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 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, } 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 } } /// 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 } } } // https://github.com/monero-project/monero/blob/cc73fe71162d564ffda8e549b79a350bca53c454 // /src/cryptonote_config.h#L216-L225 // https://gist.github.com/kayabaNerve/01c50bbc35441e0bbdcee63a9d823789 for featured const MONERO_MAINNET_BYTES: AddressBytes = match AddressBytes::new(18, 19, 42, 70) { Some(bytes) => bytes, None => panic!("mainnet byte constants conflicted"), }; // https://github.com/monero-project/monero/blob/master/src/cryptonote_config.h#L277-L281 const MONERO_STAGENET_BYTES: AddressBytes = match AddressBytes::new(24, 25, 36, 86) { Some(bytes) => bytes, None => panic!("stagenet byte constants conflicted"), }; // https://github.com/monero-project/monero/blob/master/src/cryptonote_config.h#L262-L266 const MONERO_TESTNET_BYTES: AddressBytes = match AddressBytes::new(53, 54, 63, 111) { 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, } /// Errors when decoding an address. #[derive(Clone, Copy, PartialEq, Eq, Debug)] #[cfg_attr(feature = "std", derive(thiserror::Error))] pub enum AddressError { /// 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(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, }, /// The view key was of small order despite being in a guaranteed address. #[cfg_attr(feature = "std", error("small-order view key in guaranteed address"))] SmallOrderView, } /// 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, } } 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, } } // 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 address_bytes = self.network(network); if let Some(kind) = match byte { _ 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((network, kind)); break; } } meta.ok_or(AddressError::InvalidTypeByte(byte)) } } /// 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 { 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("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 fmt::Display for Address { fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result { 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.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.kind.payment_id() { data.extend(id); } write!(f, "{}", encode_check(data)) } } impl Address { /// Create a new address. pub fn new(network: Network, kind: AddressType, spend: EdwardsPoint, view: EdwardsPoint) -> Self { Address { network, kind, spend, view } } /// 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)?; let mut raw = raw.as_slice(); 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!(kind, AddressType::Featured { .. }) { let features = read_varint::<_, u64>(&mut raw).map_err(|_| AddressError::InvalidLength)?; if (features >> 3) != 0 { Err(AddressError::UnknownFeatures(features))?; } let subaddress = (features & 1) == 1; let integrated = ((features >> 1) & 1) == 1; let guaranteed = ((features >> 2) & 1) == 1; kind = AddressType::Featured { subaddress, payment_id: integrated.then_some([0; 8]), guaranteed }; } // 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)?; } 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_with_unchecked_network(s).and_then(|addr| { if addr.network == network { Ok(addr) } else { Err(AddressError::DifferentNetwork { actual: addr.network, expected: network })? } }) } /// The network this address is intended for use on. pub fn network(&self) -> 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.kind.is_subaddress() } /// The payment ID for this address. pub fn payment_id(&self) -> Option<[u8; 8]> { 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.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<{ MONERO_BYTES.to_const_generic() }>;