monero_address/
lib.rs

1#![cfg_attr(docsrs, feature(doc_auto_cfg))]
2#![doc = include_str!("../README.md")]
3#![deny(missing_docs)]
4#![cfg_attr(not(feature = "std"), no_std)]
5
6use core::fmt::{self, Write};
7use std_shims::{
8  vec,
9  string::{String, ToString},
10};
11
12use zeroize::Zeroize;
13
14use curve25519_dalek::EdwardsPoint;
15
16use monero_io::*;
17
18mod base58check;
19use base58check::{encode_check, decode_check};
20
21#[cfg(test)]
22mod tests;
23
24/// The address type.
25///
26/// The officially specified addresses are supported, along with
27/// [Featured Addresses](https://gist.github.com/kayabaNerve/01c50bbc35441e0bbdcee63a9d823789).
28#[derive(Clone, Copy, PartialEq, Eq, Debug, Zeroize)]
29pub enum AddressType {
30  /// A legacy address type.
31  Legacy,
32  /// A legacy address with a payment ID embedded.
33  LegacyIntegrated([u8; 8]),
34  /// A subaddress.
35  ///
36  /// This is what SHOULD be used if specific functionality isn't needed.
37  Subaddress,
38  /// A featured address.
39  ///
40  /// Featured Addresses are an unofficial address specification which is meant to be extensible
41  /// and support a variety of functionality. This functionality includes being a subaddresses AND
42  /// having a payment ID, along with being immune to the burning bug.
43  ///
44  /// At this time, support for featured addresses is limited to this crate. There should be no
45  /// expectation of interoperability.
46  Featured {
47    /// If this address is a subaddress.
48    subaddress: bool,
49    /// The payment ID associated with this address.
50    payment_id: Option<[u8; 8]>,
51    /// If this address is guaranteed.
52    ///
53    /// A guaranteed address is one where any outputs scanned to it are guaranteed to be spendable
54    /// under the hardness of various cryptographic problems (which are assumed hard). This is via
55    /// a modified shared-key derivation which eliminates the burning bug.
56    guaranteed: bool,
57  },
58}
59
60impl AddressType {
61  /// If this address is a subaddress.
62  pub fn is_subaddress(&self) -> bool {
63    matches!(self, AddressType::Subaddress) ||
64      matches!(self, AddressType::Featured { subaddress: true, .. })
65  }
66
67  /// The payment ID within this address.
68  pub fn payment_id(&self) -> Option<[u8; 8]> {
69    if let AddressType::LegacyIntegrated(id) = self {
70      Some(*id)
71    } else if let AddressType::Featured { payment_id, .. } = self {
72      *payment_id
73    } else {
74      None
75    }
76  }
77
78  /// If this address is guaranteed.
79  ///
80  /// A guaranteed address is one where any outputs scanned to it are guaranteed to be spendable
81  /// under the hardness of various cryptographic problems (which are assumed hard). This is via
82  /// a modified shared-key derivation which eliminates the burning bug.
83  pub fn is_guaranteed(&self) -> bool {
84    matches!(self, AddressType::Featured { guaranteed: true, .. })
85  }
86}
87
88/// A subaddress index.
89///
90/// Subaddresses are derived from a root using a `(account, address)` tuple as an index.
91#[derive(Clone, Copy, PartialEq, Eq, Debug, Zeroize)]
92pub struct SubaddressIndex {
93  account: u32,
94  address: u32,
95}
96
97impl SubaddressIndex {
98  /// Create a new SubaddressIndex.
99  pub const fn new(account: u32, address: u32) -> Option<SubaddressIndex> {
100    if (account == 0) && (address == 0) {
101      return None;
102    }
103    Some(SubaddressIndex { account, address })
104  }
105
106  /// Get the account this subaddress index is under.
107  pub const fn account(&self) -> u32 {
108    self.account
109  }
110
111  /// Get the address this subaddress index is for, within its account.
112  pub const fn address(&self) -> u32 {
113    self.address
114  }
115}
116
117/// Bytes used as prefixes when encoding addresses.
118///
119/// These distinguish the address's type.
120#[derive(Clone, Copy, PartialEq, Eq, Debug, Zeroize)]
121pub struct AddressBytes {
122  legacy: u8,
123  legacy_integrated: u8,
124  subaddress: u8,
125  featured: u8,
126}
127
128impl AddressBytes {
129  /// Create a new set of address bytes, one for each address type.
130  pub const fn new(
131    legacy: u8,
132    legacy_integrated: u8,
133    subaddress: u8,
134    featured: u8,
135  ) -> Option<Self> {
136    if (legacy == legacy_integrated) || (legacy == subaddress) || (legacy == featured) {
137      return None;
138    }
139    if (legacy_integrated == subaddress) || (legacy_integrated == featured) {
140      return None;
141    }
142    if subaddress == featured {
143      return None;
144    }
145    Some(AddressBytes { legacy, legacy_integrated, subaddress, featured })
146  }
147
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  #[allow(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)]
203#[cfg_attr(feature = "std", derive(thiserror::Error))]
204pub enum AddressError {
205  /// The address had an invalid (network, type) byte.
206  #[cfg_attr(feature = "std", error("invalid byte for the address's network/type ({0})"))]
207  InvalidTypeByte(u8),
208  /// The address wasn't a valid Base58Check (as defined by Monero) string.
209  #[cfg_attr(feature = "std", error("invalid address encoding"))]
210  InvalidEncoding,
211  /// The data encoded wasn't the proper length.
212  #[cfg_attr(feature = "std", error("invalid length"))]
213  InvalidLength,
214  /// The address had an invalid key.
215  #[cfg_attr(feature = "std", error("invalid key"))]
216  InvalidKey,
217  /// The address was featured with unrecognized features.
218  #[cfg_attr(feature = "std", error("unknown features"))]
219  UnknownFeatures(u64),
220  /// The network was for a different network than expected.
221  #[cfg_attr(
222    feature = "std",
223    error("different network ({actual:?}) than expected ({expected:?})")
224  )]
225  DifferentNetwork {
226    /// The Network expected.
227    expected: Network,
228    /// The Network embedded within the Address.
229    actual: Network,
230  },
231}
232
233/// Bytes used as prefixes when encoding addresses, variable to the network instance.
234///
235/// These distinguish the address's network and type.
236#[derive(Clone, Copy, PartialEq, Eq, Debug, Zeroize)]
237pub struct NetworkedAddressBytes {
238  mainnet: AddressBytes,
239  stagenet: AddressBytes,
240  testnet: AddressBytes,
241}
242
243impl NetworkedAddressBytes {
244  /// Create a new set of address bytes, one for each network.
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  pub const fn to_const_generic(self) -> u128 {
281    ((self.mainnet.to_const_generic() as u128) << 96) +
282      ((self.stagenet.to_const_generic() as u128) << 64) +
283      ((self.testnet.to_const_generic() as u128) << 32)
284  }
285
286  #[allow(clippy::cast_possible_truncation)]
287  const fn from_const_generic(const_generic: u128) -> Self {
288    let mainnet = AddressBytes::from_const_generic((const_generic >> 96) as u32);
289    let stagenet =
290      AddressBytes::from_const_generic(((const_generic >> 64) & (u32::MAX as u128)) as u32);
291    let testnet =
292      AddressBytes::from_const_generic(((const_generic >> 32) & (u32::MAX as u128)) as u32);
293
294    NetworkedAddressBytes { mainnet, stagenet, testnet }
295  }
296
297  fn network(&self, network: Network) -> &AddressBytes {
298    match network {
299      Network::Mainnet => &self.mainnet,
300      Network::Stagenet => &self.stagenet,
301      Network::Testnet => &self.testnet,
302    }
303  }
304
305  fn byte(&self, network: Network, kind: AddressType) -> u8 {
306    let address_bytes = self.network(network);
307
308    match kind {
309      AddressType::Legacy => address_bytes.legacy,
310      AddressType::LegacyIntegrated(_) => address_bytes.legacy_integrated,
311      AddressType::Subaddress => address_bytes.subaddress,
312      AddressType::Featured { .. } => address_bytes.featured,
313    }
314  }
315
316  // This will return an incomplete AddressType for LegacyIntegrated/Featured.
317  fn metadata_from_byte(&self, byte: u8) -> Result<(Network, AddressType), AddressError> {
318    let mut meta = None;
319    for network in [Network::Mainnet, Network::Testnet, Network::Stagenet] {
320      let address_bytes = self.network(network);
321      if let Some(kind) = match byte {
322        _ if byte == address_bytes.legacy => Some(AddressType::Legacy),
323        _ if byte == address_bytes.legacy_integrated => Some(AddressType::LegacyIntegrated([0; 8])),
324        _ if byte == address_bytes.subaddress => Some(AddressType::Subaddress),
325        _ if byte == address_bytes.featured => {
326          Some(AddressType::Featured { subaddress: false, payment_id: None, guaranteed: false })
327        }
328        _ => None,
329      } {
330        meta = Some((network, kind));
331        break;
332      }
333    }
334
335    meta.ok_or(AddressError::InvalidTypeByte(byte))
336  }
337}
338
339/// The bytes used for distinguishing Monero addresses.
340pub const MONERO_BYTES: NetworkedAddressBytes = match NetworkedAddressBytes::new(
341  MONERO_MAINNET_BYTES,
342  MONERO_STAGENET_BYTES,
343  MONERO_TESTNET_BYTES,
344) {
345  Some(bytes) => bytes,
346  None => panic!("Monero network byte constants conflicted"),
347};
348
349/// A Monero address.
350#[derive(Clone, Copy, PartialEq, Eq, Zeroize)]
351pub struct Address<const ADDRESS_BYTES: u128> {
352  network: Network,
353  kind: AddressType,
354  spend: EdwardsPoint,
355  view: EdwardsPoint,
356}
357
358impl<const ADDRESS_BYTES: u128> fmt::Debug for Address<ADDRESS_BYTES> {
359  fn fmt(&self, fmt: &mut fmt::Formatter<'_>) -> Result<(), fmt::Error> {
360    let hex = |bytes: &[u8]| -> String {
361      let mut res = String::with_capacity(2 + (2 * bytes.len()));
362      res.push_str("0x");
363      for b in bytes {
364        write!(&mut res, "{b:02x}").unwrap();
365      }
366      res
367    };
368
369    fmt
370      .debug_struct("Address")
371      .field("network", &self.network)
372      .field("kind", &self.kind)
373      .field("spend", &hex(&self.spend.compress().to_bytes()))
374      .field("view", &hex(&self.view.compress().to_bytes()))
375      // This is not a real field yet is the most valuable thing to know when debugging
376      .field("(address)", &self.to_string())
377      .finish()
378  }
379}
380
381impl<const ADDRESS_BYTES: u128> fmt::Display for Address<ADDRESS_BYTES> {
382  fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
383    let address_bytes: NetworkedAddressBytes =
384      NetworkedAddressBytes::from_const_generic(ADDRESS_BYTES);
385
386    let mut data = vec![address_bytes.byte(self.network, self.kind)];
387    data.extend(self.spend.compress().to_bytes());
388    data.extend(self.view.compress().to_bytes());
389    if let AddressType::Featured { subaddress, payment_id, guaranteed } = self.kind {
390      let features_uint =
391        (u8::from(guaranteed) << 2) + (u8::from(payment_id.is_some()) << 1) + u8::from(subaddress);
392      write_varint(&features_uint, &mut data).unwrap();
393    }
394    if let Some(id) = self.kind.payment_id() {
395      data.extend(id);
396    }
397    write!(f, "{}", encode_check(data))
398  }
399}
400
401impl<const ADDRESS_BYTES: u128> Address<ADDRESS_BYTES> {
402  /// Create a new address.
403  pub fn new(network: Network, kind: AddressType, spend: EdwardsPoint, view: EdwardsPoint) -> Self {
404    Address { network, kind, spend, view }
405  }
406
407  /// Parse an address from a String, accepting any network it is.
408  pub fn from_str_with_unchecked_network(s: &str) -> Result<Self, AddressError> {
409    let raw = decode_check(s).ok_or(AddressError::InvalidEncoding)?;
410    let mut raw = raw.as_slice();
411
412    let address_bytes: NetworkedAddressBytes =
413      NetworkedAddressBytes::from_const_generic(ADDRESS_BYTES);
414    let (network, mut kind) = address_bytes
415      .metadata_from_byte(read_byte(&mut raw).map_err(|_| AddressError::InvalidLength)?)?;
416    let spend = read_point(&mut raw).map_err(|_| AddressError::InvalidKey)?;
417    let view = read_point(&mut raw).map_err(|_| AddressError::InvalidKey)?;
418
419    if matches!(kind, AddressType::Featured { .. }) {
420      let features = read_varint::<_, u64>(&mut raw).map_err(|_| AddressError::InvalidLength)?;
421      if (features >> 3) != 0 {
422        Err(AddressError::UnknownFeatures(features))?;
423      }
424
425      let subaddress = (features & 1) == 1;
426      let integrated = ((features >> 1) & 1) == 1;
427      let guaranteed = ((features >> 2) & 1) == 1;
428
429      kind =
430        AddressType::Featured { subaddress, payment_id: integrated.then_some([0; 8]), guaranteed };
431    }
432
433    // Read the payment ID, if there should be one
434    match kind {
435      AddressType::LegacyIntegrated(ref mut id) |
436      AddressType::Featured { payment_id: Some(ref mut id), .. } => {
437        *id = read_bytes(&mut raw).map_err(|_| AddressError::InvalidLength)?;
438      }
439      _ => {}
440    };
441
442    if !raw.is_empty() {
443      Err(AddressError::InvalidLength)?;
444    }
445
446    Ok(Address { network, kind, spend, view })
447  }
448
449  /// Create a new address from a `&str`.
450  ///
451  /// This takes in an argument for the expected network, erroring if a distinct network was used.
452  /// It also errors if the address is invalid (as expected).
453  pub fn from_str(network: Network, s: &str) -> Result<Self, AddressError> {
454    Self::from_str_with_unchecked_network(s).and_then(|addr| {
455      if addr.network == network {
456        Ok(addr)
457      } else {
458        Err(AddressError::DifferentNetwork { actual: addr.network, expected: network })?
459      }
460    })
461  }
462
463  /// The network this address is intended for use on.
464  pub fn network(&self) -> Network {
465    self.network
466  }
467
468  /// The type of address this is.
469  pub fn kind(&self) -> &AddressType {
470    &self.kind
471  }
472
473  /// If this is a subaddress.
474  pub fn is_subaddress(&self) -> bool {
475    self.kind.is_subaddress()
476  }
477
478  /// The payment ID for this address.
479  pub fn payment_id(&self) -> Option<[u8; 8]> {
480    self.kind.payment_id()
481  }
482
483  /// If this address is guaranteed.
484  ///
485  /// A guaranteed address is one where any outputs scanned to it are guaranteed to be spendable
486  /// under the hardness of various cryptographic problems (which are assumed hard). This is via
487  /// a modified shared-key derivation which eliminates the burning bug.
488  pub fn is_guaranteed(&self) -> bool {
489    self.kind.is_guaranteed()
490  }
491
492  /// The public spend key for this address.
493  pub fn spend(&self) -> EdwardsPoint {
494    self.spend
495  }
496
497  /// The public view key for this address.
498  pub fn view(&self) -> EdwardsPoint {
499    self.view
500  }
501}
502
503/// Instantiation of the Address type with Monero's network bytes.
504pub type MoneroAddress = Address<{ MONERO_BYTES.to_const_generic() }>;