serai/coins/monero/tests/send.rs
Justin Berman 397fca748f
monero-serai: make it clear that not providing a change address is fingerprintable (#472)
* Make it clear not providing a change address is fingerprintable

When no change address is provided, all change is shunted to the
fee. This PR makes it clear to the caller that it is fingerprintable
when the caller does this.

* Review comments
2023-12-08 07:42:02 -05:00

316 lines
10 KiB
Rust

use rand_core::OsRng;
use monero_serai::{
transaction::Transaction,
wallet::{
extra::Extra, address::SubaddressIndex, ReceivedOutput, SpendableOutput, Decoys,
SignableTransactionBuilder,
},
rpc::{Rpc, HttpRpc},
Protocol,
};
mod runner;
// Set up inputs, select decoys, then add them to the TX builder
async fn add_inputs(
protocol: Protocol,
rpc: &Rpc<HttpRpc>,
outputs: Vec<ReceivedOutput>,
builder: &mut SignableTransactionBuilder,
) {
let mut spendable_outputs = Vec::with_capacity(outputs.len());
for output in outputs {
spendable_outputs.push(SpendableOutput::from(rpc, output).await.unwrap());
}
let decoys = Decoys::select(
&mut OsRng,
rpc,
protocol.ring_len(),
rpc.get_height().await.unwrap() - 1,
&spendable_outputs,
)
.await
.unwrap();
let inputs = spendable_outputs.into_iter().zip(decoys).collect::<Vec<_>>();
builder.add_inputs(&inputs);
}
test!(
spend_miner_output,
(
|_, mut builder: Builder, addr| async move {
builder.add_payment(addr, 5);
(builder.build().unwrap(), ())
},
|_, tx: Transaction, mut scanner: Scanner, _| async move {
let output = scanner.scan_transaction(&tx).not_locked().swap_remove(0);
assert_eq!(output.commitment().amount, 5);
},
),
);
test!(
spend_multiple_outputs,
(
|_, mut builder: Builder, addr| async move {
builder.add_payment(addr, 1000000000000);
builder.add_payment(addr, 2000000000000);
(builder.build().unwrap(), ())
},
|_, tx: Transaction, mut scanner: Scanner, _| async move {
let mut outputs = scanner.scan_transaction(&tx).not_locked();
outputs.sort_by(|x, y| x.commitment().amount.cmp(&y.commitment().amount));
assert_eq!(outputs[0].commitment().amount, 1000000000000);
assert_eq!(outputs[1].commitment().amount, 2000000000000);
outputs
},
),
(
|protocol: Protocol, rpc, mut builder: Builder, addr, outputs: Vec<ReceivedOutput>| async move {
add_inputs(protocol, &rpc, outputs, &mut builder).await;
builder.add_payment(addr, 6);
(builder.build().unwrap(), ())
},
|_, tx: Transaction, mut scanner: Scanner, _| async move {
let output = scanner.scan_transaction(&tx).not_locked().swap_remove(0);
assert_eq!(output.commitment().amount, 6);
},
),
);
test!(
// Ideally, this would be single_R, yet it isn't feasible to apply allow(non_snake_case) here
single_r_subaddress_send,
(
// Consume this builder for an output we can use in the future
// This is needed because we can't get the input from the passed in builder
|_, mut builder: Builder, addr| async move {
builder.add_payment(addr, 1000000000000);
(builder.build().unwrap(), ())
},
|_, tx: Transaction, mut scanner: Scanner, _| async move {
let mut outputs = scanner.scan_transaction(&tx).not_locked();
outputs.sort_by(|x, y| x.commitment().amount.cmp(&y.commitment().amount));
assert_eq!(outputs[0].commitment().amount, 1000000000000);
outputs
},
),
(
|protocol, rpc: Rpc<_>, _, _, outputs: Vec<ReceivedOutput>| async move {
use monero_serai::wallet::FeePriority;
let change_view = ViewPair::new(
&random_scalar(&mut OsRng) * ED25519_BASEPOINT_TABLE,
Zeroizing::new(random_scalar(&mut OsRng)),
);
let mut builder = SignableTransactionBuilder::new(
protocol,
rpc.get_fee(protocol, FeePriority::Low).await.unwrap(),
Change::new(&change_view, false),
);
add_inputs(protocol, &rpc, vec![outputs.first().unwrap().clone()], &mut builder).await;
// Send to a subaddress
let sub_view = ViewPair::new(
&random_scalar(&mut OsRng) * ED25519_BASEPOINT_TABLE,
Zeroizing::new(random_scalar(&mut OsRng)),
);
builder.add_payment(
sub_view
.address(Network::Mainnet, AddressSpec::Subaddress(SubaddressIndex::new(0, 1).unwrap())),
1,
);
(builder.build().unwrap(), (change_view, sub_view))
},
|_, tx: Transaction, _, views: (ViewPair, ViewPair)| async move {
// Make sure the change can pick up its output
let mut change_scanner = Scanner::from_view(views.0, Some(HashSet::new()));
assert!(change_scanner.scan_transaction(&tx).not_locked().len() == 1);
// Make sure the subaddress can pick up its output
let mut sub_scanner = Scanner::from_view(views.1, Some(HashSet::new()));
sub_scanner.register_subaddress(SubaddressIndex::new(0, 1).unwrap());
let sub_outputs = sub_scanner.scan_transaction(&tx).not_locked();
assert!(sub_outputs.len() == 1);
assert_eq!(sub_outputs[0].commitment().amount, 1);
// Make sure only one R was included in TX extra
assert!(Extra::read::<&[u8]>(&mut tx.prefix.extra.as_ref())
.unwrap()
.keys()
.unwrap()
.1
.is_none());
},
),
);
test!(
spend_one_input_to_one_output_plus_change,
(
|_, mut builder: Builder, addr| async move {
builder.add_payment(addr, 2000000000000);
(builder.build().unwrap(), ())
},
|_, tx: Transaction, mut scanner: Scanner, _| async move {
let mut outputs = scanner.scan_transaction(&tx).not_locked();
outputs.sort_by(|x, y| x.commitment().amount.cmp(&y.commitment().amount));
assert_eq!(outputs[0].commitment().amount, 2000000000000);
outputs
},
),
(
|protocol: Protocol, rpc, mut builder: Builder, addr, outputs: Vec<ReceivedOutput>| async move {
add_inputs(protocol, &rpc, outputs, &mut builder).await;
builder.add_payment(addr, 2);
(builder.build().unwrap(), ())
},
|_, tx: Transaction, mut scanner: Scanner, _| async move {
let output = scanner.scan_transaction(&tx).not_locked().swap_remove(0);
assert_eq!(output.commitment().amount, 2);
},
),
);
test!(
spend_max_outputs,
(
|_, mut builder: Builder, addr| async move {
builder.add_payment(addr, 1000000000000);
(builder.build().unwrap(), ())
},
|_, tx: Transaction, mut scanner: Scanner, _| async move {
let mut outputs = scanner.scan_transaction(&tx).not_locked();
outputs.sort_by(|x, y| x.commitment().amount.cmp(&y.commitment().amount));
assert_eq!(outputs[0].commitment().amount, 1000000000000);
outputs
},
),
(
|protocol: Protocol, rpc, mut builder: Builder, addr, outputs: Vec<ReceivedOutput>| async move {
add_inputs(protocol, &rpc, outputs, &mut builder).await;
for i in 0 .. 15 {
builder.add_payment(addr, i + 1);
}
(builder.build().unwrap(), ())
},
|_, tx: Transaction, mut scanner: Scanner, _| async move {
let mut scanned_tx = scanner.scan_transaction(&tx).not_locked();
let mut output_amounts = HashSet::new();
for i in 0 .. 15 {
output_amounts.insert((i + 1) as u64);
}
for _ in 0 .. 15 {
let output = scanned_tx.swap_remove(0);
let amount = output.commitment().amount;
assert!(output_amounts.contains(&amount));
output_amounts.remove(&amount);
}
},
),
);
test!(
spend_max_outputs_to_subaddresses,
(
|_, mut builder: Builder, addr| async move {
builder.add_payment(addr, 1000000000000);
(builder.build().unwrap(), ())
},
|_, tx: Transaction, mut scanner: Scanner, _| async move {
let mut outputs = scanner.scan_transaction(&tx).not_locked();
outputs.sort_by(|x, y| x.commitment().amount.cmp(&y.commitment().amount));
assert_eq!(outputs[0].commitment().amount, 1000000000000);
outputs
},
),
(
|protocol: Protocol, rpc, mut builder: Builder, _, outputs: Vec<ReceivedOutput>| async move {
add_inputs(protocol, &rpc, outputs, &mut builder).await;
let view = runner::random_address().1;
let mut scanner = Scanner::from_view(view.clone(), Some(HashSet::new()));
let mut subaddresses = vec![];
for i in 0 .. 15 {
let subaddress = SubaddressIndex::new(0, i + 1).unwrap();
scanner.register_subaddress(subaddress);
builder.add_payment(
view.address(Network::Mainnet, AddressSpec::Subaddress(subaddress)),
(i + 1) as u64,
);
subaddresses.push(subaddress);
}
(builder.build().unwrap(), (scanner, subaddresses))
},
|_, tx: Transaction, _, mut state: (Scanner, Vec<SubaddressIndex>)| async move {
use std::collections::HashMap;
let mut scanned_tx = state.0.scan_transaction(&tx).not_locked();
let mut output_amounts_by_subaddress = HashMap::new();
for i in 0 .. 15 {
output_amounts_by_subaddress.insert((i + 1) as u64, state.1[i]);
}
for _ in 0 .. 15 {
let output = scanned_tx.swap_remove(0);
let amount = output.commitment().amount;
assert!(output_amounts_by_subaddress.contains_key(&amount));
assert_eq!(output.metadata.subaddress, Some(output_amounts_by_subaddress[&amount]));
output_amounts_by_subaddress.remove(&amount);
}
},
),
);
test!(
spend_one_input_to_two_outputs_no_change,
(
|_, mut builder: Builder, addr| async move {
builder.add_payment(addr, 1000000000000);
(builder.build().unwrap(), ())
},
|_, tx: Transaction, mut scanner: Scanner, _| async move {
let mut outputs = scanner.scan_transaction(&tx).not_locked();
outputs.sort_by(|x, y| x.commitment().amount.cmp(&y.commitment().amount));
assert_eq!(outputs[0].commitment().amount, 1000000000000);
outputs
},
),
(
|protocol, rpc: Rpc<_>, _, addr, outputs: Vec<ReceivedOutput>| async move {
use monero_serai::wallet::FeePriority;
let mut builder = SignableTransactionBuilder::new(
protocol,
rpc.get_fee(protocol, FeePriority::Low).await.unwrap(),
Change::fingerprintable(None),
);
add_inputs(protocol, &rpc, vec![outputs.first().unwrap().clone()], &mut builder).await;
builder.add_payment(addr, 10000);
builder.add_payment(addr, 50000);
(builder.build().unwrap(), ())
},
|_, tx: Transaction, mut scanner: Scanner, _| async move {
let mut outputs = scanner.scan_transaction(&tx).not_locked();
outputs.sort_by(|x, y| x.commitment().amount.cmp(&y.commitment().amount));
assert_eq!(outputs[0].commitment().amount, 10000);
assert_eq!(outputs[1].commitment().amount, 50000);
// The remainder should get shunted to fee, which is fingerprintable
assert_eq!(tx.rct_signatures.base.fee, 1000000000000 - 10000 - 50000);
},
),
);