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::{
random_scalar,
wallet::address::{Network, AddressType, AddressMeta, Address},
wallet::address::{Network, AddressType, AddressMeta, MoneroAddress},
};
const SPEND: [u8; 32] = hex!("f8631661f6ab4e6fda310c797330d86e23a682f20d5bc8cc27b18051191f16d7");
@ -30,7 +30,7 @@ const FEATURED_JSON: &'static str = include_str!("vectors/featured_addresses.jso
#[test]
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.kind, AddressType::Standard);
assert_eq!(addr.meta.kind.subaddress(), false);
@ -43,7 +43,7 @@ fn standard_address() {
#[test]
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.kind, AddressType::Integrated(PAYMENT_ID));
assert_eq!(addr.meta.kind.subaddress(), false);
@ -56,7 +56,7 @@ fn integrated_address() {
#[test]
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.kind, AddressType::Subaddress);
assert_eq!(addr.meta.kind.subaddress(), true);
@ -90,11 +90,11 @@ fn featured() {
let guaranteed = (features & GUARANTEED_FEATURE_BIT) == GUARANTEED_FEATURE_BIT;
let kind = AddressType::Featured(subaddress, id, guaranteed);
let meta = AddressMeta { network, kind };
let addr = Address::new(meta, spend, view);
let meta = AddressMeta::new(network, kind);
let addr = MoneroAddress::new(meta, spend, view);
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.view, view);
@ -146,7 +146,7 @@ fn featured_vectors() {
let view =
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.view, view);
@ -156,11 +156,11 @@ fn featured_vectors() {
assert_eq!(addr.guaranteed(), vector.guaranteed);
assert_eq!(
Address::new(
AddressMeta {
MoneroAddress::new(
AddressMeta::new(
network,
kind: AddressType::Featured(vector.subaddress, vector.payment_id, vector.guaranteed)
},
AddressType::Featured(vector.subaddress, vector.payment_id, vector.guaranteed)
),
spend,
view
)

View file

@ -1,3 +1,4 @@
use core::{marker::PhantomData, fmt::Debug};
use std::string::ToString;
use thiserror::Error;
@ -8,6 +9,7 @@ use curve25519_dalek::edwards::{EdwardsPoint, CompressedEdwardsY};
use base58_monero::base58::{encode_check, decode_check};
/// The network this address is for.
#[derive(Clone, Copy, PartialEq, Eq, Debug, Zeroize)]
pub enum Network {
Mainnet,
@ -26,14 +28,6 @@ pub enum 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 {
matches!(self, AddressType::Subaddress) || matches!(self, AddressType::Featured(true, ..))
}
@ -53,12 +47,40 @@ impl AddressType {
}
}
#[derive(Clone, Copy, PartialEq, Eq, Debug, Zeroize)]
pub struct AddressMeta {
/// A type which returns the byte for a given address.
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 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)]
pub enum AddressError {
#[error("invalid address byte")]
@ -75,10 +97,10 @@ pub enum AddressError {
DifferentNetwork,
}
impl AddressMeta {
impl<B: AddressBytes> AddressMeta<B> {
#[allow(clippy::wrong_self_convention)]
fn to_byte(&self) -> u8 {
let bytes = AddressType::network_bytes(self.network);
let bytes = B::network_bytes(self.network);
match self.kind {
AddressType::Standard => bytes.0,
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
fn from_byte(byte: u8) -> Result<AddressMeta, AddressError> {
fn from_byte(byte: u8) -> Result<Self, AddressError> {
let mut meta = None;
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 byte == standard => Some(AddressType::Standard),
_ if byte == integrated => Some(AddressType::Integrated([0; 8])),
@ -99,7 +126,7 @@ impl AddressMeta {
_ if byte == featured => Some(AddressType::Featured(false, None, false)),
_ => None,
} {
meta = Some(AddressMeta { network, kind });
meta = Some(AddressMeta::new(network, kind));
break;
}
}
@ -120,14 +147,23 @@ impl AddressMeta {
}
}
#[derive(Clone, Copy, PartialEq, Eq, Debug, Zeroize)]
pub struct Address {
pub meta: AddressMeta,
/// A Monero address, composed of metadata and a spend/view key.
#[derive(Clone, Copy, PartialEq, Eq, Debug)]
pub struct Address<B: AddressBytes> {
pub meta: AddressMeta<B>,
pub spend: 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 {
let mut data = vec![self.meta.to_byte()];
data.extend(self.spend.compress().to_bytes());
@ -145,12 +181,12 @@ impl ToString for Address {
}
}
impl Address {
pub fn new(meta: AddressMeta, spend: EdwardsPoint, view: EdwardsPoint) -> Address {
impl<B: AddressBytes> Address<B> {
pub fn new(meta: AddressMeta<B>, spend: EdwardsPoint, view: EdwardsPoint) -> Self {
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)?;
if raw.len() < (1 + 32 + 32) {
Err(AddressError::InvalidLength)?;
@ -197,7 +233,7 @@ impl Address {
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| {
if addr.meta.network == network {
Ok(addr)
@ -223,3 +259,17 @@ impl Address {
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.
pub mod address;
use address::{Network, AddressType, AddressMeta, Address};
use address::{Network, AddressType, AddressMeta, MoneroAddress};
mod scan;
pub use scan::SpendableOutput;
@ -180,23 +180,23 @@ impl Scanner {
}
/// Return the main address for this view pair.
pub fn address(&self) -> Address {
Address::new(
AddressMeta {
network: self.network,
kind: if self.burning_bug.is_none() {
pub fn address(&self) -> MoneroAddress {
MoneroAddress::new(
AddressMeta::new(
self.network,
if self.burning_bug.is_none() {
AddressType::Featured(false, None, true)
} else {
AddressType::Standard
},
},
),
self.pair.spend,
&self.pair.view * &ED25519_BASEPOINT_TABLE,
)
}
/// 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) {
return self.address();
}
@ -204,15 +204,15 @@ impl Scanner {
let spend = self.pair.spend + (&self.pair.subaddress(index) * &ED25519_BASEPOINT_TABLE);
self.subaddresses.insert(spend.compress(), index);
Address::new(
AddressMeta {
network: self.network,
kind: if self.burning_bug.is_none() {
MoneroAddress::new(
AddressMeta::new(
self.network,
if self.burning_bug.is_none() {
AddressType::Featured(true, None, true)
} else {
AddressType::Subaddress
},
},
),
spend,
self.pair.view * spend,
)

View file

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

View file

@ -6,7 +6,7 @@ use serde_json::json;
use monero_serai::{
Protocol, random_scalar,
wallet::address::{Network, AddressType, AddressMeta, Address},
wallet::address::{Network, AddressType, AddressMeta, MoneroAddress},
rpc::{EmptyResponse, RpcError, Rpc},
};
@ -18,8 +18,8 @@ pub async fn rpc() -> Rpc {
return rpc;
}
let addr = Address {
meta: AddressMeta { network: Network::Mainnet, kind: AddressType::Standard },
let addr = MoneroAddress {
meta: AddressMeta::new(Network::Mainnet, AddressType::Standard),
spend: &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,
wallet::{
ViewPair, Scanner,
address::{Network, Address},
address::{Network, MoneroAddress},
Fee, SpendableOutput, SignableTransaction as MSignableTransaction, TransactionMachine,
},
};
@ -88,7 +88,7 @@ impl Monero {
}
#[cfg(test)]
fn empty_address() -> Address {
fn empty_address() -> MoneroAddress {
Self::empty_scanner().address()
}
}
@ -105,7 +105,7 @@ impl Coin for Monero {
type SignableTransaction = SignableTransaction;
type TransactionMachine = TransactionMachine;
type Address = Address;
type Address = MoneroAddress;
const ID: &'static [u8] = b"Monero";
const CONFIRMATIONS: usize = 10;
@ -161,7 +161,7 @@ impl Coin for Monero {
transcript: RecommendedTranscript,
block_number: usize,
mut inputs: Vec<Output>,
payments: &[(Address, u64)],
payments: &[(MoneroAddress, u64)],
fee: Fee,
) -> Result<SignableTransaction, CoinError> {
let spend = keys.group_key();