mirror of
https://github.com/serai-dex/serai.git
synced 2025-01-05 18:39:23 +00:00
Correct the fee amortization algorithm for an edge case
This is technically over-agressive, as a dropped output will reduce the fee, yet this edge case is so minor the flow for it to not be over-aggressive (over a few fractions of a cent) is by no means worth it. Fixes the crash causable by the WIP send_test.
This commit is contained in:
parent
f78332453b
commit
8b14bb54bb
3 changed files with 53 additions and 18 deletions
|
@ -140,8 +140,11 @@ pub enum TransactionError {
|
||||||
TooMuchData,
|
TooMuchData,
|
||||||
#[cfg_attr(feature = "std", error("too many inputs/too much arbitrary data"))]
|
#[cfg_attr(feature = "std", error("too many inputs/too much arbitrary data"))]
|
||||||
TooLargeTransaction,
|
TooLargeTransaction,
|
||||||
#[cfg_attr(feature = "std", error("not enough funds (in {0}, out {1})"))]
|
#[cfg_attr(
|
||||||
NotEnoughFunds(u64, u64),
|
feature = "std",
|
||||||
|
error("not enough funds (inputs {inputs}, outputs {outputs}, fee {fee})")
|
||||||
|
)]
|
||||||
|
NotEnoughFunds { inputs: u64, outputs: u64, fee: u64 },
|
||||||
#[cfg_attr(feature = "std", error("wrong spend private key"))]
|
#[cfg_attr(feature = "std", error("wrong spend private key"))]
|
||||||
WrongPrivateKey,
|
WrongPrivateKey,
|
||||||
#[cfg_attr(feature = "std", error("rpc error ({0})"))]
|
#[cfg_attr(feature = "std", error("rpc error ({0})"))]
|
||||||
|
@ -509,7 +512,7 @@ impl SignableTransaction {
|
||||||
// Make sure we have enough funds
|
// Make sure we have enough funds
|
||||||
let in_amount = inputs.iter().map(|(input, _)| input.commitment().amount).sum::<u64>();
|
let in_amount = inputs.iter().map(|(input, _)| input.commitment().amount).sum::<u64>();
|
||||||
if in_amount < (out_amount + fee) {
|
if in_amount < (out_amount + fee) {
|
||||||
Err(TransactionError::NotEnoughFunds(in_amount, out_amount + fee))?;
|
Err(TransactionError::NotEnoughFunds { inputs: in_amount, outputs: out_amount, fee })?;
|
||||||
}
|
}
|
||||||
|
|
||||||
// Sanity check we have the expected number of change outputs
|
// Sanity check we have the expected number of change outputs
|
||||||
|
|
|
@ -22,7 +22,7 @@ pub mod monero;
|
||||||
#[cfg(feature = "monero")]
|
#[cfg(feature = "monero")]
|
||||||
pub use monero::Monero;
|
pub use monero::Monero;
|
||||||
|
|
||||||
use crate::Plan;
|
use crate::{Payment, Plan};
|
||||||
|
|
||||||
#[derive(Clone, Copy, Error, Debug)]
|
#[derive(Clone, Copy, Error, Debug)]
|
||||||
pub enum CoinError {
|
pub enum CoinError {
|
||||||
|
@ -204,21 +204,45 @@ pub fn amortize_fee<C: Coin>(plan: &mut Plan<C>, tx_fee: u64) -> Vec<PostFeeBran
|
||||||
return vec![];
|
return vec![];
|
||||||
}
|
}
|
||||||
|
|
||||||
// Amortize the transaction fee across outputs
|
let original_outputs = plan.payments.iter().map(|payment| payment.amount).sum::<u64>();
|
||||||
let payments_len = u64::try_from(plan.payments.len()).unwrap();
|
|
||||||
// Use a formula which will round up
|
|
||||||
let output_fee = (tx_fee + (payments_len - 1)) / payments_len;
|
|
||||||
|
|
||||||
let mut branch_outputs = vec![];
|
// Amortize the transaction fee across outputs
|
||||||
for payment in plan.payments.iter_mut() {
|
let mut payments_len = u64::try_from(plan.payments.len()).unwrap();
|
||||||
let mut post_fee = payment.amount.checked_sub(output_fee);
|
// Use a formula which will round up
|
||||||
|
let per_output_fee = |payments| (tx_fee + (payments - 1)) / payments;
|
||||||
|
|
||||||
|
let post_fee = |payment: &Payment<C>, per_output_fee| {
|
||||||
|
let mut post_fee = payment.amount.checked_sub(per_output_fee);
|
||||||
// If this is under our dust threshold, drop it
|
// If this is under our dust threshold, drop it
|
||||||
if let Some(amount) = post_fee {
|
if let Some(amount) = post_fee {
|
||||||
if amount < C::DUST {
|
if amount < C::DUST {
|
||||||
post_fee = None;
|
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
|
// Note the branch output, if this is one
|
||||||
if payment.address == C::branch_address(plan.key) {
|
if payment.address == C::branch_address(plan.key) {
|
||||||
branch_outputs.push(PostFeeBranch { expected: payment.amount, actual: post_fee });
|
branch_outputs.push(PostFeeBranch { expected: payment.amount, actual: post_fee });
|
||||||
|
@ -227,6 +251,11 @@ pub fn amortize_fee<C: Coin>(plan: &mut Plan<C>, tx_fee: u64) -> Vec<PostFeeBran
|
||||||
}
|
}
|
||||||
// Drop payments now worth 0
|
// Drop payments now worth 0
|
||||||
plan.payments = plan.payments.drain(..).filter(|payment| payment.amount != 0).collect();
|
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 + tx_fee) <= original_outputs);
|
||||||
|
|
||||||
branch_outputs
|
branch_outputs
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
|
@ -418,7 +418,7 @@ impl Coin for Monero {
|
||||||
|
|
||||||
let inputs = spendable_outputs.into_iter().zip(decoys.into_iter()).collect::<Vec<_>>();
|
let inputs = spendable_outputs.into_iter().zip(decoys.into_iter()).collect::<Vec<_>>();
|
||||||
|
|
||||||
let signable = |plan: &mut Plan<Self>, tx_fee: Option<_>| {
|
let signable = |mut plan: Plan<Self>, tx_fee: Option<_>| {
|
||||||
// Monero requires at least two outputs
|
// Monero requires at least two outputs
|
||||||
// If we only have one output planned, add a dummy payment
|
// If we only have one output planned, add a dummy payment
|
||||||
let outputs = plan.payments.len() + usize::from(u8::from(plan.change.is_some()));
|
let outputs = plan.payments.len() + usize::from(u8::from(plan.change.is_some()));
|
||||||
|
@ -480,11 +480,14 @@ impl Coin for Monero {
|
||||||
TransactionError::FrostError(_) => {
|
TransactionError::FrostError(_) => {
|
||||||
panic!("supposedly unreachable (at this time) Monero error: {e}");
|
panic!("supposedly unreachable (at this time) Monero error: {e}");
|
||||||
}
|
}
|
||||||
TransactionError::NotEnoughFunds(_, _) => {
|
TransactionError::NotEnoughFunds { inputs, outputs, fee } => {
|
||||||
if tx_fee.is_none() {
|
if let Some(tx_fee) = tx_fee {
|
||||||
Ok(None)
|
panic!(
|
||||||
|
"{}. in: {inputs}, out: {outputs}, fee: {fee}, prior estimated fee: {tx_fee}",
|
||||||
|
"didn't have enough funds for a Monero TX",
|
||||||
|
);
|
||||||
} else {
|
} else {
|
||||||
panic!("didn't have enough funds for a Monero TX");
|
Ok(None)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
TransactionError::RpcError(e) => {
|
TransactionError::RpcError(e) => {
|
||||||
|
@ -495,7 +498,7 @@ impl Coin for Monero {
|
||||||
}
|
}
|
||||||
};
|
};
|
||||||
|
|
||||||
let tx_fee = match signable(&mut plan, None)? {
|
let tx_fee = match signable(plan.clone(), None)? {
|
||||||
Some(tx) => tx.fee(),
|
Some(tx) => tx.fee(),
|
||||||
None => return Ok((None, drop_branches(&plan))),
|
None => return Ok((None, drop_branches(&plan))),
|
||||||
};
|
};
|
||||||
|
@ -505,7 +508,7 @@ impl Coin for Monero {
|
||||||
let signable = SignableTransaction {
|
let signable = SignableTransaction {
|
||||||
keys,
|
keys,
|
||||||
transcript,
|
transcript,
|
||||||
actual: match signable(&mut plan, Some(tx_fee))? {
|
actual: match signable(plan, Some(tx_fee))? {
|
||||||
Some(signable) => signable,
|
Some(signable) => signable,
|
||||||
None => return Ok((None, branch_outputs)),
|
None => return Ok((None, branch_outputs)),
|
||||||
},
|
},
|
||||||
|
|
Loading…
Reference in a new issue