mirror of
https://github.com/serai-dex/serai.git
synced 2025-12-11 21:49:26 +00:00
Smash out Monero addresses
This commit is contained in:
30
Cargo.lock
generated
30
Cargo.lock
generated
@@ -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",
|
||||
|
||||
@@ -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",
|
||||
|
||||
@@ -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"]
|
||||
|
||||
47
coins/monero/wallet/address/Cargo.toml
Normal file
47
coins/monero/wallet/address/Cargo.toml
Normal 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"]
|
||||
21
coins/monero/wallet/address/LICENSE
Normal file
21
coins/monero/wallet/address/LICENSE
Normal 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.
|
||||
6
coins/monero/wallet/address/README.md
Normal file
6
coins/monero/wallet/address/README.md
Normal 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.
|
||||
104
coins/monero/wallet/address/src/base58check.rs
Normal file
104
coins/monero/wallet/address/src/base58check.rs
Normal 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)
|
||||
}
|
||||
@@ -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)?;
|
||||
}
|
||||
@@ -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();
|
||||
@@ -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,
|
||||
|
||||
@@ -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;
|
||||
|
||||
@@ -1,2 +1 @@
|
||||
mod address;
|
||||
mod extra;
|
||||
|
||||
Reference in New Issue
Block a user