mirror of
https://github.com/serai-dex/serai.git
synced 2025-12-08 12:19:24 +00:00
Track and amortize operating costs to ensure solvency
Implements most of #297 to the point I'm fine closing it. The solution implemented is distinct than originally designed, yet much simpler. Since we have a fully-linear view of created transactions, we don't have to per-output track operating costs incurred by that output. We can track it across the entire Serai system, without hooking into the Eventuality system. Also updates documentation.
This commit is contained in:
@@ -47,8 +47,8 @@ use crate::{
|
||||
networks::{
|
||||
NetworkError, Block as BlockTrait, OutputType, Output as OutputTrait,
|
||||
Transaction as TransactionTrait, SignableTransaction as SignableTransactionTrait,
|
||||
Eventuality as EventualityTrait, EventualitiesTracker, PostFeeBranch, Network, drop_branches,
|
||||
amortize_fee,
|
||||
Eventuality as EventualityTrait, EventualitiesTracker, AmortizeFeeRes, PreparedSend, Network,
|
||||
drop_branches, amortize_fee,
|
||||
},
|
||||
Plan,
|
||||
};
|
||||
@@ -509,8 +509,8 @@ impl Network for Bitcoin {
|
||||
_: usize,
|
||||
mut plan: Plan<Self>,
|
||||
fee: Fee,
|
||||
) -> Result<(Option<(SignableTransaction, Self::Eventuality)>, Vec<PostFeeBranch>), NetworkError>
|
||||
{
|
||||
operating_costs: u64,
|
||||
) -> Result<PreparedSend<Self>, NetworkError> {
|
||||
let signable = |plan: &Plan<Self>, tx_fee: Option<_>| {
|
||||
let mut payments = vec![];
|
||||
for payment in &plan.payments {
|
||||
@@ -558,23 +558,37 @@ impl Network for Bitcoin {
|
||||
|
||||
let tx_fee = match signable(&plan, None) {
|
||||
Some(tx) => tx.needed_fee(),
|
||||
None => return Ok((None, drop_branches(&plan))),
|
||||
None => {
|
||||
return Ok(PreparedSend {
|
||||
tx: None,
|
||||
post_fee_branches: drop_branches(&plan),
|
||||
// We expected a change output of sum(inputs) - sum(outputs)
|
||||
// Since we can no longer create this change output, it becomes an operating cost
|
||||
operating_costs: operating_costs +
|
||||
plan.inputs.iter().map(|input| input.amount()).sum::<u64>() -
|
||||
plan.payments.iter().map(|payment| payment.amount).sum::<u64>(),
|
||||
});
|
||||
}
|
||||
};
|
||||
|
||||
let branch_outputs = amortize_fee(&mut plan, tx_fee);
|
||||
let AmortizeFeeRes { post_fee_branches, operating_costs } =
|
||||
amortize_fee(&mut plan, operating_costs, tx_fee);
|
||||
|
||||
let signable = signable(&plan, Some(tx_fee)).unwrap();
|
||||
|
||||
// TODO: If the change output was dropped by Bitcoin, increase operating costs
|
||||
|
||||
let plan_binding_input = *plan.inputs[0].output.outpoint();
|
||||
let outputs = signable.outputs().to_vec();
|
||||
|
||||
Ok((
|
||||
Some((
|
||||
Ok(PreparedSend {
|
||||
tx: Some((
|
||||
SignableTransaction { transcript: plan.transcript(), actual: signable },
|
||||
Eventuality { plan_binding_input, outputs },
|
||||
)),
|
||||
branch_outputs,
|
||||
))
|
||||
post_fee_branches,
|
||||
operating_costs,
|
||||
})
|
||||
}
|
||||
|
||||
async fn attempt_send(
|
||||
|
||||
@@ -209,19 +209,47 @@ pub fn drop_branches<N: Network>(plan: &Plan<N>) -> Vec<PostFeeBranch> {
|
||||
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>, tx_fee: u64) -> Vec<PostFeeBranch> {
|
||||
// No payments to amortize over
|
||||
if plan.payments.is_empty() {
|
||||
return vec![];
|
||||
}
|
||||
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| (tx_fee + (payments - 1)) / payments;
|
||||
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);
|
||||
@@ -266,9 +294,26 @@ pub fn amortize_fee<N: Network>(plan: &mut Plan<N>, tx_fee: u64) -> Vec<PostFeeB
|
||||
|
||||
// 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);
|
||||
assert!((new_outputs + total_fee) <= original_outputs);
|
||||
|
||||
branch_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> {
|
||||
/// None for the transaction if the SignableTransaction was dropped due to lack of value.
|
||||
pub tx: Option<(N::SignableTransaction, N::Eventuality)>,
|
||||
pub post_fee_branches: Vec<PostFeeBranch>,
|
||||
/// The updated operating costs after preparing this transaction.
|
||||
pub operating_costs: u64,
|
||||
}
|
||||
|
||||
#[async_trait]
|
||||
@@ -364,18 +409,15 @@ pub trait Network: 'static + Send + Sync + Clone + PartialEq + Eq + Debug {
|
||||
) -> HashMap<[u8; 32], (usize, Self::Transaction)>;
|
||||
|
||||
/// Prepare a SignableTransaction for a transaction.
|
||||
///
|
||||
/// Returns None for the transaction if the SignableTransaction was dropped due to lack of value.
|
||||
#[rustfmt::skip]
|
||||
// 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<Self>,
|
||||
fee: Self::Fee,
|
||||
) -> Result<
|
||||
(Option<(Self::SignableTransaction, Self::Eventuality)>, Vec<PostFeeBranch>),
|
||||
NetworkError
|
||||
>;
|
||||
fee_rate: Self::Fee,
|
||||
running_operating_costs: u64,
|
||||
) -> Result<PreparedSend<Self>, NetworkError>;
|
||||
|
||||
/// Attempt to sign a SignableTransaction.
|
||||
async fn attempt_send(
|
||||
|
||||
@@ -38,8 +38,8 @@ use crate::{
|
||||
networks::{
|
||||
NetworkError, Block as BlockTrait, OutputType, Output as OutputTrait,
|
||||
Transaction as TransactionTrait, SignableTransaction as SignableTransactionTrait,
|
||||
Eventuality as EventualityTrait, EventualitiesTracker, PostFeeBranch, Network, drop_branches,
|
||||
amortize_fee,
|
||||
Eventuality as EventualityTrait, EventualitiesTracker, AmortizeFeeRes, PreparedSend, Network,
|
||||
drop_branches, amortize_fee,
|
||||
},
|
||||
};
|
||||
|
||||
@@ -399,7 +399,8 @@ impl Network for Monero {
|
||||
block_number: usize,
|
||||
mut plan: Plan<Self>,
|
||||
fee: Fee,
|
||||
) -> Result<(Option<(SignableTransaction, Eventuality)>, Vec<PostFeeBranch>), NetworkError> {
|
||||
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());
|
||||
|
||||
@@ -522,20 +523,28 @@ impl Network for Monero {
|
||||
|
||||
let tx_fee = match signable(plan.clone(), None)? {
|
||||
Some(tx) => tx.fee(),
|
||||
None => return Ok((None, drop_branches(&plan))),
|
||||
None => {
|
||||
return Ok(PreparedSend {
|
||||
tx: None,
|
||||
post_fee_branches: drop_branches(&plan),
|
||||
// We expected a change output of sum(inputs) - sum(outputs)
|
||||
// Since we can no longer create this change output, it becomes an operating cost
|
||||
operating_costs: operating_costs +
|
||||
plan.inputs.iter().map(|input| input.amount()).sum::<u64>() -
|
||||
plan.payments.iter().map(|payment| payment.amount).sum::<u64>(),
|
||||
});
|
||||
}
|
||||
};
|
||||
|
||||
let branch_outputs = amortize_fee(&mut plan, tx_fee);
|
||||
let AmortizeFeeRes { post_fee_branches, operating_costs } =
|
||||
amortize_fee(&mut plan, operating_costs, tx_fee);
|
||||
|
||||
let signable = SignableTransaction {
|
||||
transcript,
|
||||
actual: match signable(plan, Some(tx_fee))? {
|
||||
Some(signable) => signable,
|
||||
None => return Ok((None, branch_outputs)),
|
||||
},
|
||||
};
|
||||
// TODO: If the change output was dropped by Monero, increase operating costs
|
||||
|
||||
let signable =
|
||||
SignableTransaction { transcript, actual: signable(plan, Some(tx_fee))?.unwrap() };
|
||||
let eventuality = signable.actual.eventuality().unwrap();
|
||||
Ok((Some((signable, eventuality)), branch_outputs))
|
||||
Ok(PreparedSend { tx: Some((signable, eventuality)), post_fee_branches, operating_costs })
|
||||
}
|
||||
|
||||
async fn attempt_send(
|
||||
|
||||
Reference in New Issue
Block a user