monero_address/
lib.rs

1#![cfg_attr(docsrs, feature(doc_cfg))]
2#![doc = include_str!("../README.md")]
3#![cfg_attr(not(test), no_std)]
4
5use core::fmt::{self, Write as _};
6extern crate alloc;
7use alloc::{
8  vec,
9  string::{String, ToString as _},
10};
11
12use zeroize::Zeroize;
13
14use monero_io::*;
15use monero_ed25519::{Point, CompressedPoint};
16use monero_primitives::UpperBound;
17
18use monero_base58::{encode_check, decode_check};
19
20#[cfg(test)]
21mod tests;
22
23/// The address type.
24///
25/// The officially specified addresses are supported, along with
26/// [Featured Addresses](https://gist.github.com/kayabaNerve/01c50bbc35441e0bbdcee63a9d823789).
27#[derive(Clone, Copy, PartialEq, Eq, Debug, Zeroize)]
28pub enum AddressType {
29  /// A legacy address type.
30  Legacy,
31  /// A legacy address with a payment ID embedded.
32  LegacyIntegrated([u8; 8]),
33  /// A subaddress.
34  ///
35  /// This is what SHOULD be used if specific functionality isn't needed.
36  Subaddress,
37  /// A featured address.
38  ///
39  /// Featured Addresses are an unofficial address specification which is meant to be extensible
40  /// and support a variety of functionality. This functionality includes being a subaddresses AND
41  /// having a payment ID, along with being immune to the burning bug.
42  ///
43  /// At this time, support for featured addresses is limited to this crate. There should be no
44  /// expectation of interoperability.
45  Featured {
46    /// If this address is a subaddress.
47    subaddress: bool,
48    /// The payment ID associated with this address.
49    payment_id: Option<[u8; 8]>,
50    /// If this address is guaranteed.
51    ///
52    /// A guaranteed address is one where any outputs scanned to it are guaranteed to be spendable
53    /// under the hardness of various cryptographic problems (which are assumed hard). This is via
54    /// a modified shared-key derivation which eliminates the burning bug.
55    guaranteed: bool,
56  },
57}
58
59impl AddressType {
60  /// If this address is a subaddress.
61  pub fn is_subaddress(&self) -> bool {
62    matches!(self, AddressType::Subaddress) ||
63      matches!(self, AddressType::Featured { subaddress: true, .. })
64  }
65
66  /// The payment ID within this address.
67  pub fn payment_id(&self) -> Option<[u8; 8]> {
68    if let AddressType::LegacyIntegrated(id) = self {
69      Some(*id)
70    } else if let AddressType::Featured { payment_id, .. } = self {
71      *payment_id
72    } else {
73      None
74    }
75  }
76
77  /// If this address is guaranteed.
78  ///
79  /// A guaranteed address is one where any outputs scanned to it are guaranteed to be spendable
80  /// under the hardness of various cryptographic problems (which are assumed hard). This is via
81  /// a modified shared-key derivation which eliminates the burning bug.
82  pub fn is_guaranteed(&self) -> bool {
83    matches!(self, AddressType::Featured { guaranteed: true, .. })
84  }
85}
86
87/// A subaddress index.
88///
89/// Subaddresses are derived from a root using a `(account, address)` tuple as an index.
90#[derive(Clone, Copy, PartialEq, Eq, Debug, Zeroize)]
91pub struct SubaddressIndex {
92  account: u32,
93  address: u32,
94}
95
96impl SubaddressIndex {
97  /// Create a new SubaddressIndex.
98  pub const fn new(account: u32, address: u32) -> Option<SubaddressIndex> {
99    if (account == 0) && (address == 0) {
100      return None;
101    }
102    Some(SubaddressIndex { account, address })
103  }
104
105  /// Get the account this subaddress index is under.
106  pub const fn account(&self) -> u32 {
107    self.account
108  }
109
110  /// Get the address this subaddress index is for, within its account.
111  pub const fn address(&self) -> u32 {
112    self.address
113  }
114}
115
116/// Bytes used as prefixes when encoding addresses.
117///
118/// These distinguish the address's type.
119#[derive(Clone, Copy, PartialEq, Eq, Debug, Zeroize)]
120pub struct AddressBytes {
121  legacy: u8,
122  legacy_integrated: u8,
123  subaddress: u8,
124  featured: u8,
125}
126
127impl AddressBytes {
128  /// Create a new set of address bytes, one for each address type.
129  pub const fn new(
130    legacy: u8,
131    legacy_integrated: u8,
132    subaddress: u8,
133    featured: u8,
134  ) -> Option<Self> {
135    if (legacy == legacy_integrated) || (legacy == subaddress) || (legacy == featured) {
136      return None;
137    }
138    if (legacy_integrated == subaddress) || (legacy_integrated == featured) {
139      return None;
140    }
141    if subaddress == featured {
142      return None;
143    }
144    Some(AddressBytes { legacy, legacy_integrated, subaddress, featured })
145  }
146
147  #[expect(clippy::as_conversions)]
148  const fn to_const_generic(self) -> u32 {
149    ((self.legacy as u32) << 24) +
150      ((self.legacy_integrated as u32) << 16) +
151      ((self.subaddress as u32) << 8) +
152      (self.featured as u32)
153  }
154
155  #[expect(clippy::as_conversions, clippy::cast_possible_truncation)]
156  const fn from_const_generic(const_generic: u32) -> Self {
157    let legacy = (const_generic >> 24) as u8;
158    let legacy_integrated = ((const_generic >> 16) & (u8::MAX as u32)) as u8;
159    let subaddress = ((const_generic >> 8) & (u8::MAX as u32)) as u8;
160    let featured = (const_generic & (u8::MAX as u32)) as u8;
161
162    AddressBytes { legacy, legacy_integrated, subaddress, featured }
163  }
164}
165
166// https://github.com/monero-project/monero/blob/cc73fe71162d564ffda8e549b79a350bca53c454
167//   /src/cryptonote_config.h#L216-L225
168// https://gist.github.com/kayabaNerve/01c50bbc35441e0bbdcee63a9d823789 for featured
169const MONERO_MAINNET_BYTES: AddressBytes = match AddressBytes::new(18, 19, 42, 70) {
170  Some(bytes) => bytes,
171  None => panic!("mainnet byte constants conflicted"),
172};
173// https://github.com/monero-project/monero/blob/cc73fe71162d564ffda8e549b79a350bca53c454
174//   /src/cryptonote_config.h#L277-L281
175const MONERO_STAGENET_BYTES: AddressBytes = match AddressBytes::new(24, 25, 36, 86) {
176  Some(bytes) => bytes,
177  None => panic!("stagenet byte constants conflicted"),
178};
179// https://github.com/monero-project/monero/blob/cc73fe71162d564ffda8e549b79a350bca53c454
180//   /src/cryptonote_config.h#L262-L266
181const MONERO_TESTNET_BYTES: AddressBytes = match AddressBytes::new(53, 54, 63, 111) {
182  Some(bytes) => bytes,
183  None => panic!("testnet byte constants conflicted"),
184};
185
186/// The network this address is for.
187#[derive(Clone, Copy, PartialEq, Eq, Debug, Zeroize)]
188pub enum Network {
189  /// A mainnet address.
190  Mainnet,
191  /// A stagenet address.
192  ///
193  /// Stagenet maintains parity with mainnet and is useful for testing integrations accordingly.
194  Stagenet,
195  /// A testnet address.
196  ///
197  /// Testnet is used to test new consensus rules and functionality.
198  Testnet,
199}
200
201/// Errors when decoding an address.
202#[derive(Clone, Copy, PartialEq, Eq, Debug, thiserror::Error)]
203pub enum AddressError {
204  /// The address had an invalid (network, type) byte.
205  #[error("invalid byte for the address's network/type ({0})")]
206  InvalidTypeByte(u8),
207  /// The address wasn't a valid Base58Check (as defined by Monero) string.
208  #[error("invalid address encoding")]
209  InvalidEncoding,
210  /// The data encoded wasn't the proper length.
211  #[error("invalid length")]
212  InvalidLength,
213  /// The address had an invalid key.
214  #[error("invalid key")]
215  InvalidKey,
216  /// The address was featured with unrecognized features.
217  #[error("unknown features")]
218  UnknownFeatures(u64),
219  /// The network was for a different network than expected.
220  #[error("different network ({actual:?}) than expected ({expected:?})")]
221  DifferentNetwork {
222    /// The Network expected.
223    expected: Network,
224    /// The Network embedded within the Address.
225    actual: Network,
226  },
227  /// The address's type does not support payment IDs.
228  #[error("address's type does not support integrated payment ID")]
229  PaymentIDsUnsupported,
230}
231
232/// Bytes used as prefixes when encoding addresses, variable to the network instance.
233///
234/// These distinguish the address's network and type.
235#[derive(Clone, Copy, PartialEq, Eq, Debug, Zeroize)]
236pub struct NetworkedAddressBytes {
237  mainnet: AddressBytes,
238  stagenet: AddressBytes,
239  testnet: AddressBytes,
240}
241
242impl NetworkedAddressBytes {
243  /// Create a new set of address bytes, one for each network.
244  #[expect(clippy::as_conversions)] // Required as this is a `const fn`
245  pub const fn new(
246    mainnet: AddressBytes,
247    stagenet: AddressBytes,
248    testnet: AddressBytes,
249  ) -> Option<Self> {
250    let res = NetworkedAddressBytes { mainnet, stagenet, testnet };
251    let all_bytes = res.to_const_generic();
252
253    let mut i = 0;
254    while i < 12 {
255      let this_byte = (all_bytes >> (32 + (i * 8))) & (u8::MAX as u128);
256
257      let mut j = 0;
258      while j < 12 {
259        if i == j {
260          j += 1;
261          continue;
262        }
263        let other_byte = (all_bytes >> (32 + (j * 8))) & (u8::MAX as u128);
264        if this_byte == other_byte {
265          return None;
266        }
267
268        j += 1;
269      }
270
271      i += 1;
272    }
273
274    Some(res)
275  }
276
277  /// Convert this set of address bytes to its representation as a u128.
278  ///
279  /// We cannot use this struct directly as a const generic unfortunately.
280  #[expect(clippy::as_conversions)]
281  pub const fn to_const_generic(self) -> u128 {
282    ((self.mainnet.to_const_generic() as u128) << 96) +
283      ((self.stagenet.to_const_generic() as u128) << 64) +
284      ((self.testnet.to_const_generic() as u128) << 32)
285  }
286
287  #[expect(clippy::as_conversions, clippy::cast_possible_truncation)]
288  const fn from_const_generic(const_generic: u128) -> Self {
289    let mainnet = AddressBytes::from_const_generic((const_generic >> 96) as u32);
290    let stagenet =
291      AddressBytes::from_const_generic(((const_generic >> 64) & (u32::MAX as u128)) as u32);
292    let testnet =
293      AddressBytes::from_const_generic(((const_generic >> 32) & (u32::MAX as u128)) as u32);
294
295    NetworkedAddressBytes { mainnet, stagenet, testnet }
296  }
297
298  fn network(&self, network: Network) -> &AddressBytes {
299    match network {
300      Network::Mainnet => &self.mainnet,
301      Network::Stagenet => &self.stagenet,
302      Network::Testnet => &self.testnet,
303    }
304  }
305
306  fn byte(&self, network: Network, kind: AddressType) -> u8 {
307    let address_bytes = self.network(network);
308
309    match kind {
310      AddressType::Legacy => address_bytes.legacy,
311      AddressType::LegacyIntegrated(_) => address_bytes.legacy_integrated,
312      AddressType::Subaddress => address_bytes.subaddress,
313      AddressType::Featured { .. } => address_bytes.featured,
314    }
315  }
316
317  // This will return an incomplete AddressType for LegacyIntegrated/Featured.
318  fn metadata_from_byte(&self, byte: u8) -> Result<(Network, AddressType), AddressError> {
319    let mut meta = None;
320    for network in [Network::Mainnet, Network::Testnet, Network::Stagenet] {
321      let address_bytes = self.network(network);
322      if let Some(kind) = match byte {
323        _ if byte == address_bytes.legacy => Some(AddressType::Legacy),
324        _ if byte == address_bytes.legacy_integrated => Some(AddressType::LegacyIntegrated([0; 8])),
325        _ if byte == address_bytes.subaddress => Some(AddressType::Subaddress),
326        _ if byte == address_bytes.featured => {
327          Some(AddressType::Featured { subaddress: false, payment_id: None, guaranteed: false })
328        }
329        _ => None,
330      } {
331        meta = Some((network, kind));
332        break;
333      }
334    }
335
336    meta.ok_or(AddressError::InvalidTypeByte(byte))
337  }
338}
339
340/// The bytes used for distinguishing Monero addresses.
341pub const MONERO_BYTES: NetworkedAddressBytes = match NetworkedAddressBytes::new(
342  MONERO_MAINNET_BYTES,
343  MONERO_STAGENET_BYTES,
344  MONERO_TESTNET_BYTES,
345) {
346  Some(bytes) => bytes,
347  None => panic!("Monero network byte constants conflicted"),
348};
349
350/// A Monero address.
351#[derive(Clone, Copy, PartialEq, Eq, Zeroize)]
352pub struct Address<const ADDRESS_BYTES: u128> {
353  network: Network,
354  kind: AddressType,
355  spend: Point,
356  view: Point,
357}
358
359impl<const ADDRESS_BYTES: u128> fmt::Debug for Address<ADDRESS_BYTES> {
360  fn fmt(&self, fmt: &mut fmt::Formatter<'_>) -> Result<(), fmt::Error> {
361    let hex = |bytes: &[u8]| -> Result<String, fmt::Error> {
362      let mut res = String::with_capacity(2 + (2 * bytes.len()));
363      res.push_str("0x");
364      for b in bytes {
365        write!(&mut res, "{b:02x}")?;
366      }
367      Ok(res)
368    };
369
370    fmt
371      .debug_struct("Address")
372      .field("network", &self.network)
373      .field("kind", &self.kind)
374      .field("spend", &hex(&self.spend.compress().to_bytes())?)
375      .field("view", &hex(&self.view.compress().to_bytes())?)
376      // This is not a real field yet is the most valuable thing to know when debugging
377      .field("(address)", &self.to_string())
378      .finish()
379  }
380}
381
382impl<const ADDRESS_BYTES: u128> fmt::Display for Address<ADDRESS_BYTES> {
383  fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
384    let address_bytes: NetworkedAddressBytes =
385      NetworkedAddressBytes::from_const_generic(ADDRESS_BYTES);
386
387    let mut data = vec![address_bytes.byte(self.network, self.kind)];
388    data.extend(self.spend.compress().to_bytes());
389    data.extend(self.view.compress().to_bytes());
390    if let AddressType::Featured { subaddress, payment_id, guaranteed } = self.kind {
391      let features_uint =
392        (u8::from(guaranteed) << 2) + (u8::from(payment_id.is_some()) << 1) + u8::from(subaddress);
393      VarInt::write(&features_uint, &mut data)
394        .expect("write failed but <Vec as io::Write> doesn't fail");
395    }
396    if let Some(id) = self.kind.payment_id() {
397      data.extend(id);
398    }
399    write!(f, "{}", encode_check(data))
400  }
401}
402
403impl<const ADDRESS_BYTES: u128> Address<ADDRESS_BYTES> {
404  /// The upper bound on an address's data, when represented as bytes without a checksum.
405  const BASE_256_UPPER_BOUND: UpperBound<usize> =
406    UpperBound(<u64 as VarInt>::UPPER_BOUND + 32 + 32 + <u64 as VarInt>::UPPER_BOUND + 8);
407  /// The upper bound on an address's data, when represented as bytes with a checksum.
408  const CHECKSUMMED_UPPER_BOUND: UpperBound<usize> = UpperBound(Self::BASE_256_UPPER_BOUND.0 + 4);
409  /// The maximum size of an encoded address.
410  // This alleges each 8-bit byte will be encoded into 5-bit chunks, when in reality Base 58 is
411  // ~5.85 bits.
412  pub const SIZE_UPPER_BOUND: UpperBound<usize> =
413    UpperBound((Self::CHECKSUMMED_UPPER_BOUND.0 * 8).div_ceil(5));
414
415  /// Create a new address.
416  pub fn new(network: Network, kind: AddressType, spend: Point, view: Point) -> Self {
417    Address { network, kind, spend, view }
418  }
419
420  /// Parse an address from a String, accepting any network it is.
421  pub fn from_str_with_unchecked_network(s: &str) -> Result<Self, AddressError> {
422    let raw = decode_check(s).ok_or(AddressError::InvalidEncoding)?;
423    let mut raw = raw.as_slice();
424
425    let address_bytes: NetworkedAddressBytes =
426      NetworkedAddressBytes::from_const_generic(ADDRESS_BYTES);
427    let (network, mut kind) = address_bytes
428      .metadata_from_byte(read_byte(&mut raw).map_err(|_| AddressError::InvalidLength)?)?;
429    let spend = CompressedPoint::read(&mut raw)
430      .ok()
431      .as_ref()
432      .and_then(CompressedPoint::decompress)
433      .ok_or(AddressError::InvalidKey)?;
434    let view = CompressedPoint::read(&mut raw)
435      .ok()
436      .as_ref()
437      .and_then(CompressedPoint::decompress)
438      .ok_or(AddressError::InvalidKey)?;
439
440    if matches!(kind, AddressType::Featured { .. }) {
441      let features = <u64 as VarInt>::read(&mut raw).map_err(|_| AddressError::InvalidLength)?;
442      if (features >> 3) != 0 {
443        Err(AddressError::UnknownFeatures(features))?;
444      }
445
446      let subaddress = (features & 1) == 1;
447      let integrated = ((features >> 1) & 1) == 1;
448      let guaranteed = ((features >> 2) & 1) == 1;
449
450      kind =
451        AddressType::Featured { subaddress, payment_id: integrated.then_some([0; 8]), guaranteed };
452    }
453
454    // Read the payment ID, if there should be one
455    match kind {
456      AddressType::LegacyIntegrated(ref mut id) |
457      AddressType::Featured { payment_id: Some(ref mut id), .. } => {
458        *id = read_bytes(&mut raw).map_err(|_| AddressError::InvalidLength)?;
459      }
460      AddressType::Legacy |
461      AddressType::Subaddress |
462      AddressType::Featured { payment_id: None, .. } => {}
463    }
464
465    if !raw.is_empty() {
466      Err(AddressError::InvalidLength)?;
467    }
468
469    Ok(Address { network, kind, spend, view })
470  }
471
472  /// Create a new address from a `&str`.
473  ///
474  /// This takes in an argument for the expected network, erroring if a distinct network was used.
475  /// It also errors if the address is invalid (as expected).
476  pub fn from_str(network: Network, s: &str) -> Result<Self, AddressError> {
477    Self::from_str_with_unchecked_network(s).and_then(|addr| {
478      if addr.network == network {
479        Ok(addr)
480      } else {
481        Err(AddressError::DifferentNetwork { actual: addr.network, expected: network })?
482      }
483    })
484  }
485
486  /// Produce this address with the specified `payment_id` embedded.
487  ///
488  /// This will convert a standard address to an integrated address. For integrated/guaranteed
489  /// addresses, this will _replace_ any existing `payment_id`.
490  ///
491  /// If the address type cannot have a `payment_id` embedded (e.g. `AddressType::Subaddress`), an
492  /// error will be returned..
493  pub fn with_payment_id(&self, payment_id: [u8; 8]) -> Result<Self, AddressError> {
494    match self.kind {
495      AddressType::Legacy | AddressType::LegacyIntegrated(_) => Ok(Self::new(
496        self.network,
497        AddressType::LegacyIntegrated(payment_id),
498        self.spend,
499        self.view,
500      )),
501      AddressType::Subaddress => Err(AddressError::PaymentIDsUnsupported),
502      AddressType::Featured { subaddress, payment_id: _, guaranteed } => Ok(Self::new(
503        self.network,
504        AddressType::Featured { subaddress, payment_id: Some(payment_id), guaranteed },
505        self.spend,
506        self.view,
507      )),
508    }
509  }
510
511  /// The network this address is intended for use on.
512  pub fn network(&self) -> Network {
513    self.network
514  }
515
516  /// The type of address this is.
517  pub fn kind(&self) -> &AddressType {
518    &self.kind
519  }
520
521  /// If this is a subaddress.
522  pub fn is_subaddress(&self) -> bool {
523    self.kind.is_subaddress()
524  }
525
526  /// The payment ID for this address.
527  pub fn payment_id(&self) -> Option<[u8; 8]> {
528    self.kind.payment_id()
529  }
530
531  /// If this address is guaranteed.
532  ///
533  /// A guaranteed address is one where any outputs scanned to it are guaranteed to be spendable
534  /// under the hardness of various cryptographic problems (which are assumed hard). This is via
535  /// a modified shared-key derivation which eliminates the burning bug.
536  pub fn is_guaranteed(&self) -> bool {
537    self.kind.is_guaranteed()
538  }
539
540  /// The public spend key for this address.
541  pub fn spend(&self) -> Point {
542    self.spend
543  }
544
545  /// The public view key for this address.
546  pub fn view(&self) -> Point {
547    self.view
548  }
549}
550
551/// Instantiation of the Address type with Monero's network bytes.
552pub type MoneroAddress = Address<{ MONERO_BYTES.to_const_generic() }>;