diff --git a/Cargo.lock b/Cargo.lock index 01edbcfe..b08cde03 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -5105,6 +5105,7 @@ dependencies = [ "hex", "modular-frost", "monero-address", + "monero-clsag", "monero-rpc", "monero-serai", "monero-simple-request-rpc", @@ -8534,8 +8535,10 @@ dependencies = [ "serai-processor-signers", "serai-processor-utxo-scheduler", "serai-processor-utxo-scheduler-primitives", + "serai-processor-view-keys", "tokio", "zalloc", + "zeroize", ] [[package]] diff --git a/processor/monero/Cargo.toml b/processor/monero/Cargo.toml index 22137b2d..6f9ce40a 100644 --- a/processor/monero/Cargo.toml +++ b/processor/monero/Cargo.toml @@ -18,6 +18,7 @@ workspace = true [dependencies] rand_core = { version = "0.6", default-features = false } +zeroize = { version = "1", default-features = false, features = ["std"] } hex = { version = "0.4", default-features = false, features = ["std"] } scale = { package = "parity-scale-codec", version = "3", default-features = false, features = ["std"] } @@ -41,6 +42,7 @@ tokio = { version = "1", default-features = false, features = ["rt-multi-thread" serai-db = { path = "../../common/db" } key-gen = { package = "serai-processor-key-gen", path = "../key-gen" } +view-keys = { package = "serai-processor-view-keys", path = "../view-keys" } primitives = { package = "serai-processor-primitives", path = "../primitives" } scheduler = { package = "serai-processor-scheduler-primitives", path = "../scheduler/primitives" } diff --git a/processor/monero/src/lib.rs b/processor/monero/src/lib.rs index f9b334ef..46ce16d3 100644 --- a/processor/monero/src/lib.rs +++ b/processor/monero/src/lib.rs @@ -1,119 +1,4 @@ /* -#![cfg_attr(docsrs, feature(doc_auto_cfg))] -#![doc = include_str!("../README.md")] -#![deny(missing_docs)] - -use std::{time::Duration, collections::HashMap, io}; - -use async_trait::async_trait; - -use zeroize::Zeroizing; - -use rand_core::SeedableRng; -use rand_chacha::ChaCha20Rng; - -use transcript::{Transcript, RecommendedTranscript}; - -use ciphersuite::group::{ff::Field, Group}; -use dalek_ff_group::{Scalar, EdwardsPoint}; -use frost::{curve::Ed25519, ThresholdKeys}; - -use monero_simple_request_rpc::SimpleRequestRpc; -use monero_wallet::{ - ringct::RctType, - transaction::Transaction, - block::Block, - rpc::{FeeRate, RpcError, Rpc}, - address::{Network as MoneroNetwork, SubaddressIndex}, - ViewPair, GuaranteedViewPair, WalletOutput, OutputWithDecoys, GuaranteedScanner, - send::{ - SendError, Change, SignableTransaction as MSignableTransaction, Eventuality, TransactionMachine, - }, -}; -#[cfg(test)] -use monero_wallet::Scanner; - -use tokio::time::sleep; - -pub use serai_client::{ - primitives::{MAX_DATA_LEN, Coin, NetworkId, Amount, Balance}, - networks::monero::Address, -}; - -use crate::{ - Payment, additional_key, - networks::{ - NetworkError, Block as BlockTrait, OutputType, Output as OutputTrait, - Transaction as TransactionTrait, SignableTransaction as SignableTransactionTrait, - Eventuality as EventualityTrait, EventualitiesTracker, Network, UtxoNetwork, - }, - multisigs::scheduler::utxo::Scheduler, -}; - -#[derive(Clone, PartialEq, Eq, Debug)] -pub struct Output(WalletOutput); - -const EXTERNAL_SUBADDRESS: Option = SubaddressIndex::new(0, 0); -const BRANCH_SUBADDRESS: Option = SubaddressIndex::new(1, 0); -const CHANGE_SUBADDRESS: Option = SubaddressIndex::new(2, 0); -const FORWARD_SUBADDRESS: Option = SubaddressIndex::new(3, 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 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.subaddress() { - EXTERNAL_SUBADDRESS => OutputType::External, - BRANCH_SUBADDRESS => OutputType::Branch, - CHANGE_SUBADDRESS => OutputType::Change, - FORWARD_SUBADDRESS => OutputType::Forwarded, - _ => panic!("unrecognized address was scanned for"), - } - } - - fn id(&self) -> Self::Id { - self.0.key().compress().to_bytes() - } - - fn tx_id(&self) -> [u8; 32] { - self.0.transaction() - } - - fn key(&self) -> EdwardsPoint { - EdwardsPoint(self.0.key() - (EdwardsPoint::generator().0 * self.0.key_offset())) - } - - fn presumed_origin(&self) -> Option
{ - None - } - - fn balance(&self) -> Balance { - Balance { coin: Coin::Monero, amount: Amount(self.0.commitment().amount) } - } - - fn data(&self) -> &[u8] { - let Some(data) = self.0.arbitrary_data().first() else { return &[] }; - // If the data is too large, prune it - // This should cause decoding the instruction to fail, and trigger a refund as appropriate - if data.len() > usize::try_from(MAX_DATA_LEN).unwrap() { - return &[]; - } - data - } - - fn write(&self, writer: &mut W) -> io::Result<()> { - self.0.write(writer)?; - Ok(()) - } - - fn read(reader: &mut R) -> io::Result { - Ok(Output(WalletOutput::read(reader)?)) - } -} - // TODO: Consider ([u8; 32], TransactionPruned) #[async_trait] impl TransactionTrait for Transaction { @@ -227,29 +112,6 @@ impl BlockTrait for Block { } } -#[derive(Clone, Debug)] -pub struct Monero { - rpc: SimpleRequestRpc, -} -// Shim required for testing/debugging purposes due to generic arguments also necessitating trait -// bounds -impl PartialEq for Monero { - fn eq(&self, _: &Self) -> bool { - true - } -} -impl Eq for Monero {} - -#[allow(clippy::needless_pass_by_value)] // Needed to satisfy API expectations -fn map_rpc_err(err: RpcError) -> NetworkError { - if let RpcError::InvalidNode(reason) = &err { - log::error!("Monero RpcError::InvalidNode({reason})"); - } else { - log::debug!("Monero RpcError {err:?}"); - } - NetworkError::ConnectionError -} - enum MakeSignableTransactionResult { Fee(u64), SignableTransaction(MSignableTransaction), @@ -461,20 +323,6 @@ impl Monero { #[async_trait] impl Network for Monero { - type Curve = Ed25519; - - type Transaction = Transaction; - type Block = Block; - - type Output = Output; - type SignableTransaction = SignableTransaction; - type Eventuality = Eventuality; - type TransactionMachine = TransactionMachine; - - type Scheduler = Scheduler; - - type Address = Address; - const NETWORK: NetworkId = NetworkId::Monero; const ID: &'static str = "Monero"; const ESTIMATED_BLOCK_TIME_IN_SECONDS: usize = 120; @@ -488,9 +336,6 @@ impl Network for Monero { // TODO const COST_TO_AGGREGATE: u64 = 0; - // Monero doesn't require/benefit from tweaking - fn tweak_keys(_: &mut ThresholdKeys) {} - #[cfg(test)] async fn external_address(&self, key: EdwardsPoint) -> Address { Self::address_internal(key, EXTERNAL_SUBADDRESS) @@ -508,121 +353,6 @@ impl Network for Monero { Some(Self::address_internal(key, FORWARD_SUBADDRESS)) } - async fn get_latest_block_number(&self) -> Result { - // Monero defines height as chain length, so subtract 1 for block number - Ok(self.rpc.get_height().await.map_err(map_rpc_err)? - 1) - } - - async fn get_block(&self, number: usize) -> Result { - Ok( - self - .rpc - .get_block(self.rpc.get_block_hash(number).await.map_err(map_rpc_err)?) - .await - .map_err(map_rpc_err)?, - ) - } - - async fn get_outputs(&self, block: &Block, key: EdwardsPoint) -> Vec { - let outputs = loop { - match self - .rpc - .get_scannable_block(block.clone()) - .await - .map_err(|e| format!("{e:?}")) - .and_then(|block| Self::scanner(key).scan(block).map_err(|e| format!("{e:?}"))) - { - Ok(outputs) => break outputs, - Err(e) => { - log::error!("couldn't scan block {}: {e:?}", hex::encode(block.id())); - sleep(Duration::from_secs(60)).await; - continue; - } - } - }; - - // Miner transactions are required to explicitly state their timelock, so this does exclude - // those (which have an extended timelock we don't want to deal with) - let raw_outputs = outputs.not_additionally_locked(); - let mut outputs = Vec::with_capacity(raw_outputs.len()); - for output in raw_outputs { - // This should be pointless as we shouldn't be able to scan for any other subaddress - // This just helps ensures nothing invalid makes it through - assert!([EXTERNAL_SUBADDRESS, BRANCH_SUBADDRESS, CHANGE_SUBADDRESS, FORWARD_SUBADDRESS] - .contains(&output.subaddress())); - - outputs.push(Output(output)); - } - - outputs - } - - async fn get_eventuality_completions( - &self, - eventualities: &mut EventualitiesTracker, - block: &Block, - ) -> HashMap<[u8; 32], (usize, [u8; 32], Transaction)> { - let mut res = HashMap::new(); - if eventualities.map.is_empty() { - return res; - } - - async fn check_block( - network: &Monero, - eventualities: &mut EventualitiesTracker, - block: &Block, - res: &mut HashMap<[u8; 32], (usize, [u8; 32], Transaction)>, - ) { - for hash in &block.transactions { - let tx = { - let mut tx; - while { - tx = network.rpc.get_transaction(*hash).await; - tx.is_err() - } { - log::error!("couldn't get transaction {}: {}", hex::encode(hash), tx.err().unwrap()); - sleep(Duration::from_secs(60)).await; - } - tx.unwrap() - }; - - if let Some((_, eventuality)) = eventualities.map.get(&tx.prefix().extra) { - if eventuality.matches(&tx.clone().into()) { - res.insert( - eventualities.map.remove(&tx.prefix().extra).unwrap().0, - (block.number().unwrap(), tx.id(), tx), - ); - } - } - } - - eventualities.block_number += 1; - assert_eq!(eventualities.block_number, block.number().unwrap()); - } - - for block_num in (eventualities.block_number + 1) .. block.number().unwrap() { - let block = { - let mut block; - while { - block = self.get_block(block_num).await; - block.is_err() - } { - log::error!("couldn't get block {}: {}", block_num, block.err().unwrap()); - sleep(Duration::from_secs(60)).await; - } - block.unwrap() - }; - - check_block(self, eventualities, &block, &mut res).await; - } - - // Also check the current block - check_block(self, eventualities, block, &mut res).await; - assert_eq!(eventualities.block_number, block.number().unwrap()); - - res - } - async fn needed_fee( &self, block_number: usize, @@ -687,19 +417,6 @@ impl Network for Monero { } } - async fn confirm_completion( - &self, - eventuality: &Eventuality, - id: &[u8; 32], - ) -> Result, NetworkError> { - let tx = self.rpc.get_transaction(*id).await.map_err(map_rpc_err)?; - if eventuality.matches(&tx.clone().into()) { - Ok(Some(tx)) - } else { - Ok(None) - } - } - #[cfg(test)] async fn get_block_number(&self, id: &[u8; 32]) -> usize { self.rpc.get_block(*id).await.unwrap().number().unwrap() diff --git a/processor/monero/src/primitives/block.rs b/processor/monero/src/primitives/block.rs index 634a0fbb..62715f8c 100644 --- a/processor/monero/src/primitives/block.rs +++ b/processor/monero/src/primitives/block.rs @@ -1,14 +1,22 @@ use std::collections::HashMap; +use zeroize::Zeroizing; + use ciphersuite::{Ciphersuite, Ed25519}; -use monero_wallet::{transaction::Transaction, block::Block as MBlock, ViewPairError, GuaranteedViewPair, GuaranteedScanner}; +use monero_wallet::{ + block::Block as MBlock, rpc::ScannableBlock as MScannableBlock, + ViewPairError, GuaranteedViewPair, ScanError, GuaranteedScanner, +}; use serai_client::networks::monero::Address; use primitives::{ReceivedOutput, EventualityTracker}; - -use crate::{EXTERNAL_SUBADDRESS, BRANCH_SUBADDRESS, CHANGE_SUBADDRESS, FORWARDED_SUBADDRESS, output::Output, transaction::Eventuality}; +use view_keys::view_key; +use crate::{ + EXTERNAL_SUBADDRESS, BRANCH_SUBADDRESS, CHANGE_SUBADDRESS, FORWARDED_SUBADDRESS, output::Output, + transaction::Eventuality, +}; #[derive(Clone, Debug)] pub(crate) struct BlockHeader(pub(crate) MBlock); @@ -22,7 +30,7 @@ impl primitives::BlockHeader for BlockHeader { } #[derive(Clone, Debug)] -pub(crate) struct Block(pub(crate) MBlock, Vec); +pub(crate) struct Block(pub(crate) MScannableBlock); impl primitives::Block for Block { type Header = BlockHeader; @@ -33,20 +41,26 @@ impl primitives::Block for Block { type Eventuality = Eventuality; fn id(&self) -> [u8; 32] { - self.0.hash() + self.0.block.hash() } fn scan_for_outputs_unordered(&self, key: Self::Key) -> Vec { - let view_pair = match GuaranteedViewPair::new(key.0, additional_key) { + let view_pair = match GuaranteedViewPair::new(key.0, Zeroizing::new(*view_key::(0))) { Ok(view_pair) => view_pair, - Err(ViewPairError::TorsionedSpendKey) => unreachable!("dalek_ff_group::EdwardsPoint has torsion"), - }; + Err(ViewPairError::TorsionedSpendKey) => { + unreachable!("dalek_ff_group::EdwardsPoint had torsion") + } + }; let mut scanner = GuaranteedScanner::new(view_pair); scanner.register_subaddress(EXTERNAL_SUBADDRESS.unwrap()); scanner.register_subaddress(BRANCH_SUBADDRESS.unwrap()); scanner.register_subaddress(CHANGE_SUBADDRESS.unwrap()); scanner.register_subaddress(FORWARDED_SUBADDRESS.unwrap()); - todo!("TODO") + match scanner.scan(self.0.clone()) { + Ok(outputs) => outputs.not_additionally_locked().into_iter().map(Output).collect(), + Err(ScanError::UnsupportedProtocol(version)) => panic!("Monero unexpectedly hard-forked (version {version})"), + Err(ScanError::InvalidScannableBlock(reason)) => panic!("fetched an invalid scannable block from the RPC: {reason}"), + } } #[allow(clippy::type_complexity)] @@ -57,6 +71,18 @@ impl primitives::Block for Block { >::TransactionId, Self::Eventuality, > { - todo!("TODO") + let mut res = HashMap::new(); + assert_eq!(self.0.block.transactions.len(), self.0.transactions.len()); + for (hash, tx) in self.0.block.transactions.iter().zip(&self.0.transactions) { + if let Some(eventuality) = eventualities.active_eventualities.get(&tx.prefix().extra) { + if eventuality.eventuality.matches(tx) { + res.insert( + *hash, + eventualities.active_eventualities.remove(&tx.prefix().extra).unwrap(), + ); + } + } + } + res } } diff --git a/processor/monero/src/primitives/output.rs b/processor/monero/src/primitives/output.rs index 385429c2..d66fd983 100644 --- a/processor/monero/src/primitives/output.rs +++ b/processor/monero/src/primitives/output.rs @@ -33,7 +33,7 @@ impl AsMut<[u8]> for OutputId { } #[derive(Clone, PartialEq, Eq, Debug)] -pub(crate) struct Output(WalletOutput); +pub(crate) struct Output(pub(crate) WalletOutput); impl Output { pub(crate) fn new(output: WalletOutput) -> Self { diff --git a/processor/monero/src/primitives/transaction.rs b/processor/monero/src/primitives/transaction.rs index 1ba49471..f6765cd9 100644 --- a/processor/monero/src/primitives/transaction.rs +++ b/processor/monero/src/primitives/transaction.rs @@ -83,7 +83,7 @@ impl scheduler::SignableTransaction for SignableTransaction { pub(crate) struct Eventuality { id: [u8; 32], singular_spent_output: Option, - eventuality: MEventuality, + pub(crate) eventuality: MEventuality, } impl primitives::Eventuality for Eventuality { diff --git a/processor/monero/src/rpc.rs b/processor/monero/src/rpc.rs index 0e0739b8..d826802b 100644 --- a/processor/monero/src/rpc.rs +++ b/processor/monero/src/rpc.rs @@ -54,7 +54,7 @@ impl ScannerFeed for Rpc { &self, number: u64, ) -> impl Send + Future> { - async move{todo!("TODO")} + async move { todo!("TODO") } } fn unchecked_block_header_by_number(