diff --git a/coins/monero/src/tests/address.rs b/coins/monero/src/tests/address.rs index 2737d87d..306a2032 100644 --- a/coins/monero/src/tests/address.rs +++ b/coins/monero/src/tests/address.rs @@ -1,6 +1,13 @@ use hex_literal::hex; -use crate::wallet::address::{Network, AddressType, Address}; +use rand_core::{RngCore, OsRng}; + +use curve25519_dalek::{constants::ED25519_BASEPOINT_TABLE, edwards::CompressedEdwardsY}; + +use crate::{ + random_scalar, + wallet::address::{Network, AddressType, AddressMeta, Address}, +}; const SPEND: [u8; 32] = hex!("f8631661f6ab4e6fda310c797330d86e23a682f20d5bc8cc27b18051191f16d7"); const VIEW: [u8; 32] = hex!("4a1535063ad1fee2dabbf909d4fd9a873e29541b401f0944754e17c9a41820ce"); @@ -19,14 +26,19 @@ const SUB_VIEW: [u8; 32] = hex!("9bc2b464de90d058468522098d5610c5019c45fd1711a95 const SUBADDRESS: &'static str = "8C5zHM5ud8nGC4hC2ULiBLSWx9infi8JUUmWEat4fcTf8J4H38iWYVdFmPCA9UmfLTZxD43RsyKnGEdZkoGij6csDeUnbEB"; +const FEATURED_JSON: &'static str = include_str!("featured_addresses.json"); + #[test] fn standard_address() { let addr = Address::from_str(STANDARD, Network::Mainnet).unwrap(); assert_eq!(addr.meta.network, Network::Mainnet); assert_eq!(addr.meta.kind, AddressType::Standard); - assert_eq!(addr.meta.guaranteed, false); + assert_eq!(addr.meta.kind.subaddress(), false); + assert_eq!(addr.meta.kind.payment_id(), None); + assert_eq!(addr.meta.kind.guaranteed(), false); assert_eq!(addr.spend.compress().to_bytes(), SPEND); assert_eq!(addr.view.compress().to_bytes(), VIEW); + assert_eq!(addr.to_string(), STANDARD); } #[test] @@ -34,9 +46,12 @@ fn integrated_address() { let addr = Address::from_str(INTEGRATED, Network::Mainnet).unwrap(); assert_eq!(addr.meta.network, Network::Mainnet); assert_eq!(addr.meta.kind, AddressType::Integrated(PAYMENT_ID)); - assert_eq!(addr.meta.guaranteed, false); + assert_eq!(addr.meta.kind.subaddress(), false); + assert_eq!(addr.meta.kind.payment_id(), Some(PAYMENT_ID)); + assert_eq!(addr.meta.kind.guaranteed(), false); assert_eq!(addr.spend.compress().to_bytes(), SPEND); assert_eq!(addr.view.compress().to_bytes(), VIEW); + assert_eq!(addr.to_string(), INTEGRATED); } #[test] @@ -44,7 +59,113 @@ fn subaddress() { let addr = Address::from_str(SUBADDRESS, Network::Mainnet).unwrap(); assert_eq!(addr.meta.network, Network::Mainnet); assert_eq!(addr.meta.kind, AddressType::Subaddress); - assert_eq!(addr.meta.guaranteed, false); + assert_eq!(addr.meta.kind.subaddress(), true); + assert_eq!(addr.meta.kind.payment_id(), None); + assert_eq!(addr.meta.kind.guaranteed(), false); assert_eq!(addr.spend.compress().to_bytes(), SUB_SPEND); assert_eq!(addr.view.compress().to_bytes(), SUB_VIEW); + assert_eq!(addr.to_string(), SUBADDRESS); +} + +#[test] +fn featured() { + for (network, first) in + [(Network::Mainnet, 'C'), (Network::Testnet, 'K'), (Network::Stagenet, 'F')] + { + for _ in 0 .. 100 { + let spend = &random_scalar(&mut OsRng) * &ED25519_BASEPOINT_TABLE; + let view = &random_scalar(&mut OsRng) * &ED25519_BASEPOINT_TABLE; + + for features in 0 .. (1 << 3) { + const SUBADDRESS_FEATURE_BIT: u8 = 1; + const INTEGRATED_FEATURE_BIT: u8 = 1 << 1; + const GUARANTEED_FEATURE_BIT: u8 = 1 << 2; + + let subaddress = (features & SUBADDRESS_FEATURE_BIT) == SUBADDRESS_FEATURE_BIT; + + let mut id = [0; 8]; + OsRng.fill_bytes(&mut id); + let id = Some(id).filter(|_| (features & INTEGRATED_FEATURE_BIT) == INTEGRATED_FEATURE_BIT); + + 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); + + assert_eq!(addr.to_string().chars().next().unwrap(), first); + assert_eq!(Address::from_str(&addr.to_string(), network).unwrap(), addr); + + assert_eq!(addr.spend, spend); + assert_eq!(addr.view, view); + + assert_eq!(addr.subaddress(), subaddress); + assert_eq!(addr.payment_id(), id); + assert_eq!(addr.guaranteed(), guaranteed); + } + } + } +} + +#[test] +fn featured_vectors() { + #[derive(serde::Deserialize)] + struct Vector { + address: String, + + network: String, + spend: String, + view: String, + + subaddress: bool, + integrated: bool, + payment_id: Option<[u8; 8]>, + guaranteed: bool, + } + + let vectors = serde_json::from_str::>(FEATURED_JSON).unwrap(); + for vector in vectors { + let first = vector.address.chars().next().unwrap(); + let network = match vector.network.as_str() { + "Mainnet" => { + assert_eq!(first, 'C'); + Network::Mainnet + } + "Testnet" => { + assert_eq!(first, 'K'); + Network::Testnet + } + "Stagenet" => { + assert_eq!(first, 'F'); + Network::Stagenet + } + _ => panic!("Unknown network"), + }; + let spend = + CompressedEdwardsY::from_slice(&hex::decode(vector.spend).unwrap()).decompress().unwrap(); + let view = + CompressedEdwardsY::from_slice(&hex::decode(vector.view).unwrap()).decompress().unwrap(); + + let addr = Address::from_str(&vector.address, network).unwrap(); + assert_eq!(addr.spend, spend); + assert_eq!(addr.view, view); + + assert_eq!(addr.subaddress(), vector.subaddress); + assert_eq!(vector.integrated, vector.payment_id.is_some()); + assert_eq!(addr.payment_id(), vector.payment_id); + assert_eq!(addr.guaranteed(), vector.guaranteed); + + assert_eq!( + Address::new( + AddressMeta { + network, + kind: AddressType::Featured(vector.subaddress, vector.payment_id, vector.guaranteed) + }, + spend, + view + ) + .to_string(), + vector.address + ); + } } diff --git a/coins/monero/src/tests/featured_addresses.json b/coins/monero/src/tests/featured_addresses.json new file mode 100644 index 00000000..dbb83fbf --- /dev/null +++ b/coins/monero/src/tests/featured_addresses.json @@ -0,0 +1,230 @@ +[ + { + "address": "CjWdTpuDaZ69nTGxzm9YarR82YDYFECi1WaaREZTMy5yDsjaRX5bC3cbC3JpcrBPd7YYpjoWKuBMidgGaKBK5Jye2v3pYyUDn", + "network": "Mainnet", + "spend": "258dfe7eef9be934839f3b8e0d40e79035fe85879c0a9eb0d7372ae2deb0004c", + "view": "f91382373045f3cc69233254ab0406bc9e008707569ff9db4718654812d839df", + "subaddress": false, + "integrated": false, + "guaranteed": false + }, + { + "address": "CjWdTpuDaZ69nTGxzm9YarR82YDYFECi1WaaREZTMy5yDsjaRX5bC3cbC3JpcrBPd7YYpjoWKuBMidgGaKBK5Jye2v3wfMHCy", + "network": "Mainnet", + "spend": "258dfe7eef9be934839f3b8e0d40e79035fe85879c0a9eb0d7372ae2deb0004c", + "view": "f91382373045f3cc69233254ab0406bc9e008707569ff9db4718654812d839df", + "subaddress": true, + "integrated": false, + "guaranteed": false + }, + { + "address": "CjWdTpuDaZ69nTGxzm9YarR82YDYFECi1WaaREZTMy5yDsjaRX5bC3cbC3JpcrBPd7YYpjoWKuBMidgGaKBK5JyeeJTo4p5ayvj36PStM5AX", + "network": "Mainnet", + "spend": "258dfe7eef9be934839f3b8e0d40e79035fe85879c0a9eb0d7372ae2deb0004c", + "view": "f91382373045f3cc69233254ab0406bc9e008707569ff9db4718654812d839df", + "subaddress": false, + "integrated": true, + "payment_id": [46, 48, 134, 34, 245, 148, 243, 195], + "guaranteed": false + }, + { + "address": "CjWdTpuDaZ69nTGxzm9YarR82YDYFECi1WaaREZTMy5yDsjaRX5bC3cbC3JpcrBPd7YYpjoWKuBMidgGaKBK5JyeeJWv5WqMCNE2hRs9rJfy", + "network": "Mainnet", + "spend": "258dfe7eef9be934839f3b8e0d40e79035fe85879c0a9eb0d7372ae2deb0004c", + "view": "f91382373045f3cc69233254ab0406bc9e008707569ff9db4718654812d839df", + "subaddress": true, + "integrated": true, + "payment_id": [153, 176, 98, 204, 151, 27, 197, 168], + "guaranteed": false + }, + { + "address": "CjWdTpuDaZ69nTGxzm9YarR82YDYFECi1WaaREZTMy5yDsjaRX5bC3cbC3JpcrBPd7YYpjoWKuBMidgGaKBK5Jye2v4DwqwH1", + "network": "Mainnet", + "spend": "258dfe7eef9be934839f3b8e0d40e79035fe85879c0a9eb0d7372ae2deb0004c", + "view": "f91382373045f3cc69233254ab0406bc9e008707569ff9db4718654812d839df", + "subaddress": false, + "integrated": false, + "guaranteed": true + }, + { + "address": "CjWdTpuDaZ69nTGxzm9YarR82YDYFECi1WaaREZTMy5yDsjaRX5bC3cbC3JpcrBPd7YYpjoWKuBMidgGaKBK5Jye2v4Pyz8bD", + "network": "Mainnet", + "spend": "258dfe7eef9be934839f3b8e0d40e79035fe85879c0a9eb0d7372ae2deb0004c", + "view": "f91382373045f3cc69233254ab0406bc9e008707569ff9db4718654812d839df", + "subaddress": true, + "integrated": false, + "guaranteed": true + }, + { + "address": "CjWdTpuDaZ69nTGxzm9YarR82YDYFECi1WaaREZTMy5yDsjaRX5bC3cbC3JpcrBPd7YYpjoWKuBMidgGaKBK5JyeeJcwt7hykou237MqZZDA", + "network": "Mainnet", + "spend": "258dfe7eef9be934839f3b8e0d40e79035fe85879c0a9eb0d7372ae2deb0004c", + "view": "f91382373045f3cc69233254ab0406bc9e008707569ff9db4718654812d839df", + "subaddress": false, + "integrated": true, + "payment_id": [88, 37, 149, 111, 171, 108, 120, 181], + "guaranteed": true + }, + { + "address": "CjWdTpuDaZ69nTGxzm9YarR82YDYFECi1WaaREZTMy5yDsjaRX5bC3cbC3JpcrBPd7YYpjoWKuBMidgGaKBK5JyeeJfTrFAp69u2MYbf5YeN", + "network": "Mainnet", + "spend": "258dfe7eef9be934839f3b8e0d40e79035fe85879c0a9eb0d7372ae2deb0004c", + "view": "f91382373045f3cc69233254ab0406bc9e008707569ff9db4718654812d839df", + "subaddress": true, + "integrated": true, + "payment_id": [125, 69, 155, 152, 140, 160, 157, 186], + "guaranteed": true + }, + { + "address": "Kgx5uCVsMSEVm7seL8tjyRGmmVXjWfEowKpKjgaXUGVyMViBYMh13VQ4mfqpB7zEVVcJx3E8FFgAuQ8cq6mg5x712U9w7ScYA", + "network": "Testnet", + "spend": "bba3a8a5bb47f7abf2e2dffeaf43385e4b308fd63a9ff6707e355f3b0a6c247a", + "view": "881713a4fa9777168a54bbdcb75290d319fb92fdf1026a8a4b125a8e341de8ab", + "subaddress": false, + "integrated": false, + "guaranteed": false + }, + { + "address": "Kgx5uCVsMSEVm7seL8tjyRGmmVXjWfEowKpKjgaXUGVyMViBYMh13VQ4mfqpB7zEVVcJx3E8FFgAuQ8cq6mg5x712UA2gCrT1", + "network": "Testnet", + "spend": "bba3a8a5bb47f7abf2e2dffeaf43385e4b308fd63a9ff6707e355f3b0a6c247a", + "view": "881713a4fa9777168a54bbdcb75290d319fb92fdf1026a8a4b125a8e341de8ab", + "subaddress": true, + "integrated": false, + "guaranteed": false + }, + { + "address": "Kgx5uCVsMSEVm7seL8tjyRGmmVXjWfEowKpKjgaXUGVyMViBYMh13VQ4mfqpB7zEVVcJx3E8FFgAuQ8cq6mg5x71Vc1DbPKwJu81cxJjqBkS", + "network": "Testnet", + "spend": "bba3a8a5bb47f7abf2e2dffeaf43385e4b308fd63a9ff6707e355f3b0a6c247a", + "view": "881713a4fa9777168a54bbdcb75290d319fb92fdf1026a8a4b125a8e341de8ab", + "subaddress": false, + "integrated": true, + "payment_id": [92, 225, 118, 220, 39, 3, 72, 51], + "guaranteed": false + }, + { + "address": "Kgx5uCVsMSEVm7seL8tjyRGmmVXjWfEowKpKjgaXUGVyMViBYMh13VQ4mfqpB7zEVVcJx3E8FFgAuQ8cq6mg5x71Vc2o1rPMaXN31Fe5J6dn", + "network": "Testnet", + "spend": "bba3a8a5bb47f7abf2e2dffeaf43385e4b308fd63a9ff6707e355f3b0a6c247a", + "view": "881713a4fa9777168a54bbdcb75290d319fb92fdf1026a8a4b125a8e341de8ab", + "subaddress": true, + "integrated": true, + "payment_id": [20, 120, 47, 89, 72, 165, 233, 115], + "guaranteed": false + }, + { + "address": "Kgx5uCVsMSEVm7seL8tjyRGmmVXjWfEowKpKjgaXUGVyMViBYMh13VQ4mfqpB7zEVVcJx3E8FFgAuQ8cq6mg5x712UAQHCRZ4", + "network": "Testnet", + "spend": "bba3a8a5bb47f7abf2e2dffeaf43385e4b308fd63a9ff6707e355f3b0a6c247a", + "view": "881713a4fa9777168a54bbdcb75290d319fb92fdf1026a8a4b125a8e341de8ab", + "subaddress": false, + "integrated": false, + "guaranteed": true + }, + { + "address": "Kgx5uCVsMSEVm7seL8tjyRGmmVXjWfEowKpKjgaXUGVyMViBYMh13VQ4mfqpB7zEVVcJx3E8FFgAuQ8cq6mg5x712UAUzqaii", + "network": "Testnet", + "spend": "bba3a8a5bb47f7abf2e2dffeaf43385e4b308fd63a9ff6707e355f3b0a6c247a", + "view": "881713a4fa9777168a54bbdcb75290d319fb92fdf1026a8a4b125a8e341de8ab", + "subaddress": true, + "integrated": false, + "guaranteed": true + }, + { + "address": "Kgx5uCVsMSEVm7seL8tjyRGmmVXjWfEowKpKjgaXUGVyMViBYMh13VQ4mfqpB7zEVVcJx3E8FFgAuQ8cq6mg5x71VcAsfQc3gJQ2gHLd5DiQ", + "network": "Testnet", + "spend": "bba3a8a5bb47f7abf2e2dffeaf43385e4b308fd63a9ff6707e355f3b0a6c247a", + "view": "881713a4fa9777168a54bbdcb75290d319fb92fdf1026a8a4b125a8e341de8ab", + "subaddress": false, + "integrated": true, + "payment_id": [193, 149, 123, 214, 180, 205, 195, 91], + "guaranteed": true + }, + { + "address": "Kgx5uCVsMSEVm7seL8tjyRGmmVXjWfEowKpKjgaXUGVyMViBYMh13VQ4mfqpB7zEVVcJx3E8FFgAuQ8cq6mg5x71VcDBAD5jbZQ3AMHFyvQB", + "network": "Testnet", + "spend": "bba3a8a5bb47f7abf2e2dffeaf43385e4b308fd63a9ff6707e355f3b0a6c247a", + "view": "881713a4fa9777168a54bbdcb75290d319fb92fdf1026a8a4b125a8e341de8ab", + "subaddress": true, + "integrated": true, + "payment_id": [205, 170, 65, 0, 51, 175, 251, 184], + "guaranteed": true + }, + { + "address": "FSDinqdKK54PbjF73GgW3nUpf7bF8QbyxFCUurENmUyeEfSxSLL2hxwANBLzq1A8gTSAzzEn65hKjetA8o5BvjV61VPJnBtTP", + "network": "Stagenet", + "spend": "4cd503040f5e43871bf37d8ca7177da655bda410859af754e24e7b44437f3151", + "view": "af60d42b6c6e4437fd93eb32657a14967efa393630d7aee27b5973c8e1c5ad39", + "subaddress": false, + "integrated": false, + "guaranteed": false + }, + { + "address": "FSDinqdKK54PbjF73GgW3nUpf7bF8QbyxFCUurENmUyeEfSxSLL2hxwANBLzq1A8gTSAzzEn65hKjetA8o5BvjV61VPUrwMvP", + "network": "Stagenet", + "spend": "4cd503040f5e43871bf37d8ca7177da655bda410859af754e24e7b44437f3151", + "view": "af60d42b6c6e4437fd93eb32657a14967efa393630d7aee27b5973c8e1c5ad39", + "subaddress": true, + "integrated": false, + "guaranteed": false + }, + { + "address": "FSDinqdKK54PbjF73GgW3nUpf7bF8QbyxFCUurENmUyeEfSxSLL2hxwANBLzq1A8gTSAzzEn65hKjetA8o5BvjV6AY5ECEhP5Nr1aCRPXdxk", + "network": "Stagenet", + "spend": "4cd503040f5e43871bf37d8ca7177da655bda410859af754e24e7b44437f3151", + "view": "af60d42b6c6e4437fd93eb32657a14967efa393630d7aee27b5973c8e1c5ad39", + "subaddress": false, + "integrated": true, + "payment_id": [173, 149, 78, 64, 215, 211, 66, 170], + "guaranteed": false + }, + { + "address": "FSDinqdKK54PbjF73GgW3nUpf7bF8QbyxFCUurENmUyeEfSxSLL2hxwANBLzq1A8gTSAzzEn65hKjetA8o5BvjV6AY882kTUS1D2LttnPvTR", + "network": "Stagenet", + "spend": "4cd503040f5e43871bf37d8ca7177da655bda410859af754e24e7b44437f3151", + "view": "af60d42b6c6e4437fd93eb32657a14967efa393630d7aee27b5973c8e1c5ad39", + "subaddress": true, + "integrated": true, + "payment_id": [254, 159, 186, 162, 1, 8, 156, 108], + "guaranteed": false + }, + { + "address": "FSDinqdKK54PbjF73GgW3nUpf7bF8QbyxFCUurENmUyeEfSxSLL2hxwANBLzq1A8gTSAzzEn65hKjetA8o5BvjV61VPpBBo8F", + "network": "Stagenet", + "spend": "4cd503040f5e43871bf37d8ca7177da655bda410859af754e24e7b44437f3151", + "view": "af60d42b6c6e4437fd93eb32657a14967efa393630d7aee27b5973c8e1c5ad39", + "subaddress": false, + "integrated": false, + "guaranteed": true + }, + { + "address": "FSDinqdKK54PbjF73GgW3nUpf7bF8QbyxFCUurENmUyeEfSxSLL2hxwANBLzq1A8gTSAzzEn65hKjetA8o5BvjV61VPuUJX3b", + "network": "Stagenet", + "spend": "4cd503040f5e43871bf37d8ca7177da655bda410859af754e24e7b44437f3151", + "view": "af60d42b6c6e4437fd93eb32657a14967efa393630d7aee27b5973c8e1c5ad39", + "subaddress": true, + "integrated": false, + "guaranteed": true + }, + { + "address": "FSDinqdKK54PbjF73GgW3nUpf7bF8QbyxFCUurENmUyeEfSxSLL2hxwANBLzq1A8gTSAzzEn65hKjetA8o5BvjV6AYCZPxVAoDu21DryMoto", + "network": "Stagenet", + "spend": "4cd503040f5e43871bf37d8ca7177da655bda410859af754e24e7b44437f3151", + "view": "af60d42b6c6e4437fd93eb32657a14967efa393630d7aee27b5973c8e1c5ad39", + "subaddress": false, + "integrated": true, + "payment_id": [3, 115, 230, 129, 172, 108, 116, 235], + "guaranteed": true + }, + { + "address": "FSDinqdKK54PbjF73GgW3nUpf7bF8QbyxFCUurENmUyeEfSxSLL2hxwANBLzq1A8gTSAzzEn65hKjetA8o5BvjV6AYFYCqKQAWL18KkpBQ8R", + "network": "Stagenet", + "spend": "4cd503040f5e43871bf37d8ca7177da655bda410859af754e24e7b44437f3151", + "view": "af60d42b6c6e4437fd93eb32657a14967efa393630d7aee27b5973c8e1c5ad39", + "subaddress": true, + "integrated": true, + "payment_id": [94, 122, 63, 167, 209, 225, 14, 180], + "guaranteed": true + } +] diff --git a/coins/monero/src/wallet/address.rs b/coins/monero/src/wallet/address.rs index b625596e..0211268e 100644 --- a/coins/monero/src/wallet/address.rs +++ b/coins/monero/src/wallet/address.rs @@ -25,23 +25,41 @@ pub enum AddressType { Standard, Integrated([u8; 8]), Subaddress, + Featured(bool, Option<[u8; 8]>, bool), } impl AddressType { - fn network_bytes(network: Network) -> (u8, u8, u8) { + fn network_bytes(network: Network) -> (u8, u8, u8, u8) { match network { - Network::Mainnet => (18, 19, 42), - Network::Testnet => (53, 54, 63), - Network::Stagenet => (24, 25, 36), + 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, ..)) + } + + pub fn payment_id(&self) -> Option<[u8; 8]> { + if let AddressType::Integrated(id) = self { + Some(*id) + } else if let AddressType::Featured(_, id, _) = self { + *id + } else { + None + } + } + + pub fn guaranteed(&self) -> bool { + matches!(self, AddressType::Featured(_, _, true)) + } } #[derive(Clone, Copy, PartialEq, Eq, Debug, Zeroize)] pub struct AddressMeta { pub network: Network, pub kind: AddressType, - pub guaranteed: bool, } #[derive(Clone, Error, Debug)] @@ -52,45 +70,57 @@ pub enum AddressError { InvalidEncoding, #[error("invalid length")] InvalidLength, - #[error("different network than expected")] - DifferentNetwork, #[error("invalid key")] InvalidKey, + #[error("unknown features")] + UnknownFeatures, + #[error("different network than expected")] + DifferentNetwork, } impl AddressMeta { #[allow(clippy::wrong_self_convention)] fn to_byte(&self) -> u8 { let bytes = AddressType::network_bytes(self.network); - let byte = match self.kind { + match self.kind { AddressType::Standard => bytes.0, AddressType::Integrated(_) => bytes.1, AddressType::Subaddress => bytes.2, - }; - byte | (if self.guaranteed { 1 << 7 } else { 0 }) + AddressType::Featured(..) => bytes.3, + } } - // Returns an incomplete type in the case of Integrated addresses + // Returns an incomplete type in the case of Integrated/Featured addresses fn from_byte(byte: u8) -> Result { - let actual = byte & 0b01111111; - let guaranteed = (byte >> 7) == 1; - let mut meta = None; for network in [Network::Mainnet, Network::Testnet, Network::Stagenet] { - let (standard, integrated, subaddress) = AddressType::network_bytes(network); - if let Some(kind) = match actual { - _ if actual == standard => Some(AddressType::Standard), - _ if actual == integrated => Some(AddressType::Integrated([0; 8])), - _ if actual == subaddress => Some(AddressType::Subaddress), + let (standard, integrated, subaddress, featured) = AddressType::network_bytes(network); + if let Some(kind) = match byte { + _ if byte == standard => Some(AddressType::Standard), + _ if byte == integrated => Some(AddressType::Integrated([0; 8])), + _ if byte == subaddress => Some(AddressType::Subaddress), + _ if byte == featured => Some(AddressType::Featured(false, None, false)), _ => None, } { - meta = Some(AddressMeta { network, kind, guaranteed }); + meta = Some(AddressMeta { network, kind }); break; } } meta.ok_or(AddressError::InvalidByte) } + + pub fn subaddress(&self) -> bool { + self.kind.subaddress() + } + + pub fn payment_id(&self) -> Option<[u8; 8]> { + self.kind.payment_id() + } + + pub fn guaranteed(&self) -> bool { + self.kind.guaranteed() + } } #[derive(Clone, Copy, PartialEq, Eq, Debug, Zeroize)] @@ -101,9 +131,9 @@ pub struct Address { } impl ViewPair { - pub fn address(&self, network: Network, kind: AddressType, guaranteed: bool) -> Address { + pub fn address(&self, network: Network, kind: AddressType) -> Address { Address { - meta: AddressMeta { network, kind, guaranteed }, + meta: AddressMeta { network, kind }, spend: self.spend, view: &self.view * &ED25519_BASEPOINT_TABLE, } @@ -115,7 +145,15 @@ impl ToString for Address { let mut data = vec![self.meta.to_byte()]; data.extend(self.spend.compress().to_bytes()); data.extend(self.view.compress().to_bytes()); - if let AddressType::Integrated(id) = self.meta.kind { + if let AddressType::Featured(subaddress, payment_id, guaranteed) = self.meta.kind { + // Technically should be a VarInt, yet we don't have enough features it's needed + data.push( + (if subaddress { 1 } else { 0 }) + + ((if payment_id.is_some() { 1 } else { 0 }) << 1) + + ((if guaranteed { 1 } else { 0 }) << 2), + ); + } + if let Some(id) = self.meta.kind.payment_id() { data.extend(id); } encode_check(&data).unwrap() @@ -123,36 +161,80 @@ impl ToString for Address { } impl Address { - pub fn from_str(s: &str, network: Network) -> Result { + pub fn new(meta: AddressMeta, spend: EdwardsPoint, view: EdwardsPoint) -> Address { + Address { meta, spend, view } + } + + pub fn from_str_raw(s: &str) -> Result { let raw = decode_check(s).map_err(|_| AddressError::InvalidEncoding)?; - if raw.len() == 1 { + if raw.len() < (1 + 32 + 32) { Err(AddressError::InvalidLength)?; } let mut meta = AddressMeta::from_byte(raw[0])?; - if meta.network != network { - Err(AddressError::DifferentNetwork)?; - } - - let len = match meta.kind { - AddressType::Standard | AddressType::Subaddress => 65, - AddressType::Integrated(_) => 73, - }; - if raw.len() != len { - Err(AddressError::InvalidLength)?; - } - let spend = CompressedEdwardsY(raw[1 .. 33].try_into().unwrap()) .decompress() .ok_or(AddressError::InvalidKey)?; let view = CompressedEdwardsY(raw[33 .. 65].try_into().unwrap()) .decompress() .ok_or(AddressError::InvalidKey)?; + let mut read = 65; - if let AddressType::Integrated(ref mut payment_id) = meta.kind { - payment_id.copy_from_slice(&raw[65 .. 73]); + if matches!(meta.kind, AddressType::Featured(..)) { + if raw[read] >= (2 << 3) { + Err(AddressError::UnknownFeatures)?; + } + + let subaddress = (raw[read] & 1) == 1; + let integrated = ((raw[read] >> 1) & 1) == 1; + let guaranteed = ((raw[read] >> 2) & 1) == 1; + + meta.kind = + AddressType::Featured(subaddress, Some([0; 8]).filter(|_| integrated), guaranteed); + read += 1; + } + + // Update read early so we can verify the length + if meta.kind.payment_id().is_some() { + read += 8; + } + if raw.len() != read { + Err(AddressError::InvalidLength)?; + } + + if let AddressType::Integrated(ref mut id) = meta.kind { + id.copy_from_slice(&raw[(read - 8) .. read]); + } + if let AddressType::Featured(_, Some(ref mut id), _) = meta.kind { + id.copy_from_slice(&raw[(read - 8) .. read]); } Ok(Address { meta, spend, view }) } + + pub fn from_str(s: &str, network: Network) -> Result { + Self::from_str_raw(s).and_then(|addr| { + if addr.meta.network == network { + Ok(addr) + } else { + Err(AddressError::DifferentNetwork)? + } + }) + } + + pub fn network(&self) -> Network { + self.meta.network + } + + pub fn subaddress(&self) -> bool { + self.meta.subaddress() + } + + pub fn payment_id(&self) -> Option<[u8; 8]> { + self.meta.payment_id() + } + + pub fn guaranteed(&self) -> bool { + self.meta.guaranteed() + } } diff --git a/coins/monero/src/wallet/send/mod.rs b/coins/monero/src/wallet/send/mod.rs index af5b5e89..19194cb1 100644 --- a/coins/monero/src/wallet/send/mod.rs +++ b/coins/monero/src/wallet/send/mod.rs @@ -21,9 +21,8 @@ use crate::{ transaction::{Input, Output, Timelock, TransactionPrefix, Transaction}, rpc::{Rpc, RpcError}, wallet::{ - address::{AddressType, Address}, - SpendableOutput, Decoys, PaymentId, ExtraField, Extra, key_image_sort, uniqueness, shared_key, - commitment_mask, amount_encryption, + address::Address, SpendableOutput, Decoys, PaymentId, ExtraField, Extra, key_image_sort, + uniqueness, shared_key, commitment_mask, amount_encryption, }, }; #[cfg(feature = "multisig")] @@ -53,19 +52,20 @@ impl SendOutput { ) -> SendOutput { let r = random_scalar(rng); let (view_tag, shared_key) = - shared_key(Some(unique).filter(|_| output.0.meta.guaranteed), &r, &output.0.view, o); + shared_key(Some(unique).filter(|_| output.0.meta.kind.guaranteed()), &r, &output.0.view, o); + + if output.0.meta.kind.payment_id().is_some() { + unimplemented!("integrated addresses aren't currently supported"); + } - let spend = output.0.spend; SendOutput { - R: match output.0.meta.kind { - AddressType::Standard => &r * &ED25519_BASEPOINT_TABLE, - AddressType::Integrated(_) => { - unimplemented!("SendOutput::new doesn't support Integrated addresses") - } - AddressType::Subaddress => r * spend, + R: if !output.0.meta.kind.subaddress() { + &r * &ED25519_BASEPOINT_TABLE + } else { + r * output.0.spend }, view_tag, - dest: ((&shared_key * &ED25519_BASEPOINT_TABLE) + spend), + dest: ((&shared_key * &ED25519_BASEPOINT_TABLE) + output.0.spend), commitment: Commitment::new(commitment_mask(shared_key), output.1), amount: amount_encryption(output.1, shared_key), } @@ -179,10 +179,13 @@ impl SignableTransaction { fee_rate: Fee, ) -> Result { // Make sure all addresses are valid - let test = |addr: Address| match addr.meta.kind { - AddressType::Standard => Ok(()), - AddressType::Integrated(..) => Err(TransactionError::InvalidAddress), - AddressType::Subaddress => Ok(()), + let test = |addr: Address| { + if addr.meta.kind.payment_id().is_some() { + // TODO + Err(TransactionError::InvalidAddress) + } else { + Ok(()) + } }; for payment in &payments { diff --git a/coins/monero/tests/rpc.rs b/coins/monero/tests/rpc.rs index b80e9b6a..6443252b 100644 --- a/coins/monero/tests/rpc.rs +++ b/coins/monero/tests/rpc.rs @@ -19,7 +19,7 @@ pub async fn rpc() -> Rpc { } let addr = Address { - meta: AddressMeta { network: Network::Mainnet, kind: AddressType::Standard, guaranteed: false }, + meta: AddressMeta { network: Network::Mainnet, kind: AddressType::Standard }, spend: &random_scalar(&mut OsRng) * &ED25519_BASEPOINT_TABLE, view: &random_scalar(&mut OsRng) * &ED25519_BASEPOINT_TABLE, } diff --git a/coins/monero/tests/send.rs b/coins/monero/tests/send.rs index 98193baf..a7944455 100644 --- a/coins/monero/tests/send.rs +++ b/coins/monero/tests/send.rs @@ -79,7 +79,7 @@ async fn send_core(test: usize, multisig: bool) { } let view_pair = ViewPair { view, spend: spend_pub }; - let addr = view_pair.address(Network::Mainnet, AddressType::Standard, false); + let addr = view_pair.address(Network::Mainnet, AddressType::Standard); let fee = rpc.get_fee().await.unwrap();