diff --git a/coins/monero/rpc/src/lib.rs b/coins/monero/rpc/src/lib.rs index f55942be..a084ff5f 100644 --- a/coins/monero/rpc/src/lib.rs +++ b/coins/monero/rpc/src/lib.rs @@ -26,7 +26,7 @@ use serde_json::{Value, json}; use monero_serai::{ io::*, - transaction::{Input, Timelock, Transaction}, + transaction::{Input, Timelock, Pruned, Transaction}, block::Block, DEFAULT_LOCK_WINDOW, }; @@ -361,14 +361,15 @@ pub trait Rpc: Sync + Clone + Debug { .iter() .enumerate() .map(|(i, res)| { - let tx = Transaction::read::<&[u8]>( - &mut rpc_hex(if !res.as_hex.is_empty() { &res.as_hex } else { &res.pruned_as_hex })? - .as_ref(), - ) - .map_err(|_| match hash_hex(&res.tx_hash) { + let buf = rpc_hex(if !res.as_hex.is_empty() { &res.as_hex } else { &res.pruned_as_hex })?; + let mut buf = buf.as_slice(); + let tx = Transaction::read(&mut buf).map_err(|_| match hash_hex(&res.tx_hash) { Ok(hash) => RpcError::InvalidTransaction(hash), Err(err) => err, })?; + if !buf.is_empty() { + Err(RpcError::InvalidNode("transaction had extra bytes after it".to_string()))?; + } // https://github.com/monero-project/monero/issues/8311 if res.as_hex.is_empty() { @@ -391,6 +392,61 @@ pub trait Rpc: Sync + Clone + Debug { .collect() } + /// Get the specified transactions in their pruned format. + async fn get_pruned_transactions( + &self, + hashes: &[[u8; 32]], + ) -> Result>, RpcError> { + if hashes.is_empty() { + return Ok(vec![]); + } + + let mut hashes_hex = hashes.iter().map(hex::encode).collect::>(); + let mut all_txs = Vec::with_capacity(hashes.len()); + while !hashes_hex.is_empty() { + // Monero errors if more than 100 is requested unless using a non-restricted RPC + // TODO: Cite + // TODO: Deduplicate with above + const TXS_PER_REQUEST: usize = 100; + let this_count = TXS_PER_REQUEST.min(hashes_hex.len()); + + let txs: TransactionsResponse = self + .rpc_call( + "get_transactions", + Some(json!({ + "txs_hashes": hashes_hex.drain(.. this_count).collect::>(), + "prune": true, + })), + ) + .await?; + + if !txs.missed_tx.is_empty() { + Err(RpcError::TransactionsNotFound( + txs.missed_tx.iter().map(|hash| hash_hex(hash)).collect::>()?, + ))?; + } + + all_txs.extend(txs.txs); + } + + all_txs + .iter() + .map(|res| { + let buf = rpc_hex(&res.pruned_as_hex)?; + let mut buf = buf.as_slice(); + let tx = + Transaction::::read(&mut buf).map_err(|_| match hash_hex(&res.tx_hash) { + Ok(hash) => RpcError::InvalidTransaction(hash), + Err(err) => err, + })?; + if !buf.is_empty() { + Err(RpcError::InvalidNode("pruned transaction had extra bytes after it".to_string()))?; + } + Ok(tx) + }) + .collect() + } + /// Get the specified transaction. /// /// The received transaction will be hashed in order to verify the correct transaction was @@ -399,6 +455,11 @@ pub trait Rpc: Sync + Clone + Debug { self.get_transactions(&[tx]).await.map(|mut txs| txs.swap_remove(0)) } + /// Get the specified transaction in its pruned format. + async fn get_pruned_transaction(&self, tx: [u8; 32]) -> Result, RpcError> { + self.get_pruned_transactions(&[tx]).await.map(|mut txs| txs.swap_remove(0)) + } + /// Get the hash of a block from the node. /// /// `number` is the block's zero-indexed position on the blockchain (`0` for the genesis block, @@ -469,35 +530,6 @@ pub trait Rpc: Sync + Clone + Debug { } } - /// Get the transactions within a block. - /// - /// This function returns all transactions in the block, including the miner's transaction. - /// - /// This function does not verify the returned transactions are the ones committed to by the - /// block's header. - async fn get_block_transactions(&self, hash: [u8; 32]) -> Result, RpcError> { - let block = self.get_block(hash).await?; - let mut res = vec![block.miner_transaction]; - res.extend(self.get_transactions(&block.transactions).await?); - Ok(res) - } - - /// Get the transactions within a block. - /// - /// This function returns all transactions in the block, including the miner's transaction. - /// - /// This function does not verify the returned transactions are the ones committed to by the - /// block's header. - async fn get_block_transactions_by_number( - &self, - number: usize, - ) -> Result, RpcError> { - let block = self.get_block_by_number(number).await?; - let mut res = vec![block.miner_transaction]; - res.extend(self.get_transactions(&block.transactions).await?); - Ok(res) - } - /// Get the currently estimated fee rate from the node. /// /// This may be manipulated to unsafe levels and MUST be sanity checked. diff --git a/coins/monero/verify-chain/src/main.rs b/coins/monero/verify-chain/src/main.rs index 84539606..b4853f53 100644 --- a/coins/monero/verify-chain/src/main.rs +++ b/coins/monero/verify-chain/src/main.rs @@ -57,63 +57,31 @@ async fn check_block(rpc: impl Rpc, block_i: usize) { let txs_len = 1 + block.transactions.len(); if !block.transactions.is_empty() { - #[derive(Deserialize, Debug)] - struct TransactionResponse { - tx_hash: String, - as_hex: String, - } - #[derive(Deserialize, Debug)] - struct TransactionsResponse { - #[serde(default)] - missed_tx: Vec, - txs: Vec, + // Test getting pruned transactions + loop { + match rpc.get_pruned_transactions(&block.transactions).await { + Ok(_) => break, + Err(RpcError::ConnectionError(e)) => { + println!("get_pruned_transactions ConnectionError: {e}"); + continue; + } + Err(e) => panic!("couldn't call get_pruned_transactions: {e:?}"), + } } - let mut hashes_hex = block.transactions.iter().map(hex::encode).collect::>(); - let mut all_txs = vec![]; - while !hashes_hex.is_empty() { - let txs: TransactionsResponse = loop { - match rpc - .rpc_call( - "get_transactions", - Some(json!({ - "txs_hashes": hashes_hex.drain(.. hashes_hex.len().min(100)).collect::>(), - })), - ) - .await - { - Ok(txs) => break txs, - Err(RpcError::ConnectionError(e)) => { - println!("get_transactions ConnectionError: {e}"); - continue; - } - Err(e) => panic!("couldn't call get_transactions: {e:?}"), + let txs = loop { + match rpc.get_transactions(&block.transactions).await { + Ok(txs) => break txs, + Err(RpcError::ConnectionError(e)) => { + println!("get_transactions ConnectionError: {e}"); + continue; } - }; - assert!(txs.missed_tx.is_empty()); - all_txs.extend(txs.txs); - } + Err(e) => panic!("couldn't call get_transactions: {e:?}"), + } + }; let mut batch = BatchVerifier::new(); - for (tx_hash, tx_res) in block.transactions.into_iter().zip(all_txs) { - assert_eq!( - tx_res.tx_hash, - hex::encode(tx_hash), - "node returned a transaction with different hash" - ); - - let tx = Transaction::read( - &mut hex::decode(&tx_res.as_hex).expect("node returned non-hex transaction").as_slice(), - ) - .expect("couldn't deserialize transaction"); - - assert_eq!( - hex::encode(tx.serialize()), - tx_res.as_hex, - "Transaction serialization was different" - ); - assert_eq!(tx.hash(), tx_hash, "Transaction hash was different"); - + for tx in txs { match tx { Transaction::V1 { prefix: _, signatures } => { assert!(!signatures.is_empty()); diff --git a/coins/monero/wallet/src/scan.rs b/coins/monero/wallet/src/scan.rs index 73c98522..07dc81c3 100644 --- a/coins/monero/wallet/src/scan.rs +++ b/coins/monero/wallet/src/scan.rs @@ -9,7 +9,7 @@ use monero_rpc::{RpcError, Rpc}; use monero_serai::{ io::*, primitives::Commitment, - transaction::{Timelock, Transaction}, + transaction::{Timelock, Pruned, Transaction}, block::Block, }; use crate::{ @@ -108,7 +108,8 @@ impl InternalScanner { fn scan_transaction( &self, tx_start_index_on_blockchain: u64, - tx: &Transaction, + tx_hash: [u8; 32], + tx: &Transaction, ) -> Result { // Only scan TXs creating RingCT outputs // For the full details on why this check is equivalent, please see the documentation in `scan` @@ -218,7 +219,7 @@ impl InternalScanner { res.push(WalletOutput { absolute_id: AbsoluteId { - transaction: tx.hash(), + transaction: tx_hash, index_in_transaction: o.try_into().unwrap(), }, relative_id: RelativeId { @@ -251,8 +252,14 @@ impl InternalScanner { } // We obtain all TXs in full - let mut txs = vec![block.miner_transaction.clone()]; - txs.extend(rpc.get_transactions(&block.transactions).await?); + let mut txs_with_hashes = vec![( + block.miner_transaction.hash(), + Transaction::::from(block.miner_transaction.clone()), + )]; + let txs = rpc.get_pruned_transactions(&block.transactions).await?; + for (hash, tx) in block.transactions.iter().zip(txs) { + txs_with_hashes.push((*hash, tx)); + } /* Requesting the output index for each output we sucessfully scan would cause a loss of privacy @@ -295,13 +302,13 @@ impl InternalScanner { // Get the starting index let mut tx_start_index_on_blockchain = { let mut tx_start_index_on_blockchain = None; - for tx in &txs { + for (hash, tx) in &txs_with_hashes { // If this isn't a RingCT output, or there are no outputs, move to the next TX if (!matches!(tx, Transaction::V2 { .. })) || tx.prefix().outputs.is_empty() { continue; } - let index = *rpc.get_o_indexes(tx.hash()).await?.first().ok_or_else(|| { + let index = *rpc.get_o_indexes(*hash).await?.first().ok_or_else(|| { RpcError::InvalidNode( "requested output indexes for a TX with outputs and got none".to_string(), ) @@ -317,12 +324,12 @@ impl InternalScanner { }; let mut res = Timelocked(vec![]); - for tx in txs { + for (hash, tx) in txs_with_hashes { // Push all outputs into our result { let mut this_txs_outputs = vec![]; core::mem::swap( - &mut self.scan_transaction(tx_start_index_on_blockchain, &tx)?.0, + &mut self.scan_transaction(tx_start_index_on_blockchain, hash, &tx)?.0, &mut this_txs_outputs, ); res.0.extend(this_txs_outputs); diff --git a/coins/monero/wallet/src/send/eventuality.rs b/coins/monero/wallet/src/send/eventuality.rs index ff93c1ac..cd2543a4 100644 --- a/coins/monero/wallet/src/send/eventuality.rs +++ b/coins/monero/wallet/src/send/eventuality.rs @@ -3,8 +3,8 @@ use std_shims::{vec::Vec, io}; use zeroize::Zeroize; use crate::{ - ringct::RctProofs, - transaction::{Input, Timelock, Transaction}, + ringct::PrunedRctProofs, + transaction::{Input, Timelock, Pruned, Transaction}, send::SignableTransaction, }; @@ -55,7 +55,7 @@ impl Eventuality { /// intended payments don't match for each other's `Eventuality`s (as they'll have distinct /// inputs intended). #[must_use] - pub fn matches(&self, tx: &Transaction) -> bool { + pub fn matches(&self, tx: &Transaction) -> bool { // Verify extra if self.0.extra() != tx.prefix().extra { return false; @@ -91,7 +91,7 @@ impl Eventuality { // Check the encrypted amounts and commitments let commitments_and_encrypted_amounts = self.0.commitments_and_encrypted_amounts(&key_images); - let Transaction::V2 { proofs: Some(RctProofs { ref base, .. }), .. } = tx else { + let Transaction::V2 { proofs: Some(PrunedRctProofs { ref base, .. }), .. } = tx else { return false; }; if base.commitments != diff --git a/coins/monero/wallet/tests/eventuality.rs b/coins/monero/wallet/tests/eventuality.rs index 0c25e86e..c9e1d9eb 100644 --- a/coins/monero/wallet/tests/eventuality.rs +++ b/coins/monero/wallet/tests/eventuality.rs @@ -66,7 +66,7 @@ test!( assert_eq!(tx.prefix().extra, eventuality.extra()); // The TX should match - assert!(eventuality.matches(&tx)); + assert!(eventuality.matches(&tx.clone().into())); // Mutate the TX let Transaction::V2 { proofs: Some(ref mut proofs), .. } = tx else { @@ -74,7 +74,7 @@ test!( }; proofs.base.commitments[0] += ED25519_BASEPOINT_POINT; // Verify it no longer matches - assert!(!eventuality.matches(&tx)); + assert!(!eventuality.matches(&tx.clone().into())); }, ), ); diff --git a/coins/monero/wallet/tests/runner/mod.rs b/coins/monero/wallet/tests/runner/mod.rs index f8c1a4ea..7fe6ac53 100644 --- a/coins/monero/wallet/tests/runner/mod.rs +++ b/coins/monero/wallet/tests/runner/mod.rs @@ -289,7 +289,7 @@ macro_rules! test { }; assert_eq!(&eventuality.extra(), &tx.prefix().extra, "eventuality extra was distinct"); - assert!(eventuality.matches(&tx), "eventuality didn't match"); + assert!(eventuality.matches(&tx.clone().into()), "eventuality didn't match"); tx }; diff --git a/processor/src/networks/monero.rs b/processor/src/networks/monero.rs index 5311de69..54a3af24 100644 --- a/processor/src/networks/monero.rs +++ b/processor/src/networks/monero.rs @@ -109,6 +109,7 @@ impl OutputTrait for Output { } } +// TODO: Consider ([u8; 32], TransactionPruned) #[async_trait] impl TransactionTrait for Transaction { type Id = [u8; 32]; @@ -575,7 +576,7 @@ impl Network for Monero { }; if let Some((_, eventuality)) = eventualities.map.get(&tx.prefix().extra) { - if eventuality.matches(&tx) { + if eventuality.matches(&tx.clone().into()) { res.insert( eventualities.map.remove(&tx.prefix().extra).unwrap().0, (block.number().unwrap(), tx.id(), tx), @@ -681,7 +682,7 @@ impl Network for Monero { id: &[u8; 32], ) -> Result, NetworkError> { let tx = self.rpc.get_transaction(*id).await.map_err(map_rpc_err)?; - if eventuality.matches(&tx) { + if eventuality.matches(&tx.clone().into()) { Ok(Some(tx)) } else { Ok(None) @@ -699,7 +700,7 @@ impl Network for Monero { eventuality: &Self::Eventuality, claim: &[u8; 32], ) -> bool { - return eventuality.matches(&self.rpc.get_transaction(*claim).await.unwrap()); + return eventuality.matches(&self.rpc.get_pruned_transaction(*claim).await.unwrap()); } #[cfg(test)] @@ -711,7 +712,7 @@ impl Network for Monero { let block = self.rpc.get_block_by_number(block).await.unwrap(); for tx in &block.transactions { let tx = self.rpc.get_transaction(*tx).await.unwrap(); - if eventuality.matches(&tx) { + if eventuality.matches(&tx.clone().into()) { return tx; } }