mirror of
https://github.com/serai-dex/serai.git
synced 2025-12-10 05:09:22 +00:00
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:
162
coins/monero/tests/decoys.rs
Normal file
162
coins/monero/tests/decoys.rs
Normal 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());
|
||||
},
|
||||
),
|
||||
);
|
||||
Reference in New Issue
Block a user