Have monero-wallet use Transaction<Pruned>, not Transaction

This commit is contained in:
Luke Parker 2024-07-14 19:30:50 -04:00
parent 7b8bcae396
commit 85fc31fd82
No known key found for this signature in database
7 changed files with 115 additions and 107 deletions

View file

@ -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<Vec<Transaction<Pruned>>, RpcError> {
if hashes.is_empty() {
return Ok(vec![]);
}
let mut hashes_hex = hashes.iter().map(hex::encode).collect::<Vec<_>>();
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::<Vec<_>>(),
"prune": true,
})),
)
.await?;
if !txs.missed_tx.is_empty() {
Err(RpcError::TransactionsNotFound(
txs.missed_tx.iter().map(|hash| hash_hex(hash)).collect::<Result<_, _>>()?,
))?;
}
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::<Pruned>::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<Transaction<Pruned>, 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<Vec<Transaction>, 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<Vec<Transaction>, 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.

View file

@ -57,31 +57,20 @@ 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,
// 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:?}"),
}
#[derive(Deserialize, Debug)]
struct TransactionsResponse {
#[serde(default)]
missed_tx: Vec<String>,
txs: Vec<TransactionResponse>,
}
let mut hashes_hex = block.transactions.iter().map(hex::encode).collect::<Vec<_>>();
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::<Vec<_>>(),
})),
)
.await
{
let txs = loop {
match rpc.get_transactions(&block.transactions).await {
Ok(txs) => break txs,
Err(RpcError::ConnectionError(e)) => {
println!("get_transactions ConnectionError: {e}");
@ -90,30 +79,9 @@ async fn check_block(rpc: impl Rpc, block_i: usize) {
Err(e) => panic!("couldn't call get_transactions: {e:?}"),
}
};
assert!(txs.missed_tx.is_empty());
all_txs.extend(txs.txs);
}
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());

View file

@ -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<Pruned>,
) -> Result<Timelocked, RpcError> {
// 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::<Pruned>::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);

View file

@ -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<Pruned>) -> 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 !=

View file

@ -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()));
},
),
);

View file

@ -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
};

View file

@ -109,6 +109,7 @@ impl OutputTrait<Monero> for Output {
}
}
// TODO: Consider ([u8; 32], TransactionPruned)
#[async_trait]
impl TransactionTrait<Monero> 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<Option<Transaction>, 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;
}
}