Support handling addresses from other networks

A type alias of MoneroAddress is provided to abstract away the generic. 
To keep the rest of the library sane, MoneroAddress is used everywhere.

If someone wants to use this library with another coin, they *should* be 
able to parse a custom address and then recreate it as a Monero address. 
While that's annoying to them, better them than any person using this 
lib for Monero.

Closes #152.
This commit is contained in:
Luke Parker 2022-11-15 00:06:15 -05:00
parent 83060a914a
commit 4a3178ed8f
No known key found for this signature in database
GPG key ID: F9F1386DB1E119B6
6 changed files with 111 additions and 61 deletions

View file

@ -6,7 +6,7 @@ use curve25519_dalek::{constants::ED25519_BASEPOINT_TABLE, edwards::CompressedEd
use crate::{ use crate::{
random_scalar, random_scalar,
wallet::address::{Network, AddressType, AddressMeta, Address}, wallet::address::{Network, AddressType, AddressMeta, MoneroAddress},
}; };
const SPEND: [u8; 32] = hex!("f8631661f6ab4e6fda310c797330d86e23a682f20d5bc8cc27b18051191f16d7"); const SPEND: [u8; 32] = hex!("f8631661f6ab4e6fda310c797330d86e23a682f20d5bc8cc27b18051191f16d7");
@ -30,7 +30,7 @@ const FEATURED_JSON: &'static str = include_str!("vectors/featured_addresses.jso
#[test] #[test]
fn standard_address() { fn standard_address() {
let addr = Address::from_str(STANDARD, Network::Mainnet).unwrap(); let addr = MoneroAddress::from_str(Network::Mainnet, STANDARD).unwrap();
assert_eq!(addr.meta.network, Network::Mainnet); assert_eq!(addr.meta.network, Network::Mainnet);
assert_eq!(addr.meta.kind, AddressType::Standard); assert_eq!(addr.meta.kind, AddressType::Standard);
assert_eq!(addr.meta.kind.subaddress(), false); assert_eq!(addr.meta.kind.subaddress(), false);
@ -43,7 +43,7 @@ fn standard_address() {
#[test] #[test]
fn integrated_address() { fn integrated_address() {
let addr = Address::from_str(INTEGRATED, Network::Mainnet).unwrap(); let addr = MoneroAddress::from_str(Network::Mainnet, INTEGRATED).unwrap();
assert_eq!(addr.meta.network, Network::Mainnet); assert_eq!(addr.meta.network, Network::Mainnet);
assert_eq!(addr.meta.kind, AddressType::Integrated(PAYMENT_ID)); assert_eq!(addr.meta.kind, AddressType::Integrated(PAYMENT_ID));
assert_eq!(addr.meta.kind.subaddress(), false); assert_eq!(addr.meta.kind.subaddress(), false);
@ -56,7 +56,7 @@ fn integrated_address() {
#[test] #[test]
fn subaddress() { fn subaddress() {
let addr = Address::from_str(SUBADDRESS, Network::Mainnet).unwrap(); let addr = MoneroAddress::from_str(Network::Mainnet, SUBADDRESS).unwrap();
assert_eq!(addr.meta.network, Network::Mainnet); assert_eq!(addr.meta.network, Network::Mainnet);
assert_eq!(addr.meta.kind, AddressType::Subaddress); assert_eq!(addr.meta.kind, AddressType::Subaddress);
assert_eq!(addr.meta.kind.subaddress(), true); assert_eq!(addr.meta.kind.subaddress(), true);
@ -90,11 +90,11 @@ fn featured() {
let guaranteed = (features & GUARANTEED_FEATURE_BIT) == GUARANTEED_FEATURE_BIT; let guaranteed = (features & GUARANTEED_FEATURE_BIT) == GUARANTEED_FEATURE_BIT;
let kind = AddressType::Featured(subaddress, id, guaranteed); let kind = AddressType::Featured(subaddress, id, guaranteed);
let meta = AddressMeta { network, kind }; let meta = AddressMeta::new(network, kind);
let addr = Address::new(meta, spend, view); let addr = MoneroAddress::new(meta, spend, view);
assert_eq!(addr.to_string().chars().next().unwrap(), first); assert_eq!(addr.to_string().chars().next().unwrap(), first);
assert_eq!(Address::from_str(&addr.to_string(), network).unwrap(), addr); assert_eq!(MoneroAddress::from_str(network, &addr.to_string()).unwrap(), addr);
assert_eq!(addr.spend, spend); assert_eq!(addr.spend, spend);
assert_eq!(addr.view, view); assert_eq!(addr.view, view);
@ -146,7 +146,7 @@ fn featured_vectors() {
let view = let view =
CompressedEdwardsY::from_slice(&hex::decode(vector.view).unwrap()).decompress().unwrap(); CompressedEdwardsY::from_slice(&hex::decode(vector.view).unwrap()).decompress().unwrap();
let addr = Address::from_str(&vector.address, network).unwrap(); let addr = MoneroAddress::from_str(network, &vector.address).unwrap();
assert_eq!(addr.spend, spend); assert_eq!(addr.spend, spend);
assert_eq!(addr.view, view); assert_eq!(addr.view, view);
@ -156,11 +156,11 @@ fn featured_vectors() {
assert_eq!(addr.guaranteed(), vector.guaranteed); assert_eq!(addr.guaranteed(), vector.guaranteed);
assert_eq!( assert_eq!(
Address::new( MoneroAddress::new(
AddressMeta { AddressMeta::new(
network, network,
kind: AddressType::Featured(vector.subaddress, vector.payment_id, vector.guaranteed) AddressType::Featured(vector.subaddress, vector.payment_id, vector.guaranteed)
}, ),
spend, spend,
view view
) )

View file

@ -1,3 +1,4 @@
use core::{marker::PhantomData, fmt::Debug};
use std::string::ToString; use std::string::ToString;
use thiserror::Error; use thiserror::Error;
@ -8,6 +9,7 @@ use curve25519_dalek::edwards::{EdwardsPoint, CompressedEdwardsY};
use base58_monero::base58::{encode_check, decode_check}; use base58_monero::base58::{encode_check, decode_check};
/// The network this address is for.
#[derive(Clone, Copy, PartialEq, Eq, Debug, Zeroize)] #[derive(Clone, Copy, PartialEq, Eq, Debug, Zeroize)]
pub enum Network { pub enum Network {
Mainnet, Mainnet,
@ -26,14 +28,6 @@ pub enum AddressType {
} }
impl AddressType { impl AddressType {
fn network_bytes(network: Network) -> (u8, u8, u8, u8) {
match network {
Network::Mainnet => (18, 19, 42, 70),
Network::Testnet => (53, 54, 63, 111),
Network::Stagenet => (24, 25, 36, 86),
}
}
pub fn subaddress(&self) -> bool { pub fn subaddress(&self) -> bool {
matches!(self, AddressType::Subaddress) || matches!(self, AddressType::Featured(true, ..)) matches!(self, AddressType::Subaddress) || matches!(self, AddressType::Featured(true, ..))
} }
@ -53,12 +47,40 @@ impl AddressType {
} }
} }
#[derive(Clone, Copy, PartialEq, Eq, Debug, Zeroize)] /// A type which returns the byte for a given address.
pub struct AddressMeta { pub trait AddressBytes: Clone + Copy + PartialEq + Eq + Debug {
fn network_bytes(network: Network) -> (u8, u8, u8, u8);
}
/// Address bytes for Monero.
#[derive(Clone, Copy, PartialEq, Eq, Debug)]
pub struct MoneroAddressBytes;
impl AddressBytes for MoneroAddressBytes {
fn network_bytes(network: Network) -> (u8, u8, u8, u8) {
match network {
Network::Mainnet => (18, 19, 42, 70),
Network::Testnet => (53, 54, 63, 111),
Network::Stagenet => (24, 25, 36, 86),
}
}
}
/// Address metadata.
#[derive(Clone, Copy, PartialEq, Eq, Debug)]
pub struct AddressMeta<B: AddressBytes> {
_bytes: PhantomData<B>,
pub network: Network, pub network: Network,
pub kind: AddressType, pub kind: AddressType,
} }
impl<B: AddressBytes> Zeroize for AddressMeta<B> {
fn zeroize(&mut self) {
self.network.zeroize();
self.kind.zeroize();
}
}
/// Error when decoding an address.
#[derive(Clone, Error, Debug)] #[derive(Clone, Error, Debug)]
pub enum AddressError { pub enum AddressError {
#[error("invalid address byte")] #[error("invalid address byte")]
@ -75,10 +97,10 @@ pub enum AddressError {
DifferentNetwork, DifferentNetwork,
} }
impl AddressMeta { impl<B: AddressBytes> AddressMeta<B> {
#[allow(clippy::wrong_self_convention)] #[allow(clippy::wrong_self_convention)]
fn to_byte(&self) -> u8 { fn to_byte(&self) -> u8 {
let bytes = AddressType::network_bytes(self.network); let bytes = B::network_bytes(self.network);
match self.kind { match self.kind {
AddressType::Standard => bytes.0, AddressType::Standard => bytes.0,
AddressType::Integrated(_) => bytes.1, AddressType::Integrated(_) => bytes.1,
@ -87,11 +109,16 @@ impl AddressMeta {
} }
} }
/// Create an address's metadata.
pub fn new(network: Network, kind: AddressType) -> Self {
AddressMeta { _bytes: PhantomData, network, kind }
}
// Returns an incomplete type in the case of Integrated/Featured addresses // Returns an incomplete type in the case of Integrated/Featured addresses
fn from_byte(byte: u8) -> Result<AddressMeta, AddressError> { fn from_byte(byte: u8) -> Result<Self, AddressError> {
let mut meta = None; let mut meta = None;
for network in [Network::Mainnet, Network::Testnet, Network::Stagenet] { for network in [Network::Mainnet, Network::Testnet, Network::Stagenet] {
let (standard, integrated, subaddress, featured) = AddressType::network_bytes(network); let (standard, integrated, subaddress, featured) = B::network_bytes(network);
if let Some(kind) = match byte { if let Some(kind) = match byte {
_ if byte == standard => Some(AddressType::Standard), _ if byte == standard => Some(AddressType::Standard),
_ if byte == integrated => Some(AddressType::Integrated([0; 8])), _ if byte == integrated => Some(AddressType::Integrated([0; 8])),
@ -99,7 +126,7 @@ impl AddressMeta {
_ if byte == featured => Some(AddressType::Featured(false, None, false)), _ if byte == featured => Some(AddressType::Featured(false, None, false)),
_ => None, _ => None,
} { } {
meta = Some(AddressMeta { network, kind }); meta = Some(AddressMeta::new(network, kind));
break; break;
} }
} }
@ -120,14 +147,23 @@ impl AddressMeta {
} }
} }
#[derive(Clone, Copy, PartialEq, Eq, Debug, Zeroize)] /// A Monero address, composed of metadata and a spend/view key.
pub struct Address { #[derive(Clone, Copy, PartialEq, Eq, Debug)]
pub meta: AddressMeta, pub struct Address<B: AddressBytes> {
pub meta: AddressMeta<B>,
pub spend: EdwardsPoint, pub spend: EdwardsPoint,
pub view: EdwardsPoint, pub view: EdwardsPoint,
} }
impl ToString for Address { impl<B: AddressBytes> Zeroize for Address<B> {
fn zeroize(&mut self) {
self.meta.zeroize();
self.spend.zeroize();
self.view.zeroize();
}
}
impl<B: AddressBytes> ToString for Address<B> {
fn to_string(&self) -> String { fn to_string(&self) -> String {
let mut data = vec![self.meta.to_byte()]; let mut data = vec![self.meta.to_byte()];
data.extend(self.spend.compress().to_bytes()); data.extend(self.spend.compress().to_bytes());
@ -145,12 +181,12 @@ impl ToString for Address {
} }
} }
impl Address { impl<B: AddressBytes> Address<B> {
pub fn new(meta: AddressMeta, spend: EdwardsPoint, view: EdwardsPoint) -> Address { pub fn new(meta: AddressMeta<B>, spend: EdwardsPoint, view: EdwardsPoint) -> Self {
Address { meta, spend, view } Address { meta, spend, view }
} }
pub fn from_str_raw(s: &str) -> Result<Address, AddressError> { pub fn from_str_raw(s: &str) -> Result<Self, AddressError> {
let raw = decode_check(s).map_err(|_| AddressError::InvalidEncoding)?; let raw = decode_check(s).map_err(|_| AddressError::InvalidEncoding)?;
if raw.len() < (1 + 32 + 32) { if raw.len() < (1 + 32 + 32) {
Err(AddressError::InvalidLength)?; Err(AddressError::InvalidLength)?;
@ -197,7 +233,7 @@ impl Address {
Ok(Address { meta, spend, view }) Ok(Address { meta, spend, view })
} }
pub fn from_str(s: &str, network: Network) -> Result<Address, AddressError> { pub fn from_str(network: Network, s: &str) -> Result<Self, AddressError> {
Self::from_str_raw(s).and_then(|addr| { Self::from_str_raw(s).and_then(|addr| {
if addr.meta.network == network { if addr.meta.network == network {
Ok(addr) Ok(addr)
@ -223,3 +259,17 @@ impl Address {
self.meta.guaranteed() self.meta.guaranteed()
} }
} }
/// Instantiation of the Address type with Monero's network bytes.
pub type MoneroAddress = Address<MoneroAddressBytes>;
// Allow re-interpreting of an arbitrary address as a monero address so it can be used with the
// rest of this library. Doesn't use From as it was conflicting with From<T> for T.
impl MoneroAddress {
pub fn from<B: AddressBytes>(address: Address<B>) -> MoneroAddress {
MoneroAddress::new(
AddressMeta::new(address.meta.network, address.meta.kind),
address.spend,
address.view,
)
}
}

View file

@ -15,7 +15,7 @@ pub(crate) use extra::{PaymentId, ExtraField, Extra};
/// Address encoding and decoding functionality. /// Address encoding and decoding functionality.
pub mod address; pub mod address;
use address::{Network, AddressType, AddressMeta, Address}; use address::{Network, AddressType, AddressMeta, MoneroAddress};
mod scan; mod scan;
pub use scan::SpendableOutput; pub use scan::SpendableOutput;
@ -180,23 +180,23 @@ impl Scanner {
} }
/// Return the main address for this view pair. /// Return the main address for this view pair.
pub fn address(&self) -> Address { pub fn address(&self) -> MoneroAddress {
Address::new( MoneroAddress::new(
AddressMeta { AddressMeta::new(
network: self.network, self.network,
kind: if self.burning_bug.is_none() { if self.burning_bug.is_none() {
AddressType::Featured(false, None, true) AddressType::Featured(false, None, true)
} else { } else {
AddressType::Standard AddressType::Standard
}, },
}, ),
self.pair.spend, self.pair.spend,
&self.pair.view * &ED25519_BASEPOINT_TABLE, &self.pair.view * &ED25519_BASEPOINT_TABLE,
) )
} }
/// Return the specified subaddress for this view pair. /// Return the specified subaddress for this view pair.
pub fn subaddress(&mut self, index: (u32, u32)) -> Address { pub fn subaddress(&mut self, index: (u32, u32)) -> MoneroAddress {
if index == (0, 0) { if index == (0, 0) {
return self.address(); return self.address();
} }
@ -204,15 +204,15 @@ impl Scanner {
let spend = self.pair.spend + (&self.pair.subaddress(index) * &ED25519_BASEPOINT_TABLE); let spend = self.pair.spend + (&self.pair.subaddress(index) * &ED25519_BASEPOINT_TABLE);
self.subaddresses.insert(spend.compress(), index); self.subaddresses.insert(spend.compress(), index);
Address::new( MoneroAddress::new(
AddressMeta { AddressMeta::new(
network: self.network, self.network,
kind: if self.burning_bug.is_none() { if self.burning_bug.is_none() {
AddressType::Featured(true, None, true) AddressType::Featured(true, None, true)
} else { } else {
AddressType::Subaddress AddressType::Subaddress
}, },
}, ),
spend, spend,
self.pair.view * spend, self.pair.view * spend,
) )

View file

@ -23,7 +23,7 @@ use crate::{
transaction::{Input, Output, Timelock, TransactionPrefix, Transaction}, transaction::{Input, Output, Timelock, TransactionPrefix, Transaction},
rpc::{Rpc, RpcError}, rpc::{Rpc, RpcError},
wallet::{ wallet::{
address::Address, SpendableOutput, Decoys, PaymentId, ExtraField, Extra, key_image_sort, address::MoneroAddress, SpendableOutput, Decoys, PaymentId, ExtraField, Extra, key_image_sort,
uniqueness, shared_key, commitment_mask, amount_encryption, uniqueness, shared_key, commitment_mask, amount_encryption,
}, },
}; };
@ -47,7 +47,7 @@ impl SendOutput {
fn new<R: RngCore + CryptoRng>( fn new<R: RngCore + CryptoRng>(
rng: &mut R, rng: &mut R,
unique: [u8; 32], unique: [u8; 32],
output: (usize, (Address, u64)), output: (usize, (MoneroAddress, u64)),
) -> (SendOutput, Option<[u8; 8]>) { ) -> (SendOutput, Option<[u8; 8]>) {
let o = output.0; let o = output.0;
let output = output.1; let output = output.1;
@ -173,7 +173,7 @@ impl Fee {
pub struct SignableTransaction { pub struct SignableTransaction {
protocol: Protocol, protocol: Protocol,
inputs: Vec<SpendableOutput>, inputs: Vec<SpendableOutput>,
payments: Vec<(Address, u64)>, payments: Vec<(MoneroAddress, u64)>,
data: Option<Vec<u8>>, data: Option<Vec<u8>>,
fee: u64, fee: u64,
} }
@ -186,15 +186,15 @@ impl SignableTransaction {
pub fn new( pub fn new(
protocol: Protocol, protocol: Protocol,
inputs: Vec<SpendableOutput>, inputs: Vec<SpendableOutput>,
mut payments: Vec<(Address, u64)>, mut payments: Vec<(MoneroAddress, u64)>,
change_address: Option<Address>, change_address: Option<MoneroAddress>,
data: Option<Vec<u8>>, data: Option<Vec<u8>>,
fee_rate: Fee, fee_rate: Fee,
) -> Result<SignableTransaction, TransactionError> { ) -> Result<SignableTransaction, TransactionError> {
// Make sure there's only one payment ID // Make sure there's only one payment ID
{ {
let mut payment_ids = 0; let mut payment_ids = 0;
let mut count = |addr: Address| { let mut count = |addr: MoneroAddress| {
if addr.payment_id().is_some() { if addr.payment_id().is_some() {
payment_ids += 1 payment_ids += 1
} }

View file

@ -6,7 +6,7 @@ use serde_json::json;
use monero_serai::{ use monero_serai::{
Protocol, random_scalar, Protocol, random_scalar,
wallet::address::{Network, AddressType, AddressMeta, Address}, wallet::address::{Network, AddressType, AddressMeta, MoneroAddress},
rpc::{EmptyResponse, RpcError, Rpc}, rpc::{EmptyResponse, RpcError, Rpc},
}; };
@ -18,8 +18,8 @@ pub async fn rpc() -> Rpc {
return rpc; return rpc;
} }
let addr = Address { let addr = MoneroAddress {
meta: AddressMeta { network: Network::Mainnet, kind: AddressType::Standard }, meta: AddressMeta::new(Network::Mainnet, AddressType::Standard),
spend: &random_scalar(&mut OsRng) * &ED25519_BASEPOINT_TABLE, spend: &random_scalar(&mut OsRng) * &ED25519_BASEPOINT_TABLE,
view: &random_scalar(&mut OsRng) * &ED25519_BASEPOINT_TABLE, view: &random_scalar(&mut OsRng) * &ED25519_BASEPOINT_TABLE,
} }

View file

@ -12,7 +12,7 @@ use monero_serai::{
rpc::Rpc, rpc::Rpc,
wallet::{ wallet::{
ViewPair, Scanner, ViewPair, Scanner,
address::{Network, Address}, address::{Network, MoneroAddress},
Fee, SpendableOutput, SignableTransaction as MSignableTransaction, TransactionMachine, Fee, SpendableOutput, SignableTransaction as MSignableTransaction, TransactionMachine,
}, },
}; };
@ -88,7 +88,7 @@ impl Monero {
} }
#[cfg(test)] #[cfg(test)]
fn empty_address() -> Address { fn empty_address() -> MoneroAddress {
Self::empty_scanner().address() Self::empty_scanner().address()
} }
} }
@ -105,7 +105,7 @@ impl Coin for Monero {
type SignableTransaction = SignableTransaction; type SignableTransaction = SignableTransaction;
type TransactionMachine = TransactionMachine; type TransactionMachine = TransactionMachine;
type Address = Address; type Address = MoneroAddress;
const ID: &'static [u8] = b"Monero"; const ID: &'static [u8] = b"Monero";
const CONFIRMATIONS: usize = 10; const CONFIRMATIONS: usize = 10;
@ -161,7 +161,7 @@ impl Coin for Monero {
transcript: RecommendedTranscript, transcript: RecommendedTranscript,
block_number: usize, block_number: usize,
mut inputs: Vec<Output>, mut inputs: Vec<Output>,
payments: &[(Address, u64)], payments: &[(MoneroAddress, u64)],
fee: Fee, fee: Fee,
) -> Result<SignableTransaction, CoinError> { ) -> Result<SignableTransaction, CoinError> {
let spend = keys.group_key(); let spend = keys.group_key();