Rename the coins folder to networks (#583)

* Rename the coins folder to networks

Ethereum isn't a coin. It's a network.

Resolves #357.

* More renames of coins -> networks in orchestration

* Correct paths in tests/

* cargo fmt
This commit is contained in:
Luke Parker
2024-07-18 12:16:45 -07:00
committed by GitHub
parent 40cc180853
commit 7d2d739042
244 changed files with 102 additions and 99 deletions

View File

@@ -0,0 +1,49 @@
[package]
name = "monero-address"
version = "0.1.0"
description = "Rust implementation of Monero addresses"
license = "MIT"
repository = "https://github.com/serai-dex/serai/tree/develop/networks/monero/wallet/address"
authors = ["Luke Parker <lukeparker5132@gmail.com>"]
edition = "2021"
rust-version = "1.79"
[package.metadata.docs.rs]
all-features = true
rustdoc-args = ["--cfg", "docsrs"]
[lints]
workspace = true
[dependencies]
std-shims = { path = "../../../../common/std-shims", version = "^0.1.1", default-features = false }
thiserror = { version = "1", default-features = false, optional = true }
zeroize = { version = "^1.5", default-features = false, features = ["zeroize_derive"] }
curve25519-dalek = { version = "4", default-features = false, features = ["alloc", "zeroize"] }
monero-io = { path = "../../io", default-features = false }
monero-primitives = { path = "../../primitives", default-features = false }
[dev-dependencies]
rand_core = { version = "0.6", default-features = false, features = ["std"] }
hex-literal = { version = "0.4", default-features = false }
hex = { version = "0.4", default-features = false, features = ["alloc"] }
serde = { version = "1", default-features = false, features = ["derive", "alloc"] }
serde_json = { version = "1", default-features = false, features = ["alloc"] }
[features]
std = [
"std-shims/std",
"thiserror",
"zeroize/std",
"monero-io/std",
]
default = ["std"]

View File

@@ -0,0 +1,21 @@
MIT License
Copyright (c) 2022-2024 Luke Parker
Permission is hereby granted, free of charge, to any person obtaining a copy
of this software and associated documentation files (the "Software"), to deal
in the Software without restriction, including without limitation the rights
to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
copies of the Software, and to permit persons to whom the Software is
furnished to do so, subject to the following conditions:
The above copyright notice and this permission notice shall be included in all
copies or substantial portions of the Software.
THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
SOFTWARE.

View File

@@ -0,0 +1,11 @@
# Monero Address
Rust implementation of Monero addresses.
This library is usable under no-std when the `std` feature (on by default) is
disabled.
### Cargo Features
- `std` (on by default): Enables `std` (and with it, more efficient internal
implementations).

View File

@@ -0,0 +1,106 @@
use std_shims::{vec::Vec, string::String};
use monero_primitives::keccak256;
const ALPHABET_LEN: u64 = 58;
const ALPHABET: &[u8] = b"123456789ABCDEFGHJKLMNPQRSTUVWXYZabcdefghijkmnopqrstuvwxyz";
pub(crate) const BLOCK_LEN: usize = 8;
const ENCODED_BLOCK_LEN: usize = 11;
const CHECKSUM_LEN: usize = 4;
// The maximum possible length of an encoding of this many bytes
//
// This is used for determining padding/how many bytes an encoding actually uses
pub(crate) fn encoded_len_for_bytes(bytes: usize) -> usize {
let bits = u64::try_from(bytes).expect("length exceeded 2**64") * 8;
let mut max = if bits == 64 { u64::MAX } else { (1 << bits) - 1 };
let mut i = 0;
while max != 0 {
max /= ALPHABET_LEN;
i += 1;
}
i
}
// Encode an arbitrary-length stream of data
pub(crate) fn encode(bytes: &[u8]) -> String {
let mut res = String::with_capacity(bytes.len().div_ceil(BLOCK_LEN) * ENCODED_BLOCK_LEN);
for chunk in bytes.chunks(BLOCK_LEN) {
// Convert to a u64
let mut fixed_len_chunk = [0; BLOCK_LEN];
fixed_len_chunk[(BLOCK_LEN - chunk.len()) ..].copy_from_slice(chunk);
let mut val = u64::from_be_bytes(fixed_len_chunk);
// Convert to the base58 encoding
let mut chunk_str = [char::from(ALPHABET[0]); ENCODED_BLOCK_LEN];
let mut i = 0;
while val > 0 {
chunk_str[i] = ALPHABET[usize::try_from(val % ALPHABET_LEN)
.expect("ALPHABET_LEN exceeds usize despite being a usize")]
.into();
i += 1;
val /= ALPHABET_LEN;
}
// Only take used bytes, and since we put the LSBs in the first byte, reverse the byte order
for c in chunk_str.into_iter().take(encoded_len_for_bytes(chunk.len())).rev() {
res.push(c);
}
}
res
}
// Decode an arbitrary-length stream of data
pub(crate) fn decode(data: &str) -> Option<Vec<u8>> {
let mut res = Vec::with_capacity((data.len() / ENCODED_BLOCK_LEN) * BLOCK_LEN);
for chunk in data.as_bytes().chunks(ENCODED_BLOCK_LEN) {
// Convert the chunk back to a u64
let mut sum = 0u64;
for this_char in chunk {
sum = sum.checked_mul(ALPHABET_LEN)?;
sum += u64::try_from(ALPHABET.iter().position(|a| a == this_char)?)
.expect("alphabet len exceeded 2**64");
}
// From the size of the encoding, determine the size of the bytes
let mut used_bytes = None;
for i in 1 ..= BLOCK_LEN {
if encoded_len_for_bytes(i) == chunk.len() {
used_bytes = Some(i);
break;
}
}
// Only push on the used bytes
res.extend(&sum.to_be_bytes()[(BLOCK_LEN - used_bytes.unwrap()) ..]);
}
Some(res)
}
// Encode an arbitrary-length stream of data, with a checksum
pub(crate) fn encode_check(mut data: Vec<u8>) -> String {
let checksum = keccak256(&data);
data.extend(&checksum[.. CHECKSUM_LEN]);
encode(&data)
}
// Decode an arbitrary-length stream of data, with a checksum
pub(crate) fn decode_check(data: &str) -> Option<Vec<u8>> {
if data.len() < CHECKSUM_LEN {
None?;
}
let mut res = decode(data)?;
let checksum_pos = res.len() - CHECKSUM_LEN;
if keccak256(&res[.. checksum_pos])[.. CHECKSUM_LEN] != res[checksum_pos ..] {
None?;
}
res.truncate(checksum_pos);
Some(res)
}

View File

@@ -0,0 +1,507 @@
#![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<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 }
}
}
// 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/cc73fe71162d564ffda8e549b79a350bca53c454
// /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/cc73fe71162d564ffda8e549b79a350bca53c454
// /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<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() }>;

