From 4852dcaab7668038c581276313aa78a085cea79a Mon Sep 17 00:00:00 2001 From: Luke Parker Date: Fri, 20 Oct 2023 04:42:08 -0400 Subject: [PATCH] Move common code from prepare_send into Network trait --- processor/src/networks/bitcoin.rs | 173 +++++++--------- processor/src/networks/mod.rs | 292 +++++++++++++++++---------- processor/src/networks/monero.rs | 316 ++++++++++++++---------------- 3 files changed, 401 insertions(+), 380 deletions(-) diff --git a/processor/src/networks/bitcoin.rs b/processor/src/networks/bitcoin.rs index 3e70bd6e..a673648e 100644 --- a/processor/src/networks/bitcoin.rs +++ b/processor/src/networks/bitcoin.rs @@ -47,8 +47,7 @@ use crate::{ networks::{ NetworkError, Block as BlockTrait, OutputType, Output as OutputTrait, Transaction as TransactionTrait, SignableTransaction as SignableTransactionTrait, - Eventuality as EventualityTrait, EventualitiesTracker, AmortizeFeeRes, PreparedSend, Network, - drop_branches, amortize_fee, + Eventuality as EventualityTrait, EventualitiesTracker, Network, }, Plan, }; @@ -320,6 +319,52 @@ impl Bitcoin { .unwrap() } } + + async fn make_signable_transaction( + &self, + plan: &Plan, + fee: Fee, + calculating_fee: bool, + ) -> Option { + let mut payments = vec![]; + for payment in &plan.payments { + // If we're solely estimating the fee, don't specify the actual amount + // This won't affect the fee calculation yet will ensure we don't hit a not enough funds + // error + payments.push(( + payment.address.0.clone(), + if calculating_fee { Self::DUST } else { payment.amount }, + )); + } + + match BSignableTransaction::new( + plan.inputs.iter().map(|input| input.output.clone()).collect(), + &payments, + plan.change.as_ref().map(|change| change.0.clone()), + None, + fee.0, + ) { + Ok(signable) => Some(signable), + Err(TransactionError::NoInputs) => { + panic!("trying to create a bitcoin transaction without inputs") + } + // No outputs left and the change isn't worth enough + Err(TransactionError::NoOutputs) => None, + // amortize_fee removes payments which fall below the dust threshold + Err(TransactionError::DustPayment) => panic!("dust payment despite removing dust"), + Err(TransactionError::TooMuchData) => panic!("too much data despite not specifying data"), + Err(TransactionError::TooLowFee) => { + panic!("created a transaction whose fee is below the minimum") + } + Err(TransactionError::NotEnoughFunds) => { + // Mot even enough funds to pay the fee + None + } + Err(TransactionError::TooLargeTransaction) => { + panic!("created a too large transaction despite limiting inputs/outputs") + } + } + } } #[async_trait] @@ -504,110 +549,34 @@ impl Network for Bitcoin { res } - async fn prepare_send( + async fn needed_fee( &self, _: usize, - mut plan: Plan, - fee: Fee, - operating_costs: u64, - ) -> Result, NetworkError> { - let signable = |plan: &Plan, tx_fee: Option<_>| { - let mut payments = vec![]; - for payment in &plan.payments { - // If we're solely estimating the fee, don't specify the actual amount - // This won't affect the fee calculation yet will ensure we don't hit a not enough funds - // error - payments.push(( - payment.address.0.clone(), - if tx_fee.is_none() { Self::DUST } else { payment.amount }, - )); - } + plan: &Plan, + fee_rate: Fee, + ) -> Result, NetworkError> { + Ok( + self + .make_signable_transaction(plan, fee_rate, true) + .await + .map(|signable| signable.needed_fee()), + ) + } - match BSignableTransaction::new( - plan.inputs.iter().map(|input| input.output.clone()).collect(), - &payments, - plan.change.as_ref().map(|change| change.0.clone()), - None, - fee.0, - ) { - Ok(signable) => Some(signable), - Err(TransactionError::NoInputs) => { - panic!("trying to create a bitcoin transaction without inputs") - } - // No outputs left and the change isn't worth enough - Err(TransactionError::NoOutputs) => None, - // amortize_fee removes payments which fall below the dust threshold - Err(TransactionError::DustPayment) => panic!("dust payment despite removing dust"), - Err(TransactionError::TooMuchData) => panic!("too much data despite not specifying data"), - Err(TransactionError::TooLowFee) => { - panic!("created a transaction whose fee is below the minimum") - } - Err(TransactionError::NotEnoughFunds) => { - if tx_fee.is_none() { - // Mot even enough funds to pay the fee - None - } else { - panic!("not enough funds for bitcoin TX despite amortizing the fee") - } - } - Err(TransactionError::TooLargeTransaction) => { - panic!("created a too large transaction despite limiting inputs/outputs") - } - } - }; - - let tx_fee = match signable(&plan, None) { - Some(tx) => tx.needed_fee(), - None => { - return Ok(PreparedSend { - tx: None, - post_fee_branches: drop_branches(&plan), - // This plan expects a change output valued at sum(inputs) - sum(outputs) - // Since we can no longer create this change output, it becomes an operating cost - // TODO: Look at input restoration to reduce this operating cost - operating_costs: operating_costs + - if plan.change.is_some() { plan.expected_change() } else { 0 }, - }); - } - }; - - let AmortizeFeeRes { post_fee_branches, mut operating_costs } = - amortize_fee(&mut plan, operating_costs, tx_fee); - - let signable = signable(&plan, Some(tx_fee)).unwrap(); - - if plan.change.is_some() { - // Now that we've amortized the fee (which may raise the expected change value), grab it - // again - // Then, subtract the TX fee - // - // The first `expected_change` call gets the theoretically expected change from the - // theoretical Plan object, and accordingly doesn't subtract the fee (expecting the payments - // to bare it) - // This call wants the actual value, post-amortization over outputs, and since Plan is - // unaware of the fee, has to manually adjust - let on_chain_expected_change = plan.expected_change() - tx_fee; - // If the change value is less than the dust threshold, it becomes an operating cost - // This may be slightly inaccurate as dropping payments may reduce the fee, raising the - // change above dust - // That's fine since it'd have to be in a very precarious state AND then it's over-eager in - // tabulating costs - if on_chain_expected_change < Self::DUST { - operating_costs += on_chain_expected_change; - } - } - - let plan_binding_input = *plan.inputs[0].output.outpoint(); - let outputs = signable.outputs().to_vec(); - - Ok(PreparedSend { - tx: Some(( + async fn signable_transaction( + &self, + _: usize, + plan: &Plan, + fee_rate: Fee, + ) -> Result, NetworkError> { + Ok(self.make_signable_transaction(plan, fee_rate, false).await.map(|signable| { + let plan_binding_input = *plan.inputs[0].output.outpoint(); + let outputs = signable.outputs().to_vec(); + ( SignableTransaction { transcript: plan.transcript(), actual: signable }, Eventuality { plan_binding_input, outputs }, - )), - post_fee_branches, - operating_costs, - }) + ) + })) } async fn attempt_send( @@ -650,7 +619,7 @@ impl Network for Bitcoin { } #[cfg(test)] - async fn get_fee(&self) -> Self::Fee { + async fn get_fee(&self) -> Fee { Fee(1) } diff --git a/processor/src/networks/mod.rs b/processor/src/networks/mod.rs index 46b65c6e..ad809073 100644 --- a/processor/src/networks/mod.rs +++ b/processor/src/networks/mod.rs @@ -199,7 +199,7 @@ pub struct PostFeeBranch { } // Return the PostFeeBranches needed when dropping a transaction -pub fn drop_branches(plan: &Plan) -> Vec { +fn drop_branches(plan: &Plan) -> Vec { let mut branch_outputs = vec![]; for payment in &plan.payments { if payment.address == N::branch_address(plan.key) { @@ -209,105 +209,6 @@ pub fn drop_branches(plan: &Plan) -> Vec { branch_outputs } -pub struct AmortizeFeeRes { - post_fee_branches: Vec, - operating_costs: u64, -} - -// Amortize a fee over the plan's payments -pub fn amortize_fee( - plan: &mut Plan, - operating_costs: u64, - tx_fee: u64, -) -> AmortizeFeeRes { - let total_fee = { - let mut total_fee = tx_fee; - // Since we're creating a change output, letting us recoup coins, amortize the operating costs - // as well - if plan.change.is_some() { - total_fee += operating_costs; - } - total_fee - }; - - let original_outputs = plan.payments.iter().map(|payment| payment.amount).sum::(); - // If this isn't enough for the total fee, drop and move on - if original_outputs < total_fee { - let mut remaining_operating_costs = operating_costs; - if plan.change.is_some() { - // Operating costs increase by the TX fee - remaining_operating_costs += tx_fee; - // Yet decrease by the payments we managed to drop - remaining_operating_costs = remaining_operating_costs.saturating_sub(original_outputs); - } - return AmortizeFeeRes { - post_fee_branches: drop_branches(plan), - operating_costs: remaining_operating_costs, - }; - } - - // Amortize the transaction fee across outputs - let mut payments_len = u64::try_from(plan.payments.len()).unwrap(); - // Use a formula which will round up - let per_output_fee = |payments| (total_fee + (payments - 1)) / payments; - - let post_fee = |payment: &Payment, per_output_fee| { - let mut post_fee = payment.amount.checked_sub(per_output_fee); - // If this is under our dust threshold, drop it - if let Some(amount) = post_fee { - if amount < N::DUST { - post_fee = None; - } - } - post_fee - }; - - // If we drop outputs for being less than the fee, we won't successfully reduce the amount spent - // (dropping a 800 output due to a 1000 fee leaves 200 we still have to deduct) - // Do initial runs until the amount of output we will drop is known - while { - let last = payments_len; - payments_len = u64::try_from( - plan - .payments - .iter() - .filter(|payment| post_fee(payment, per_output_fee(payments_len)).is_some()) - .count(), - ) - .unwrap(); - last != payments_len - } {} - - // Now that we know how many outputs will survive, calculate the actual per_output_fee - let per_output_fee = per_output_fee(payments_len); - let mut branch_outputs = vec![]; - for payment in plan.payments.iter_mut() { - let post_fee = post_fee(payment, per_output_fee); - // Note the branch output, if this is one - if payment.address == N::branch_address(plan.key) { - branch_outputs.push(PostFeeBranch { expected: payment.amount, actual: post_fee }); - } - payment.amount = post_fee.unwrap_or(0); - } - // Drop payments now worth 0 - plan.payments = plan.payments.drain(..).filter(|payment| payment.amount != 0).collect(); - - // Sanity check the fee wa successfully amortized - let new_outputs = plan.payments.iter().map(|payment| payment.amount).sum::(); - assert!((new_outputs + total_fee) <= original_outputs); - - AmortizeFeeRes { - post_fee_branches: branch_outputs, - operating_costs: if plan.change.is_none() { - // If the change is None, this had no effect on the operating costs - operating_costs - } else { - // Since the change is some, and we successfully amortized, the operating costs were recouped - 0 - }, - } -} - pub struct PreparedSend { /// None for the transaction if the SignableTransaction was dropped due to lack of value. pub tx: Option<(N::SignableTransaction, N::Eventuality)>, @@ -323,7 +224,7 @@ pub trait Network: 'static + Send + Sync + Clone + PartialEq + Eq + Debug { /// The type representing the fee for this network. // This should likely be a u64, wrapped in a type which implements appropriate fee logic. - type Fee: Copy; + type Fee: Send + Copy; /// The type representing the transaction for this network. type Transaction: Transaction; @@ -408,16 +309,195 @@ pub trait Network: 'static + Send + Sync + Clone + PartialEq + Eq + Debug { block: &Self::Block, ) -> HashMap<[u8; 32], (usize, Self::Transaction)>; + /// Returns the needed fee to fulfill this Plan at this fee rate. + /// + /// Returns None if this Plan isn't fulfillable (such as when the fee exceeds the input value). + async fn needed_fee( + &self, + block_number: usize, + plan: &Plan, + fee_rate: Self::Fee, + ) -> Result, NetworkError>; + + /// Create a SignableTransaction for the given Plan. + /// + /// The expected flow is: + /// 1) Call needed_fee + /// 2) If the Plan is fulfillable, amortize the fee + /// 3) Call signable_transaction *which MUST NOT return None if the above was done properly* + async fn signable_transaction( + &self, + block_number: usize, + plan: &Plan, + fee_rate: Self::Fee, + ) -> Result, NetworkError>; + /// Prepare a SignableTransaction for a transaction. - // TODO: These have common code inside them - // Provide prepare_send, have coins offers prepare_send_inner async fn prepare_send( &self, block_number: usize, - plan: Plan, + mut plan: Plan, fee_rate: Self::Fee, - running_operating_costs: u64, - ) -> Result, NetworkError>; + operating_costs: u64, + ) -> Result, NetworkError> { + // Sanity check this has at least one output planned + assert!((!plan.payments.is_empty()) || plan.change.is_some()); + + let Some(fee) = self.needed_fee(block_number, &plan, fee_rate).await? else { + // This Plan is not fulfillable + // TODO: Have Plan explicitly distinguish payments and branches in two separate Vecs? + return Ok(PreparedSend { + tx: None, + // Have all of its branches dropped + post_fee_branches: drop_branches(&plan), + // This plan expects a change output valued at sum(inputs) - sum(outputs) + // Since we can no longer create this change output, it becomes an operating cost + // TODO: Look at input restoration to reduce this operating cost + operating_costs: operating_costs + + if plan.change.is_some() { plan.expected_change() } else { 0 }, + }); + }; + + let (post_fee_branches, mut operating_costs) = { + pub struct AmortizeFeeRes { + post_fee_branches: Vec, + operating_costs: u64, + } + + // Amortize a fee over the plan's payments + fn amortize_fee( + plan: &mut Plan, + operating_costs: u64, + tx_fee: u64, + ) -> AmortizeFeeRes { + let total_fee = { + let mut total_fee = tx_fee; + // Since we're creating a change output, letting us recoup coins, amortize the operating + // costs + // as well + if plan.change.is_some() { + total_fee += operating_costs; + } + total_fee + }; + + let original_outputs = plan.payments.iter().map(|payment| payment.amount).sum::(); + // If this isn't enough for the total fee, drop and move on + if original_outputs < total_fee { + let mut remaining_operating_costs = operating_costs; + if plan.change.is_some() { + // Operating costs increase by the TX fee + remaining_operating_costs += tx_fee; + // Yet decrease by the payments we managed to drop + remaining_operating_costs = remaining_operating_costs.saturating_sub(original_outputs); + } + return AmortizeFeeRes { + post_fee_branches: drop_branches(plan), + operating_costs: remaining_operating_costs, + }; + } + + // Amortize the transaction fee across outputs + let mut payments_len = u64::try_from(plan.payments.len()).unwrap(); + // Use a formula which will round up + let per_output_fee = |payments| (total_fee + (payments - 1)) / payments; + + let post_fee = |payment: &Payment, per_output_fee| { + let mut post_fee = payment.amount.checked_sub(per_output_fee); + // If this is under our dust threshold, drop it + if let Some(amount) = post_fee { + if amount < N::DUST { + post_fee = None; + } + } + post_fee + }; + + // If we drop outputs for being less than the fee, we won't successfully reduce the amount + // spent (dropping a 800 output due to a 1000 fee leaves 200 we still have to deduct) + // Do initial runs until the amount of output we will drop is known + while { + let last = payments_len; + payments_len = u64::try_from( + plan + .payments + .iter() + .filter(|payment| post_fee(payment, per_output_fee(payments_len)).is_some()) + .count(), + ) + .unwrap(); + last != payments_len + } {} + + // Now that we know how many outputs will survive, calculate the actual per_output_fee + let per_output_fee = per_output_fee(payments_len); + let mut branch_outputs = vec![]; + for payment in plan.payments.iter_mut() { + let post_fee = post_fee(payment, per_output_fee); + // Note the branch output, if this is one + if payment.address == N::branch_address(plan.key) { + branch_outputs.push(PostFeeBranch { expected: payment.amount, actual: post_fee }); + } + payment.amount = post_fee.unwrap_or(0); + } + // Drop payments now worth 0 + plan.payments = plan.payments.drain(..).filter(|payment| payment.amount != 0).collect(); + + // Sanity check the fee wa successfully amortized + let new_outputs = plan.payments.iter().map(|payment| payment.amount).sum::(); + assert!((new_outputs + total_fee) <= original_outputs); + + AmortizeFeeRes { + post_fee_branches: branch_outputs, + operating_costs: if plan.change.is_none() { + // If the change is None, this had no effect on the operating costs + operating_costs + } else { + // Since the change is some, and we successfully amortized, the operating costs were + // recouped + 0 + }, + } + } + + let AmortizeFeeRes { post_fee_branches, operating_costs } = + amortize_fee(&mut plan, operating_costs, fee); + + (post_fee_branches, operating_costs) + }; + + let Some(tx) = self.signable_transaction(block_number, &plan, fee_rate).await? else { + panic!( + "{}. post-amortization plan: {:?}, successfully amoritized fee: {}", + "signable_transaction returned None for a TX we prior successfully calculated the fee for", + &plan, + fee, + ) + }; + + if plan.change.is_some() { + // Now that we've amortized the fee (which may raise the expected change value), grab it + // again + // Then, subtract the TX fee + // + // The first `expected_change` call gets the theoretically expected change from the + // theoretical Plan object, and accordingly doesn't subtract the fee (expecting the payments + // to bare it) + // This call wants the actual value, post-amortization over outputs, and since Plan is + // unaware of the fee, has to manually adjust + let on_chain_expected_change = plan.expected_change() - fee; + // If the change value is less than the dust threshold, it becomes an operating cost + // This may be slightly inaccurate as dropping payments may reduce the fee, raising the + // change above dust + // That's fine since it'd have to be in a very precarious state AND then it's over-eager in + // tabulating costs + if on_chain_expected_change < Self::DUST { + operating_costs += on_chain_expected_change; + } + } + + Ok(PreparedSend { tx: Some(tx), post_fee_branches, operating_costs }) + } /// Attempt to sign a SignableTransaction. async fn attempt_send( diff --git a/processor/src/networks/monero.rs b/processor/src/networks/monero.rs index da6a9b91..eb2aadbb 100644 --- a/processor/src/networks/monero.rs +++ b/processor/src/networks/monero.rs @@ -38,8 +38,7 @@ use crate::{ networks::{ NetworkError, Block as BlockTrait, OutputType, Output as OutputTrait, Transaction as TransactionTrait, SignableTransaction as SignableTransactionTrait, - Eventuality as EventualityTrait, EventualitiesTracker, AmortizeFeeRes, PreparedSend, Network, - drop_branches, amortize_fee, + Eventuality as EventualityTrait, EventualitiesTracker, Network, }, }; @@ -207,6 +206,125 @@ impl Monero { scanner } + async fn make_signable_transaction( + &self, + block_number: usize, + mut plan: Plan, + fee_rate: Fee, + calculating_fee: bool, + ) -> Result, NetworkError> { + // Get the protocol for the specified block number + // For now, this should just be v16, the latest deployed protocol, since there's no upcoming + // hard fork to be mindful of + let get_protocol = || Protocol::v16; + + #[cfg(not(test))] + let protocol = get_protocol(); + // If this is a test, we won't be using a mainnet node and need a distinct protocol + // determination + // Just use whatever the node expects + #[cfg(test)] + let protocol = self.rpc.get_protocol().await.unwrap(); + + // Hedge against the above codegen failing by having an always included runtime check + if !cfg!(test) { + assert_eq!(protocol, get_protocol()); + } + + // 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::>(); + + let mut transcript = plan.transcript(); + + // All signers need to select the same decoys + // All signers use the same height and a seeded RNG to make sure they do so. + let decoys = Decoys::select( + &mut ChaCha20Rng::from_seed(transcript.rng_seed(b"decoys")), + &self.rpc, + protocol.ring_len(), + block_number + 1, + &spendable_outputs, + ) + .await + .map_err(|_| NetworkError::ConnectionError)?; + + let inputs = spendable_outputs.into_iter().zip(decoys).collect::>(); + + // 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())); + if outputs == 0 { + return Ok(None); + } else if outputs == 1 { + plan.payments.push(Payment { + address: Address::new( + ViewPair::new(EdwardsPoint::generator().0, Zeroizing::new(Scalar::ONE.0)) + .address(MoneroNetwork::Mainnet, AddressSpec::Standard), + ) + .unwrap(), + amount: 0, + data: None, + }); + } + + let mut payments = vec![]; + for payment in &plan.payments { + // 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 })); + } + + 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())), + inputs.clone(), + payments, + plan.change.map(|change| Change::fingerprintable(change.into())), + vec![], + fee_rate, + ) { + Ok(signable) => Ok(Some((transcript, signable))), + Err(e) => match e { + TransactionError::MultiplePaymentIds => { + panic!("multiple payment IDs despite not supporting integrated addresses"); + } + TransactionError::NoInputs | + TransactionError::NoOutputs | + TransactionError::InvalidDecoyQuantity | + TransactionError::NoChange | + TransactionError::TooManyOutputs | + TransactionError::TooMuchData | + TransactionError::TooLargeTransaction | + TransactionError::WrongPrivateKey => { + panic!("created an Monero invalid transaction: {e}"); + } + TransactionError::ClsagError(_) | + TransactionError::InvalidTransaction(_) | + TransactionError::FrostError(_) => { + panic!("supposedly unreachable (at this time) Monero error: {e}"); + } + TransactionError::NotEnoughFunds { inputs, outputs, fee } => { + log::debug!( + "Monero NotEnoughFunds. inputs: {:?}, outputs: {:?}, fee: {fee}", + inputs, + outputs + ); + Ok(None) + } + TransactionError::RpcError(e) => { + log::error!("RpcError when preparing transaction: {e:?}"); + Err(NetworkError::ConnectionError) + } + }, + } + } + #[cfg(test)] fn test_view_pair() -> ViewPair { ViewPair::new(*EdwardsPoint::generator(), Zeroizing::new(Scalar::ONE.0)) @@ -394,179 +512,33 @@ impl Network for Monero { res } - async fn prepare_send( + async fn needed_fee( &self, block_number: usize, - mut plan: Plan, - fee: Fee, - operating_costs: u64, - ) -> Result, NetworkError> { - // Sanity check this has at least one output planned - assert!((!plan.payments.is_empty()) || plan.change.is_some()); - - // Get the protocol for the specified block number - // For now, this should just be v16, the latest deployed protocol, since there's no upcoming - // hard fork to be mindful of - let get_protocol = || Protocol::v16; - - #[cfg(not(test))] - let protocol = get_protocol(); - // If this is a test, we won't be using a mainnet node and need a distinct protocol - // determination - // Just use whatever the node expects - #[cfg(test)] - let protocol = self.rpc.get_protocol().await.unwrap(); - - // Hedge against the above codegen failing by having an always included runtime check - if !cfg!(test) { - assert_eq!(protocol, get_protocol()); - } - - // 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::>(); - - let mut transcript = plan.transcript(); - - // All signers need to select the same decoys - // All signers use the same height and a seeded RNG to make sure they do so. - let decoys = Decoys::select( - &mut ChaCha20Rng::from_seed(transcript.rng_seed(b"decoys")), - &self.rpc, - protocol.ring_len(), - block_number + 1, - &spendable_outputs, + plan: &Plan, + fee_rate: Fee, + ) -> Result, NetworkError> { + Ok( + self + .make_signable_transaction(block_number, plan.clone(), fee_rate, true) + .await? + .map(|(_, signable)| signable.fee()), ) - .await - .map_err(|_| NetworkError::ConnectionError)?; + } - let inputs = spendable_outputs.into_iter().zip(decoys).collect::>(); - - let signable = |mut plan: Plan, tx_fee: Option<_>| { - // 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())); - if outputs == 0 { - return Ok(None); - } else if outputs == 1 { - plan.payments.push(Payment { - address: Address::new( - ViewPair::new(EdwardsPoint::generator().0, Zeroizing::new(Scalar::ONE.0)) - .address(MoneroNetwork::Mainnet, AddressSpec::Standard), - ) - .unwrap(), - amount: 0, - data: None, - }); - } - - let mut payments = vec![]; - for payment in &plan.payments { - // 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 tx_fee.is_none() { 0 } else { payment.amount }, - )); - } - - 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())), - inputs.clone(), - payments, - plan.change.map(|change| Change::fingerprintable(change.into())), - vec![], - fee, - ) { - Ok(signable) => Ok(Some(signable)), - Err(e) => match e { - TransactionError::MultiplePaymentIds => { - panic!("multiple payment IDs despite not supporting integrated addresses"); - } - TransactionError::NoInputs | - TransactionError::NoOutputs | - TransactionError::InvalidDecoyQuantity | - TransactionError::NoChange | - TransactionError::TooManyOutputs | - TransactionError::TooMuchData | - TransactionError::TooLargeTransaction | - TransactionError::WrongPrivateKey => { - panic!("created an Monero invalid transaction: {e}"); - } - TransactionError::ClsagError(_) | - TransactionError::InvalidTransaction(_) | - TransactionError::FrostError(_) => { - panic!("supposedly unreachable (at this time) Monero error: {e}"); - } - TransactionError::NotEnoughFunds { inputs, outputs, fee } => { - if let Some(tx_fee) = tx_fee { - panic!( - "{}. in: {inputs}, out: {outputs}, fee: {fee}, prior estimated fee: {tx_fee}", - "didn't have enough funds for a Monero TX", - ); - } else { - Ok(None) - } - } - TransactionError::RpcError(e) => { - log::error!("RpcError when preparing transaction: {e:?}"); - Err(NetworkError::ConnectionError) - } - }, - } - }; - - let tx_fee = match signable(plan.clone(), None)? { - Some(tx) => tx.fee(), - None => { - return Ok(PreparedSend { - tx: None, - post_fee_branches: drop_branches(&plan), - // This plan expects a change output valued at sum(inputs) - sum(outputs) - // Since we can no longer create this change output, it becomes an operating cost - // TODO: Look at input restoration to reduce this operating cost - operating_costs: operating_costs + - if plan.change.is_some() { plan.expected_change() } else { 0 }, - }); - } - }; - - let AmortizeFeeRes { post_fee_branches, mut operating_costs } = - amortize_fee(&mut plan, operating_costs, tx_fee); - let plan_change_is_some = plan.change.is_some(); - let plan_expected_change = plan.expected_change(); - - let signable = signable(plan, Some(tx_fee))?.unwrap(); - - if plan_change_is_some { - // Now that we've amortized the fee (which may raise the expected change value), grab it - // again - // Then, subtract the TX fee - // - // The first `expected_change` call gets the theoretically expected change from the - // theoretical Plan object, and accordingly doesn't subtract the fee (expecting the payments - // to bare it) - // This call wants the actual value, post-amortization over outputs, and since Plan is - // unaware of the fee, has to manually adjust - let on_chain_expected_change = plan_expected_change - tx_fee; - // If the change value is less than the dust threshold, it becomes an operating cost - // This may be slightly inaccurate as dropping payments may reduce the fee, raising the - // change above dust - // That's fine since it'd have to be in a very precarious state AND then it's over-eager in - // tabulating costs - if on_chain_expected_change < Self::DUST { - operating_costs += on_chain_expected_change; - } - } - - let signable = SignableTransaction { transcript, actual: signable }; - let eventuality = signable.actual.eventuality().unwrap(); - Ok(PreparedSend { tx: Some((signable, eventuality)), post_fee_branches, operating_costs }) + async fn signable_transaction( + &self, + block_number: usize, + plan: &Plan, + fee_rate: Fee, + ) -> Result, 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) + }, + )) } async fn attempt_send( @@ -604,7 +576,7 @@ impl Network for Monero { } #[cfg(test)] - async fn get_fee(&self) -> Self::Fee { + async fn get_fee(&self) -> Fee { use monero_serai::wallet::FeePriority; self.rpc.get_fee(self.rpc.get_protocol().await.unwrap(), FeePriority::Low).await.unwrap()