diff --git a/processor/src/coin/mod.rs b/processor/src/coin/mod.rs index 136df59b..6cb19509 100644 --- a/processor/src/coin/mod.rs +++ b/processor/src/coin/mod.rs @@ -13,15 +13,24 @@ use frost::{ pub mod monero; pub use self::monero::Monero; -#[derive(Clone, Error, Debug)] +#[derive(Clone, Copy, Error, Debug)] pub enum CoinError { #[error("failed to connect to coin daemon")] ConnectionError, } +#[derive(Clone, Copy, PartialEq, Eq, Debug)] +pub enum OutputType { + External, + Branch, + Change, +} + pub trait Output: Sized + Clone { type Id: AsRef<[u8]>; + fn kind(&self) -> OutputType; + fn id(&self) -> Self::Id; fn amount(&self) -> u64; @@ -48,8 +57,11 @@ pub trait Coin { const MAX_INPUTS: usize; const MAX_OUTPUTS: usize; // TODO: Decide if this includes change or not + /// Address for the given group key to receive external coins to. // Doesn't have to take self, enables some level of caching which is pleasant fn address(&self, key: ::G) -> Self::Address; + /// Address for the given group key to use for scheduled branches. + fn branch_address(&self, key: ::G) -> Self::Address; async fn get_latest_block_number(&self) -> Result; async fn get_block(&self, number: usize) -> Result; @@ -59,9 +71,7 @@ pub trait Coin { key: ::G, ) -> Result, CoinError>; - // TODO: Remove - async fn is_confirmed(&self, tx: &[u8]) -> Result; - + #[allow(clippy::too_many_arguments)] async fn prepare_send( &self, keys: ThresholdKeys, @@ -69,6 +79,7 @@ pub trait Coin { block_number: usize, inputs: Vec, payments: &[(Self::Address, u64)], + change: Option<::G>, fee: Self::Fee, ) -> Result; diff --git a/processor/src/coin/monero.rs b/processor/src/coin/monero.rs index dc14fc0d..1a132147 100644 --- a/processor/src/coin/monero.rs +++ b/processor/src/coin/monero.rs @@ -21,7 +21,7 @@ use monero_serai::{ use crate::{ additional_key, - coin::{CoinError, Output as OutputTrait, Coin}, + coin::{CoinError, OutputType, Output as OutputTrait, Coin}, }; #[derive(Clone, Debug)] @@ -32,12 +32,25 @@ impl From for Output { } } +const EXTERNAL_SUBADDRESS: (u32, u32) = (0, 0); +const BRANCH_SUBADDRESS: (u32, u32) = (1, 0); +const CHANGE_SUBADDRESS: (u32, u32) = (2, 0); + impl OutputTrait for Output { // While we could use (tx, o), using the key ensures we won't be susceptible to the burning bug. - // While the Monero library offers a variant which allows senders to ensure their TXs have unique - // output keys, Serai can still be targeted using the classic burning bug + // While we already are immune, thanks to using featured address, this doesn't hurt and is + // technically more efficient. type Id = [u8; 32]; + fn kind(&self) -> OutputType { + match self.0.output.metadata.subaddress { + EXTERNAL_SUBADDRESS => OutputType::External, + BRANCH_SUBADDRESS => OutputType::Branch, + CHANGE_SUBADDRESS => OutputType::Change, + _ => panic!("unrecognized address was scanned for"), + } + } + fn id(&self) -> Self::Id { self.0.output.data.key.compress().to_bytes() } @@ -79,8 +92,18 @@ impl Monero { ViewPair::new(spend.0, self.view.clone()) } + fn address_internal(&self, spend: dfg::EdwardsPoint, subaddress: (u32, u32)) -> MoneroAddress { + self + .view_pair(spend) + .address(Network::Mainnet, AddressSpec::Featured(Some(subaddress), None, true)) + } + fn scanner(&self, spend: dfg::EdwardsPoint) -> Scanner { - Scanner::from_view(self.view_pair(spend), None) + let mut scanner = Scanner::from_view(self.view_pair(spend), None); + scanner.register_subaddress(EXTERNAL_SUBADDRESS); // Pointless as (0, 0) is already registered + scanner.register_subaddress(BRANCH_SUBADDRESS); + scanner.register_subaddress(CHANGE_SUBADDRESS); + scanner } #[cfg(test)] @@ -126,7 +149,11 @@ impl Coin for Monero { const MAX_OUTPUTS: usize = 16; fn address(&self, key: dfg::EdwardsPoint) -> Self::Address { - self.view_pair(key).address(Network::Mainnet, AddressSpec::Featured(None, None, true)) + self.address_internal(key, EXTERNAL_SUBADDRESS) + } + + fn branch_address(&self, key: dfg::EdwardsPoint) -> Self::Address { + self.address_internal(key, BRANCH_SUBADDRESS) } async fn get_latest_block_number(&self) -> Result { @@ -151,21 +178,20 @@ impl Coin for Monero { .map_err(|_| CoinError::ConnectionError)? .iter() .flat_map(|outputs| outputs.not_locked()) - .map(Output::from) + // This should be pointless as we shouldn't be able to scan for any other subaddress + // This just ensures nothing invalid makes it in + .filter_map(|output| { + if ![EXTERNAL_SUBADDRESS, BRANCH_SUBADDRESS, CHANGE_SUBADDRESS] + .contains(&output.output.metadata.subaddress) + { + return None; + } + Some(Output::from(output)) + }) .collect(), ) } - async fn is_confirmed(&self, tx: &[u8]) -> Result { - let tx_block_number = self - .rpc - .get_transaction_block_number(tx) - .await - .map_err(|_| CoinError::ConnectionError)? - .unwrap_or(usize::MAX); - Ok((self.get_latest_block_number().await?.saturating_sub(tx_block_number) + 1) >= 10) - } - async fn prepare_send( &self, keys: ThresholdKeys, @@ -173,9 +199,9 @@ impl Coin for Monero { block_number: usize, mut inputs: Vec, payments: &[(MoneroAddress, u64)], + change: Option, fee: Fee, ) -> Result { - let spend = keys.group_key(); Ok(SignableTransaction { keys, transcript, @@ -184,7 +210,7 @@ impl Coin for Monero { self.rpc.get_protocol().await.unwrap(), // TODO: Make this deterministic inputs.drain(..).map(|input| input.0).collect(), payments.to_vec(), - Some(self.address(spend)), + change.map(|change| self.address_internal(change, CHANGE_SUBADDRESS)), vec![], fee, )