Smash out Monero addresses

This commit is contained in:
Luke Parker
2024-06-23 14:17:04 -04:00
parent 0b20004ba1
commit eb0c19bfff
13 changed files with 257 additions and 30 deletions

30
Cargo.lock generated
View File

@@ -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",

View File

@@ -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",

View File

@@ -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"]

View File

@@ -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 <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"] }
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"]

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,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.

View File

@@ -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<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

@@ -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<B: AddressBytes> fmt::Display for Address<B> {
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<B: AddressBytes> Address<B> {
}
pub fn from_str_raw(s: &str) -> Result<Self, AddressError> {
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)?;
}

View File

@@ -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();

View File

@@ -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,

View File

@@ -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;

View File

@@ -1,2 +1 @@
mod address;
mod extra;