From eb0c19bffffd792c05640c36c7a550fdafa342ba Mon Sep 17 00:00:00 2001 From: Luke Parker Date: Sun, 23 Jun 2024 14:17:04 -0400 Subject: [PATCH] Smash out Monero addresses --- Cargo.lock | 30 +++-- Cargo.toml | 1 + coins/monero/wallet/Cargo.toml | 13 +-- coins/monero/wallet/address/Cargo.toml | 47 ++++++++ coins/monero/wallet/address/LICENSE | 21 ++++ coins/monero/wallet/address/README.md | 6 + .../monero/wallet/address/src/base58check.rs | 104 ++++++++++++++++++ .../{src/address.rs => address/src/lib.rs} | 17 ++- .../tests/address.rs => address/src/tests.rs} | 39 ++++++- .../src}/vectors/featured_addresses.json | 0 coins/monero/wallet/seed/src/lib.rs | 5 + coins/monero/wallet/src/lib.rs | 3 +- coins/monero/wallet/src/tests/mod.rs | 1 - 13 files changed, 257 insertions(+), 30 deletions(-) create mode 100644 coins/monero/wallet/address/Cargo.toml create mode 100644 coins/monero/wallet/address/LICENSE create mode 100644 coins/monero/wallet/address/README.md create mode 100644 coins/monero/wallet/address/src/base58check.rs rename coins/monero/wallet/{src/address.rs => address/src/lib.rs} (95%) rename coins/monero/wallet/{src/tests/address.rs => address/src/tests.rs} (82%) rename coins/monero/wallet/{src/tests => address/src}/vectors/featured_addresses.json (100%) diff --git a/Cargo.lock b/Cargo.lock index e0a0871f..6a01d98b 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -880,16 +880,6 @@ version = "0.2.0" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "4c7f02d4ea65f2c1853089ffd8d2787bdbc63de2f0d29dedbcf8ccdfa0ccd4cf" -[[package]] -name = "base58-monero" -version = "2.0.0" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "978e81a45367d2409ecd33369a45dda2e9a3ca516153ec194de1fbda4b9fb79d" -dependencies = [ - "thiserror", - "tiny-keccak", -] - [[package]] name = "base58ck" version = "0.1.0" @@ -4751,6 +4741,23 @@ dependencies = [ "zeroize", ] +[[package]] +name = "monero-address" +version = "0.1.0" +dependencies = [ + "curve25519-dalek", + "hex", + "hex-literal", + "monero-io", + "monero-primitives", + "rand_core", + "serde", + "serde_json", + "std-shims", + "thiserror", + "zeroize", +] + [[package]] name = "monero-borromean" version = "0.1.0" @@ -4910,14 +4917,13 @@ name = "monero-wallet" version = "0.1.0" dependencies = [ "async-trait", - "base58-monero", "curve25519-dalek", "dalek-ff-group", "flexible-transcript", "group", "hex", - "hex-literal", "modular-frost", + "monero-address", "monero-rpc", "monero-serai", "monero-simple-request-rpc", diff --git a/Cargo.toml b/Cargo.toml index d33ce871..32da7712 100644 --- a/Cargo.toml +++ b/Cargo.toml @@ -53,6 +53,7 @@ members = [ "coins/monero", "coins/monero/rpc", "coins/monero/rpc/simple-request", + "coins/monero/wallet/address", "coins/monero/wallet", "coins/monero/wallet/seed", "coins/monero/wallet/polyseed", diff --git a/coins/monero/wallet/Cargo.toml b/coins/monero/wallet/Cargo.toml index 65273d3e..4aa7dd6e 100644 --- a/coins/monero/wallet/Cargo.toml +++ b/coins/monero/wallet/Cargo.toml @@ -39,15 +39,14 @@ dalek-ff-group = { path = "../../../crypto/dalek-ff-group", version = "0.4", def frost = { package = "modular-frost", path = "../../../crypto/frost", default-features = false, features = ["ed25519"], optional = true } hex = { version = "0.4", default-features = false, features = ["alloc"] } -base58-monero = { version = "2", default-features = false, features = ["check"] } -serde = { version = "1", default-features = false, features = ["derive", "alloc"] } -serde_json = { version = "1", default-features = false, features = ["alloc"] } monero-serai = { path = "..", default-features = false } monero-rpc = { path = "../rpc", default-features = false } +monero-address = { path = "./address", default-features = false } [dev-dependencies] -hex-literal = "0.4" +serde = { version = "1", default-features = false, features = ["derive", "alloc", "std"] } +serde_json = { version = "1", default-features = false, features = ["alloc", "std"] } frost = { package = "modular-frost", path = "../../../crypto/frost", default-features = false, features = ["ed25519", "tests"] } @@ -68,13 +67,9 @@ std = [ "rand_chacha/std", "rand_distr/std", - "hex/std", - "base58-monero/std", - "serde/std", - "serde_json/std", - "monero-serai/std", "monero-rpc/std", + "monero-address/std", ] compile-time-generators = ["curve25519-dalek/precomputed-tables", "monero-serai/compile-time-generators"] multisig = ["transcript", "dalek-ff-group", "frost", "monero-serai/multisig", "std"] diff --git a/coins/monero/wallet/address/Cargo.toml b/coins/monero/wallet/address/Cargo.toml new file mode 100644 index 00000000..c823f61f --- /dev/null +++ b/coins/monero/wallet/address/Cargo.toml @@ -0,0 +1,47 @@ +[package] +name = "monero-address" +version = "0.1.0" +description = "Monero addresses and associated functionality" +license = "MIT" +repository = "https://github.com/serai-dex/serai/tree/develop/coins/monero/wallet/address" +authors = ["Luke Parker "] +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"] } +hex = { version = "0.4", default-features = false, features = ["alloc"] } + +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] +hex-literal = { version = "0.4", default-features = false } +rand_core = { version = "0.6", default-features = false, features = ["std"] } +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"] diff --git a/coins/monero/wallet/address/LICENSE b/coins/monero/wallet/address/LICENSE new file mode 100644 index 00000000..91d893c1 --- /dev/null +++ b/coins/monero/wallet/address/LICENSE @@ -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. diff --git a/coins/monero/wallet/address/README.md b/coins/monero/wallet/address/README.md new file mode 100644 index 00000000..c94f5cc5 --- /dev/null +++ b/coins/monero/wallet/address/README.md @@ -0,0 +1,6 @@ +# Polyseed + +Rust implementation of [Polyseed](https://github.com/tevador/polyseed). + +This library is usable under no-std when the `std` feature (on by default) is +disabled. diff --git a/coins/monero/wallet/address/src/base58check.rs b/coins/monero/wallet/address/src/base58check.rs new file mode 100644 index 00000000..fcd418b3 --- /dev/null +++ b/coins/monero/wallet/address/src/base58check.rs @@ -0,0 +1,104 @@ +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> { + 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) -> 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> { + 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) +} diff --git a/coins/monero/wallet/src/address.rs b/coins/monero/wallet/address/src/lib.rs similarity index 95% rename from coins/monero/wallet/src/address.rs rename to coins/monero/wallet/address/src/lib.rs index 3111c545..9455f3c1 100644 --- a/coins/monero/wallet/src/address.rs +++ b/coins/monero/wallet/address/src/lib.rs @@ -1,3 +1,8 @@ +#![cfg_attr(docsrs, feature(doc_auto_cfg))] +#![doc = include_str!("../README.md")] +// #![deny(missing_docs)] // TODO +#![cfg_attr(not(feature = "std"), no_std)] + use core::{marker::PhantomData, fmt}; use std_shims::string::ToString; @@ -5,9 +10,13 @@ use zeroize::Zeroize; use curve25519_dalek::edwards::EdwardsPoint; -use monero_serai::io::decompress_point; +use monero_io::decompress_point; -use base58_monero::base58::{encode_check, decode_check}; +mod base58check; +use base58check::{encode_check, decode_check}; + +#[cfg(test)] +mod tests; /// The network this address is for. #[derive(Clone, Copy, PartialEq, Eq, Debug, Zeroize)] @@ -226,7 +235,7 @@ impl fmt::Display for Address { if let Some(id) = self.meta.kind.payment_id() { data.extend(id); } - write!(f, "{}", encode_check(&data).unwrap()) + write!(f, "{}", encode_check(data)) } } @@ -236,7 +245,7 @@ impl Address { } pub fn from_str_raw(s: &str) -> Result { - let raw = decode_check(s).map_err(|_| AddressError::InvalidEncoding)?; + let raw = decode_check(s).ok_or(AddressError::InvalidEncoding)?; if raw.len() < (1 + 32 + 32) { Err(AddressError::InvalidLength)?; } diff --git a/coins/monero/wallet/src/tests/address.rs b/coins/monero/wallet/address/src/tests.rs similarity index 82% rename from coins/monero/wallet/src/tests/address.rs rename to coins/monero/wallet/address/src/tests.rs index 06e7f4e9..0f386d19 100644 --- a/coins/monero/wallet/src/tests/address.rs +++ b/coins/monero/wallet/address/src/tests.rs @@ -4,9 +4,9 @@ use rand_core::{RngCore, OsRng}; use curve25519_dalek::{constants::ED25519_BASEPOINT_TABLE, scalar::Scalar}; -use monero_serai::io::decompress_point; +use monero_io::decompress_point; -use crate::address::{Network, AddressType, AddressMeta, MoneroAddress}; +use crate::{Network, AddressType, AddressMeta, MoneroAddress}; const SPEND: [u8; 32] = hex!("f8631661f6ab4e6fda310c797330d86e23a682f20d5bc8cc27b18051191f16d7"); const VIEW: [u8; 32] = hex!("4a1535063ad1fee2dabbf909d4fd9a873e29541b401f0944754e17c9a41820ce"); @@ -27,6 +27,41 @@ const SUBADDRESS: &str = 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(); diff --git a/coins/monero/wallet/src/tests/vectors/featured_addresses.json b/coins/monero/wallet/address/src/vectors/featured_addresses.json similarity index 100% rename from coins/monero/wallet/src/tests/vectors/featured_addresses.json rename to coins/monero/wallet/address/src/vectors/featured_addresses.json diff --git a/coins/monero/wallet/seed/src/lib.rs b/coins/monero/wallet/seed/src/lib.rs index 7035d560..600632b5 100644 --- a/coins/monero/wallet/seed/src/lib.rs +++ b/coins/monero/wallet/seed/src/lib.rs @@ -1,3 +1,8 @@ +#![cfg_attr(docsrs, feature(doc_auto_cfg))] +#![doc = include_str!("../README.md")] +#![deny(missing_docs)] +#![cfg_attr(not(feature = "std"), no_std)] + use core::{ops::Deref, fmt}; use std_shims::{ sync::OnceLock, diff --git a/coins/monero/wallet/src/lib.rs b/coins/monero/wallet/src/lib.rs index ce503988..86354cbf 100644 --- a/coins/monero/wallet/src/lib.rs +++ b/coins/monero/wallet/src/lib.rs @@ -31,8 +31,7 @@ pub use monero_rpc as rpc; pub mod extra; pub(crate) use extra::{PaymentId, ExtraField, Extra}; -/// Address encoding and decoding functionality. -pub mod address; +pub use monero_address as address; use address::{Network, AddressType, SubaddressIndex, AddressSpec, AddressMeta, MoneroAddress}; mod scan; diff --git a/coins/monero/wallet/src/tests/mod.rs b/coins/monero/wallet/src/tests/mod.rs index f2b9d4d6..d8fa7cb0 100644 --- a/coins/monero/wallet/src/tests/mod.rs +++ b/coins/monero/wallet/src/tests/mod.rs @@ -1,2 +1 @@ -mod address; mod extra;