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
3 changed files with 401 additions and 380 deletions

View File

@@ -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<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)]
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<Self>,
fee: Fee,
operating_costs: u64,
) -> Result<PreparedSend<Self>, 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::<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,
plan: &Plan<Self>,
fee_rate: Fee,
) -> Result<Option<u64>, 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::<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;
}
}
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<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 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()