mirror of
https://github.com/serai-dex/serai.git
synced 2025-12-09 12:49:23 +00:00
Add non-transaction-chaining scheduler
This commit is contained in:
@@ -9,7 +9,7 @@ to build and sign a transaction spending it.
|
||||
|
||||
The scheduler is designed to achieve fulfillment of all expected payments with
|
||||
an `O(1)` delay (regardless of prior scheduler state), `O(log n)` time, and
|
||||
`O(n)` computational complexity.
|
||||
`O(log(n) + n)` computational complexity.
|
||||
|
||||
Due to the ability to chain transactions, we can immediately plan/sign dependent
|
||||
transactions. For the time/computational complexity, we use a tree to fulfill
|
||||
|
||||
@@ -7,7 +7,7 @@ use std::collections::HashMap;
|
||||
|
||||
use group::GroupEncoding;
|
||||
|
||||
use serai_primitives::{Coin, Amount, Balance};
|
||||
use serai_primitives::{Coin, Amount};
|
||||
|
||||
use serai_db::DbTxn;
|
||||
|
||||
@@ -22,114 +22,6 @@ use utxo_scheduler_primitives::*;
|
||||
mod db;
|
||||
use db::Db;
|
||||
|
||||
#[derive(Clone)]
|
||||
enum TreeTransaction<S: ScannerFeed> {
|
||||
Leaves { payments: Vec<Payment<AddressFor<S>>>, value: u64 },
|
||||
Branch { children: Vec<Self>, value: u64 },
|
||||
}
|
||||
impl<S: ScannerFeed> TreeTransaction<S> {
|
||||
fn children(&self) -> usize {
|
||||
match self {
|
||||
Self::Leaves { payments, .. } => payments.len(),
|
||||
Self::Branch { children, .. } => children.len(),
|
||||
}
|
||||
}
|
||||
fn value(&self) -> u64 {
|
||||
match self {
|
||||
Self::Leaves { value, .. } | Self::Branch { value, .. } => *value,
|
||||
}
|
||||
}
|
||||
fn payments(
|
||||
&self,
|
||||
coin: Coin,
|
||||
branch_address: &AddressFor<S>,
|
||||
input_value: u64,
|
||||
) -> Option<Vec<Payment<AddressFor<S>>>> {
|
||||
// Fetch the amounts for the payments we'll make
|
||||
let mut amounts: Vec<_> = match self {
|
||||
Self::Leaves { payments, .. } => {
|
||||
payments.iter().map(|payment| Some(payment.balance().amount.0)).collect()
|
||||
}
|
||||
Self::Branch { children, .. } => children.iter().map(|child| Some(child.value())).collect(),
|
||||
};
|
||||
|
||||
// We need to reduce them so their sum is our input value
|
||||
assert!(input_value <= self.value());
|
||||
let amount_to_amortize = self.value() - input_value;
|
||||
|
||||
// If any payments won't survive the reduction, set them to None
|
||||
let mut amortized = 0;
|
||||
'outer: while amounts.iter().any(Option::is_some) && (amortized < amount_to_amortize) {
|
||||
let adjusted_fee = amount_to_amortize - amortized;
|
||||
let amounts_len =
|
||||
u64::try_from(amounts.iter().filter(|amount| amount.is_some()).count()).unwrap();
|
||||
let per_payment_fee_check = adjusted_fee.div_ceil(amounts_len);
|
||||
|
||||
// Check each amount to see if it's not viable
|
||||
let mut i = 0;
|
||||
while i < amounts.len() {
|
||||
if let Some(amount) = amounts[i] {
|
||||
if amount.saturating_sub(per_payment_fee_check) < S::dust(coin).0 {
|
||||
amounts[i] = None;
|
||||
amortized += amount;
|
||||
// If this amount wasn't viable, re-run with the new fee/amortization amounts
|
||||
continue 'outer;
|
||||
}
|
||||
}
|
||||
i += 1;
|
||||
}
|
||||
|
||||
// Now that we have the payments which will survive, reduce them
|
||||
for (i, amount) in amounts.iter_mut().enumerate() {
|
||||
if let Some(amount) = amount {
|
||||
*amount -= adjusted_fee / amounts_len;
|
||||
if i < usize::try_from(adjusted_fee % amounts_len).unwrap() {
|
||||
*amount -= 1;
|
||||
}
|
||||
}
|
||||
}
|
||||
break;
|
||||
}
|
||||
|
||||
// Now that we have the reduced amounts, create the payments
|
||||
let payments: Vec<_> = match self {
|
||||
Self::Leaves { payments, .. } => {
|
||||
payments
|
||||
.iter()
|
||||
.zip(amounts)
|
||||
.filter_map(|(payment, amount)| {
|
||||
amount.map(|amount| {
|
||||
// The existing payment, with the new amount
|
||||
Payment::new(
|
||||
payment.address().clone(),
|
||||
Balance { coin, amount: Amount(amount) },
|
||||
payment.data().clone(),
|
||||
)
|
||||
})
|
||||
})
|
||||
.collect()
|
||||
}
|
||||
Self::Branch { .. } => {
|
||||
amounts
|
||||
.into_iter()
|
||||
.filter_map(|amount| {
|
||||
amount.map(|amount| {
|
||||
// A branch output with the new amount
|
||||
Payment::new(branch_address.clone(), Balance { coin, amount: Amount(amount) }, None)
|
||||
})
|
||||
})
|
||||
.collect()
|
||||
}
|
||||
};
|
||||
|
||||
// Use None for vec![] so we never actually use vec![]
|
||||
if payments.is_empty() {
|
||||
None?;
|
||||
}
|
||||
Some(payments)
|
||||
}
|
||||
}
|
||||
|
||||
/// The outputs which will be effected by a PlannedTransaction and received by Serai.
|
||||
pub struct EffectedReceivedOutputs<S: ScannerFeed>(Vec<OutputFor<S>>);
|
||||
|
||||
@@ -306,30 +198,8 @@ impl<S: ScannerFeed, P: TransactionPlanner<S, EffectedReceivedOutputs<S>>> Sched
|
||||
assert!(Db::<S>::queued_payments(txn, key, coin).unwrap().is_empty());
|
||||
}
|
||||
|
||||
// Create a tree to fulfillthe payments
|
||||
// This variable is for the current layer of the tree being built
|
||||
let mut tree = Vec::with_capacity(payments.len().div_ceil(P::MAX_OUTPUTS));
|
||||
|
||||
// Push the branches for the leaves (the payments out)
|
||||
for payments in payments.chunks(P::MAX_OUTPUTS) {
|
||||
let value = payments.iter().map(|payment| payment.balance().amount.0).sum::<u64>();
|
||||
tree.push(TreeTransaction::<S>::Leaves { payments: payments.to_vec(), value });
|
||||
}
|
||||
|
||||
// While we haven't calculated a tree root, or the tree root doesn't support a change output,
|
||||
// keep working
|
||||
while (tree.len() != 1) || (tree[0].children() == P::MAX_OUTPUTS) {
|
||||
let mut branch_layer = vec![];
|
||||
for children in tree.chunks(P::MAX_OUTPUTS) {
|
||||
branch_layer.push(TreeTransaction::<S>::Branch {
|
||||
children: children.to_vec(),
|
||||
value: children.iter().map(TreeTransaction::value).sum(),
|
||||
});
|
||||
}
|
||||
tree = branch_layer;
|
||||
}
|
||||
assert_eq!(tree.len(), 1);
|
||||
assert!((tree[0].children() + 1) <= P::MAX_OUTPUTS);
|
||||
// Create a tree to fulfill the payments
|
||||
let mut tree = vec![P::tree(&payments)];
|
||||
|
||||
// Create the transaction for the root of the tree
|
||||
let mut branch_outputs = {
|
||||
@@ -343,7 +213,7 @@ impl<S: ScannerFeed, P: TransactionPlanner<S, EffectedReceivedOutputs<S>>> Sched
|
||||
P::fee_rate(block, coin),
|
||||
outputs.clone(),
|
||||
tree[0]
|
||||
.payments(coin, &branch_address, tree[0].value())
|
||||
.payments::<S>(coin, &branch_address, tree[0].value())
|
||||
.expect("payments were dropped despite providing an input of the needed value"),
|
||||
Some(key_for_change),
|
||||
) else {
|
||||
@@ -355,7 +225,7 @@ impl<S: ScannerFeed, P: TransactionPlanner<S, EffectedReceivedOutputs<S>>> Sched
|
||||
};
|
||||
|
||||
// If this doesn't have a change output, increase operating costs and try again
|
||||
if !planned.auxilliary.0.iter().any(|output| output.kind() == OutputType::Change) {
|
||||
if !planned.has_change {
|
||||
/*
|
||||
Since we'll create a change output if it's worth at least dust, amortizing dust from
|
||||
the payments should solve this. If the new transaction can't afford those operating
|
||||
@@ -399,11 +269,13 @@ impl<S: ScannerFeed, P: TransactionPlanner<S, EffectedReceivedOutputs<S>>> Sched
|
||||
TreeTransaction::Branch { children, .. } => children,
|
||||
};
|
||||
while !tree.is_empty() {
|
||||
// Sort the branch outputs by their value
|
||||
// Sort the branch outputs by their value (high to low)
|
||||
branch_outputs.sort_by_key(|a| a.balance().amount.0);
|
||||
branch_outputs.reverse();
|
||||
// Sort the transactions we should create by their value so they share an order with the
|
||||
// branch outputs
|
||||
tree.sort_by_key(TreeTransaction::value);
|
||||
tree.reverse();
|
||||
|
||||
// If we dropped any Branch outputs, drop the associated children
|
||||
tree.truncate(branch_outputs.len());
|
||||
@@ -417,7 +289,8 @@ impl<S: ScannerFeed, P: TransactionPlanner<S, EffectedReceivedOutputs<S>>> Sched
|
||||
for (branch_output, tx) in branch_outputs_for_this_layer.into_iter().zip(this_layer) {
|
||||
assert_eq!(branch_output.kind(), OutputType::Branch);
|
||||
|
||||
let Some(payments) = tx.payments(coin, &branch_address, branch_output.balance().amount.0)
|
||||
let Some(payments) =
|
||||
tx.payments::<S>(coin, &branch_address, branch_output.balance().amount.0)
|
||||
else {
|
||||
// If this output has become too small to satisfy this branch, drop it
|
||||
continue;
|
||||
@@ -550,8 +423,9 @@ impl<S: ScannerFeed, P: TransactionPlanner<S, EffectedReceivedOutputs<S>>> Sched
|
||||
// Fulfill the payments we prior couldn't
|
||||
let mut eventualities = HashMap::new();
|
||||
for (key, _stage) in active_keys {
|
||||
eventualities
|
||||
.insert(key.to_bytes().as_ref().to_vec(), Self::step(txn, active_keys, block, *key));
|
||||
assert!(eventualities
|
||||
.insert(key.to_bytes().as_ref().to_vec(), Self::step(txn, active_keys, block, *key))
|
||||
.is_none());
|
||||
}
|
||||
|
||||
// If this key has been flushed, forward all outputs
|
||||
|
||||
Reference in New Issue
Block a user