mirror of
https://github.com/serai-dex/serai.git
synced 2025-12-12 22:19:26 +00:00
Compare commits
4 Commits
7b8bcae396
...
8e7e61adbd
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
8e7e61adbd | ||
|
|
0cb24dde02 | ||
|
|
97bfb183e8 | ||
|
|
85fc31fd82 |
530
Cargo.lock
generated
530
Cargo.lock
generated
File diff suppressed because it is too large
Load Diff
@@ -26,17 +26,22 @@ use serde_json::{Value, json};
|
||||
|
||||
use monero_serai::{
|
||||
io::*,
|
||||
transaction::{Input, Timelock, Transaction},
|
||||
transaction::{Input, Timelock, Pruned, Transaction},
|
||||
block::Block,
|
||||
DEFAULT_LOCK_WINDOW,
|
||||
};
|
||||
use monero_address::Address;
|
||||
|
||||
// Number of blocks the fee estimate will be valid for
|
||||
// https://github.com/monero-project/monero/blob/94e67bf96bbc010241f29ada6abc89f49a81759c/
|
||||
// src/wallet/wallet2.cpp#L121
|
||||
// https://github.com/monero-project/monero/blob/94e67bf96bbc010241f29ada6abc89f49a81759c
|
||||
// /src/wallet/wallet2.cpp#L121
|
||||
const GRACE_BLOCKS_FOR_FEE_ESTIMATE: u64 = 10;
|
||||
|
||||
// Monero errors if more than 100 is requested unless using a non-restricted RPC
|
||||
// https://github.com/monero-project/monero/blob/cc73fe71162d564ffda8e549b79a350bca53c454
|
||||
// /src/rpc/core_rpc_server.cpp#L75
|
||||
const TXS_PER_REQUEST: usize = 100;
|
||||
|
||||
/// An error from the RPC.
|
||||
#[derive(Clone, PartialEq, Eq, Debug)]
|
||||
#[cfg_attr(feature = "std", derive(thiserror::Error))]
|
||||
@@ -335,8 +340,6 @@ pub trait Rpc: Sync + Clone + Debug {
|
||||
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
|
||||
const TXS_PER_REQUEST: usize = 100;
|
||||
let this_count = TXS_PER_REQUEST.min(hashes_hex.len());
|
||||
|
||||
let txs: TransactionsResponse = self
|
||||
@@ -361,14 +364,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 +395,57 @@ 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() {
|
||||
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 +454,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 +529,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.
|
||||
@@ -961,7 +992,13 @@ impl<R: Rpc> DecoyRpc for R {
|
||||
outs: Vec<OutputResponse>,
|
||||
}
|
||||
|
||||
let res: OutsResponse = self
|
||||
// https://github.com/monero-project/monero/blob/cc73fe71162d564ffda8e549b79a350bca53c454
|
||||
// /src/rpc/core_rpc_server.cpp#L67
|
||||
const MAX_OUTS: usize = 5000;
|
||||
|
||||
let mut res = Vec::with_capacity(indexes.len());
|
||||
for indexes in indexes.chunks(MAX_OUTS) {
|
||||
let rpc_res: OutsResponse = self
|
||||
.rpc_call(
|
||||
"get_outs",
|
||||
Some(json!({
|
||||
@@ -974,12 +1011,12 @@ impl<R: Rpc> DecoyRpc for R {
|
||||
)
|
||||
.await?;
|
||||
|
||||
if res.status != "OK" {
|
||||
if rpc_res.status != "OK" {
|
||||
Err(RpcError::InvalidNode("bad response to get_outs".to_string()))?;
|
||||
}
|
||||
|
||||
Ok(
|
||||
res
|
||||
res.extend(
|
||||
rpc_res
|
||||
.outs
|
||||
.into_iter()
|
||||
.map(|output| {
|
||||
@@ -996,7 +1033,10 @@ impl<R: Rpc> DecoyRpc for R {
|
||||
})
|
||||
})
|
||||
.collect::<Result<Vec<_>, RpcError>>()?,
|
||||
)
|
||||
);
|
||||
}
|
||||
|
||||
Ok(res)
|
||||
}
|
||||
|
||||
async fn get_unlocked_outputs(
|
||||
|
||||
@@ -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());
|
||||
|
||||
@@ -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
|
||||
};
|
||||
|
||||
@@ -12,7 +12,7 @@ use crate::{
|
||||
};
|
||||
|
||||
use tendermint::{
|
||||
verify_tendermint_evience,
|
||||
verify_tendermint_evidence,
|
||||
ext::{Network, Commit},
|
||||
};
|
||||
|
||||
@@ -68,7 +68,7 @@ pub(crate) fn verify_tendermint_tx<N: Network>(
|
||||
tx.verify()?;
|
||||
|
||||
match tx {
|
||||
TendermintTx::SlashEvidence(ev) => verify_tendermint_evience::<N>(ev, schema, commit)
|
||||
TendermintTx::SlashEvidence(ev) => verify_tendermint_evidence::<N>(ev, schema, commit)
|
||||
.map_err(|_| TransactionError::InvalidContent)?,
|
||||
}
|
||||
|
||||
|
||||
@@ -158,7 +158,7 @@ fn decode_and_verify_signed_message<N: Network>(
|
||||
Ok(msg)
|
||||
}
|
||||
|
||||
pub fn verify_tendermint_evience<N: Network>(
|
||||
pub fn verify_tendermint_evidence<N: Network>(
|
||||
evidence: &Evidence,
|
||||
schema: &N::SignatureScheme,
|
||||
commit: impl Fn(u64) -> Option<Commit<N::SignatureScheme>>,
|
||||
|
||||
@@ -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;
|
||||
}
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user