View File

@@ -0,0 +1,205 @@
use hex_literal::hex;
use rand_core::{RngCore, OsRng};
use curve25519_dalek::{constants::ED25519_BASEPOINT_TABLE, scalar::Scalar};
use monero_io::decompress_point;
use crate::{Network, AddressType, MoneroAddress};
const SPEND: [u8; 32] = hex!("f8631661f6ab4e6fda310c797330d86e23a682f20d5bc8cc27b18051191f16d7");
const VIEW: [u8; 32] = hex!("4a1535063ad1fee2dabbf909d4fd9a873e29541b401f0944754e17c9a41820ce");
const STANDARD: &str =
"4B33mFPMq6mKi7Eiyd5XuyKRVMGVZz1Rqb9ZTyGApXW5d1aT7UBDZ89ewmnWFkzJ5wPd2SFbn313vCT8a4E2Qf4KQH4pNey";
const PAYMENT_ID: [u8; 8] = hex!("b8963a57855cf73f");
const INTEGRATED: &str =
"4Ljin4CrSNHKi7Eiyd5XuyKRVMGVZz1Rqb9ZTyGApXW5d1aT7UBDZ89ewmnWFkzJ5wPd2SFbn313vCT8a4E2Qf4KbaTH6Mn\
pXSn88oBX35";
const SUB_SPEND: [u8; 32] =
hex!("fe358188b528335ad1cfdc24a22a23988d742c882b6f19a602892eaab3c1b62b");
const SUB_VIEW: [u8; 32] = hex!("9bc2b464de90d058468522098d5610c5019c45fd1711a9517db1eea7794f5470");
const SUBADDRESS: &str =
"8C5zHM5ud8nGC4hC2ULiBLSWx9infi8JUUmWEat4fcTf8J4H38iWYVdFmPCA9UmfLTZxD43RsyKnGEdZkoGij6csDeUnbEB";
const FEATURED_JSON: &str = include_str!("vectors/featured_addresses.json");
#[test]
fn test_encoded_len_for_bytes() {
// For an encoding of length `l`, we prune to the amount of bytes which encodes with length `l`
// This assumes length `l` -> amount of bytes has a singular answer, which is tested here
use crate::base58check::*;
let mut set = std::collections::HashSet::new();
for i in 0 .. BLOCK_LEN {
set.insert(encoded_len_for_bytes(i));
}
assert_eq!(set.len(), BLOCK_LEN);
}
#[test]
fn base58check() {
use crate::base58check::*;
assert_eq!(encode(&[]), String::new());
assert!(decode("").unwrap().is_empty());
let full_block = &[1, 2, 3, 4, 5, 6, 7, 8];
assert_eq!(&decode(&encode(full_block)).unwrap(), full_block);
let partial_block = &[1, 2, 3];
assert_eq!(&decode(&encode(partial_block)).unwrap(), partial_block);
let max_encoded_block = &[u8::MAX; 8];
assert_eq!(&decode(&encode(max_encoded_block)).unwrap(), max_encoded_block);
let max_decoded_block = "zzzzzzzzzzz";
assert!(decode(max_decoded_block).is_none());
let full_and_partial_block = &[1, 2, 3, 4, 5, 6, 7, 8, 9, 10, 11];
assert_eq!(&decode(&encode(full_and_partial_block)).unwrap(), full_and_partial_block);
}
#[test]
fn standard_address() {
let addr = MoneroAddress::from_str(Network::Mainnet, STANDARD).unwrap();
assert_eq!(addr.network(), Network::Mainnet);
assert_eq!(addr.kind(), &AddressType::Legacy);
assert!(!addr.is_subaddress());
assert_eq!(addr.payment_id(), None);
assert!(!addr.is_guaranteed());
assert_eq!(addr.spend.compress().to_bytes(), SPEND);
assert_eq!(addr.view.compress().to_bytes(), VIEW);
assert_eq!(addr.to_string(), STANDARD);
}
#[test]
fn integrated_address() {
let addr = MoneroAddress::from_str(Network::Mainnet, INTEGRATED).unwrap();
assert_eq!(addr.network(), Network::Mainnet);
assert_eq!(addr.kind(), &AddressType::LegacyIntegrated(PAYMENT_ID));
assert!(!addr.is_subaddress());
assert_eq!(addr.payment_id(), Some(PAYMENT_ID));
assert!(!addr.is_guaranteed());
assert_eq!(addr.spend.compress().to_bytes(), SPEND);
assert_eq!(addr.view.compress().to_bytes(), VIEW);
assert_eq!(addr.to_string(), INTEGRATED);
}
#[test]
fn subaddress() {
let addr = MoneroAddress::from_str(Network::Mainnet, SUBADDRESS).unwrap();
assert_eq!(addr.network(), Network::Mainnet);
assert_eq!(addr.kind(), &AddressType::Subaddress);
assert!(addr.is_subaddress());
assert_eq!(addr.payment_id(), None);
assert!(!addr.is_guaranteed());
assert_eq!(addr.spend.compress().to_bytes(), SUB_SPEND);
assert_eq!(addr.view.compress().to_bytes(), SUB_VIEW);
assert_eq!(addr.to_string(), SUBADDRESS);
}
#[test]
fn featured() {
for (network, first) in
[(Network::Mainnet, 'C'), (Network::Testnet, 'K'), (Network::Stagenet, 'F')]
{
for _ in 0 .. 100 {
let spend = &Scalar::random(&mut OsRng) * ED25519_BASEPOINT_TABLE;
let view = &Scalar::random(&mut OsRng) * ED25519_BASEPOINT_TABLE;
for features in 0 .. (1 << 3) {
const SUBADDRESS_FEATURE_BIT: u8 = 1;
const INTEGRATED_FEATURE_BIT: u8 = 1 << 1;
const GUARANTEED_FEATURE_BIT: u8 = 1 << 2;
let subaddress = (features & SUBADDRESS_FEATURE_BIT) == SUBADDRESS_FEATURE_BIT;
let mut payment_id = [0; 8];
OsRng.fill_bytes(&mut payment_id);
let payment_id = Some(payment_id)
.filter(|_| (features & INTEGRATED_FEATURE_BIT) == INTEGRATED_FEATURE_BIT);
let guaranteed = (features & GUARANTEED_FEATURE_BIT) == GUARANTEED_FEATURE_BIT;
let kind = AddressType::Featured { subaddress, payment_id, guaranteed };
let addr = MoneroAddress::new(network, kind, spend, view);
assert_eq!(addr.to_string().chars().next().unwrap(), first);
assert_eq!(MoneroAddress::from_str(network, &addr.to_string()).unwrap(), addr);
assert_eq!(addr.spend, spend);
assert_eq!(addr.view, view);
assert_eq!(addr.is_subaddress(), subaddress);
assert_eq!(addr.payment_id(), payment_id);
assert_eq!(addr.is_guaranteed(), guaranteed);
}
}
}
}
#[test]
fn featured_vectors() {
#[derive(serde::Deserialize)]
struct Vector {
address: String,
network: String,
spend: String,
view: String,
subaddress: bool,
integrated: bool,
payment_id: Option<[u8; 8]>,
guaranteed: bool,
}
let vectors = serde_json::from_str::<Vec<Vector>>(FEATURED_JSON).unwrap();
for vector in vectors {
let first = vector.address.chars().next().unwrap();
let network = match vector.network.as_str() {
"Mainnet" => {
assert_eq!(first, 'C');
Network::Mainnet
}
"Testnet" => {
assert_eq!(first, 'K');
Network::Testnet
}
"Stagenet" => {
assert_eq!(first, 'F');
Network::Stagenet
}
_ => panic!("Unknown network"),
};
let spend = decompress_point(hex::decode(vector.spend).unwrap().try_into().unwrap()).unwrap();
let view = decompress_point(hex::decode(vector.view).unwrap().try_into().unwrap()).unwrap();
let addr = MoneroAddress::from_str(network, &vector.address).unwrap();
assert_eq!(addr.spend, spend);
assert_eq!(addr.view, view);
assert_eq!(addr.is_subaddress(), vector.subaddress);
assert_eq!(vector.integrated, vector.payment_id.is_some());
assert_eq!(addr.payment_id(), vector.payment_id);
assert_eq!(addr.is_guaranteed(), vector.guaranteed);
assert_eq!(
MoneroAddress::new(
network,
AddressType::Featured {
subaddress: vector.subaddress,
payment_id: vector.payment_id,
guaranteed: vector.guaranteed
},
spend,
view
)
.to_string(),
vector.address
);
}
}

