diff --git a/processor/Cargo.toml b/processor/Cargo.toml index c82e4586..14c0c487 100644 --- a/processor/Cargo.toml +++ b/processor/Cargo.toml @@ -21,4 +21,5 @@ monero = { version = "0.16", features = ["experimental"] } monero-serai = { path = "../coins/monero", features = ["multisig"] } [dev-dependencies] +rand = "0.8" tokio = { version = "1", features = ["full"] } diff --git a/processor/src/coins/monero.rs b/processor/src/coins/monero.rs index 614b01a4..55b5e911 100644 --- a/processor/src/coins/monero.rs +++ b/processor/src/coins/monero.rs @@ -1,24 +1,30 @@ use async_trait::async_trait; use rand_core::{RngCore, CryptoRng}; -use curve25519_dalek::scalar::Scalar; +use curve25519_dalek::{scalar::Scalar, edwards::CompressedEdwardsY}; use dalek_ff_group as dfg; use frost::MultisigKeys; use monero::util::address::Address; -use monero_serai::{frost::Ed25519, rpc::Rpc, wallet::{SpendableOutput, SignableTransaction}}; +use monero_serai::{ + frost::Ed25519, + transaction::Transaction, + rpc::Rpc, + wallet::{SpendableOutput, SignableTransaction} +}; use crate::{Output as OutputTrait, CoinError, Coin, view_key}; pub struct Output(SpendableOutput); impl OutputTrait for Output { - // If Monero ever does support more than 255 outputs at once, which it could, this u8 could be a - // u16 which serializes as little endian, dropping the last byte if empty, without conflict - type Id = ([u8; 32], u8); + // 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 + type Id = CompressedEdwardsY; fn id(&self) -> Self::Id { - (self.0.tx, self.0.o.try_into().unwrap()) + self.0.key.compress() } fn amount(&self) -> u64 { @@ -59,34 +65,32 @@ impl Coin for Monero { type Curve = Ed25519; type Output = Output; + type Block = Vec; type SignableTransaction = SignableTransaction; type Address = Address; fn id() -> &'static [u8] { b"Monero" } - async fn confirmations() -> usize { 10 } + fn confirmations() -> usize { 10 } // Testnet TX bb4d188a4c571f2f0de70dca9d475abc19078c10ffa8def26dd4f63ce1bcfd79 uses 146 inputs // while using less than 100kb of space, albeit with just 2 outputs (though outputs share a BP) // The TX size limit is half the contextual median block weight, where said weight is >= 300,000 // This means any TX which fits into 150kb will be accepted by Monero // 128, even with 16 outputs, should fit into 100kb. Further efficiency by 192 may be viable // TODO: Get hard numbers and tune - async fn max_inputs() -> usize { 128 } - async fn max_outputs() -> usize { 16 } + fn max_inputs() -> usize { 128 } + fn max_outputs() -> usize { 16 } async fn get_height(&self) -> Result { self.rpc.get_height().await.map_err(|_| CoinError::ConnectionError) } - async fn get_outputs_in_block( - &self, - height: usize, - key: dfg::EdwardsPoint - ) -> Result, CoinError> { - Ok( - self.rpc.get_block_transactions_possible(height).await.map_err(|_| CoinError::ConnectionError)? - .iter().flat_map(|tx| tx.scan(self.view, key.0)).map(Output::from).collect() - ) + async fn get_block(&self, height: usize) -> Result { + self.rpc.get_block_transactions_possible(height).await.map_err(|_| CoinError::ConnectionError) + } + + async fn get_outputs(&self, block: &Self::Block, key: dfg::EdwardsPoint) -> Vec { + block.iter().flat_map(|tx| tx.scan(self.view, key.0)).map(Output::from).collect() } async fn prepare_send( diff --git a/processor/src/lib.rs b/processor/src/lib.rs index 037c45b0..e87ef456 100644 --- a/processor/src/lib.rs +++ b/processor/src/lib.rs @@ -14,7 +14,7 @@ mod wallet; #[cfg(test)] mod tests; -trait Output: Sized { +pub trait Output: Sized { type Id; fn id(&self) -> Self::Id; @@ -25,31 +25,33 @@ trait Output: Sized { } #[derive(Clone, Error, Debug)] -enum CoinError { +pub enum CoinError { #[error("failed to connect to coin daemon")] ConnectionError } #[async_trait] -trait Coin { +pub trait Coin { type Curve: Curve; type Output: Output; + type Block; type SignableTransaction; type Address: Send; fn id() -> &'static [u8]; - async fn confirmations() -> usize; - async fn max_inputs() -> usize; - async fn max_outputs() -> usize; + fn confirmations() -> usize; + fn max_inputs() -> usize; + fn max_outputs() -> usize; async fn get_height(&self) -> Result; - async fn get_outputs_in_block( + async fn get_block(&self, height: usize) -> Result; + async fn get_outputs( &self, - height: usize, + block: &Self::Block, key: ::G - ) -> Result, CoinError>; + ) -> Vec; async fn prepare_send( &self, @@ -73,6 +75,6 @@ trait Coin { // Takes an index, k, for more modern privacy protocols which use multiple view keys // Doesn't run Curve::hash_to_F, instead returning the hash object, due to hash_to_F being a FROST // definition instead of a wide reduction from a hash object -fn view_key(k: u64) -> Blake2b512 { +pub fn view_key(k: u64) -> Blake2b512 { Blake2b512::new().chain(b"Serai DEX View Key").chain(C::id()).chain(k.to_le_bytes()) } diff --git a/processor/src/tests/mod.rs b/processor/src/tests/mod.rs index 272342f4..1bddc91a 100644 --- a/processor/src/tests/mod.rs +++ b/processor/src/tests/mod.rs @@ -1,6 +1,16 @@ -use crate::{Coin, coins::monero::Monero}; +use std::rc::Rc; + +use rand::rngs::OsRng; + +use crate::{Coin, coins::monero::Monero, wallet::{WalletKeys, Wallet}}; #[tokio::test] async fn test() { - println!("{}", Monero::new("http://127.0.0.1:18081".to_string()).get_height().await.unwrap()); + let monero = Monero::new("http://127.0.0.1:18081".to_string()); + println!("{}", monero.get_height().await.unwrap()); + let mut keys = frost::tests::key_gen::<_, ::Curve>(&mut OsRng); + let mut wallet = Wallet::new(monero); + wallet.acknowledge_height(0, 0); + wallet.add_keys(&WalletKeys::new(Rc::try_unwrap(keys.remove(&1).take().unwrap()).unwrap(), 0)); + dbg!(0); } diff --git a/processor/src/wallet.rs b/processor/src/wallet.rs index 347bd787..c83a7f5a 100644 --- a/processor/src/wallet.rs +++ b/processor/src/wallet.rs @@ -1,30 +1,102 @@ +use std::collections::HashMap; + use frost::{Curve, MultisigKeys}; -use crate::Coin; +use crate::{CoinError, Coin}; -struct Wallet { - keys: MultisigKeys, +pub struct WalletKeys { + keys: MultisigKeys, + creation_height: usize +} + +impl WalletKeys { + pub fn new(keys: MultisigKeys, creation_height: usize) -> WalletKeys { + WalletKeys { keys, creation_height } + } + + // Bind this key to a specific network by applying an additive offset + // While it would be fine to just C::id(), including the group key creates distinct + // offsets instead of static offsets. Under a statically offset system, a BTC key could + // have X subtracted to find the potential group key, and then have Y added to find the + // potential ETH group key. While this shouldn't be an issue, as this isn't a private + // system, there are potentially other benefits to binding this to a specific group key + // It's no longer possible to influence group key gen to key cancel without breaking the hash + // function, although that degree of influence means key gen is broken already + fn bind(&self, chain: &[u8]) -> MultisigKeys { + self.keys.offset( + C::hash_to_F( + &[ + b"Serai Processor Wallet", + chain, + &C::G_to_bytes(&self.keys.group_key()) + ].concat() + ) + ) + } +} + +pub struct CoinDb { + // Height this coin has been scanned to + scanned_height: usize, + // Acknowledged height for a given canonical height + acknowledged_heights: HashMap +} + +pub struct Wallet { + db: CoinDb, + coin: C, + keys: Vec>, + pending: Vec<(usize, MultisigKeys)>, outputs: Vec } impl Wallet { - fn new(keys: &MultisigKeys) -> Wallet { + pub fn new(coin: C) -> Wallet { Wallet { - keys: keys.offset( - C::Curve::hash_to_F( - // Use distinct keys on each network by applying an additive offset - // While it would be fine to just C::id(), including the group key creates distinct - // offsets instead of static offsets. Under a statically offset system, a BTC key could - // have X subtracted to find the potential group key, and then have Y added to find the - // potential BCH group key. While this shouldn't be an issue, as this isn't a private - // system, there are potentially other benefits to binding this to a specific group key - &[b"Serai Processor Wallet", C::id(), &C::Curve::G_to_bytes(&keys.group_key())].concat() - ) - ), + db: CoinDb { + scanned_height: 0, + acknowledged_heights: HashMap::new(), + }, + coin, + + keys: vec![], + pending: vec![], outputs: vec![] } } - async fn poll() { todo!() } + pub fn scanned_height(&self) -> usize { self.db.scanned_height } + pub fn acknowledge_height(&mut self, canonical: usize, height: usize) { + debug_assert!(!self.db.acknowledged_heights.contains_key(&canonical)); + self.db.acknowledged_heights.insert(canonical, height); + } + pub fn acknowledged_height(&self, canonical: usize) -> usize { + self.db.acknowledged_heights[&canonical] + } + + pub fn add_keys(&mut self, keys: &WalletKeys) { + // Doesn't use +1 as this is height, not block index, and poll moves by block index + self.pending.push((self.acknowledged_height(keys.creation_height), keys.bind(C::id()))); + } + + pub async fn poll(&mut self) -> Result<(), CoinError> { + let confirmed_height = self.coin.get_height().await? - C::confirmations(); + for h in self.scanned_height() .. confirmed_height { + let mut k = 0; + while k < self.pending.len() { + if h == self.pending[k].0 { + self.keys.push(self.pending.swap_remove(k).1); + } else { + k += 1; + } + } + + let block = self.coin.get_block(h).await?; + for keys in &self.keys { + let outputs = self.coin.get_outputs(&block, keys.group_key()); + } + } + Ok(()) + } }