mirror of
https://github.com/serai-dex/serai.git
synced 2025-01-15 15:24:52 +00:00
503 lines
16 KiB
Rust
503 lines
16 KiB
Rust
|
#![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.
|
||
|
// TODO: wallet-core PaymentId? TX extra crate imported here?
|
||
|
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<SubaddressIndex> {
|
||
|
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<Self> {
|
||
|
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(24, 25, 36, 86) {
|
||
|
Some(bytes) => bytes,
|
||
|
None => panic!("stagenet byte constants conflicted"),
|
||
|
};
|
||
|
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<Self> {
|
||
|
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<const ADDRESS_BYTES: u128> {
|
||
|
network: Network,
|
||
|
kind: AddressType,
|
||
|
spend: EdwardsPoint,
|
||
|
view: EdwardsPoint,
|
||
|
}
|
||
|
|
||
|
impl<const ADDRESS_BYTES: u128> fmt::Debug for Address<ADDRESS_BYTES> {
|
||
|
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<const ADDRESS_BYTES: u128> fmt::Display for Address<ADDRESS_BYTES> {
|
||
|
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<const ADDRESS_BYTES: u128> Address<ADDRESS_BYTES> {
|
||
|
/// 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<Self, AddressError> {
|
||
|
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, AddressError> {
|
||
|
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() }>;
|