mirror of
https://github.com/serai-dex/serai.git
synced 2024-12-22 11:39:35 +00:00
Have monero-wallet use Transaction<Pruned>, not Transaction
This commit is contained in:
parent
7b8bcae396
commit
85fc31fd82
7 changed files with 115 additions and 107 deletions
|
@ -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.
|
||||
|
|
|
@ -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<String>,
|
||||
txs: Vec<TransactionResponse>,
|
||||
// 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::<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
|
||||
{
|
||||
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());
|
||||
|
|
|
@ -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);
|
||||
|
|
|
@ -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 !=
|
||||
|
|
|
@ -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()));
|
||||
},
|
||||
),
|
||||
);
|
||||
|
|
|
@ -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
|
||||
};
|
||||
|
|
|
@ -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;
|
||||
}
|
||||
}
|
||||
|
|
Loading…
Reference in a new issue