mirror of
https://github.com/serai-dex/serai.git
synced 2024-12-25 13:09:30 +00:00
397fca748f
* 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
316 lines
10 KiB
Rust
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);
|
|
},
|
|
),
|
|
);
|