From 5751204a98a110c05c0aa4d665a4de3664abf2c9 Mon Sep 17 00:00:00 2001 From: Luke Parker Date: Mon, 22 Aug 2022 08:32:09 -0400 Subject: [PATCH] Introduce a subaddress capable scanner type --- coins/monero/src/wallet/address.rs | 17 +---- coins/monero/src/wallet/mod.rs | 108 ++++++++++++++++++++++++++++- coins/monero/src/wallet/scan.rs | 37 +++++----- coins/monero/tests/send.rs | 15 ++-- processor/src/coin/monero.rs | 42 +++++------ 5 files changed, 152 insertions(+), 67 deletions(-) diff --git a/coins/monero/src/wallet/address.rs b/coins/monero/src/wallet/address.rs index 0211268e..36587215 100644 --- a/coins/monero/src/wallet/address.rs +++ b/coins/monero/src/wallet/address.rs @@ -4,15 +4,10 @@ use thiserror::Error; use zeroize::Zeroize; -use curve25519_dalek::{ - constants::ED25519_BASEPOINT_TABLE, - edwards::{EdwardsPoint, CompressedEdwardsY}, -}; +use curve25519_dalek::edwards::{EdwardsPoint, CompressedEdwardsY}; use base58_monero::base58::{encode_check, decode_check}; -use crate::wallet::ViewPair; - #[derive(Clone, Copy, PartialEq, Eq, Debug, Zeroize)] pub enum Network { Mainnet, @@ -130,16 +125,6 @@ pub struct Address { pub view: EdwardsPoint, } -impl ViewPair { - pub fn address(&self, network: Network, kind: AddressType) -> Address { - Address { - meta: AddressMeta { network, kind }, - spend: self.spend, - view: &self.view * &ED25519_BASEPOINT_TABLE, - } - } -} - impl ToString for Address { fn to_string(&self) -> String { let mut data = vec![self.meta.to_byte()]; diff --git a/coins/monero/src/wallet/mod.rs b/coins/monero/src/wallet/mod.rs index 1f0cd87f..37b9059b 100644 --- a/coins/monero/src/wallet/mod.rs +++ b/coins/monero/src/wallet/mod.rs @@ -1,6 +1,12 @@ +use std::collections::HashMap; + use zeroize::{Zeroize, ZeroizeOnDrop}; -use curve25519_dalek::{scalar::Scalar, edwards::EdwardsPoint}; +use curve25519_dalek::{ + constants::ED25519_BASEPOINT_TABLE, + scalar::Scalar, + edwards::{EdwardsPoint, CompressedEdwardsY}, +}; use crate::{hash, hash_to_scalar, serialize::write_varint, transaction::Input}; @@ -8,6 +14,7 @@ mod extra; pub(crate) use extra::{PaymentId, ExtraField, Extra}; pub mod address; +use address::{Network, AddressType, AddressMeta, Address}; mod scan; pub use scan::SpendableOutput; @@ -87,6 +94,101 @@ pub(crate) fn commitment_mask(shared_key: Scalar) -> Scalar { #[derive(Clone, Zeroize, ZeroizeOnDrop)] pub struct ViewPair { - pub spend: EdwardsPoint, - pub view: Scalar, + spend: EdwardsPoint, + view: Scalar, +} + +impl ViewPair { + pub fn new(spend: EdwardsPoint, view: Scalar) -> ViewPair { + ViewPair { spend, view } + } + + pub(crate) fn subaddress(&self, index: (u32, u32)) -> Scalar { + if index == (0, 0) { + return Scalar::zero(); + } + + hash_to_scalar( + &[ + b"SubAddr\0".as_ref(), + &self.view.to_bytes(), + &index.0.to_le_bytes(), + &index.1.to_le_bytes(), + ] + .concat(), + ) + } +} + +#[derive(Clone)] +pub struct Scanner { + pair: ViewPair, + network: Network, + guaranteed: bool, + pub(crate) subaddresses: HashMap, +} + +impl Zeroize for Scanner { + fn zeroize(&mut self) { + self.pair.zeroize(); + self.network.zeroize(); + self.guaranteed.zeroize(); + for (mut key, mut value) in self.subaddresses.drain() { + key.zeroize(); + value.zeroize(); + } + } +} + +impl Drop for Scanner { + fn drop(&mut self) { + self.zeroize(); + } +} + +impl ZeroizeOnDrop for Scanner {} + +impl Scanner { + pub fn from_view(pair: ViewPair, network: Network, guaranteed: bool) -> Scanner { + let mut subaddresses = HashMap::new(); + subaddresses.insert(pair.spend.compress(), (0, 0)); + Scanner { pair, network, guaranteed, subaddresses } + } + + pub fn address(&self) -> Address { + Address::new( + AddressMeta { + network: self.network, + kind: if self.guaranteed { + AddressType::Featured(false, None, true) + } else { + AddressType::Standard + }, + }, + self.pair.spend, + &self.pair.view * &ED25519_BASEPOINT_TABLE, + ) + } + + pub fn subaddress(&mut self, index: (u32, u32)) -> Address { + if index == (0, 0) { + return self.address(); + } + + 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.guaranteed { + AddressType::Featured(true, None, true) + } else { + AddressType::Subaddress + }, + }, + spend, + self.pair.view * spend, + ) + } } diff --git a/coins/monero/src/wallet/scan.rs b/coins/monero/src/wallet/scan.rs index 6005dda1..ceee25b8 100644 --- a/coins/monero/src/wallet/scan.rs +++ b/coins/monero/src/wallet/scan.rs @@ -8,9 +8,7 @@ use crate::{ Commitment, serialize::{read_byte, read_u32, read_u64, read_bytes, read_scalar, read_point}, transaction::{Timelock, Transaction}, - wallet::{ - ViewPair, PaymentId, Extra, uniqueness, shared_key, amount_decryption, commitment_mask, - }, + wallet::{PaymentId, Extra, Scanner, uniqueness, shared_key, amount_decryption, commitment_mask}, }; #[derive(Clone, PartialEq, Eq, Debug, Zeroize, ZeroizeOnDrop)] @@ -19,9 +17,13 @@ pub struct SpendableOutput { pub o: u8, pub key: EdwardsPoint, + // Absolute difference between the spend key and the key in this output, inclusive of any + // subaddress offset pub key_offset: Scalar, pub commitment: Commitment, + // Metadata to know how to process this output + // Does not have to be an Option since the 0 subaddress is the main address pub subaddress: (u32, u32), // Can be an Option, as extra doesn't necessarily have a payment ID, yet all Monero TXs should @@ -92,24 +94,24 @@ impl SpendableOutput { } } -impl Transaction { - pub fn scan(&self, view: &ViewPair, guaranteed: bool) -> Timelocked { - let extra = Extra::deserialize(&mut Cursor::new(&self.prefix.extra)); +impl Scanner { + pub fn scan(&self, tx: &Transaction) -> Timelocked { + let extra = Extra::deserialize(&mut Cursor::new(&tx.prefix.extra)); let keys; let extra = if let Ok(extra) = extra { keys = extra.keys(); extra } else { - return Timelocked(self.prefix.timelock, vec![]); + return Timelocked(tx.prefix.timelock, vec![]); }; let payment_id = extra.payment_id(); let mut res = vec![]; - for (o, output) in self.prefix.outputs.iter().enumerate() { + for (o, output) in tx.prefix.outputs.iter().enumerate() { for key in &keys { let (view_tag, key_offset, payment_id_xor) = shared_key( - Some(uniqueness(&self.prefix.inputs)).filter(|_| guaranteed), - &view.view, + if self.guaranteed { Some(uniqueness(&tx.prefix.inputs)) } else { None }, + &self.pair.view, key, o, ); @@ -128,7 +130,10 @@ impl Transaction { } // P - shared == spend - if (output.key - (&key_offset * &ED25519_BASEPOINT_TABLE)) != view.spend { + let subaddress = self + .subaddresses + .get(&(output.key - (&key_offset * &ED25519_BASEPOINT_TABLE)).compress()); + if subaddress.is_none() { continue; } @@ -140,7 +145,7 @@ impl Transaction { commitment.amount = output.amount; // Regular transaction } else { - let amount = match self.rct_signatures.base.ecdh_info.get(o) { + let amount = match tx.rct_signatures.base.ecdh_info.get(o) { Some(amount) => amount_decryption(*amount, key_offset), // This should never happen, yet it may be possible with miner transactions? // Using get just decreases the possibility of a panic and lets us move on in that case @@ -151,18 +156,18 @@ impl Transaction { commitment = Commitment::new(commitment_mask(key_offset), amount); // If this is a malicious commitment, move to the next output // Any other R value will calculate to a different spend key and are therefore ignorable - if Some(&commitment.calculate()) != self.rct_signatures.base.commitments.get(o) { + if Some(&commitment.calculate()) != tx.rct_signatures.base.commitments.get(o) { break; } } if commitment.amount != 0 { res.push(SpendableOutput { - tx: self.hash(), + tx: tx.hash(), o: o.try_into().unwrap(), key: output.key, - key_offset, + key_offset: key_offset + self.pair.subaddress(*subaddress.unwrap()), commitment, subaddress: (0, 0), @@ -175,6 +180,6 @@ impl Transaction { } } - Timelocked(self.prefix.timelock, res) + Timelocked(tx.prefix.timelock, res) } } diff --git a/coins/monero/tests/send.rs b/coins/monero/tests/send.rs index a7944455..4d1bdafb 100644 --- a/coins/monero/tests/send.rs +++ b/coins/monero/tests/send.rs @@ -23,11 +23,7 @@ use frost::{ use monero_serai::{ random_scalar, - wallet::{ - ViewPair, - address::{Network, AddressType}, - SignableTransaction, - }, + wallet::{ViewPair, Scanner, address::Network, SignableTransaction}, }; mod rpc; @@ -78,8 +74,9 @@ 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); + let view_pair = ViewPair::new(spend_pub, view); + let scanner = Scanner::from_view(view_pair, Network::Mainnet, false); + let addr = scanner.address(); let fee = rpc.get_fee().await.unwrap(); @@ -101,7 +98,7 @@ async fn send_core(test: usize, multisig: bool) { // Grab the largest output available let output = { - let mut outputs = tx.as_ref().unwrap().scan(&view_pair, false).ignore_timelock(); + let mut outputs = scanner.scan(tx.as_ref().unwrap()).ignore_timelock(); outputs.sort_by(|x, y| x.commitment.amount.cmp(&y.commitment.amount).reverse()); outputs.swap_remove(0) }; @@ -126,7 +123,7 @@ async fn send_core(test: usize, multisig: bool) { for i in (start + 1) .. (start + 9) { let tx = rpc.get_block_transactions(i).await.unwrap().swap_remove(0); - let output = tx.scan(&view_pair, false).ignore_timelock().swap_remove(0); + let output = scanner.scan(&tx).ignore_timelock().swap_remove(0); amount += output.commitment.amount; outputs.push(output); } diff --git a/processor/src/coin/monero.rs b/processor/src/coin/monero.rs index 9bb2764c..d4c2abdf 100644 --- a/processor/src/coin/monero.rs +++ b/processor/src/coin/monero.rs @@ -10,8 +10,8 @@ use monero_serai::{ transaction::Transaction, rpc::Rpc, wallet::{ - ViewPair, - address::{Network, AddressType, Address}, + ViewPair, Scanner, + address::{Network, Address}, Fee, SpendableOutput, SignableTransaction as MSignableTransaction, TransactionMachine, }, }; @@ -71,19 +71,23 @@ impl Monero { Monero { rpc: Rpc::new(url), view: view_key::(0).0 } } - fn view_pair(&self, spend: dfg::EdwardsPoint) -> ViewPair { - ViewPair { spend: spend.0, view: self.view } + fn scanner(&self, spend: dfg::EdwardsPoint) -> Scanner { + Scanner::from_view(ViewPair::new(spend.0, self.view), Network::Mainnet, true) } #[cfg(test)] - fn empty_view_pair(&self) -> ViewPair { + fn empty_scanner() -> Scanner { use group::Group; - self.view_pair(dfg::EdwardsPoint::generator()) + Scanner::from_view( + ViewPair::new(*dfg::EdwardsPoint::generator(), Scalar::one()), + Network::Mainnet, + false, + ) } #[cfg(test)] - fn empty_address(&self) -> Address { - self.empty_view_pair().address(Network::Mainnet, AddressType::Standard) + fn empty_address() -> Address { + Self::empty_scanner().address() } } @@ -113,7 +117,7 @@ impl Coin for Monero { const MAX_OUTPUTS: usize = 16; fn address(&self, key: dfg::EdwardsPoint) -> Self::Address { - self.view_pair(key).address(Network::Mainnet, AddressType::Featured(false, None, true)) + self.scanner(key).address() } async fn get_height(&self) -> Result { @@ -125,11 +129,8 @@ impl Coin for Monero { } async fn get_outputs(&self, block: &Self::Block, key: dfg::EdwardsPoint) -> Vec { - block - .iter() - .flat_map(|tx| tx.scan(&self.view_pair(key), true).not_locked()) - .map(Output::from) - .collect() + let scanner = self.scanner(key); + block.iter().flat_map(|tx| scanner.scan(tx).not_locked()).map(Output::from).collect() } async fn prepare_send( @@ -199,7 +200,7 @@ impl Coin for Monero { Some(serde_json::json!({ "method": "generateblocks", "params": { - "wallet_address": self.empty_address().to_string(), + "wallet_address": Self::empty_address().to_string(), "amount_of_blocks": 10 }, })), @@ -219,13 +220,8 @@ impl Coin for Monero { self.mine_block().await; } - let outputs = self - .rpc - .get_block_transactions_possible(height) - .await - .unwrap() - .swap_remove(0) - .scan(&self.empty_view_pair(), false) + let outputs = Self::empty_scanner() + .scan(&self.rpc.get_block_transactions_possible(height).await.unwrap().swap_remove(0)) .ignore_timelock(); let amount = outputs[0].commitment.amount; @@ -234,7 +230,7 @@ impl Coin for Monero { self.rpc.get_protocol().await.unwrap(), outputs, vec![(address, amount - fee)], - Some(self.empty_address()), + Some(Self::empty_address()), self.rpc.get_fee().await.unwrap(), ) .unwrap()