Add OutputType, either external, branch, or change

Used to delineate, by address received to, the intention of the output.
This commit is contained in:
Luke Parker 2023-01-07 02:39:50 -05:00
parent a646ec5aaa
commit b303649f9d
No known key found for this signature in database
2 changed files with 59 additions and 22 deletions

View file

@ -13,15 +13,24 @@ use frost::{
pub mod monero; pub mod monero;
pub use self::monero::Monero; pub use self::monero::Monero;
#[derive(Clone, Error, Debug)] #[derive(Clone, Copy, Error, Debug)]
pub enum CoinError { pub enum CoinError {
#[error("failed to connect to coin daemon")] #[error("failed to connect to coin daemon")]
ConnectionError, ConnectionError,
} }
#[derive(Clone, Copy, PartialEq, Eq, Debug)]
pub enum OutputType {
External,
Branch,
Change,
}
pub trait Output: Sized + Clone { pub trait Output: Sized + Clone {
type Id: AsRef<[u8]>; type Id: AsRef<[u8]>;
fn kind(&self) -> OutputType;
fn id(&self) -> Self::Id; fn id(&self) -> Self::Id;
fn amount(&self) -> u64; fn amount(&self) -> u64;
@ -48,8 +57,11 @@ pub trait Coin {
const MAX_INPUTS: usize; const MAX_INPUTS: usize;
const MAX_OUTPUTS: usize; // TODO: Decide if this includes change or not 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 // Doesn't have to take self, enables some level of caching which is pleasant
fn address(&self, key: <Self::Curve as Ciphersuite>::G) -> Self::Address; fn address(&self, key: <Self::Curve as Ciphersuite>::G) -> Self::Address;
/// Address for the given group key to use for scheduled branches.
fn branch_address(&self, key: <Self::Curve as Ciphersuite>::G) -> Self::Address;
async fn get_latest_block_number(&self) -> Result<usize, CoinError>; async fn get_latest_block_number(&self) -> Result<usize, CoinError>;
async fn get_block(&self, number: usize) -> Result<Self::Block, CoinError>; async fn get_block(&self, number: usize) -> Result<Self::Block, CoinError>;
@ -59,9 +71,7 @@ pub trait Coin {
key: <Self::Curve as Ciphersuite>::G, key: <Self::Curve as Ciphersuite>::G,
) -> Result<Vec<Self::Output>, CoinError>; ) -> Result<Vec<Self::Output>, CoinError>;
// TODO: Remove #[allow(clippy::too_many_arguments)]
async fn is_confirmed(&self, tx: &[u8]) -> Result<bool, CoinError>;
async fn prepare_send( async fn prepare_send(
&self, &self,
keys: ThresholdKeys<Self::Curve>, keys: ThresholdKeys<Self::Curve>,
@ -69,6 +79,7 @@ pub trait Coin {
block_number: usize, block_number: usize,
inputs: Vec<Self::Output>, inputs: Vec<Self::Output>,
payments: &[(Self::Address, u64)], payments: &[(Self::Address, u64)],
change: Option<<Self::Curve as Ciphersuite>::G>,
fee: Self::Fee, fee: Self::Fee,
) -> Result<Self::SignableTransaction, CoinError>; ) -> Result<Self::SignableTransaction, CoinError>;

View file

@ -21,7 +21,7 @@ use monero_serai::{
use crate::{ use crate::{
additional_key, additional_key,
coin::{CoinError, Output as OutputTrait, Coin}, coin::{CoinError, OutputType, Output as OutputTrait, Coin},
}; };
#[derive(Clone, Debug)] #[derive(Clone, Debug)]
@ -32,12 +32,25 @@ impl From<SpendableOutput> 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 { impl OutputTrait for Output {
// While we could use (tx, o), using the key ensures we won't be susceptible to the burning bug. // 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 // While we already are immune, thanks to using featured address, this doesn't hurt and is
// output keys, Serai can still be targeted using the classic burning bug // technically more efficient.
type Id = [u8; 32]; 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 { fn id(&self) -> Self::Id {
self.0.output.data.key.compress().to_bytes() self.0.output.data.key.compress().to_bytes()
} }
@ -79,8 +92,18 @@ impl Monero {
ViewPair::new(spend.0, self.view.clone()) 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 { 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)] #[cfg(test)]
@ -126,7 +149,11 @@ impl Coin for Monero {
const MAX_OUTPUTS: usize = 16; const MAX_OUTPUTS: usize = 16;
fn address(&self, key: dfg::EdwardsPoint) -> Self::Address { 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<usize, CoinError> { async fn get_latest_block_number(&self) -> Result<usize, CoinError> {
@ -151,21 +178,20 @@ impl Coin for Monero {
.map_err(|_| CoinError::ConnectionError)? .map_err(|_| CoinError::ConnectionError)?
.iter() .iter()
.flat_map(|outputs| outputs.not_locked()) .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(), .collect(),
) )
} }
async fn is_confirmed(&self, tx: &[u8]) -> Result<bool, CoinError> {
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( async fn prepare_send(
&self, &self,
keys: ThresholdKeys<Ed25519>, keys: ThresholdKeys<Ed25519>,
@ -173,9 +199,9 @@ impl Coin for Monero {
block_number: usize, block_number: usize,
mut inputs: Vec<Output>, mut inputs: Vec<Output>,
payments: &[(MoneroAddress, u64)], payments: &[(MoneroAddress, u64)],
change: Option<dfg::EdwardsPoint>,
fee: Fee, fee: Fee,
) -> Result<SignableTransaction, CoinError> { ) -> Result<SignableTransaction, CoinError> {
let spend = keys.group_key();
Ok(SignableTransaction { Ok(SignableTransaction {
keys, keys,
transcript, transcript,
@ -184,7 +210,7 @@ impl Coin for Monero {
self.rpc.get_protocol().await.unwrap(), // TODO: Make this deterministic self.rpc.get_protocol().await.unwrap(), // TODO: Make this deterministic
inputs.drain(..).map(|input| input.0).collect(), inputs.drain(..).map(|input| input.0).collect(),
payments.to_vec(), payments.to_vec(),
Some(self.address(spend)), change.map(|change| self.address_internal(change, CHANGE_SUBADDRESS)),
vec![], vec![],
fee, fee,
) )