Move common code from prepare_send into Network trait

This commit is contained in:
Luke Parker 2023-10-20 04:42:08 -04:00
parent d6bc1c1ea3
commit 4852dcaab7
No known key found for this signature in database
3 changed files with 401 additions and 380 deletions

View file

@ -47,8 +47,7 @@ use crate::{
networks::{ networks::{
NetworkError, Block as BlockTrait, OutputType, Output as OutputTrait, NetworkError, Block as BlockTrait, OutputType, Output as OutputTrait,
Transaction as TransactionTrait, SignableTransaction as SignableTransactionTrait, Transaction as TransactionTrait, SignableTransaction as SignableTransactionTrait,
Eventuality as EventualityTrait, EventualitiesTracker, AmortizeFeeRes, PreparedSend, Network, Eventuality as EventualityTrait, EventualitiesTracker, Network,
drop_branches, amortize_fee,
}, },
Plan, Plan,
}; };
@ -320,6 +319,52 @@ impl Bitcoin {
.unwrap() .unwrap()
} }
} }
async fn make_signable_transaction(
&self,
plan: &Plan<Self>,
fee: Fee,
calculating_fee: bool,
) -> Option<BSignableTransaction> {
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] #[async_trait]
@ -504,110 +549,34 @@ impl Network for Bitcoin {
res res
} }
async fn prepare_send( async fn needed_fee(
&self, &self,
_: usize, _: usize,
mut plan: Plan<Self>, plan: &Plan<Self>,
fee: Fee, fee_rate: Fee,
operating_costs: u64, ) -> Result<Option<u64>, NetworkError> {
) -> Result<PreparedSend<Self>, NetworkError> { Ok(
let signable = |plan: &Plan<Self>, tx_fee: Option<_>| { self
let mut payments = vec![]; .make_signable_transaction(plan, fee_rate, true)
for payment in &plan.payments { .await
// If we're solely estimating the fee, don't specify the actual amount .map(|signable| signable.needed_fee()),
// 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 },
));
}
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;
}
} }
async fn signable_transaction(
&self,
_: usize,
plan: &Plan<Self>,
fee_rate: Fee,
) -> Result<Option<(Self::SignableTransaction, Self::Eventuality)>, NetworkError> {
Ok(self.make_signable_transaction(plan, fee_rate, false).await.map(|signable| {
let plan_binding_input = *plan.inputs[0].output.outpoint(); let plan_binding_input = *plan.inputs[0].output.outpoint();
let outputs = signable.outputs().to_vec(); let outputs = signable.outputs().to_vec();
(
Ok(PreparedSend {
tx: Some((
SignableTransaction { transcript: plan.transcript(), actual: signable }, SignableTransaction { transcript: plan.transcript(), actual: signable },
Eventuality { plan_binding_input, outputs }, Eventuality { plan_binding_input, outputs },
)), )
post_fee_branches, }))
operating_costs,
})
} }
async fn attempt_send( async fn attempt_send(
@ -650,7 +619,7 @@ impl Network for Bitcoin {
} }
#[cfg(test)] #[cfg(test)]
async fn get_fee(&self) -> Self::Fee { async fn get_fee(&self) -> Fee {
Fee(1) Fee(1)
} }

View file

@ -199,7 +199,7 @@ pub struct PostFeeBranch {
} }
// Return the PostFeeBranches needed when dropping a transaction // Return the PostFeeBranches needed when dropping a transaction
pub fn drop_branches<N: Network>(plan: &Plan<N>) -> Vec<PostFeeBranch> { fn drop_branches<N: Network>(plan: &Plan<N>) -> Vec<PostFeeBranch> {
let mut branch_outputs = vec![]; let mut branch_outputs = vec![];
for payment in &plan.payments { for payment in &plan.payments {
if payment.address == N::branch_address(plan.key) { if payment.address == N::branch_address(plan.key) {
@ -209,105 +209,6 @@ pub fn drop_branches<N: Network>(plan: &Plan<N>) -> Vec<PostFeeBranch> {
branch_outputs branch_outputs
} }
pub struct AmortizeFeeRes {
post_fee_branches: Vec<PostFeeBranch>,
operating_costs: u64,
}
// Amortize a fee over the plan's payments
pub fn amortize_fee<N: Network>(
plan: &mut Plan<N>,
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::<u64>();
// 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<N>, 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::<u64>();
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<N: Network> { pub struct PreparedSend<N: Network> {
/// None for the transaction if the SignableTransaction was dropped due to lack of value. /// None for the transaction if the SignableTransaction was dropped due to lack of value.
pub tx: Option<(N::SignableTransaction, N::Eventuality)>, 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. /// The type representing the fee for this network.
// This should likely be a u64, wrapped in a type which implements appropriate fee logic. // 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. /// The type representing the transaction for this network.
type Transaction: Transaction<Self>; type Transaction: Transaction<Self>;
@ -408,16 +309,195 @@ pub trait Network: 'static + Send + Sync + Clone + PartialEq + Eq + Debug {
block: &Self::Block, block: &Self::Block,
) -> HashMap<[u8; 32], (usize, Self::Transaction)>; ) -> 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<Self>,
fee_rate: Self::Fee,
) -> Result<Option<u64>, 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<Self>,
fee_rate: Self::Fee,
) -> Result<Option<(Self::SignableTransaction, Self::Eventuality)>, NetworkError>;
/// Prepare a SignableTransaction for a transaction. /// 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( async fn prepare_send(
&self, &self,
block_number: usize, block_number: usize,
plan: Plan<Self>, mut plan: Plan<Self>,
fee_rate: Self::Fee, fee_rate: Self::Fee,
running_operating_costs: u64, operating_costs: u64,
) -> Result<PreparedSend<Self>, NetworkError>; ) -> Result<PreparedSend<Self>, 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<PostFeeBranch>,
operating_costs: u64,
}
// Amortize a fee over the plan's payments
fn amortize_fee<N: Network>(
plan: &mut Plan<N>,
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::<u64>();
// 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<N>, 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::<u64>();
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. /// Attempt to sign a SignableTransaction.
async fn attempt_send( async fn attempt_send(

View file

@ -38,8 +38,7 @@ use crate::{
networks::{ networks::{
NetworkError, Block as BlockTrait, OutputType, Output as OutputTrait, NetworkError, Block as BlockTrait, OutputType, Output as OutputTrait,
Transaction as TransactionTrait, SignableTransaction as SignableTransactionTrait, Transaction as TransactionTrait, SignableTransaction as SignableTransactionTrait,
Eventuality as EventualityTrait, EventualitiesTracker, AmortizeFeeRes, PreparedSend, Network, Eventuality as EventualityTrait, EventualitiesTracker, Network,
drop_branches, amortize_fee,
}, },
}; };
@ -207,6 +206,125 @@ impl Monero {
scanner scanner
} }
async fn make_signable_transaction(
&self,
block_number: usize,
mut plan: Plan<Self>,
fee_rate: Fee,
calculating_fee: bool,
) -> Result<Option<(RecommendedTranscript, MSignableTransaction)>, 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::<Vec<_>>();
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::<Vec<_>>();
// 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)] #[cfg(test)]
fn test_view_pair() -> ViewPair { fn test_view_pair() -> ViewPair {
ViewPair::new(*EdwardsPoint::generator(), Zeroizing::new(Scalar::ONE.0)) ViewPair::new(*EdwardsPoint::generator(), Zeroizing::new(Scalar::ONE.0))
@ -394,179 +512,33 @@ impl Network for Monero {
res res
} }
async fn prepare_send( async fn needed_fee(
&self, &self,
block_number: usize, block_number: usize,
mut plan: Plan<Self>, plan: &Plan<Self>,
fee: Fee, fee_rate: Fee,
operating_costs: u64, ) -> Result<Option<u64>, NetworkError> {
) -> Result<PreparedSend<Self>, NetworkError> { Ok(
// Sanity check this has at least one output planned self
assert!((!plan.payments.is_empty()) || plan.change.is_some()); .make_signable_transaction(block_number, plan.clone(), fee_rate, true)
.await?
// Get the protocol for the specified block number .map(|(_, signable)| signable.fee()),
// 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::<Vec<_>>();
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::<Vec<_>>();
let signable = |mut plan: Plan<Self>, 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;
}
} }
async fn signable_transaction(
&self,
block_number: usize,
plan: &Plan<Self>,
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 signable = SignableTransaction { transcript, actual: signable };
let eventuality = signable.actual.eventuality().unwrap(); let eventuality = signable.actual.eventuality().unwrap();
Ok(PreparedSend { tx: Some((signable, eventuality)), post_fee_branches, operating_costs }) (signable, eventuality)
},
))
} }
async fn attempt_send( async fn attempt_send(
@ -604,7 +576,7 @@ impl Network for Monero {
} }
#[cfg(test)] #[cfg(test)]
async fn get_fee(&self) -> Self::Fee { async fn get_fee(&self) -> Fee {
use monero_serai::wallet::FeePriority; use monero_serai::wallet::FeePriority;
self.rpc.get_fee(self.rpc.get_protocol().await.unwrap(), FeePriority::Low).await.unwrap() self.rpc.get_fee(self.rpc.get_protocol().await.unwrap(), FeePriority::Low).await.unwrap()