View File

@@ -0,0 +1,230 @@
[
{
"address": "CjWdTpuDaZ69nTGxzm9YarR82YDYFECi1WaaREZTMy5yDsjaRX5bC3cbC3JpcrBPd7YYpjoWKuBMidgGaKBK5Jye2v3pYyUDn",
"network": "Mainnet",
"spend": "258dfe7eef9be934839f3b8e0d40e79035fe85879c0a9eb0d7372ae2deb0004c",
"view": "f91382373045f3cc69233254ab0406bc9e008707569ff9db4718654812d839df",
"subaddress": false,
"integrated": false,
"guaranteed": false
},
{
"address": "CjWdTpuDaZ69nTGxzm9YarR82YDYFECi1WaaREZTMy5yDsjaRX5bC3cbC3JpcrBPd7YYpjoWKuBMidgGaKBK5Jye2v3wfMHCy",
"network": "Mainnet",
"spend": "258dfe7eef9be934839f3b8e0d40e79035fe85879c0a9eb0d7372ae2deb0004c",
"view": "f91382373045f3cc69233254ab0406bc9e008707569ff9db4718654812d839df",
"subaddress": true,
"integrated": false,
"guaranteed": false
},
{
"address": "CjWdTpuDaZ69nTGxzm9YarR82YDYFECi1WaaREZTMy5yDsjaRX5bC3cbC3JpcrBPd7YYpjoWKuBMidgGaKBK5JyeeJTo4p5ayvj36PStM5AX",
"network": "Mainnet",
"spend": "258dfe7eef9be934839f3b8e0d40e79035fe85879c0a9eb0d7372ae2deb0004c",
"view": "f91382373045f3cc69233254ab0406bc9e008707569ff9db4718654812d839df",
"subaddress": false,
"integrated": true,
"payment_id": [46, 48, 134, 34, 245, 148, 243, 195],
"guaranteed": false
},
{
"address": "CjWdTpuDaZ69nTGxzm9YarR82YDYFECi1WaaREZTMy5yDsjaRX5bC3cbC3JpcrBPd7YYpjoWKuBMidgGaKBK5JyeeJWv5WqMCNE2hRs9rJfy",
"network": "Mainnet",
"spend": "258dfe7eef9be934839f3b8e0d40e79035fe85879c0a9eb0d7372ae2deb0004c",
"view": "f91382373045f3cc69233254ab0406bc9e008707569ff9db4718654812d839df",
"subaddress": true,
"integrated": true,
"payment_id": [153, 176, 98, 204, 151, 27, 197, 168],
"guaranteed": false
},
{
"address": "CjWdTpuDaZ69nTGxzm9YarR82YDYFECi1WaaREZTMy5yDsjaRX5bC3cbC3JpcrBPd7YYpjoWKuBMidgGaKBK5Jye2v4DwqwH1",
"network": "Mainnet",
"spend": "258dfe7eef9be934839f3b8e0d40e79035fe85879c0a9eb0d7372ae2deb0004c",
"view": "f91382373045f3cc69233254ab0406bc9e008707569ff9db4718654812d839df",
"subaddress": false,
"integrated": false,
"guaranteed": true
},
{
"address": "CjWdTpuDaZ69nTGxzm9YarR82YDYFECi1WaaREZTMy5yDsjaRX5bC3cbC3JpcrBPd7YYpjoWKuBMidgGaKBK5Jye2v4Pyz8bD",
"network": "Mainnet",
"spend": "258dfe7eef9be934839f3b8e0d40e79035fe85879c0a9eb0d7372ae2deb0004c",
"view": "f91382373045f3cc69233254ab0406bc9e008707569ff9db4718654812d839df",
"subaddress": true,
"integrated": false,
"guaranteed": true
},
{
"address": "CjWdTpuDaZ69nTGxzm9YarR82YDYFECi1WaaREZTMy5yDsjaRX5bC3cbC3JpcrBPd7YYpjoWKuBMidgGaKBK5JyeeJcwt7hykou237MqZZDA",
"network": "Mainnet",
"spend": "258dfe7eef9be934839f3b8e0d40e79035fe85879c0a9eb0d7372ae2deb0004c",
"view": "f91382373045f3cc69233254ab0406bc9e008707569ff9db4718654812d839df",
"subaddress": false,
"integrated": true,
"payment_id": [88, 37, 149, 111, 171, 108, 120, 181],
"guaranteed": true
},
{
"address": "CjWdTpuDaZ69nTGxzm9YarR82YDYFECi1WaaREZTMy5yDsjaRX5bC3cbC3JpcrBPd7YYpjoWKuBMidgGaKBK5JyeeJfTrFAp69u2MYbf5YeN",
"network": "Mainnet",
"spend": "258dfe7eef9be934839f3b8e0d40e79035fe85879c0a9eb0d7372ae2deb0004c",
"view": "f91382373045f3cc69233254ab0406bc9e008707569ff9db4718654812d839df",
"subaddress": true,
"integrated": true,
"payment_id": [125, 69, 155, 152, 140, 160, 157, 186],
"guaranteed": true
},
{
"address": "Kgx5uCVsMSEVm7seL8tjyRGmmVXjWfEowKpKjgaXUGVyMViBYMh13VQ4mfqpB7zEVVcJx3E8FFgAuQ8cq6mg5x712U9w7ScYA",
"network": "Testnet",
"spend": "bba3a8a5bb47f7abf2e2dffeaf43385e4b308fd63a9ff6707e355f3b0a6c247a",
"view": "881713a4fa9777168a54bbdcb75290d319fb92fdf1026a8a4b125a8e341de8ab",
"subaddress": false,
"integrated": false,
"guaranteed": false
},
{
"address": "Kgx5uCVsMSEVm7seL8tjyRGmmVXjWfEowKpKjgaXUGVyMViBYMh13VQ4mfqpB7zEVVcJx3E8FFgAuQ8cq6mg5x712UA2gCrT1",
"network": "Testnet",
"spend": "bba3a8a5bb47f7abf2e2dffeaf43385e4b308fd63a9ff6707e355f3b0a6c247a",
"view": "881713a4fa9777168a54bbdcb75290d319fb92fdf1026a8a4b125a8e341de8ab",
"subaddress": true,
"integrated": false,
"guaranteed": false
},
{
"address": "Kgx5uCVsMSEVm7seL8tjyRGmmVXjWfEowKpKjgaXUGVyMViBYMh13VQ4mfqpB7zEVVcJx3E8FFgAuQ8cq6mg5x71Vc1DbPKwJu81cxJjqBkS",
"network": "Testnet",
"spend": "bba3a8a5bb47f7abf2e2dffeaf43385e4b308fd63a9ff6707e355f3b0a6c247a",
"view": "881713a4fa9777168a54bbdcb75290d319fb92fdf1026a8a4b125a8e341de8ab",
"subaddress": false,
"integrated": true,
"payment_id": [92, 225, 118, 220, 39, 3, 72, 51],
"guaranteed": false
},
{
"address": "Kgx5uCVsMSEVm7seL8tjyRGmmVXjWfEowKpKjgaXUGVyMViBYMh13VQ4mfqpB7zEVVcJx3E8FFgAuQ8cq6mg5x71Vc2o1rPMaXN31Fe5J6dn",
"network": "Testnet",
"spend": "bba3a8a5bb47f7abf2e2dffeaf43385e4b308fd63a9ff6707e355f3b0a6c247a",
"view": "881713a4fa9777168a54bbdcb75290d319fb92fdf1026a8a4b125a8e341de8ab",
"subaddress": true,
"integrated": true,
"payment_id": [20, 120, 47, 89, 72, 165, 233, 115],
"guaranteed": false
},
{
"address": "Kgx5uCVsMSEVm7seL8tjyRGmmVXjWfEowKpKjgaXUGVyMViBYMh13VQ4mfqpB7zEVVcJx3E8FFgAuQ8cq6mg5x712UAQHCRZ4",
"network": "Testnet",
"spend": "bba3a8a5bb47f7abf2e2dffeaf43385e4b308fd63a9ff6707e355f3b0a6c247a",
"view": "881713a4fa9777168a54bbdcb75290d319fb92fdf1026a8a4b125a8e341de8ab",
"subaddress": false,
"integrated": false,
"guaranteed": true
},
{
"address": "Kgx5uCVsMSEVm7seL8tjyRGmmVXjWfEowKpKjgaXUGVyMViBYMh13VQ4mfqpB7zEVVcJx3E8FFgAuQ8cq6mg5x712UAUzqaii",
"network": "Testnet",
"spend": "bba3a8a5bb47f7abf2e2dffeaf43385e4b308fd63a9ff6707e355f3b0a6c247a",
"view": "881713a4fa9777168a54bbdcb75290d319fb92fdf1026a8a4b125a8e341de8ab",
"subaddress": true,
"integrated": false,
"guaranteed": true
},
{
"address": "Kgx5uCVsMSEVm7seL8tjyRGmmVXjWfEowKpKjgaXUGVyMViBYMh13VQ4mfqpB7zEVVcJx3E8FFgAuQ8cq6mg5x71VcAsfQc3gJQ2gHLd5DiQ",
"network": "Testnet",
"spend": "bba3a8a5bb47f7abf2e2dffeaf43385e4b308fd63a9ff6707e355f3b0a6c247a",
"view": "881713a4fa9777168a54bbdcb75290d319fb92fdf1026a8a4b125a8e341de8ab",
"subaddress": false,
"integrated": true,
"payment_id": [193, 149, 123, 214, 180, 205, 195, 91],
"guaranteed": true
},
{
"address": "Kgx5uCVsMSEVm7seL8tjyRGmmVXjWfEowKpKjgaXUGVyMViBYMh13VQ4mfqpB7zEVVcJx3E8FFgAuQ8cq6mg5x71VcDBAD5jbZQ3AMHFyvQB",
"network": "Testnet",
"spend": "bba3a8a5bb47f7abf2e2dffeaf43385e4b308fd63a9ff6707e355f3b0a6c247a",
"view": "881713a4fa9777168a54bbdcb75290d319fb92fdf1026a8a4b125a8e341de8ab",
"subaddress": true,
"integrated": true,
"payment_id": [205, 170, 65, 0, 51, 175, 251, 184],
"guaranteed": true
},
{
"address": "FSDinqdKK54PbjF73GgW3nUpf7bF8QbyxFCUurENmUyeEfSxSLL2hxwANBLzq1A8gTSAzzEn65hKjetA8o5BvjV61VPJnBtTP",
"network": "Stagenet",
"spend": "4cd503040f5e43871bf37d8ca7177da655bda410859af754e24e7b44437f3151",
"view": "af60d42b6c6e4437fd93eb32657a14967efa393630d7aee27b5973c8e1c5ad39",
"subaddress": false,
"integrated": false,
"guaranteed": false
},
{
"address": "FSDinqdKK54PbjF73GgW3nUpf7bF8QbyxFCUurENmUyeEfSxSLL2hxwANBLzq1A8gTSAzzEn65hKjetA8o5BvjV61VPUrwMvP",
"network": "Stagenet",
"spend": "4cd503040f5e43871bf37d8ca7177da655bda410859af754e24e7b44437f3151",
"view": "af60d42b6c6e4437fd93eb32657a14967efa393630d7aee27b5973c8e1c5ad39",
"subaddress": true,
"integrated": false,
"guaranteed": false
},
{
"address": "FSDinqdKK54PbjF73GgW3nUpf7bF8QbyxFCUurENmUyeEfSxSLL2hxwANBLzq1A8gTSAzzEn65hKjetA8o5BvjV6AY5ECEhP5Nr1aCRPXdxk",
"network": "Stagenet",
"spend": "4cd503040f5e43871bf37d8ca7177da655bda410859af754e24e7b44437f3151",
"view": "af60d42b6c6e4437fd93eb32657a14967efa393630d7aee27b5973c8e1c5ad39",
"subaddress": false,
"integrated": true,
"payment_id": [173, 149, 78, 64, 215, 211, 66, 170],
"guaranteed": false
},
{
"address": "FSDinqdKK54PbjF73GgW3nUpf7bF8QbyxFCUurENmUyeEfSxSLL2hxwANBLzq1A8gTSAzzEn65hKjetA8o5BvjV6AY882kTUS1D2LttnPvTR",
"network": "Stagenet",
"spend": "4cd503040f5e43871bf37d8ca7177da655bda410859af754e24e7b44437f3151",
"view": "af60d42b6c6e4437fd93eb32657a14967efa393630d7aee27b5973c8e1c5ad39",
"subaddress": true,
"integrated": true,
"payment_id": [254, 159, 186, 162, 1, 8, 156, 108],
"guaranteed": false
},
{
"address": "FSDinqdKK54PbjF73GgW3nUpf7bF8QbyxFCUurENmUyeEfSxSLL2hxwANBLzq1A8gTSAzzEn65hKjetA8o5BvjV61VPpBBo8F",
"network": "Stagenet",
"spend": "4cd503040f5e43871bf37d8ca7177da655bda410859af754e24e7b44437f3151",
"view": "af60d42b6c6e4437fd93eb32657a14967efa393630d7aee27b5973c8e1c5ad39",
"subaddress": false,
"integrated": false,
"guaranteed": true
},
{
"address": "FSDinqdKK54PbjF73GgW3nUpf7bF8QbyxFCUurENmUyeEfSxSLL2hxwANBLzq1A8gTSAzzEn65hKjetA8o5BvjV61VPuUJX3b",
"network": "Stagenet",
"spend": "4cd503040f5e43871bf37d8ca7177da655bda410859af754e24e7b44437f3151",
"view": "af60d42b6c6e4437fd93eb32657a14967efa393630d7aee27b5973c8e1c5ad39",
"subaddress": true,
"integrated": false,
"guaranteed": true
},
{
"address": "FSDinqdKK54PbjF73GgW3nUpf7bF8QbyxFCUurENmUyeEfSxSLL2hxwANBLzq1A8gTSAzzEn65hKjetA8o5BvjV6AYCZPxVAoDu21DryMoto",
"network": "Stagenet",
"spend": "4cd503040f5e43871bf37d8ca7177da655bda410859af754e24e7b44437f3151",
"view": "af60d42b6c6e4437fd93eb32657a14967efa393630d7aee27b5973c8e1c5ad39",
"subaddress": false,
"integrated": true,
"payment_id": [3, 115, 230, 129, 172, 108, 116, 235],
"guaranteed": true
},
{
"address": "FSDinqdKK54PbjF73GgW3nUpf7bF8QbyxFCUurENmUyeEfSxSLL2hxwANBLzq1A8gTSAzzEn65hKjetA8o5BvjV6AYFYCqKQAWL18KkpBQ8R",
"network": "Stagenet",
"spend": "4cd503040f5e43871bf37d8ca7177da655bda410859af754e24e7b44437f3151",
"view": "af60d42b6c6e4437fd93eb32657a14967efa393630d7aee27b5973c8e1c5ad39",
"subaddress": true,
"integrated": true,
"payment_id": [94, 122, 63, 167, 209, 225, 14, 180],
"guaranteed": true
}
]