Monero: fix decoy selection algo and add test for latest spendable (#384)

* Monero: fix decoy selection algo and add test for latest spendable

- DSA only selected coinbase outputs and didn't match the wallet2
implementation
- Added test to make sure DSA will select a decoy output from the
most recent unlocked block
- Made usage of "height" in DSA consistent with other usage of
"height" in Monero code (height == num blocks in chain)
- Rely on monerod RPC response for output's unlocked status

* xmr runner tests mine until outputs are unlocked

* fingerprintable canoncial select decoys

* Separate fingerprintable canonical function

Makes it simpler for callers who are unconcered with consistent
canonical output selection across multiple clients to rely on
the simpler Decoy::select and not worry about fingerprintable
canonical

* fix merge conflicts

* Put back TODO for issue #104

* Fix incorrect check on distribution len

The RingCT distribution on mainnet doesn't start until well after
genesis, so the distribution length is expected to be < height.

To be clear, this was my mistake from this series of changes
to the DSA. I noticed this mistake because the DSA would error
when running on mainnet.
This commit is contained in:
Justin Berman 2024-02-19 18:34:10 -08:00 committed by GitHub
parent 4f1f7984a6
commit 92d8b91be9
No known key found for this signature in database
GPG key ID: B5690EEEBB952194
10 changed files with 444 additions and 188 deletions

View file

@ -46,6 +46,10 @@ pub mod wallet;
#[cfg(test)] #[cfg(test)]
mod tests; mod tests;
pub const DEFAULT_LOCK_WINDOW: usize = 10;
pub const COINBASE_LOCK_WINDOW: usize = 60;
pub const BLOCK_TIME: usize = 120;
static INV_EIGHT_CELL: OnceLock<Scalar> = OnceLock::new(); static INV_EIGHT_CELL: OnceLock<Scalar> = OnceLock::new();
#[allow(non_snake_case)] #[allow(non_snake_case)]
pub(crate) fn INV_EIGHT() -> Scalar { pub(crate) fn INV_EIGHT() -> Scalar {

View file

@ -54,6 +54,15 @@ struct TransactionsResponse {
txs: Vec<TransactionResponse>, txs: Vec<TransactionResponse>,
} }
#[derive(Deserialize, Debug)]
pub struct OutputResponse {
pub height: usize,
pub unlocked: bool,
key: String,
mask: String,
txid: String,
}
#[derive(Clone, PartialEq, Eq, Debug)] #[derive(Clone, PartialEq, Eq, Debug)]
#[cfg_attr(feature = "std", derive(thiserror::Error))] #[cfg_attr(feature = "std", derive(thiserror::Error))]
pub enum RpcError { pub enum RpcError {
@ -534,29 +543,15 @@ impl<R: RpcConnection> Rpc<R> {
Ok(distributions.distributions.swap_remove(0).distribution) Ok(distributions.distributions.swap_remove(0).distribution)
} }
/// Get the specified outputs from the RingCT (zero-amount) pool, but only return them if their /// Get the specified outputs from the RingCT (zero-amount) pool
/// timelock has been satisfied. pub async fn get_outs(&self, indexes: &[u64]) -> Result<Vec<OutputResponse>, RpcError> {
///
/// The timelock being satisfied is distinct from being free of the 10-block lock applied to all
/// Monero transactions.
pub async fn get_unlocked_outputs(
&self,
indexes: &[u64],
height: usize,
) -> Result<Vec<Option<[EdwardsPoint; 2]>>, RpcError> {
#[derive(Deserialize, Debug)] #[derive(Deserialize, Debug)]
struct Out { struct OutsResponse {
key: String, status: String,
mask: String, outs: Vec<OutputResponse>,
txid: String,
} }
#[derive(Deserialize, Debug)] let res: OutsResponse = self
struct Outs {
outs: Vec<Out>,
}
let outs: Outs = self
.rpc_call( .rpc_call(
"get_outs", "get_outs",
Some(json!({ Some(json!({
@ -569,15 +564,39 @@ impl<R: RpcConnection> Rpc<R> {
) )
.await?; .await?;
let txs = self if res.status != "OK" {
Err(RpcError::InvalidNode("bad response to get_outs".to_string()))?;
}
Ok(res.outs)
}
/// Get the specified outputs from the RingCT (zero-amount) pool, but only return them if their
/// timelock has been satisfied.
///
/// The timelock being satisfied is distinct from being free of the 10-block lock applied to all
/// Monero transactions.
pub async fn get_unlocked_outputs(
&self,
indexes: &[u64],
height: usize,
fingerprintable_canonical: bool,
) -> Result<Vec<Option<[EdwardsPoint; 2]>>, RpcError> {
let outs: Vec<OutputResponse> = self.get_outs(indexes).await?;
// Only need to fetch txs to do canonical check on timelock
let txs = if fingerprintable_canonical {
self
.get_transactions( .get_transactions(
&outs.outs.iter().map(|out| hash_hex(&out.txid)).collect::<Result<Vec<_>, _>>()?, &outs.iter().map(|out| hash_hex(&out.txid)).collect::<Result<Vec<_>, _>>()?,
) )
.await?; .await?
} else {
Vec::new()
};
// TODO: https://github.com/serai-dex/serai/issues/104 // TODO: https://github.com/serai-dex/serai/issues/104
outs outs
.outs
.iter() .iter()
.enumerate() .enumerate()
.map(|(i, out)| { .map(|(i, out)| {
@ -593,10 +612,13 @@ impl<R: RpcConnection> Rpc<R> {
) else { ) else {
return Ok(None); return Ok(None);
}; };
Ok( Ok(Some([key, rpc_point(&out.mask)?]).filter(|_| {
Some([key, rpc_point(&out.mask)?]) if fingerprintable_canonical {
.filter(|_| Timelock::Block(height) >= txs[i].prefix.timelock), Timelock::Block(height) >= txs[i].prefix.timelock
) } else {
out.unlocked
}
}))
}) })
.collect() .collect()
} }
@ -713,13 +735,14 @@ impl<R: RpcConnection> Rpc<R> {
&self, &self,
address: &str, address: &str,
block_count: usize, block_count: usize,
) -> Result<Vec<[u8; 32]>, RpcError> { ) -> Result<(Vec<[u8; 32]>, usize), RpcError> {
#[derive(Debug, Deserialize)] #[derive(Debug, Deserialize)]
struct BlocksResponse { struct BlocksResponse {
blocks: Vec<String>, blocks: Vec<String>,
height: usize,
} }
let block_strs = self let res = self
.json_rpc_call::<BlocksResponse>( .json_rpc_call::<BlocksResponse>(
"generateblocks", "generateblocks",
Some(json!({ Some(json!({
@ -727,13 +750,12 @@ impl<R: RpcConnection> Rpc<R> {
"amount_of_blocks": block_count "amount_of_blocks": block_count
})), })),
) )
.await? .await?;
.blocks;
let mut blocks = Vec::with_capacity(block_strs.len()); let mut blocks = Vec::with_capacity(res.blocks.len());
for block in block_strs { for block in res.blocks {
blocks.push(hash_hex(&block)?); blocks.push(hash_hex(&block)?);
} }
Ok(blocks) Ok((blocks, res.height))
} }
} }

View file

@ -161,7 +161,9 @@ impl Timelock {
impl PartialOrd for Timelock { impl PartialOrd for Timelock {
fn partial_cmp(&self, other: &Self) -> Option<Ordering> { fn partial_cmp(&self, other: &Self) -> Option<Ordering> {
match (self, other) { match (self, other) {
(Timelock::None, Timelock::None) => Some(Ordering::Equal),
(Timelock::None, _) => Some(Ordering::Less), (Timelock::None, _) => Some(Ordering::Less),
(_, Timelock::None) => Some(Ordering::Greater),
(Timelock::Block(a), Timelock::Block(b)) => a.partial_cmp(b), (Timelock::Block(a), Timelock::Block(b)) => a.partial_cmp(b),
(Timelock::Time(a), Timelock::Time(b)) => a.partial_cmp(b), (Timelock::Time(a), Timelock::Time(b)) => a.partial_cmp(b),
_ => None, _ => None,

View file

@ -21,15 +21,13 @@ use crate::{
serialize::varint_len, serialize::varint_len,
wallet::SpendableOutput, wallet::SpendableOutput,
rpc::{RpcError, RpcConnection, Rpc}, rpc::{RpcError, RpcConnection, Rpc},
DEFAULT_LOCK_WINDOW, COINBASE_LOCK_WINDOW, BLOCK_TIME,
}; };
const LOCK_WINDOW: usize = 10;
const MATURITY: u64 = 60;
const RECENT_WINDOW: usize = 15; const RECENT_WINDOW: usize = 15;
const BLOCK_TIME: usize = 120;
const BLOCKS_PER_YEAR: usize = 365 * 24 * 60 * 60 / BLOCK_TIME; const BLOCKS_PER_YEAR: usize = 365 * 24 * 60 * 60 / BLOCK_TIME;
#[allow(clippy::cast_precision_loss)] #[allow(clippy::cast_precision_loss)]
const TIP_APPLICATION: f64 = (LOCK_WINDOW * BLOCK_TIME) as f64; const TIP_APPLICATION: f64 = (DEFAULT_LOCK_WINDOW * BLOCK_TIME) as f64;
// TODO: Resolve safety of this in case a reorg occurs/the network changes // TODO: Resolve safety of this in case a reorg occurs/the network changes
// TODO: Update this when scanning a block, as possible // TODO: Update this when scanning a block, as possible
@ -52,8 +50,10 @@ async fn select_n<'a, R: RngCore + CryptoRng, RPC: RpcConnection>(
real: &[u64], real: &[u64],
used: &mut HashSet<u64>, used: &mut HashSet<u64>,
count: usize, count: usize,
fingerprintable_canonical: bool,
) -> Result<Vec<(u64, [EdwardsPoint; 2])>, RpcError> { ) -> Result<Vec<(u64, [EdwardsPoint; 2])>, RpcError> {
if height >= rpc.get_height().await? { // TODO: consider removing this extra RPC and expect the caller to handle it
if fingerprintable_canonical && height > rpc.get_height().await? {
// TODO: Don't use InternalError for the caller's failure // TODO: Don't use InternalError for the caller's failure
Err(RpcError::InternalError("decoys being requested from too young blocks"))?; Err(RpcError::InternalError("decoys being requested from too young blocks"))?;
} }
@ -64,6 +64,8 @@ async fn select_n<'a, R: RngCore + CryptoRng, RPC: RpcConnection>(
// Retries on failure. Retries are obvious as decoys, yet should be minimal // Retries on failure. Retries are obvious as decoys, yet should be minimal
while confirmed.len() != count { while confirmed.len() != count {
let remaining = count - confirmed.len(); let remaining = count - confirmed.len();
// TODO: over-request candidates in case some are locked to avoid needing
// round trips to the daemon (and revealing obvious decoys to the daemon)
let mut candidates = Vec::with_capacity(remaining); let mut candidates = Vec::with_capacity(remaining);
while candidates.len() != remaining { while candidates.len() != remaining {
#[cfg(test)] #[cfg(test)]
@ -117,7 +119,14 @@ async fn select_n<'a, R: RngCore + CryptoRng, RPC: RpcConnection>(
} }
} }
for (i, output) in rpc.get_unlocked_outputs(&candidates, height).await?.iter_mut().enumerate() { // TODO: make sure that the real output is included in the response, and
// that mask and key are equal to expected
for (i, output) in rpc
.get_unlocked_outputs(&candidates, height, fingerprintable_canonical)
.await?
.iter_mut()
.enumerate()
{
// Don't include the real spend as a decoy, despite requesting it // Don't include the real spend as a decoy, despite requesting it
if real_indexes.contains(&i) { if real_indexes.contains(&i) {
continue; continue;
@ -141,32 +150,14 @@ fn offset(ring: &[u64]) -> Vec<u64> {
res res
} }
/// Decoy data, containing the actual member as well (at index `i`). async fn select_decoys<R: RngCore + CryptoRng, RPC: RpcConnection>(
#[derive(Clone, PartialEq, Eq, Debug, Zeroize, ZeroizeOnDrop)]
pub struct Decoys {
pub(crate) i: u8,
pub(crate) offsets: Vec<u64>,
pub(crate) ring: Vec<[EdwardsPoint; 2]>,
}
#[allow(clippy::len_without_is_empty)]
impl Decoys {
pub fn fee_weight(offsets: &[u64]) -> usize {
varint_len(offsets.len()) + offsets.iter().map(|offset| varint_len(*offset)).sum::<usize>()
}
pub fn len(&self) -> usize {
self.offsets.len()
}
/// Select decoys using the same distribution as Monero.
pub async fn select<R: RngCore + CryptoRng, RPC: RpcConnection>(
rng: &mut R, rng: &mut R,
rpc: &Rpc<RPC>, rpc: &Rpc<RPC>,
ring_len: usize, ring_len: usize,
height: usize, height: usize,
inputs: &[SpendableOutput], inputs: &[SpendableOutput],
) -> Result<Vec<Decoys>, RpcError> { fingerprintable_canonical: bool,
) -> Result<Vec<Decoys>, RpcError> {
#[cfg(feature = "cache-distribution")] #[cfg(feature = "cache-distribution")]
#[cfg(not(feature = "std"))] #[cfg(not(feature = "std"))]
let mut distribution = DISTRIBUTION().lock(); let mut distribution = DISTRIBUTION().lock();
@ -187,19 +178,25 @@ impl Decoys {
outputs.push((real[real.len() - 1], [input.key(), input.commitment().calculate()])); outputs.push((real[real.len() - 1], [input.key(), input.commitment().calculate()]));
} }
if distribution.len() <= height { if distribution.len() < height {
let extension = rpc.get_output_distribution(distribution.len(), height).await?; // TODO: verify distribution elems are strictly increasing
let extension =
rpc.get_output_distribution(distribution.len(), height.saturating_sub(1)).await?;
distribution.extend(extension); distribution.extend(extension);
} }
// If asked to use an older height than previously asked, truncate to ensure accuracy // If asked to use an older height than previously asked, truncate to ensure accuracy
// Should never happen, yet risks desyncing if it did // Should never happen, yet risks desyncing if it did
distribution.truncate(height + 1); // height is inclusive, and 0 is a valid height distribution.truncate(height);
if distribution.len() < DEFAULT_LOCK_WINDOW {
Err(RpcError::InternalError("not enough decoy candidates"))?;
}
let high = distribution[distribution.len() - 1];
#[allow(clippy::cast_precision_loss)] #[allow(clippy::cast_precision_loss)]
let per_second = { let per_second = {
let blocks = distribution.len().min(BLOCKS_PER_YEAR); let blocks = distribution.len().min(BLOCKS_PER_YEAR);
let outputs = high - distribution[distribution.len().saturating_sub(blocks + 1)]; let initial = distribution[distribution.len().saturating_sub(blocks + 1)];
let outputs = distribution[distribution.len() - 1].saturating_sub(initial);
(outputs as f64) / ((blocks * BLOCK_TIME) as f64) (outputs as f64) / ((blocks * BLOCK_TIME) as f64)
}; };
@ -209,8 +206,11 @@ impl Decoys {
} }
// TODO: Create a TX with less than the target amount, as allowed by the protocol // TODO: Create a TX with less than the target amount, as allowed by the protocol
if (high - MATURITY) < u64::try_from(inputs.len() * ring_len).unwrap() { let high = distribution[distribution.len() - DEFAULT_LOCK_WINDOW];
Err(RpcError::InternalError("not enough decoy candidates"))?; if high.saturating_sub(COINBASE_LOCK_WINDOW as u64) <
u64::try_from(inputs.len() * ring_len).unwrap()
{
Err(RpcError::InternalError("not enough coinbase candidates"))?;
} }
// Select all decoys for this transaction, assuming we generate a sane transaction // Select all decoys for this transaction, assuming we generate a sane transaction
@ -226,6 +226,7 @@ impl Decoys {
&real, &real,
&mut used, &mut used,
inputs.len() * decoy_count, inputs.len() * decoy_count,
fingerprintable_canonical,
) )
.await?; .await?;
real.zeroize(); real.zeroize();
@ -275,6 +276,7 @@ impl Decoys {
&[], &[],
&mut used, &mut used,
ring_len - ring.len(), ring_len - ring.len(),
fingerprintable_canonical,
) )
.await?, .await?,
); );
@ -294,5 +296,61 @@ impl Decoys {
} }
Ok(res) Ok(res)
}
/// Decoy data, containing the actual member as well (at index `i`).
#[derive(Clone, PartialEq, Eq, Debug, Zeroize, ZeroizeOnDrop)]
pub struct Decoys {
pub(crate) i: u8,
pub(crate) offsets: Vec<u64>,
pub(crate) ring: Vec<[EdwardsPoint; 2]>,
}
#[allow(clippy::len_without_is_empty)]
impl Decoys {
pub fn fee_weight(offsets: &[u64]) -> usize {
varint_len(offsets.len()) + offsets.iter().map(|offset| varint_len(*offset)).sum::<usize>()
}
pub fn len(&self) -> usize {
self.offsets.len()
}
pub fn indexes(&self) -> Vec<u64> {
let mut res = vec![self.offsets[0]; self.len()];
for m in 1 .. res.len() {
res[m] = res[m - 1] + self.offsets[m];
}
res
}
/// Select decoys using the same distribution as Monero. Relies on the monerod RPC
/// response for an output's unlocked status, minimizing trips to the daemon.
pub async fn select<R: RngCore + CryptoRng, RPC: RpcConnection>(
rng: &mut R,
rpc: &Rpc<RPC>,
ring_len: usize,
height: usize,
inputs: &[SpendableOutput],
) -> Result<Vec<Decoys>, RpcError> {
select_decoys(rng, rpc, ring_len, height, inputs, false).await
}
/// If no reorg has occurred and an honest RPC, any caller who passes the same height to this
/// function will use the same distribution to select decoys. It is fingerprintable
/// because a caller using this will not be able to select decoys that are timelocked
/// with a timestamp. Any transaction which includes timestamp timelocked decoys in its
/// rings could not be constructed using this function.
///
/// TODO: upstream change to monerod get_outs RPC to accept a height param for checking
/// output's unlocked status and remove all usage of fingerprintable_canonical
pub async fn fingerprintable_canonical_select<R: RngCore + CryptoRng, RPC: RpcConnection>(
rng: &mut R,
rpc: &Rpc<RPC>,
ring_len: usize,
height: usize,
inputs: &[SpendableOutput],
) -> Result<Vec<Decoys>, RpcError> {
select_decoys(rng, rpc, ring_len, height, inputs, true).await
} }
} }

View file

@ -0,0 +1,162 @@
use monero_serai::{
transaction::Transaction,
wallet::SpendableOutput,
rpc::{Rpc, OutputResponse},
Protocol, DEFAULT_LOCK_WINDOW,
};
mod runner;
test!(
select_latest_output_as_decoy_canonical,
(
// First make an initial tx0
|_, mut builder: Builder, addr| async move {
builder.add_payment(addr, 2000000000000);
(builder.build().unwrap(), ())
},
|rpc: Rpc<_>, tx: Transaction, mut scanner: Scanner, _| async move {
let output = scanner.scan_transaction(&tx).not_locked().swap_remove(0);
assert_eq!(output.commitment().amount, 2000000000000);
SpendableOutput::from(&rpc, output).await.unwrap()
},
),
(
// Then make a second tx1
|protocol: Protocol, rpc: Rpc<_>, mut builder: Builder, addr, state: _| async move {
let output_tx0: SpendableOutput = state;
let decoys = Decoys::fingerprintable_canonical_select(
&mut OsRng,
&rpc,
protocol.ring_len(),
rpc.get_height().await.unwrap(),
&[output_tx0.clone()],
)
.await
.unwrap();
let inputs = [output_tx0.clone()].into_iter().zip(decoys).collect::<Vec<_>>();
builder.add_inputs(&inputs);
builder.add_payment(addr, 1000000000000);
(builder.build().unwrap(), (protocol, output_tx0))
},
// Then make sure DSA selects freshly unlocked output from tx1 as a decoy
|rpc: Rpc<_>, tx: Transaction, mut scanner: Scanner, state: (_, _)| async move {
use rand_core::OsRng;
let height = rpc.get_height().await.unwrap();
let output_tx1 =
SpendableOutput::from(&rpc, scanner.scan_transaction(&tx).not_locked().swap_remove(0))
.await
.unwrap();
// Make sure output from tx1 is in the block in which it unlocks
let out_tx1: OutputResponse =
rpc.get_outs(&[output_tx1.global_index]).await.unwrap().swap_remove(0);
assert_eq!(out_tx1.height, height - DEFAULT_LOCK_WINDOW);
assert!(out_tx1.unlocked);
// Select decoys using spendable output from tx0 as the real, and make sure DSA selects
// the freshly unlocked output from tx1 as a decoy
let (protocol, output_tx0): (Protocol, SpendableOutput) = state;
let mut selected_fresh_decoy = false;
let mut attempts = 1000;
while !selected_fresh_decoy && attempts > 0 {
let decoys = Decoys::fingerprintable_canonical_select(
&mut OsRng, // TODO: use a seeded RNG to consistently select the latest output
&rpc,
protocol.ring_len(),
height,
&[output_tx0.clone()],
)
.await
.unwrap();
selected_fresh_decoy = decoys[0].indexes().contains(&output_tx1.global_index);
attempts -= 1;
}
assert!(selected_fresh_decoy);
assert_eq!(height, rpc.get_height().await.unwrap());
},
),
);
test!(
select_latest_output_as_decoy,
(
// First make an initial tx0
|_, mut builder: Builder, addr| async move {
builder.add_payment(addr, 2000000000000);
(builder.build().unwrap(), ())
},
|rpc: Rpc<_>, tx: Transaction, mut scanner: Scanner, _| async move {
let output = scanner.scan_transaction(&tx).not_locked().swap_remove(0);
assert_eq!(output.commitment().amount, 2000000000000);
SpendableOutput::from(&rpc, output).await.unwrap()
},
),
(
// Then make a second tx1
|protocol: Protocol, rpc: Rpc<_>, mut builder: Builder, addr, state: _| async move {
let output_tx0: SpendableOutput = state;
let decoys = Decoys::select(
&mut OsRng,
&rpc,
protocol.ring_len(),
rpc.get_height().await.unwrap(),
&[output_tx0.clone()],
)
.await
.unwrap();
let inputs = [output_tx0.clone()].into_iter().zip(decoys).collect::<Vec<_>>();
builder.add_inputs(&inputs);
builder.add_payment(addr, 1000000000000);
(builder.build().unwrap(), (protocol, output_tx0))
},
// Then make sure DSA selects freshly unlocked output from tx1 as a decoy
|rpc: Rpc<_>, tx: Transaction, mut scanner: Scanner, state: (_, _)| async move {
use rand_core::OsRng;
let height = rpc.get_height().await.unwrap();
let output_tx1 =
SpendableOutput::from(&rpc, scanner.scan_transaction(&tx).not_locked().swap_remove(0))
.await
.unwrap();
// Make sure output from tx1 is in the block in which it unlocks
let out_tx1: OutputResponse =
rpc.get_outs(&[output_tx1.global_index]).await.unwrap().swap_remove(0);
assert_eq!(out_tx1.height, height - DEFAULT_LOCK_WINDOW);
assert!(out_tx1.unlocked);
// Select decoys using spendable output from tx0 as the real, and make sure DSA selects
// the freshly unlocked output from tx1 as a decoy
let (protocol, output_tx0): (Protocol, SpendableOutput) = state;
let mut selected_fresh_decoy = false;
let mut attempts = 1000;
while !selected_fresh_decoy && attempts > 0 {
let decoys = Decoys::select(
&mut OsRng, // TODO: use a seeded RNG to consistently select the latest output
&rpc,
protocol.ring_len(),
height,
&[output_tx0.clone()],
)
.await
.unwrap();
selected_fresh_decoy = decoys[0].indexes().contains(&output_tx1.global_index);
attempts -= 1;
}
assert!(selected_fresh_decoy);
assert_eq!(height, rpc.get_height().await.unwrap());
},
),
);

View file

@ -17,6 +17,7 @@ use monero_serai::{
SpendableOutput, Fee, SpendableOutput, Fee,
}, },
transaction::Transaction, transaction::Transaction,
DEFAULT_LOCK_WINDOW,
}; };
pub fn random_address() -> (Scalar, ViewPair, MoneroAddress) { pub fn random_address() -> (Scalar, ViewPair, MoneroAddress) {
@ -36,7 +37,6 @@ pub fn random_address() -> (Scalar, ViewPair, MoneroAddress) {
// TODO: Support transactions already on-chain // TODO: Support transactions already on-chain
// TODO: Don't have a side effect of mining blocks more blocks than needed under race conditions // TODO: Don't have a side effect of mining blocks more blocks than needed under race conditions
// TODO: mine as much as needed instead of default 10 blocks
pub async fn mine_until_unlocked(rpc: &Rpc<HttpRpc>, addr: &str, tx_hash: [u8; 32]) { pub async fn mine_until_unlocked(rpc: &Rpc<HttpRpc>, addr: &str, tx_hash: [u8; 32]) {
// mine until tx is in a block // mine until tx is in a block
let mut height = rpc.get_height().await.unwrap(); let mut height = rpc.get_height().await.unwrap();
@ -46,15 +46,23 @@ pub async fn mine_until_unlocked(rpc: &Rpc<HttpRpc>, addr: &str, tx_hash: [u8; 3
found = match block.txs.iter().find(|&&x| x == tx_hash) { found = match block.txs.iter().find(|&&x| x == tx_hash) {
Some(_) => true, Some(_) => true,
None => { None => {
rpc.generate_blocks(addr, 1).await.unwrap(); height = rpc.generate_blocks(addr, 1).await.unwrap().1 + 1;
height += 1;
false false
} }
} }
} }
// mine 9 more blocks to unlock the tx // Mine until tx's outputs are unlocked
rpc.generate_blocks(addr, 9).await.unwrap(); let o_indexes: Vec<u64> = rpc.get_o_indexes(tx_hash).await.unwrap();
while rpc
.get_outs(&o_indexes)
.await
.unwrap()
.into_iter()
.all(|o| (!(o.unlocked && height >= (o.height + DEFAULT_LOCK_WINDOW))))
{
height = rpc.generate_blocks(addr, 1).await.unwrap().1 + 1;
}
} }
// Mines 60 blocks and returns an unlocked miner TX output. // Mines 60 blocks and returns an unlocked miner TX output.
@ -260,12 +268,12 @@ macro_rules! test {
let temp = Box::new({ let temp = Box::new({
let mut builder = builder.clone(); let mut builder = builder.clone();
let decoys = Decoys::select( let decoys = Decoys::fingerprintable_canonical_select(
&mut OsRng, &mut OsRng,
&rpc, &rpc,
protocol.ring_len(), protocol.ring_len(),
rpc.get_height().await.unwrap() - 1, rpc.get_height().await.unwrap(),
&[miner_tx.clone()] &[miner_tx.clone()],
) )
.await .await
.unwrap(); .unwrap();

View file

@ -24,11 +24,11 @@ async fn add_inputs(
spendable_outputs.push(SpendableOutput::from(rpc, output).await.unwrap()); spendable_outputs.push(SpendableOutput::from(rpc, output).await.unwrap());
} }
let decoys = Decoys::select( let decoys = Decoys::fingerprintable_canonical_select(
&mut OsRng, &mut OsRng,
rpc, rpc,
protocol.ring_len(), protocol.ring_len(),
rpc.get_height().await.unwrap() - 1, rpc.get_height().await.unwrap(),
&spendable_outputs, &spendable_outputs,
) )
.await .await

View file

@ -338,7 +338,7 @@ impl Monero {
// All signers need to select the same decoys // All signers need to select the same decoys
// All signers use the same height and a seeded RNG to make sure they do so. // All signers use the same height and a seeded RNG to make sure they do so.
let decoys = Decoys::select( let decoys = Decoys::fingerprintable_canonical_select(
&mut ChaCha20Rng::from_seed(transcript.rng_seed(b"decoys")), &mut ChaCha20Rng::from_seed(transcript.rng_seed(b"decoys")),
&self.rpc, &self.rpc,
protocol.ring_len(), protocol.ring_len(),
@ -742,11 +742,11 @@ impl Network for Monero {
let protocol = self.rpc.get_protocol().await.unwrap(); let protocol = self.rpc.get_protocol().await.unwrap();
let decoys = Decoys::select( let decoys = Decoys::fingerprintable_canonical_select(
&mut OsRng, &mut OsRng,
&self.rpc, &self.rpc,
protocol.ring_len(), protocol.ring_len(),
self.rpc.get_height().await.unwrap() - 1, self.rpc.get_height().await.unwrap(),
&outputs, &outputs,
) )
.await .await

View file

@ -100,7 +100,7 @@ async fn mint_and_burn_test() {
let rpc = producer_handles.monero(ops).await; let rpc = producer_handles.monero(ops).await;
let mut res = Vec::with_capacity(count); let mut res = Vec::with_capacity(count);
for _ in 0 .. count { for _ in 0 .. count {
let block = rpc.get_block(rpc.generate_blocks(&addr, 1).await.unwrap()[0]).await.unwrap(); let block = rpc.get_block(rpc.generate_blocks(&addr, 1).await.unwrap().0[0]).await.unwrap();
let mut txs = Vec::with_capacity(block.txs.len()); let mut txs = Vec::with_capacity(block.txs.len());
for tx in &block.txs { for tx in &block.txs {
@ -360,11 +360,11 @@ async fn mint_and_burn_test() {
.unwrap() .unwrap()
.swap_remove(0); .swap_remove(0);
let decoys = Decoys::select( let decoys = Decoys::fingerprintable_canonical_select(
&mut OsRng, &mut OsRng,
&rpc, &rpc,
Protocol::v16.ring_len(), Protocol::v16.ring_len(),
rpc.get_height().await.unwrap() - 1, rpc.get_height().await.unwrap(),
&[output.clone()], &[output.clone()],
) )
.await .await

View file

@ -308,11 +308,11 @@ impl Wallet {
.expect("prior transaction was never published"), .expect("prior transaction was never published"),
); );
} }
let mut decoys = Decoys::select( let mut decoys = Decoys::fingerprintable_canonical_select(
&mut OsRng, &mut OsRng,
&rpc, &rpc,
Protocol::v16.ring_len(), Protocol::v16.ring_len(),
rpc.get_height().await.unwrap() - 1, rpc.get_height().await.unwrap(),
&these_inputs, &these_inputs,
) )
.await .await