diff --git a/coins/monero/src/rpc.rs b/coins/monero/src/rpc.rs index a609901c..7a3edf80 100644 --- a/coins/monero/src/rpc.rs +++ b/coins/monero/src/rpc.rs @@ -34,6 +34,7 @@ pub enum RpcError { InvalidTransaction([u8; 32]) } +#[derive(Clone, Debug)] pub struct Rpc(String); fn rpc_hex(value: &str) -> Result, RpcError> { diff --git a/coins/monero/src/wallet/send/mod.rs b/coins/monero/src/wallet/send/mod.rs index 8ad88a5e..531ea529 100644 --- a/coins/monero/src/wallet/send/mod.rs +++ b/coins/monero/src/wallet/send/mod.rs @@ -75,7 +75,6 @@ impl SendOutput { } } - #[derive(Clone, Error, Debug)] pub enum TransactionError { #[error("no inputs")] diff --git a/crypto/frost/src/lib.rs b/crypto/frost/src/lib.rs index bf876f51..c96e0333 100644 --- a/crypto/frost/src/lib.rs +++ b/crypto/frost/src/lib.rs @@ -268,6 +268,7 @@ impl MultisigKeys { // Enables schemes like Monero's subaddresses which have a per-subaddress offset and then a // one-time-key offset res.offset = Some(offset + res.offset.unwrap_or(C::F::zero())); + res.group_key += C::GENERATOR_TABLE * offset; res } @@ -275,7 +276,7 @@ impl MultisigKeys { self.params } - pub fn secret_share(&self) -> C::F { + fn secret_share(&self) -> C::F { self.secret_share } @@ -283,7 +284,7 @@ impl MultisigKeys { self.group_key } - pub fn verification_shares(&self) -> HashMap { + fn verification_shares(&self) -> HashMap { self.verification_shares.clone() } @@ -297,7 +298,7 @@ impl MultisigKeys { let offset_share = offset * C::F::from(included.len().try_into().unwrap()).invert().unwrap(); Ok(MultisigView { - group_key: self.group_key + (C::GENERATOR_TABLE * offset), + group_key: self.group_key, secret_share: secret_share + offset_share, verification_shares: self.verification_shares.iter().map( |(l, share)| ( diff --git a/processor/Cargo.toml b/processor/Cargo.toml index 19171348..4eaf4890 100644 --- a/processor/Cargo.toml +++ b/processor/Cargo.toml @@ -11,6 +11,10 @@ async-trait = "0.1" rand_core = "0.6" thiserror = "1" +hex = "0.4" +serde = { version = "1.0", features = ["derive"] } +serde_json = "1.0" + curve25519-dalek = { version = "3", features = ["std"] } blake2 = "0.10" @@ -22,5 +26,7 @@ monero = { version = "0.16", features = ["experimental"] } monero-serai = { path = "../coins/monero", features = ["multisig"] } [dev-dependencies] +group = "0.12" rand = "0.8" +futures = "0.3" tokio = { version = "1", features = ["full"] } diff --git a/processor/src/coins/monero.rs b/processor/src/coins/monero.rs index 2b694984..a5f6fb68 100644 --- a/processor/src/coins/monero.rs +++ b/processor/src/coins/monero.rs @@ -1,16 +1,12 @@ use std::sync::Arc; use async_trait::async_trait; -use rand_core::{RngCore, CryptoRng}; +use rand_core::OsRng; -use curve25519_dalek::{ - constants::ED25519_BASEPOINT_TABLE, - scalar::Scalar, - edwards::CompressedEdwardsY -}; +use curve25519_dalek::{constants::ED25519_BASEPOINT_TABLE, scalar::Scalar}; use dalek_ff_group as dfg; -use frost::MultisigKeys; +use frost::{MultisigKeys, sign::StateMachine}; use monero::{PublicKey, network::Network, util::address::Address}; use monero_serai::{ @@ -20,9 +16,15 @@ use monero_serai::{ wallet::{SpendableOutput, SignableTransaction as MSignableTransaction} }; -use crate::{Transcript, Output as OutputTrait, CoinError, Coin, view_key}; +use crate::{ + Transcript, + CoinError, SignError, + Network as NetworkTrait, + Output as OutputTrait, Coin, + view_key +}; -#[derive(Clone)] +#[derive(Clone, Debug)] pub struct Output(SpendableOutput); impl OutputTrait for Output { // While we could use (tx, o), using the key ensures we won't be susceptible to the burning bug. @@ -53,6 +55,7 @@ impl From for Output { } } +#[derive(Debug)] pub struct SignableTransaction( Arc>, Transcript, @@ -60,10 +63,11 @@ pub struct SignableTransaction( MSignableTransaction ); +#[derive(Clone, Debug)] pub struct Monero { rpc: Rpc, view: Scalar, - view_pub: CompressedEdwardsY + view_pub: PublicKey } impl Monero { @@ -72,7 +76,7 @@ impl Monero { Monero { rpc: Rpc::new(url), view, - view_pub: (&view * &ED25519_BASEPOINT_TABLE).compress() + view_pub: PublicKey { point: (&view * &ED25519_BASEPOINT_TABLE).compress() } } } } @@ -98,6 +102,10 @@ impl Coin for Monero { const MAX_INPUTS: usize = 128; const MAX_OUTPUTS: usize = 16; + fn address(&self, key: dfg::EdwardsPoint) -> Self::Address { + Address::standard(Network::Mainnet, PublicKey { point: key.compress().0 }, self.view_pub) + } + async fn get_height(&self) -> Result { self.rpc.get_height().await.map_err(|_| CoinError::ConnectionError) } @@ -129,7 +137,7 @@ impl Coin for Monero { mut inputs: Vec, payments: &[(Address, u64)] ) -> Result { - let spend = keys.group_key().0.compress(); + let spend = keys.group_key(); Ok( SignableTransaction( keys, @@ -138,40 +146,86 @@ impl Coin for Monero { MSignableTransaction::new( inputs.drain(..).map(|input| input.0).collect(), payments.to_vec(), - Address::standard( - Network::Mainnet, - PublicKey { point: spend }, - PublicKey { point: self.view_pub } - ), - 100000000 + self.address(spend), + 100000000 // TODO ).map_err(|_| CoinError::ConnectionError)? ) ) } - async fn attempt_send( + async fn attempt_send( &self, - rng: &mut R, + network: &mut N, transaction: SignableTransaction, included: &[u16] - ) -> Result<(Vec, Vec<::Id>), CoinError> { - let attempt = transaction.3.clone().multisig( - rng, + ) -> Result<(Vec, Vec<::Id>), SignError> { + let mut attempt = transaction.3.clone().multisig( + &mut OsRng, &self.rpc, (*transaction.0).clone(), transaction.1.clone(), transaction.2, included.to_vec() - ).await.map_err(|_| CoinError::ConnectionError)?; + ).await.map_err(|_| SignError::CoinError(CoinError::ConnectionError))?; - /* - let tx = None; - self.rpc.publish_transaction(tx).await.map_err(|_| CoinError::ConnectionError)?; - Ok( + let commitments = network.round( + attempt.preprocess(&mut OsRng).unwrap() + ).await.map_err(|e| SignError::NetworkError(e))?; + let shares = network.round( + attempt.sign(commitments, b"").map_err(|e| SignError::FrostError(e))? + ).await.map_err(|e| SignError::NetworkError(e))?; + let tx = attempt.complete(shares).map_err(|e| SignError::FrostError(e))?; + + self.rpc.publish_transaction( + &tx + ).await.map_err(|_| SignError::CoinError(CoinError::ConnectionError))?; + + Ok(( tx.hash().to_vec(), - tx.outputs.iter().map(|output| output.key.compress().to_bytes().collect()) - ) - */ - Ok((vec![], vec![])) + tx.prefix.outputs.iter().map(|output| output.key.compress().to_bytes()).collect() + )) + } + + #[cfg(test)] + async fn mine_block(&self, address: Self::Address) { + #[derive(serde::Deserialize, Debug)] + struct EmptyResponse {} + let _: EmptyResponse = self.rpc.rpc_call("json_rpc", Some(serde_json::json!({ + "method": "generateblocks", + "params": { + "wallet_address": address.to_string(), + "amount_of_blocks": 10 + }, + }))).await.unwrap(); + } + + #[cfg(test)] + async fn test_send(&self, address: Self::Address) { + use group::Group; + + use rand::rngs::OsRng; + + let height = self.get_height().await.unwrap(); + + let temp = self.address(dfg::EdwardsPoint::generator()); + self.mine_block(temp).await; + for _ in 0 .. 7 { + self.mine_block(temp).await; + } + + let outputs = self.rpc + .get_block_transactions_possible(height).await.unwrap() + .swap_remove(0).scan(self.view, dfg::EdwardsPoint::generator().0).0; + + let amount = outputs[0].commitment.amount; + let fee = 1000000000; // TODO + let tx = MSignableTransaction::new( + outputs, + vec![(address, amount - fee)], + temp, + fee / 2000 + ).unwrap().sign(&mut OsRng, &self.rpc, &Scalar::one()).await.unwrap(); + self.rpc.publish_transaction(&tx).await.unwrap(); + self.mine_block(temp).await; } } diff --git a/processor/src/lib.rs b/processor/src/lib.rs index 337a409f..47e12e85 100644 --- a/processor/src/lib.rs +++ b/processor/src/lib.rs @@ -1,10 +1,9 @@ -use std::{marker::Send, sync::Arc}; +use std::{marker::Send, sync::Arc, collections::HashMap}; use async_trait::async_trait; use thiserror::Error; -use rand_core::{RngCore, CryptoRng}; -use frost::{Curve, MultisigKeys}; +use frost::{Curve, FrostError, MultisigKeys}; pub(crate) use monero_serai::frost::Transcript; @@ -14,6 +13,30 @@ mod wallet; #[cfg(test)] mod tests; +#[derive(Clone, Error, Debug)] +pub enum CoinError { + #[error("failed to connect to coin daemon")] + ConnectionError +} + +#[derive(Clone, Error, Debug)] +pub enum NetworkError {} + +#[derive(Clone, Error, Debug)] +pub enum SignError { + #[error("coin had an error {0}")] + CoinError(CoinError), + #[error("network had an error {0}")] + NetworkError(NetworkError), + #[error("FROST had an error {0}")] + FrostError(FrostError) +} + +#[async_trait] +pub trait Network: Send { + async fn round(&mut self, data: Vec) -> Result>, NetworkError>; +} + pub trait Output: Sized + Clone { type Id: AsRef<[u8]>; @@ -24,12 +47,6 @@ pub trait Output: Sized + Clone { fn deserialize(reader: &mut R) -> std::io::Result; } -#[derive(Clone, Error, Debug)] -pub enum CoinError { - #[error("failed to connect to coin daemon")] - ConnectionError -} - #[async_trait] pub trait Coin { type Curve: Curve; @@ -43,7 +60,10 @@ pub trait Coin { const ID: &'static [u8]; const CONFIRMATIONS: usize; const MAX_INPUTS: usize; - const MAX_OUTPUTS: usize; + const MAX_OUTPUTS: usize; // TODO: Decide if this includes change or not + + // Doesn't have to take self, enables some level of caching which is pleasant + fn address(&self, key: ::G) -> Self::Address; async fn get_height(&self) -> Result; async fn get_block(&self, height: usize) -> Result; @@ -62,12 +82,18 @@ pub trait Coin { payments: &[(Self::Address, u64)] ) -> Result; - async fn attempt_send( + async fn attempt_send( &self, - rng: &mut R, + network: &mut N, transaction: Self::SignableTransaction, included: &[u16] - ) -> Result<(Vec, Vec<::Id>), CoinError>; + ) -> Result<(Vec, Vec<::Id>), SignError>; + + #[cfg(test)] + async fn mine_block(&self, address: Self::Address); + + #[cfg(test)] + async fn test_send(&self, key: Self::Address); } // Generate a static view key for a given chain in a globally consistent manner diff --git a/processor/src/tests/mod.rs b/processor/src/tests/mod.rs index abc338d3..7e640241 100644 --- a/processor/src/tests/mod.rs +++ b/processor/src/tests/mod.rs @@ -1,16 +1,107 @@ -use std::sync::Arc; +use std::{sync::{Arc, RwLock}, collections::HashMap}; + +use async_trait::async_trait; use rand::rngs::OsRng; -use crate::{Coin, coins::monero::Monero, wallet::{WalletKeys, MemCoinDb, Wallet}}; +use group::Group; + +use crate::{ + NetworkError, Network, + Coin, coins::monero::Monero, + wallet::{WalletKeys, MemCoinDb, Wallet} +}; + +#[derive(Clone)] +struct LocalNetwork { + i: u16, + size: u16, + round: usize, + rounds: Arc>>>> +} + +impl LocalNetwork { + fn new(size: u16) -> Vec { + let rounds = Arc::new(RwLock::new(vec![])); + let mut res = vec![]; + for i in 1 ..= size { + res.push(LocalNetwork { i, size, round: 0, rounds: rounds.clone() }); + } + res + } +} + +#[async_trait] +impl Network for LocalNetwork { + async fn round(&mut self, data: Vec) -> Result>, NetworkError> { + { + let mut rounds = self.rounds.write().unwrap(); + if rounds.len() == self.round { + rounds.push(HashMap::new()); + } + rounds[self.round].insert(self.i, data); + } + + while { + let read = self.rounds.try_read().unwrap(); + read[self.round].len() != usize::from(self.size) + } { + tokio::task::yield_now().await; + } + + let res = self.rounds.try_read().unwrap()[self.round].clone(); + self.round += 1; + Ok(res) + } +} #[tokio::test] async fn test() { let monero = Monero::new("http://127.0.0.1:18081".to_string()); - println!("{}", monero.get_height().await.unwrap()); + // Mine a block so there's a confirmed height + monero.mine_block(monero.address(dalek_ff_group::EdwardsPoint::generator())).await; + let height = monero.get_height().await.unwrap(); + + let mut networks = LocalNetwork::new(3); + let mut keys = frost::tests::key_gen::<_, ::Curve>(&mut OsRng); - let mut wallet = Wallet::new(MemCoinDb::new(), monero); - wallet.acknowledge_height(0, 0); - wallet.add_keys(&WalletKeys::new(Arc::try_unwrap(keys.remove(&1).take().unwrap()).unwrap(), 0)); - dbg!(0); + let mut wallets = vec![]; + for i in 1 ..= 3 { + let mut wallet = Wallet::new(MemCoinDb::new(), monero.clone()); + wallet.acknowledge_height(0, height); + wallet.add_keys( + &WalletKeys::new(Arc::try_unwrap(keys.remove(&i).take().unwrap()).unwrap(), 0) + ); + wallets.push(wallet); + } + + // Get the chain to a height where blocks have sufficient confirmations + while (height + Monero::CONFIRMATIONS) > monero.get_height().await.unwrap() { + monero.mine_block(monero.address(dalek_ff_group::EdwardsPoint::generator())).await; + } + + for wallet in wallets.iter_mut() { + // Poll to activate the keys + wallet.poll().await.unwrap(); + } + + monero.test_send(wallets[0].address()).await; + + let mut futures = vec![]; + for (i, network) in networks.iter_mut().enumerate() { + let wallet = &mut wallets[i]; + wallet.poll().await.unwrap(); + + let height = monero.get_height().await.unwrap(); + wallet.acknowledge_height(1, height - 10); + let signable = wallet.prepare_sends( + 1, + vec![(wallet.address(), 10000000000)] + ).await.unwrap().1.swap_remove(0); + futures.push(monero.attempt_send(network, signable, &[1, 2, 3])); + } + println!( + "{:?}", + hex::encode(futures::future::join_all(futures).await.swap_remove(0).unwrap().0) + ); } diff --git a/processor/src/wallet.rs b/processor/src/wallet.rs index 25514ce8..d6bcaf92 100644 --- a/processor/src/wallet.rs +++ b/processor/src/wallet.rs @@ -121,6 +121,9 @@ impl Wallet { pub fn scanned_height(&self) -> usize { self.db.scanned_height() } pub fn acknowledge_height(&mut self, canonical: usize, height: usize) { self.db.acknowledge_height(canonical, height); + if height > self.db.scanned_height() { + self.db.scanned_to_height(height); + } } pub fn acknowledged_height(&self, canonical: usize) -> usize { self.db.acknowledged_height(canonical) @@ -131,17 +134,25 @@ impl Wallet { self.pending.push((self.acknowledged_height(keys.creation_height), keys.bind(C::ID))); } + pub fn address(&self) -> C::Address { + self.coin.address(self.keys[self.keys.len() - 1].0.group_key()) + } + pub async fn poll(&mut self) -> Result<(), CoinError> { - let confirmed_height = self.coin.get_height().await? - C::CONFIRMATIONS; - for height in self.scanned_height() .. confirmed_height { + if self.coin.get_height().await? < C::CONFIRMATIONS { + return Ok(()); + } + let confirmed_block = self.coin.get_height().await? - C::CONFIRMATIONS; + + for b in self.scanned_height() ..= confirmed_block { // If any keys activated at this height, shift them over { let mut k = 0; while k < self.pending.len() { // TODO - //if height < self.pending[k].0 { - //} else if height == self.pending[k].0 { - if height <= self.pending[k].0 { + //if b < self.pending[k].0 { + //} else if b == self.pending[k].0 { + if b <= self.pending[k].0 { self.keys.push((Arc::new(self.pending.swap_remove(k).1), vec![])); } else { k += 1; @@ -149,7 +160,7 @@ impl Wallet { } } - let block = self.coin.get_block(height).await?; + let block = self.coin.get_block(b).await?; for (keys, outputs) in self.keys.iter_mut() { outputs.extend( self.coin.get_outputs(&block, keys.group_key()).await.iter().cloned().filter( @@ -157,7 +168,11 @@ impl Wallet { ) ); } + + // Blocks are zero-indexed while heights aren't + self.db.scanned_to_height(b + 1); } + Ok(()) }