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(test), no_std)]
5
6use core::fmt::{self, Write};
7extern crate alloc;
8use alloc::{
9  vec,
10  string::{String, ToString},
11};
12
13use zeroize::Zeroize;
14
15use curve25519_dalek::EdwardsPoint;
16
17use monero_io::*;
18
19use monero_base58::{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, 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}
228
229/// Bytes used as prefixes when encoding addresses, variable to the network instance.
230///
231/// These distinguish the address's network and type.
232#[derive(Clone, Copy, PartialEq, Eq, Debug, Zeroize)]
233pub struct NetworkedAddressBytes {
234  mainnet: AddressBytes,
235  stagenet: AddressBytes,
236  testnet: AddressBytes,
237}
238
239impl NetworkedAddressBytes {
240  /// Create a new set of address bytes, one for each network.
241  pub const fn new(
242    mainnet: AddressBytes,
243    stagenet: AddressBytes,
244    testnet: AddressBytes,
245  ) -> Option<Self> {
246    let res = NetworkedAddressBytes { mainnet, stagenet, testnet };
247    let all_bytes = res.to_const_generic();
248
249    let mut i = 0;
250    while i < 12 {
251      let this_byte = (all_bytes >> (32 + (i * 8))) & (u8::MAX as u128);
252
253      let mut j = 0;
254      while j < 12 {
255        if i == j {
256          j += 1;
257          continue;
258        }
259        let other_byte = (all_bytes >> (32 + (j * 8))) & (u8::MAX as u128);
260        if this_byte == other_byte {
261          return None;
262        }
263
264        j += 1;
265      }
266
267      i += 1;
268    }
269
270    Some(res)
271  }
272
273  /// Convert this set of address bytes to its representation as a u128.
274  ///
275  /// We cannot use this struct directly as a const generic unfortunately.
276  pub const fn to_const_generic(self) -> u128 {
277    ((self.mainnet.to_const_generic() as u128) << 96) +
278      ((self.stagenet.to_const_generic() as u128) << 64) +
279      ((self.testnet.to_const_generic() as u128) << 32)
280  }
281
282  #[allow(clippy::cast_possible_truncation)]
283  const fn from_const_generic(const_generic: u128) -> Self {
284    let mainnet = AddressBytes::from_const_generic((const_generic >> 96) as u32);
285    let stagenet =
286      AddressBytes::from_const_generic(((const_generic >> 64) & (u32::MAX as u128)) as u32);
287    let testnet =
288      AddressBytes::from_const_generic(((const_generic >> 32) & (u32::MAX as u128)) as u32);
289
290    NetworkedAddressBytes { mainnet, stagenet, testnet }
291  }
292
293  fn network(&self, network: Network) -> &AddressBytes {
294    match network {
295      Network::Mainnet => &self.mainnet,
296      Network::Stagenet => &self.stagenet,
297      Network::Testnet => &self.testnet,
298    }
299  }
300
301  fn byte(&self, network: Network, kind: AddressType) -> u8 {
302    let address_bytes = self.network(network);
303
304    match kind {
305      AddressType::Legacy => address_bytes.legacy,
306      AddressType::LegacyIntegrated(_) => address_bytes.legacy_integrated,
307      AddressType::Subaddress => address_bytes.subaddress,
308      AddressType::Featured { .. } => address_bytes.featured,
309    }
310  }
311
312  // This will return an incomplete AddressType for LegacyIntegrated/Featured.
313  fn metadata_from_byte(&self, byte: u8) -> Result<(Network, AddressType), AddressError> {
314    let mut meta = None;
315    for network in [Network::Mainnet, Network::Testnet, Network::Stagenet] {
316      let address_bytes = self.network(network);
317      if let Some(kind) = match byte {
318        _ if byte == address_bytes.legacy => Some(AddressType::Legacy),
319        _ if byte == address_bytes.legacy_integrated => Some(AddressType::LegacyIntegrated([0; 8])),
320        _ if byte == address_bytes.subaddress => Some(AddressType::Subaddress),
321        _ if byte == address_bytes.featured => {
322          Some(AddressType::Featured { subaddress: false, payment_id: None, guaranteed: false })
323        }
324        _ => None,
325      } {
326        meta = Some((network, kind));
327        break;
328      }
329    }
330
331    meta.ok_or(AddressError::InvalidTypeByte(byte))
332  }
333}
334
335/// The bytes used for distinguishing Monero addresses.
336pub const MONERO_BYTES: NetworkedAddressBytes = match NetworkedAddressBytes::new(
337  MONERO_MAINNET_BYTES,
338  MONERO_STAGENET_BYTES,
339  MONERO_TESTNET_BYTES,
340) {
341  Some(bytes) => bytes,
342  None => panic!("Monero network byte constants conflicted"),
343};
344
345/// A Monero address.
346#[derive(Clone, Copy, PartialEq, Eq, Zeroize)]
347pub struct Address<const ADDRESS_BYTES: u128> {
348  network: Network,
349  kind: AddressType,
350  spend: EdwardsPoint,
351  view: EdwardsPoint,
352}
353
354impl<const ADDRESS_BYTES: u128> fmt::Debug for Address<ADDRESS_BYTES> {
355  fn fmt(&self, fmt: &mut fmt::Formatter<'_>) -> Result<(), fmt::Error> {
356    let hex = |bytes: &[u8]| -> Result<String, fmt::Error> {
357      let mut res = String::with_capacity(2 + (2 * bytes.len()));
358      res.push_str("0x");
359      for b in bytes {
360        write!(&mut res, "{b:02x}")?;
361      }
362      Ok(res)
363    };
364
365    fmt
366      .debug_struct("Address")
367      .field("network", &self.network)
368      .field("kind", &self.kind)
369      .field("spend", &hex(&self.spend.compress().to_bytes())?)
370      .field("view", &hex(&self.view.compress().to_bytes())?)
371      // This is not a real field yet is the most valuable thing to know when debugging
372      .field("(address)", &self.to_string())
373      .finish()
374  }
375}
376
377impl<const ADDRESS_BYTES: u128> fmt::Display for Address<ADDRESS_BYTES> {
378  fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
379    let address_bytes: NetworkedAddressBytes =
380      NetworkedAddressBytes::from_const_generic(ADDRESS_BYTES);
381
382    let mut data = vec![address_bytes.byte(self.network, self.kind)];
383    data.extend(self.spend.compress().to_bytes());
384    data.extend(self.view.compress().to_bytes());
385    if let AddressType::Featured { subaddress, payment_id, guaranteed } = self.kind {
386      let features_uint =
387        (u8::from(guaranteed) << 2) + (u8::from(payment_id.is_some()) << 1) + u8::from(subaddress);
388      write_varint(&features_uint, &mut data)
389        .expect("write failed but <Vec as io::Write> doesn't fail");
390    }
391    if let Some(id) = self.kind.payment_id() {
392      data.extend(id);
393    }
394    write!(f, "{}", encode_check(data))
395  }
396}
397
398impl<const ADDRESS_BYTES: u128> Address<ADDRESS_BYTES> {
399  /// Create a new address.
400  pub fn new(network: Network, kind: AddressType, spend: EdwardsPoint, view: EdwardsPoint) -> Self {
401    Address { network, kind, spend, view }
402  }
403
404  /// Parse an address from a String, accepting any network it is.
405  pub fn from_str_with_unchecked_network(s: &str) -> Result<Self, AddressError> {
406    let raw = decode_check(s).ok_or(AddressError::InvalidEncoding)?;
407    let mut raw = raw.as_slice();
408
409    let address_bytes: NetworkedAddressBytes =
410      NetworkedAddressBytes::from_const_generic(ADDRESS_BYTES);
411    let (network, mut kind) = address_bytes
412      .metadata_from_byte(read_byte(&mut raw).map_err(|_| AddressError::InvalidLength)?)?;
413    let spend = read_point(&mut raw).map_err(|_| AddressError::InvalidKey)?;
414    let view = read_point(&mut raw).map_err(|_| AddressError::InvalidKey)?;
415
416    if matches!(kind, AddressType::Featured { .. }) {
417      let features = read_varint::<_, u64>(&mut raw).map_err(|_| AddressError::InvalidLength)?;
418      if (features >> 3) != 0 {
419        Err(AddressError::UnknownFeatures(features))?;
420      }
421
422      let subaddress = (features & 1) == 1;
423      let integrated = ((features >> 1) & 1) == 1;
424      let guaranteed = ((features >> 2) & 1) == 1;
425
426      kind =
427        AddressType::Featured { subaddress, payment_id: integrated.then_some([0; 8]), guaranteed };
428    }
429
430    // Read the payment ID, if there should be one
431    match kind {
432      AddressType::LegacyIntegrated(ref mut id) |
433      AddressType::Featured { payment_id: Some(ref mut id), .. } => {
434        *id = read_bytes(&mut raw).map_err(|_| AddressError::InvalidLength)?;
435      }
436      _ => {}
437    };
438
439    if !raw.is_empty() {
440      Err(AddressError::InvalidLength)?;
441    }
442
443    Ok(Address { network, kind, spend, view })
444  }
445
446  /// Create a new address from a `&str`.
447  ///
448  /// This takes in an argument for the expected network, erroring if a distinct network was used.
449  /// It also errors if the address is invalid (as expected).
450  pub fn from_str(network: Network, s: &str) -> Result<Self, AddressError> {
451    Self::from_str_with_unchecked_network(s).and_then(|addr| {
452      if addr.network == network {
453        Ok(addr)
454      } else {
455        Err(AddressError::DifferentNetwork { actual: addr.network, expected: network })?
456      }
457    })
458  }
459
460  /// The network this address is intended for use on.
461  pub fn network(&self) -> Network {
462    self.network
463  }
464
465  /// The type of address this is.
466  pub fn kind(&self) -> &AddressType {
467    &self.kind
468  }
469
470  /// If this is a subaddress.
471  pub fn is_subaddress(&self) -> bool {
472    self.kind.is_subaddress()
473  }
474
475  /// The payment ID for this address.
476  pub fn payment_id(&self) -> Option<[u8; 8]> {
477    self.kind.payment_id()
478  }
479
480  /// If this address is guaranteed.
481  ///
482  /// A guaranteed address is one where any outputs scanned to it are guaranteed to be spendable
483  /// under the hardness of various cryptographic problems (which are assumed hard). This is via
484  /// a modified shared-key derivation which eliminates the burning bug.
485  pub fn is_guaranteed(&self) -> bool {
486    self.kind.is_guaranteed()
487  }
488
489  /// The public spend key for this address.
490  pub fn spend(&self) -> EdwardsPoint {
491    self.spend
492  }
493
494  /// The public view key for this address.
495  pub fn view(&self) -> EdwardsPoint {
496    self.view
497  }
498}
499
500/// Instantiation of the Address type with Monero's network bytes.
501pub type MoneroAddress = Address<{ MONERO_BYTES.to_const_generic() }>;