Add non-transaction-chaining scheduler

This commit is contained in:
Luke Parker
2024-09-04 03:54:12 -04:00
parent 0c1aec29bb
commit 6e9cb74022
17 changed files with 951 additions and 145 deletions

View File

@@ -19,6 +19,8 @@ workspace = true
[dependencies]
async-trait = { version = "0.1", default-features = false }
borsh = { version = "1", default-features = false, features = ["std", "derive", "de_strict_order"] }
serai-primitives = { path = "../../../../substrate/primitives", default-features = false, features = ["std"] }
primitives = { package = "serai-processor-primitives", path = "../../../primitives" }

View File

@@ -8,6 +8,9 @@ use primitives::{ReceivedOutput, Payment};
use scanner::{ScannerFeed, KeyFor, AddressFor, OutputFor, EventualityFor, BlockFor};
use scheduler_primitives::*;
mod tree;
pub use tree::*;
/// A planned transaction.
pub struct PlannedTransaction<S: ScannerFeed, ST: SignableTransaction, A> {
/// The signable transaction.
@@ -18,6 +21,23 @@ pub struct PlannedTransaction<S: ScannerFeed, ST: SignableTransaction, A> {
pub auxilliary: A,
}
/// A planned transaction which was created via amortizing the fee.
pub struct AmortizePlannedTransaction<S: ScannerFeed, ST: SignableTransaction, A> {
/// The amounts the included payments were worth.
///
/// If the payments passed as an argument are sorted from highest to lowest valued, these `n`
/// amounts will be for the first `n` payments.
pub effected_payments: Vec<Amount>,
/// Whether or not the planned transaction had a change output.
pub has_change: bool,
/// The signable transaction.
pub signable: ST,
/// The Eventuality to watch for.
pub eventuality: EventualityFor<S>,
/// The auxilliary data for this transaction.
pub auxilliary: A,
}
/// An object able to plan a transaction.
#[async_trait::async_trait]
pub trait TransactionPlanner<S: ScannerFeed, A>: 'static + Send + Sync {
@@ -60,7 +80,8 @@ pub trait TransactionPlanner<S: ScannerFeed, A>: 'static + Send + Sync {
/// This must only require the same fee as would be returned by `calculate_fee`. The caller is
/// trusted to maintain `sum(inputs) - sum(payments) >= if change.is_some() { DUST } else { 0 }`.
///
/// `change` will always be an address belonging to the Serai network.
/// `change` will always be an address belonging to the Serai network. If it is `Some`, a change
/// output must be created.
fn plan(
fee_rate: Self::FeeRate,
inputs: Vec<OutputFor<S>>,
@@ -82,7 +103,7 @@ pub trait TransactionPlanner<S: ScannerFeed, A>: 'static + Send + Sync {
inputs: Vec<OutputFor<S>>,
mut payments: Vec<Payment<AddressFor<S>>>,
mut change: Option<KeyFor<S>>,
) -> Option<PlannedTransaction<S, Self::SignableTransaction, A>> {
) -> Option<AmortizePlannedTransaction<S, Self::SignableTransaction, A>> {
// If there's no change output, we can't recoup any operating costs we would amortize
// We also don't have any losses if the inputs are written off/the change output is reduced
let mut operating_costs_if_no_change = 0;
@@ -192,6 +213,48 @@ pub trait TransactionPlanner<S: ScannerFeed, A>: 'static + Send + Sync {
}
// Because we amortized, or accrued as operating costs, the fee, make the transaction
Some(Self::plan(fee_rate, inputs, payments, change))
let effected_payments = payments.iter().map(|payment| payment.balance().amount).collect();
let has_change = change.is_some();
let PlannedTransaction { signable, eventuality, auxilliary } =
Self::plan(fee_rate, inputs, payments, change);
Some(AmortizePlannedTransaction {
effected_payments,
has_change,
signable,
eventuality,
auxilliary,
})
}
/// Create a tree to fulfill a set of payments.
///
/// Returns a `TreeTransaction` whose children (and arbitrary children of children) fulfill all
/// these payments. This tree root will be able to be made with a change output.
fn tree(payments: &[Payment<AddressFor<S>>]) -> TreeTransaction<AddressFor<S>> {
// This variable is for the current layer of the tree being built
let mut tree = Vec::with_capacity(payments.len().div_ceil(Self::MAX_OUTPUTS));
// Push the branches for the leaves (the payments out)
for payments in payments.chunks(Self::MAX_OUTPUTS) {
let value = payments.iter().map(|payment| payment.balance().amount.0).sum::<u64>();
tree.push(TreeTransaction::<AddressFor<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() == Self::MAX_OUTPUTS) {
let mut branch_layer = vec![];
for children in tree.chunks(Self::MAX_OUTPUTS) {
branch_layer.push(TreeTransaction::<AddressFor<S>>::Branch {
children: children.to_vec(),
value: children.iter().map(TreeTransaction::value).sum(),
});
}
tree = branch_layer;
}
assert_eq!(tree.len(), 1);
let tree_root = tree.remove(0);
assert!((tree_root.children() + 1) <= Self::MAX_OUTPUTS);
tree_root
}
}

View File

@@ -0,0 +1,146 @@
use borsh::{BorshSerialize, BorshDeserialize};
use serai_primitives::{Coin, Amount, Balance};
use primitives::{Address, Payment};
use scanner::ScannerFeed;
/// A transaction within a tree to fulfill payments.
#[derive(Clone, BorshSerialize, BorshDeserialize)]
pub enum TreeTransaction<A: Address> {
/// A transaction for the leaves (payments) of the tree.
Leaves {
/// The payments within this transaction.
payments: Vec<Payment<A>>,
/// The sum value of the payments.
value: u64,
},
/// A transaction for the branches of the tree.
Branch {
/// The child transactions.
children: Vec<Self>,
/// The sum value of the child transactions.
value: u64,
},
}
impl<A: Address> TreeTransaction<A> {
/// How many children this transaction has.
///
/// A child is defined as any dependent, whether payment or transaction.
pub fn children(&self) -> usize {
match self {
Self::Leaves { payments, .. } => payments.len(),
Self::Branch { children, .. } => children.len(),
}
}
/// The value this transaction wants to spend.
pub fn value(&self) -> u64 {
match self {
Self::Leaves { value, .. } | Self::Branch { value, .. } => *value,
}
}
/// The payments to make to enable this transaction's children.
///
/// A child is defined as any dependent, whether payment or transaction.
///
/// The input value given to this transaction MUST be less than or equal to the desired value.
/// The difference will be amortized over all dependents.
///
/// Returns None if no payments should be made. Returns Some containing a non-empty Vec if any
/// payments should be made.
pub fn payments<S: ScannerFeed>(
&self,
coin: Coin,
branch_address: &A,
input_value: u64,
) -> Option<Vec<Payment<A>>> {
// Fetch the amounts for the payments we'll make
let mut amounts: Vec<_> = match self {
Self::Leaves { payments, .. } => payments
.iter()
.map(|payment| {
assert_eq!(payment.balance().coin, coin);
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)
}
}