Don't mutate Plans when signing

This is achieved by not using the Plan struct anymore, yet rather its
decomposition. While less ergonomic, it meets our wants re: safety.
This commit is contained in:
Luke Parker
2023-10-20 10:56:18 -04:00
parent 7b6181ecdb
commit 5977121c48
4 changed files with 150 additions and 111 deletions

View File

@@ -34,7 +34,7 @@ pub use serai_client::{
};
use crate::{
Payment, Plan, additional_key,
Payment, additional_key,
networks::{
NetworkError, Block as BlockTrait, OutputType, Output as OutputTrait,
Transaction as TransactionTrait, SignableTransaction as SignableTransactionTrait,
@@ -206,10 +206,14 @@ impl Monero {
scanner
}
#[allow(clippy::too_many_arguments)]
async fn make_signable_transaction(
&self,
block_number: usize,
mut plan: Plan<Self>,
plan_id: &[u8; 32],
inputs: &[Output],
payments: &[Payment<Self>],
change: &Option<Address>,
fee_rate: Fee,
calculating_fee: bool,
) -> Result<Option<(RecommendedTranscript, MSignableTransaction)>, NetworkError> {
@@ -234,9 +238,11 @@ impl Monero {
// Check a fork hasn't occurred which this processor hasn't been updated for
assert_eq!(protocol, self.rpc.get_protocol().await.map_err(|_| NetworkError::ConnectionError)?);
let spendable_outputs = plan.inputs.iter().cloned().map(|input| input.0).collect::<Vec<_>>();
let spendable_outputs = inputs.iter().map(|input| input.0.clone()).collect::<Vec<_>>();
let mut transcript = plan.transcript();
let mut transcript =
RecommendedTranscript::new(b"Serai Processor Monero Transaction Transcript");
transcript.append_message(b"plan", plan_id);
// All signers need to select the same decoys
// All signers use the same height and a seeded RNG to make sure they do so.
@@ -254,11 +260,12 @@ impl Monero {
// Monero requires at least two outputs
// If we only have one output planned, add a dummy payment
let outputs = plan.payments.len() + usize::from(u8::from(plan.change.is_some()));
let mut payments = payments.to_vec();
let outputs = payments.len() + usize::from(u8::from(change.is_some()));
if outputs == 0 {
return Ok(None);
} else if outputs == 1 {
plan.payments.push(Payment {
payments.push(Payment {
address: Address::new(
ViewPair::new(EdwardsPoint::generator().0, Zeroizing::new(Scalar::ONE.0))
.address(MoneroNetwork::Mainnet, AddressSpec::Standard),
@@ -269,23 +276,22 @@ impl Monero {
});
}
let mut payments = vec![];
for payment in &plan.payments {
let payments = payments
.into_iter()
// If we're solely estimating the fee, don't actually specify an amount
// This won't affect the fee calculation yet will ensure we don't hit an out of funds error
payments
.push((payment.address.clone().into(), if calculating_fee { 0 } else { payment.amount }));
}
.map(|payment| (payment.address.into(), if calculating_fee { 0 } else { payment.amount }))
.collect::<Vec<_>>();
match MSignableTransaction::new(
protocol,
// Use the plan ID as the r_seed
// This perfectly binds the plan while simultaneously allowing verifying the plan was
// executed with no additional communication
Some(Zeroizing::new(plan.id())),
Some(Zeroizing::new(*plan_id)),
inputs.clone(),
payments,
plan.change.map(|change| Change::fingerprintable(change.into())),
change.clone().map(|change| Change::fingerprintable(change.into())),
vec![],
fee_rate,
) {
@@ -375,15 +381,15 @@ impl Network for Monero {
// Monero doesn't require/benefit from tweaking
fn tweak_keys(_: &mut ThresholdKeys<Self::Curve>) {}
fn address(key: EdwardsPoint) -> Self::Address {
fn address(key: EdwardsPoint) -> Address {
Self::address_internal(key, EXTERNAL_SUBADDRESS)
}
fn branch_address(key: EdwardsPoint) -> Self::Address {
fn branch_address(key: EdwardsPoint) -> Address {
Self::address_internal(key, BRANCH_SUBADDRESS)
}
fn change_address(key: EdwardsPoint) -> Self::Address {
fn change_address(key: EdwardsPoint) -> Address {
Self::address_internal(key, CHANGE_SUBADDRESS)
}
@@ -404,7 +410,7 @@ impl Network for Monero {
)
}
async fn get_outputs(&self, block: &Block, key: EdwardsPoint) -> Vec<Self::Output> {
async fn get_outputs(&self, block: &Block, key: EdwardsPoint) -> Vec<Output> {
let outputs = loop {
match Self::scanner(key).scan(&self.rpc, block).await {
Ok(outputs) => break outputs,
@@ -515,12 +521,15 @@ impl Network for Monero {
async fn needed_fee(
&self,
block_number: usize,
plan: &Plan<Self>,
plan_id: &[u8; 32],
inputs: &[Output],
payments: &[Payment<Self>],
change: &Option<Address>,
fee_rate: Fee,
) -> Result<Option<u64>, NetworkError> {
Ok(
self
.make_signable_transaction(block_number, plan.clone(), fee_rate, true)
.make_signable_transaction(block_number, plan_id, inputs, payments, change, fee_rate, true)
.await?
.map(|(_, signable)| signable.fee()),
)
@@ -529,16 +538,22 @@ impl Network for Monero {
async fn signable_transaction(
&self,
block_number: usize,
plan: &Plan<Self>,
plan_id: &[u8; 32],
inputs: &[Output],
payments: &[Payment<Self>],
change: &Option<Address>,
fee_rate: Fee,
) -> Result<Option<(Self::SignableTransaction, Self::Eventuality)>, NetworkError> {
Ok(self.make_signable_transaction(block_number, plan.clone(), fee_rate, false).await?.map(
|(transcript, signable)| {
let signable = SignableTransaction { transcript, actual: signable };
let eventuality = signable.actual.eventuality().unwrap();
(signable, eventuality)
},
))
Ok(
self
.make_signable_transaction(block_number, plan_id, inputs, payments, change, fee_rate, false)
.await?
.map(|(transcript, signable)| {
let signable = SignableTransaction { transcript, actual: signable };
let eventuality = signable.actual.eventuality().unwrap();
(signable, eventuality)
}),
)
}
async fn attempt_send(
@@ -606,7 +621,7 @@ impl Network for Monero {
}
#[cfg(test)]
async fn test_send(&self, address: Self::Address) -> Block {
async fn test_send(&self, address: Address) -> Block {
use zeroize::Zeroizing;
use rand_core::OsRng;
use monero_serai::wallet::FeePriority;