mirror of
https://github.com/serai-dex/serai.git
synced 2024-12-23 03:59:22 +00:00
Move common code from prepare_send into Network trait
This commit is contained in:
parent
d6bc1c1ea3
commit
4852dcaab7
3 changed files with 401 additions and 380 deletions
|
@ -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(
|
async fn signable_transaction(
|
||||||
plan.inputs.iter().map(|input| input.output.clone()).collect(),
|
&self,
|
||||||
&payments,
|
_: usize,
|
||||||
plan.change.as_ref().map(|change| change.0.clone()),
|
plan: &Plan<Self>,
|
||||||
None,
|
fee_rate: Fee,
|
||||||
fee.0,
|
) -> Result<Option<(Self::SignableTransaction, Self::Eventuality)>, NetworkError> {
|
||||||
) {
|
Ok(self.make_signable_transaction(plan, fee_rate, false).await.map(|signable| {
|
||||||
Ok(signable) => Some(signable),
|
let plan_binding_input = *plan.inputs[0].output.outpoint();
|
||||||
Err(TransactionError::NoInputs) => {
|
let outputs = signable.outputs().to_vec();
|
||||||
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((
|
|
||||||
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)
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
|
@ -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(
|
||||||
|
|
|
@ -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<_>>();
|
async fn signable_transaction(
|
||||||
|
&self,
|
||||||
let signable = |mut plan: Plan<Self>, tx_fee: Option<_>| {
|
block_number: usize,
|
||||||
// Monero requires at least two outputs
|
plan: &Plan<Self>,
|
||||||
// If we only have one output planned, add a dummy payment
|
fee_rate: Fee,
|
||||||
let outputs = plan.payments.len() + usize::from(u8::from(plan.change.is_some()));
|
) -> Result<Option<(Self::SignableTransaction, Self::Eventuality)>, NetworkError> {
|
||||||
if outputs == 0 {
|
Ok(self.make_signable_transaction(block_number, plan.clone(), fee_rate, false).await?.map(
|
||||||
return Ok(None);
|
|(transcript, signable)| {
|
||||||
} else if outputs == 1 {
|
let signable = SignableTransaction { transcript, actual: signable };
|
||||||
plan.payments.push(Payment {
|
let eventuality = signable.actual.eventuality().unwrap();
|
||||||
address: Address::new(
|
(signable, eventuality)
|
||||||
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 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()
|
||||||
|
|
Loading…
Reference in a new issue