mirror of
https://github.com/serai-dex/serai.git
synced 2025-12-08 12:19:24 +00:00
Remove vast swaths of legacy code in the processor
This commit is contained in:
@@ -102,10 +102,6 @@ pub mod sign {
|
|||||||
Shares { id: SignId, shares: HashMap<Participant, Vec<u8>> },
|
Shares { id: SignId, shares: HashMap<Participant, Vec<u8>> },
|
||||||
// Re-attempt a signing protocol.
|
// Re-attempt a signing protocol.
|
||||||
Reattempt { id: SignId },
|
Reattempt { id: SignId },
|
||||||
/* TODO
|
|
||||||
// Completed a signing protocol already.
|
|
||||||
Completed { session: Session, id: [u8; 32], tx: Vec<u8> },
|
|
||||||
*/
|
|
||||||
}
|
}
|
||||||
|
|
||||||
impl CoordinatorMessage {
|
impl CoordinatorMessage {
|
||||||
@@ -118,7 +114,6 @@ pub mod sign {
|
|||||||
CoordinatorMessage::Preprocesses { id, .. } |
|
CoordinatorMessage::Preprocesses { id, .. } |
|
||||||
CoordinatorMessage::Shares { id, .. } |
|
CoordinatorMessage::Shares { id, .. } |
|
||||||
CoordinatorMessage::Reattempt { id, .. } => id.session,
|
CoordinatorMessage::Reattempt { id, .. } => id.session,
|
||||||
// TODO CoordinatorMessage::Completed { session, .. } => *session,
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@@ -131,8 +126,6 @@ pub mod sign {
|
|||||||
Preprocesses { id: SignId, preprocesses: Vec<Vec<u8>> },
|
Preprocesses { id: SignId, preprocesses: Vec<Vec<u8>> },
|
||||||
// Signed shares for the specified signing protocol.
|
// Signed shares for the specified signing protocol.
|
||||||
Shares { id: SignId, shares: Vec<Vec<u8>> },
|
Shares { id: SignId, shares: Vec<Vec<u8>> },
|
||||||
// Completed a signing protocol already.
|
|
||||||
// TODO Completed { session: Session, id: [u8; 32], tx: Vec<u8> },
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -330,11 +323,6 @@ impl CoordinatorMessage {
|
|||||||
sign::CoordinatorMessage::Preprocesses { id, .. } => (0, id),
|
sign::CoordinatorMessage::Preprocesses { id, .. } => (0, id),
|
||||||
sign::CoordinatorMessage::Shares { id, .. } => (1, id),
|
sign::CoordinatorMessage::Shares { id, .. } => (1, id),
|
||||||
sign::CoordinatorMessage::Reattempt { id, .. } => (2, id),
|
sign::CoordinatorMessage::Reattempt { id, .. } => (2, id),
|
||||||
// The coordinator should report all reported completions to the processor
|
|
||||||
// Accordingly, the intent is a combination of plan ID and actual TX
|
|
||||||
// While transaction alone may suffice, that doesn't cover cross-chain TX ID conflicts,
|
|
||||||
// which are possible
|
|
||||||
// TODO sign::CoordinatorMessage::Completed { id, tx, .. } => (3, (id, tx).encode()),
|
|
||||||
};
|
};
|
||||||
|
|
||||||
let mut res = vec![COORDINATOR_UID, TYPE_SIGN_UID, sub];
|
let mut res = vec![COORDINATOR_UID, TYPE_SIGN_UID, sub];
|
||||||
@@ -406,8 +394,6 @@ impl ProcessorMessage {
|
|||||||
// Unique since SignId
|
// Unique since SignId
|
||||||
sign::ProcessorMessage::Preprocesses { id, .. } => (1, id.encode()),
|
sign::ProcessorMessage::Preprocesses { id, .. } => (1, id.encode()),
|
||||||
sign::ProcessorMessage::Shares { id, .. } => (2, id.encode()),
|
sign::ProcessorMessage::Shares { id, .. } => (2, id.encode()),
|
||||||
// Unique since a processor will only sign a TX once
|
|
||||||
// TODO sign::ProcessorMessage::Completed { id, .. } => (3, id.to_vec()),
|
|
||||||
};
|
};
|
||||||
|
|
||||||
let mut res = vec![PROCESSOR_UID, TYPE_SIGN_UID, sub];
|
let mut res = vec![PROCESSOR_UID, TYPE_SIGN_UID, sub];
|
||||||
|
|||||||
@@ -1,260 +0,0 @@
|
|||||||
use std::io;
|
|
||||||
|
|
||||||
use ciphersuite::Ciphersuite;
|
|
||||||
pub use serai_db::*;
|
|
||||||
|
|
||||||
use scale::{Encode, Decode};
|
|
||||||
use serai_client::{primitives::Balance, in_instructions::primitives::InInstructionWithBalance};
|
|
||||||
|
|
||||||
use crate::{
|
|
||||||
Get, Plan,
|
|
||||||
networks::{Output, Transaction, Network},
|
|
||||||
};
|
|
||||||
|
|
||||||
#[derive(Clone, PartialEq, Eq, Debug)]
|
|
||||||
pub enum PlanFromScanning<N: Network> {
|
|
||||||
Refund(N::Output, N::Address),
|
|
||||||
Forward(N::Output),
|
|
||||||
}
|
|
||||||
|
|
||||||
impl<N: Network> PlanFromScanning<N> {
|
|
||||||
fn read<R: io::Read>(reader: &mut R) -> io::Result<Self> {
|
|
||||||
let mut kind = [0xff];
|
|
||||||
reader.read_exact(&mut kind)?;
|
|
||||||
match kind[0] {
|
|
||||||
0 => {
|
|
||||||
let output = N::Output::read(reader)?;
|
|
||||||
|
|
||||||
let mut address_vec_len = [0; 4];
|
|
||||||
reader.read_exact(&mut address_vec_len)?;
|
|
||||||
let mut address_vec =
|
|
||||||
vec![0; usize::try_from(u32::from_le_bytes(address_vec_len)).unwrap()];
|
|
||||||
reader.read_exact(&mut address_vec)?;
|
|
||||||
let address =
|
|
||||||
N::Address::try_from(address_vec).map_err(|_| "invalid address saved to disk").unwrap();
|
|
||||||
|
|
||||||
Ok(PlanFromScanning::Refund(output, address))
|
|
||||||
}
|
|
||||||
1 => {
|
|
||||||
let output = N::Output::read(reader)?;
|
|
||||||
Ok(PlanFromScanning::Forward(output))
|
|
||||||
}
|
|
||||||
_ => panic!("reading unrecognized PlanFromScanning"),
|
|
||||||
}
|
|
||||||
}
|
|
||||||
fn write<W: io::Write>(&self, writer: &mut W) -> io::Result<()> {
|
|
||||||
match self {
|
|
||||||
PlanFromScanning::Refund(output, address) => {
|
|
||||||
writer.write_all(&[0])?;
|
|
||||||
output.write(writer)?;
|
|
||||||
|
|
||||||
let address_vec: Vec<u8> =
|
|
||||||
address.clone().try_into().map_err(|_| "invalid address being refunded to").unwrap();
|
|
||||||
writer.write_all(&u32::try_from(address_vec.len()).unwrap().to_le_bytes())?;
|
|
||||||
writer.write_all(&address_vec)
|
|
||||||
}
|
|
||||||
PlanFromScanning::Forward(output) => {
|
|
||||||
writer.write_all(&[1])?;
|
|
||||||
output.write(writer)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
create_db!(
|
|
||||||
MultisigsDb {
|
|
||||||
NextBatchDb: () -> u32,
|
|
||||||
PlanDb: (id: &[u8]) -> Vec<u8>,
|
|
||||||
PlansFromScanningDb: (block_number: u64) -> Vec<u8>,
|
|
||||||
OperatingCostsDb: () -> u64,
|
|
||||||
ResolvedDb: (tx: &[u8]) -> [u8; 32],
|
|
||||||
SigningDb: (key: &[u8]) -> Vec<u8>,
|
|
||||||
ForwardedOutputDb: (balance: Balance) -> Vec<u8>,
|
|
||||||
DelayedOutputDb: () -> Vec<u8>
|
|
||||||
}
|
|
||||||
);
|
|
||||||
|
|
||||||
impl PlanDb {
|
|
||||||
pub fn save_active_plan<N: Network>(
|
|
||||||
txn: &mut impl DbTxn,
|
|
||||||
key: &[u8],
|
|
||||||
block_number: usize,
|
|
||||||
plan: &Plan<N>,
|
|
||||||
operating_costs_at_time: u64,
|
|
||||||
) {
|
|
||||||
let id = plan.id();
|
|
||||||
|
|
||||||
{
|
|
||||||
let mut signing = SigningDb::get(txn, key).unwrap_or_default();
|
|
||||||
|
|
||||||
// If we've already noted we're signing this, return
|
|
||||||
assert_eq!(signing.len() % 32, 0);
|
|
||||||
for i in 0 .. (signing.len() / 32) {
|
|
||||||
if signing[(i * 32) .. ((i + 1) * 32)] == id {
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
signing.extend(&id);
|
|
||||||
SigningDb::set(txn, key, &signing);
|
|
||||||
}
|
|
||||||
|
|
||||||
{
|
|
||||||
let mut buf = block_number.to_le_bytes().to_vec();
|
|
||||||
plan.write(&mut buf).unwrap();
|
|
||||||
buf.extend(&operating_costs_at_time.to_le_bytes());
|
|
||||||
Self::set(txn, &id, &buf);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
pub fn active_plans<N: Network>(getter: &impl Get, key: &[u8]) -> Vec<(u64, Plan<N>, u64)> {
|
|
||||||
let signing = SigningDb::get(getter, key).unwrap_or_default();
|
|
||||||
let mut res = vec![];
|
|
||||||
|
|
||||||
assert_eq!(signing.len() % 32, 0);
|
|
||||||
for i in 0 .. (signing.len() / 32) {
|
|
||||||
let id = &signing[(i * 32) .. ((i + 1) * 32)];
|
|
||||||
let buf = Self::get(getter, id).unwrap();
|
|
||||||
|
|
||||||
let block_number = u64::from_le_bytes(buf[.. 8].try_into().unwrap());
|
|
||||||
let plan = Plan::<N>::read::<&[u8]>(&mut &buf[8 ..]).unwrap();
|
|
||||||
assert_eq!(id, &plan.id());
|
|
||||||
let operating_costs = u64::from_le_bytes(buf[(buf.len() - 8) ..].try_into().unwrap());
|
|
||||||
res.push((block_number, plan, operating_costs));
|
|
||||||
}
|
|
||||||
res
|
|
||||||
}
|
|
||||||
|
|
||||||
pub fn plan_by_key_with_self_change<N: Network>(
|
|
||||||
getter: &impl Get,
|
|
||||||
key: <N::Curve as Ciphersuite>::G,
|
|
||||||
id: [u8; 32],
|
|
||||||
) -> bool {
|
|
||||||
let plan = Plan::<N>::read::<&[u8]>(&mut &Self::get(getter, &id).unwrap()[8 ..]).unwrap();
|
|
||||||
assert_eq!(plan.id(), id);
|
|
||||||
if let Some(change) = N::change_address(plan.key) {
|
|
||||||
(key == plan.key) && (Some(change) == plan.change)
|
|
||||||
} else {
|
|
||||||
false
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
impl OperatingCostsDb {
|
|
||||||
pub fn take_operating_costs(txn: &mut impl DbTxn) -> u64 {
|
|
||||||
let existing = Self::get(txn).unwrap_or_default();
|
|
||||||
txn.del(Self::key());
|
|
||||||
existing
|
|
||||||
}
|
|
||||||
pub fn set_operating_costs(txn: &mut impl DbTxn, amount: u64) {
|
|
||||||
if amount != 0 {
|
|
||||||
Self::set(txn, &amount);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
impl ResolvedDb {
|
|
||||||
pub fn resolve_plan<N: Network>(
|
|
||||||
txn: &mut impl DbTxn,
|
|
||||||
key: &[u8],
|
|
||||||
plan: [u8; 32],
|
|
||||||
resolution: &<N::Transaction as Transaction<N>>::Id,
|
|
||||||
) {
|
|
||||||
let mut signing = SigningDb::get(txn, key).unwrap_or_default();
|
|
||||||
assert_eq!(signing.len() % 32, 0);
|
|
||||||
|
|
||||||
let mut found = false;
|
|
||||||
for i in 0 .. (signing.len() / 32) {
|
|
||||||
let start = i * 32;
|
|
||||||
let end = i + 32;
|
|
||||||
if signing[start .. end] == plan {
|
|
||||||
found = true;
|
|
||||||
signing = [&signing[.. start], &signing[end ..]].concat();
|
|
||||||
break;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
if !found {
|
|
||||||
log::warn!("told to finish signing {} yet wasn't actively signing it", hex::encode(plan));
|
|
||||||
}
|
|
||||||
SigningDb::set(txn, key, &signing);
|
|
||||||
Self::set(txn, resolution.as_ref(), &plan);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
impl PlansFromScanningDb {
|
|
||||||
pub fn set_plans_from_scanning<N: Network>(
|
|
||||||
txn: &mut impl DbTxn,
|
|
||||||
block_number: usize,
|
|
||||||
plans: Vec<PlanFromScanning<N>>,
|
|
||||||
) {
|
|
||||||
let mut buf = vec![];
|
|
||||||
for plan in plans {
|
|
||||||
plan.write(&mut buf).unwrap();
|
|
||||||
}
|
|
||||||
Self::set(txn, block_number.try_into().unwrap(), &buf);
|
|
||||||
}
|
|
||||||
|
|
||||||
pub fn take_plans_from_scanning<N: Network>(
|
|
||||||
txn: &mut impl DbTxn,
|
|
||||||
block_number: usize,
|
|
||||||
) -> Option<Vec<PlanFromScanning<N>>> {
|
|
||||||
let block_number = u64::try_from(block_number).unwrap();
|
|
||||||
let res = Self::get(txn, block_number).map(|plans| {
|
|
||||||
let mut plans_ref = plans.as_slice();
|
|
||||||
let mut res = vec![];
|
|
||||||
while !plans_ref.is_empty() {
|
|
||||||
res.push(PlanFromScanning::<N>::read(&mut plans_ref).unwrap());
|
|
||||||
}
|
|
||||||
res
|
|
||||||
});
|
|
||||||
if res.is_some() {
|
|
||||||
txn.del(Self::key(block_number));
|
|
||||||
}
|
|
||||||
res
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
impl ForwardedOutputDb {
|
|
||||||
pub fn save_forwarded_output(txn: &mut impl DbTxn, instruction: &InInstructionWithBalance) {
|
|
||||||
let mut existing = Self::get(txn, instruction.balance).unwrap_or_default();
|
|
||||||
existing.extend(instruction.encode());
|
|
||||||
Self::set(txn, instruction.balance, &existing);
|
|
||||||
}
|
|
||||||
|
|
||||||
pub fn take_forwarded_output(
|
|
||||||
txn: &mut impl DbTxn,
|
|
||||||
balance: Balance,
|
|
||||||
) -> Option<InInstructionWithBalance> {
|
|
||||||
let outputs = Self::get(txn, balance)?;
|
|
||||||
let mut outputs_ref = outputs.as_slice();
|
|
||||||
let res = InInstructionWithBalance::decode(&mut outputs_ref).unwrap();
|
|
||||||
assert!(outputs_ref.len() < outputs.len());
|
|
||||||
if outputs_ref.is_empty() {
|
|
||||||
txn.del(Self::key(balance));
|
|
||||||
} else {
|
|
||||||
Self::set(txn, balance, &outputs);
|
|
||||||
}
|
|
||||||
Some(res)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
impl DelayedOutputDb {
|
|
||||||
pub fn save_delayed_output(txn: &mut impl DbTxn, instruction: &InInstructionWithBalance) {
|
|
||||||
let mut existing = Self::get(txn).unwrap_or_default();
|
|
||||||
existing.extend(instruction.encode());
|
|
||||||
Self::set(txn, &existing);
|
|
||||||
}
|
|
||||||
|
|
||||||
pub fn take_delayed_outputs(txn: &mut impl DbTxn) -> Vec<InInstructionWithBalance> {
|
|
||||||
let Some(outputs) = Self::get(txn) else { return vec![] };
|
|
||||||
txn.del(Self::key());
|
|
||||||
|
|
||||||
let mut outputs_ref = outputs.as_slice();
|
|
||||||
let mut res = vec![];
|
|
||||||
while !outputs_ref.is_empty() {
|
|
||||||
res.push(InInstructionWithBalance::decode(&mut outputs_ref).unwrap());
|
|
||||||
}
|
|
||||||
res
|
|
||||||
}
|
|
||||||
}
|
|
||||||
File diff suppressed because it is too large
Load Diff
@@ -1,96 +0,0 @@
|
|||||||
use core::fmt::Debug;
|
|
||||||
use std::io;
|
|
||||||
|
|
||||||
use ciphersuite::Ciphersuite;
|
|
||||||
|
|
||||||
use serai_client::primitives::{NetworkId, Balance};
|
|
||||||
|
|
||||||
use crate::{networks::Network, Db, Payment, Plan};
|
|
||||||
|
|
||||||
pub(crate) mod utxo;
|
|
||||||
pub(crate) mod smart_contract;
|
|
||||||
|
|
||||||
pub trait SchedulerAddendum: Send + Clone + PartialEq + Debug {
|
|
||||||
fn read<R: io::Read>(reader: &mut R) -> io::Result<Self>;
|
|
||||||
fn write<W: io::Write>(&self, writer: &mut W) -> io::Result<()>;
|
|
||||||
}
|
|
||||||
|
|
||||||
impl SchedulerAddendum for () {
|
|
||||||
fn read<R: io::Read>(_: &mut R) -> io::Result<Self> {
|
|
||||||
Ok(())
|
|
||||||
}
|
|
||||||
fn write<W: io::Write>(&self, _: &mut W) -> io::Result<()> {
|
|
||||||
Ok(())
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
pub trait Scheduler<N: Network>: Sized + Clone + PartialEq + Debug {
|
|
||||||
type Addendum: SchedulerAddendum;
|
|
||||||
|
|
||||||
/// Check if this Scheduler is empty.
|
|
||||||
fn empty(&self) -> bool;
|
|
||||||
|
|
||||||
/// Create a new Scheduler.
|
|
||||||
fn new<D: Db>(
|
|
||||||
txn: &mut D::Transaction<'_>,
|
|
||||||
key: <N::Curve as Ciphersuite>::G,
|
|
||||||
network: NetworkId,
|
|
||||||
) -> Self;
|
|
||||||
|
|
||||||
/// Load a Scheduler from the DB.
|
|
||||||
fn from_db<D: Db>(
|
|
||||||
db: &D,
|
|
||||||
key: <N::Curve as Ciphersuite>::G,
|
|
||||||
network: NetworkId,
|
|
||||||
) -> io::Result<Self>;
|
|
||||||
|
|
||||||
/// Check if a branch is usable.
|
|
||||||
fn can_use_branch(&self, balance: Balance) -> bool;
|
|
||||||
|
|
||||||
/// Schedule a series of outputs/payments.
|
|
||||||
fn schedule<D: Db>(
|
|
||||||
&mut self,
|
|
||||||
txn: &mut D::Transaction<'_>,
|
|
||||||
utxos: Vec<N::Output>,
|
|
||||||
payments: Vec<Payment<N>>,
|
|
||||||
// TODO: Tighten this to multisig_for_any_change
|
|
||||||
key_for_any_change: <N::Curve as Ciphersuite>::G,
|
|
||||||
force_spend: bool,
|
|
||||||
) -> Vec<Plan<N>>;
|
|
||||||
|
|
||||||
/// Consume all payments still pending within this Scheduler, without scheduling them.
|
|
||||||
fn consume_payments<D: Db>(&mut self, txn: &mut D::Transaction<'_>) -> Vec<Payment<N>>;
|
|
||||||
|
|
||||||
/// Note a branch output as having been created, with the amount it was actually created with,
|
|
||||||
/// or not having been created due to being too small.
|
|
||||||
fn created_output<D: Db>(
|
|
||||||
&mut self,
|
|
||||||
txn: &mut D::Transaction<'_>,
|
|
||||||
expected: u64,
|
|
||||||
actual: Option<u64>,
|
|
||||||
);
|
|
||||||
|
|
||||||
/// Refund a specific output.
|
|
||||||
fn refund_plan<D: Db>(
|
|
||||||
&mut self,
|
|
||||||
txn: &mut D::Transaction<'_>,
|
|
||||||
output: N::Output,
|
|
||||||
refund_to: N::Address,
|
|
||||||
) -> Plan<N>;
|
|
||||||
|
|
||||||
/// Shim the forwarding Plan as necessary to obtain a fee estimate.
|
|
||||||
///
|
|
||||||
/// If this Scheduler is for a Network which requires forwarding, this must return Some with a
|
|
||||||
/// plan with identical fee behavior. If forwarding isn't necessary, returns None.
|
|
||||||
fn shim_forward_plan(output: N::Output, to: <N::Curve as Ciphersuite>::G) -> Option<Plan<N>>;
|
|
||||||
|
|
||||||
/// Forward a specific output to the new multisig.
|
|
||||||
///
|
|
||||||
/// Returns None if no forwarding is necessary. Must return Some if forwarding is necessary.
|
|
||||||
fn forward_plan<D: Db>(
|
|
||||||
&mut self,
|
|
||||||
txn: &mut D::Transaction<'_>,
|
|
||||||
output: N::Output,
|
|
||||||
to: <N::Curve as Ciphersuite>::G,
|
|
||||||
) -> Option<Plan<N>>;
|
|
||||||
}
|
|
||||||
@@ -1,631 +0,0 @@
|
|||||||
use std::{
|
|
||||||
io::{self, Read},
|
|
||||||
collections::{VecDeque, HashMap},
|
|
||||||
};
|
|
||||||
|
|
||||||
use ciphersuite::{group::GroupEncoding, Ciphersuite};
|
|
||||||
|
|
||||||
use serai_client::primitives::{NetworkId, Coin, Amount, Balance};
|
|
||||||
|
|
||||||
use crate::{
|
|
||||||
DbTxn, Db, Payment, Plan,
|
|
||||||
networks::{OutputType, Output, Network, UtxoNetwork},
|
|
||||||
multisigs::scheduler::Scheduler as SchedulerTrait,
|
|
||||||
};
|
|
||||||
|
|
||||||
/// Deterministic output/payment manager.
|
|
||||||
#[derive(Clone, PartialEq, Eq, Debug)]
|
|
||||||
pub struct Scheduler<N: UtxoNetwork> {
|
|
||||||
key: <N::Curve as Ciphersuite>::G,
|
|
||||||
coin: Coin,
|
|
||||||
|
|
||||||
// Serai, when it has more outputs expected than it can handle in a single transaction, will
|
|
||||||
// schedule the outputs to be handled later. Immediately, it just creates additional outputs
|
|
||||||
// which will eventually handle those outputs
|
|
||||||
//
|
|
||||||
// These maps map output amounts, which we'll receive in the future, to the payments they should
|
|
||||||
// be used on
|
|
||||||
//
|
|
||||||
// When those output amounts appear, their payments should be scheduled
|
|
||||||
// The Vec<Payment> is for all payments that should be done per output instance
|
|
||||||
// The VecDeque allows multiple sets of payments with the same sum amount to properly co-exist
|
|
||||||
//
|
|
||||||
// queued_plans are for outputs which we will create, yet when created, will have their amount
|
|
||||||
// reduced by the fee it cost to be created. The Scheduler will then be told how what amount the
|
|
||||||
// output actually has, and it'll be moved into plans
|
|
||||||
queued_plans: HashMap<u64, VecDeque<Vec<Payment<N>>>>,
|
|
||||||
plans: HashMap<u64, VecDeque<Vec<Payment<N>>>>,
|
|
||||||
|
|
||||||
// UTXOs available
|
|
||||||
utxos: Vec<N::Output>,
|
|
||||||
|
|
||||||
// Payments awaiting scheduling due to the output availability problem
|
|
||||||
payments: VecDeque<Payment<N>>,
|
|
||||||
}
|
|
||||||
|
|
||||||
fn scheduler_key<D: Db, G: GroupEncoding>(key: &G) -> Vec<u8> {
|
|
||||||
D::key(b"SCHEDULER", b"scheduler", key.to_bytes())
|
|
||||||
}
|
|
||||||
|
|
||||||
impl<N: UtxoNetwork<Scheduler = Self>> Scheduler<N> {
|
|
||||||
pub fn empty(&self) -> bool {
|
|
||||||
self.queued_plans.is_empty() &&
|
|
||||||
self.plans.is_empty() &&
|
|
||||||
self.utxos.is_empty() &&
|
|
||||||
self.payments.is_empty()
|
|
||||||
}
|
|
||||||
|
|
||||||
fn read<R: Read>(
|
|
||||||
key: <N::Curve as Ciphersuite>::G,
|
|
||||||
coin: Coin,
|
|
||||||
reader: &mut R,
|
|
||||||
) -> io::Result<Self> {
|
|
||||||
let mut read_plans = || -> io::Result<_> {
|
|
||||||
let mut all_plans = HashMap::new();
|
|
||||||
let mut all_plans_len = [0; 4];
|
|
||||||
reader.read_exact(&mut all_plans_len)?;
|
|
||||||
for _ in 0 .. u32::from_le_bytes(all_plans_len) {
|
|
||||||
let mut amount = [0; 8];
|
|
||||||
reader.read_exact(&mut amount)?;
|
|
||||||
let amount = u64::from_le_bytes(amount);
|
|
||||||
|
|
||||||
let mut plans = VecDeque::new();
|
|
||||||
let mut plans_len = [0; 4];
|
|
||||||
reader.read_exact(&mut plans_len)?;
|
|
||||||
for _ in 0 .. u32::from_le_bytes(plans_len) {
|
|
||||||
let mut payments = vec![];
|
|
||||||
let mut payments_len = [0; 4];
|
|
||||||
reader.read_exact(&mut payments_len)?;
|
|
||||||
|
|
||||||
for _ in 0 .. u32::from_le_bytes(payments_len) {
|
|
||||||
payments.push(Payment::read(reader)?);
|
|
||||||
}
|
|
||||||
plans.push_back(payments);
|
|
||||||
}
|
|
||||||
all_plans.insert(amount, plans);
|
|
||||||
}
|
|
||||||
Ok(all_plans)
|
|
||||||
};
|
|
||||||
let queued_plans = read_plans()?;
|
|
||||||
let plans = read_plans()?;
|
|
||||||
|
|
||||||
let mut utxos = vec![];
|
|
||||||
let mut utxos_len = [0; 4];
|
|
||||||
reader.read_exact(&mut utxos_len)?;
|
|
||||||
for _ in 0 .. u32::from_le_bytes(utxos_len) {
|
|
||||||
utxos.push(N::Output::read(reader)?);
|
|
||||||
}
|
|
||||||
|
|
||||||
let mut payments = VecDeque::new();
|
|
||||||
let mut payments_len = [0; 4];
|
|
||||||
reader.read_exact(&mut payments_len)?;
|
|
||||||
for _ in 0 .. u32::from_le_bytes(payments_len) {
|
|
||||||
payments.push_back(Payment::read(reader)?);
|
|
||||||
}
|
|
||||||
|
|
||||||
Ok(Scheduler { key, coin, queued_plans, plans, utxos, payments })
|
|
||||||
}
|
|
||||||
|
|
||||||
// TODO2: Get rid of this
|
|
||||||
// We reserialize the entire scheduler on any mutation to save it to the DB which is horrible
|
|
||||||
// We should have an incremental solution
|
|
||||||
fn serialize(&self) -> Vec<u8> {
|
|
||||||
let mut res = Vec::with_capacity(4096);
|
|
||||||
|
|
||||||
let mut write_plans = |plans: &HashMap<u64, VecDeque<Vec<Payment<N>>>>| {
|
|
||||||
res.extend(u32::try_from(plans.len()).unwrap().to_le_bytes());
|
|
||||||
for (amount, list_of_plans) in plans {
|
|
||||||
res.extend(amount.to_le_bytes());
|
|
||||||
res.extend(u32::try_from(list_of_plans.len()).unwrap().to_le_bytes());
|
|
||||||
for plan in list_of_plans {
|
|
||||||
res.extend(u32::try_from(plan.len()).unwrap().to_le_bytes());
|
|
||||||
for payment in plan {
|
|
||||||
payment.write(&mut res).unwrap();
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
};
|
|
||||||
write_plans(&self.queued_plans);
|
|
||||||
write_plans(&self.plans);
|
|
||||||
|
|
||||||
res.extend(u32::try_from(self.utxos.len()).unwrap().to_le_bytes());
|
|
||||||
for utxo in &self.utxos {
|
|
||||||
utxo.write(&mut res).unwrap();
|
|
||||||
}
|
|
||||||
|
|
||||||
res.extend(u32::try_from(self.payments.len()).unwrap().to_le_bytes());
|
|
||||||
for payment in &self.payments {
|
|
||||||
payment.write(&mut res).unwrap();
|
|
||||||
}
|
|
||||||
|
|
||||||
debug_assert_eq!(&Self::read(self.key, self.coin, &mut res.as_slice()).unwrap(), self);
|
|
||||||
res
|
|
||||||
}
|
|
||||||
|
|
||||||
pub fn new<D: Db>(
|
|
||||||
txn: &mut D::Transaction<'_>,
|
|
||||||
key: <N::Curve as Ciphersuite>::G,
|
|
||||||
network: NetworkId,
|
|
||||||
) -> Self {
|
|
||||||
assert!(N::branch_address(key).is_some());
|
|
||||||
assert!(N::change_address(key).is_some());
|
|
||||||
assert!(N::forward_address(key).is_some());
|
|
||||||
|
|
||||||
let coin = {
|
|
||||||
let coins = network.coins();
|
|
||||||
assert_eq!(coins.len(), 1);
|
|
||||||
coins[0]
|
|
||||||
};
|
|
||||||
|
|
||||||
let res = Scheduler {
|
|
||||||
key,
|
|
||||||
coin,
|
|
||||||
queued_plans: HashMap::new(),
|
|
||||||
plans: HashMap::new(),
|
|
||||||
utxos: vec![],
|
|
||||||
payments: VecDeque::new(),
|
|
||||||
};
|
|
||||||
// Save it to disk so from_db won't panic if we don't mutate it before rebooting
|
|
||||||
txn.put(scheduler_key::<D, _>(&res.key), res.serialize());
|
|
||||||
res
|
|
||||||
}
|
|
||||||
|
|
||||||
pub fn from_db<D: Db>(
|
|
||||||
db: &D,
|
|
||||||
key: <N::Curve as Ciphersuite>::G,
|
|
||||||
network: NetworkId,
|
|
||||||
) -> io::Result<Self> {
|
|
||||||
let coin = {
|
|
||||||
let coins = network.coins();
|
|
||||||
assert_eq!(coins.len(), 1);
|
|
||||||
coins[0]
|
|
||||||
};
|
|
||||||
|
|
||||||
let scheduler = db.get(scheduler_key::<D, _>(&key)).unwrap_or_else(|| {
|
|
||||||
panic!("loading scheduler from DB without scheduler for {}", hex::encode(key.to_bytes()))
|
|
||||||
});
|
|
||||||
let mut reader_slice = scheduler.as_slice();
|
|
||||||
let reader = &mut reader_slice;
|
|
||||||
|
|
||||||
Self::read(key, coin, reader)
|
|
||||||
}
|
|
||||||
|
|
||||||
pub fn can_use_branch(&self, balance: Balance) -> bool {
|
|
||||||
assert_eq!(balance.coin, self.coin);
|
|
||||||
self.plans.contains_key(&balance.amount.0)
|
|
||||||
}
|
|
||||||
|
|
||||||
fn execute(
|
|
||||||
&mut self,
|
|
||||||
inputs: Vec<N::Output>,
|
|
||||||
mut payments: Vec<Payment<N>>,
|
|
||||||
key_for_any_change: <N::Curve as Ciphersuite>::G,
|
|
||||||
) -> Plan<N> {
|
|
||||||
let mut change = false;
|
|
||||||
let mut max = N::MAX_OUTPUTS;
|
|
||||||
|
|
||||||
let payment_amounts = |payments: &Vec<Payment<N>>| {
|
|
||||||
payments.iter().map(|payment| payment.balance.amount.0).sum::<u64>()
|
|
||||||
};
|
|
||||||
|
|
||||||
// Requires a change output
|
|
||||||
if inputs.iter().map(|output| output.balance().amount.0).sum::<u64>() !=
|
|
||||||
payment_amounts(&payments)
|
|
||||||
{
|
|
||||||
change = true;
|
|
||||||
max -= 1;
|
|
||||||
}
|
|
||||||
|
|
||||||
let mut add_plan = |payments| {
|
|
||||||
let amount = payment_amounts(&payments);
|
|
||||||
self.queued_plans.entry(amount).or_insert(VecDeque::new()).push_back(payments);
|
|
||||||
amount
|
|
||||||
};
|
|
||||||
|
|
||||||
let branch_address = N::branch_address(self.key).unwrap();
|
|
||||||
|
|
||||||
// If we have more payments than we can handle in a single TX, create plans for them
|
|
||||||
// TODO2: This isn't perfect. For 258 outputs, and a MAX_OUTPUTS of 16, this will create:
|
|
||||||
// 15 branches of 16 leaves
|
|
||||||
// 1 branch of:
|
|
||||||
// - 1 branch of 16 leaves
|
|
||||||
// - 2 leaves
|
|
||||||
// If this was perfect, the heaviest branch would have 1 branch of 3 leaves and 15 leaves
|
|
||||||
while payments.len() > max {
|
|
||||||
// The resulting TX will have the remaining payments and a new branch payment
|
|
||||||
let to_remove = (payments.len() + 1) - N::MAX_OUTPUTS;
|
|
||||||
// Don't remove more than possible
|
|
||||||
let to_remove = to_remove.min(N::MAX_OUTPUTS);
|
|
||||||
|
|
||||||
// Create the plan
|
|
||||||
let removed = payments.drain((payments.len() - to_remove) ..).collect::<Vec<_>>();
|
|
||||||
assert_eq!(removed.len(), to_remove);
|
|
||||||
let amount = add_plan(removed);
|
|
||||||
|
|
||||||
// Create the payment for the plan
|
|
||||||
// Push it to the front so it's not moved into a branch until all lower-depth items are
|
|
||||||
payments.insert(
|
|
||||||
0,
|
|
||||||
Payment {
|
|
||||||
address: branch_address.clone(),
|
|
||||||
data: None,
|
|
||||||
balance: Balance { coin: self.coin, amount: Amount(amount) },
|
|
||||||
},
|
|
||||||
);
|
|
||||||
}
|
|
||||||
|
|
||||||
Plan {
|
|
||||||
key: self.key,
|
|
||||||
inputs,
|
|
||||||
payments,
|
|
||||||
change: Some(N::change_address(key_for_any_change).unwrap()).filter(|_| change),
|
|
||||||
scheduler_addendum: (),
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
fn add_outputs(
|
|
||||||
&mut self,
|
|
||||||
mut utxos: Vec<N::Output>,
|
|
||||||
key_for_any_change: <N::Curve as Ciphersuite>::G,
|
|
||||||
) -> Vec<Plan<N>> {
|
|
||||||
log::info!("adding {} outputs", utxos.len());
|
|
||||||
|
|
||||||
let mut txs = vec![];
|
|
||||||
|
|
||||||
for utxo in utxos.drain(..) {
|
|
||||||
if utxo.kind() == OutputType::Branch {
|
|
||||||
let amount = utxo.balance().amount.0;
|
|
||||||
if let Some(plans) = self.plans.get_mut(&amount) {
|
|
||||||
// Execute the first set of payments possible with an output of this amount
|
|
||||||
let payments = plans.pop_front().unwrap();
|
|
||||||
// They won't be equal if we dropped payments due to being dust
|
|
||||||
assert!(amount >= payments.iter().map(|payment| payment.balance.amount.0).sum::<u64>());
|
|
||||||
|
|
||||||
// If we've grabbed the last plan for this output amount, remove it from the map
|
|
||||||
if plans.is_empty() {
|
|
||||||
self.plans.remove(&amount);
|
|
||||||
}
|
|
||||||
|
|
||||||
// Create a TX for these payments
|
|
||||||
txs.push(self.execute(vec![utxo], payments, key_for_any_change));
|
|
||||||
continue;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
self.utxos.push(utxo);
|
|
||||||
}
|
|
||||||
|
|
||||||
log::info!("{} planned TXs have had their required inputs confirmed", txs.len());
|
|
||||||
txs
|
|
||||||
}
|
|
||||||
|
|
||||||
// Schedule a series of outputs/payments.
|
|
||||||
pub fn schedule<D: Db>(
|
|
||||||
&mut self,
|
|
||||||
txn: &mut D::Transaction<'_>,
|
|
||||||
utxos: Vec<N::Output>,
|
|
||||||
mut payments: Vec<Payment<N>>,
|
|
||||||
key_for_any_change: <N::Curve as Ciphersuite>::G,
|
|
||||||
force_spend: bool,
|
|
||||||
) -> Vec<Plan<N>> {
|
|
||||||
for utxo in &utxos {
|
|
||||||
assert_eq!(utxo.balance().coin, self.coin);
|
|
||||||
}
|
|
||||||
for payment in &payments {
|
|
||||||
assert_eq!(payment.balance.coin, self.coin);
|
|
||||||
}
|
|
||||||
|
|
||||||
// Drop payments to our own branch address
|
|
||||||
/*
|
|
||||||
created_output will be called any time we send to a branch address. If it's called, and it
|
|
||||||
wasn't expecting to be called, that's almost certainly an error. The only way to guarantee
|
|
||||||
this however is to only have us send to a branch address when creating a branch, hence the
|
|
||||||
dropping of pointless payments.
|
|
||||||
|
|
||||||
This is not comprehensive as a payment may still be made to another active multisig's branch
|
|
||||||
address, depending on timing. This is safe as the issue only occurs when a multisig sends to
|
|
||||||
its *own* branch address, since created_output is called on the signer's Scheduler.
|
|
||||||
*/
|
|
||||||
{
|
|
||||||
let branch_address = N::branch_address(self.key).unwrap();
|
|
||||||
payments =
|
|
||||||
payments.drain(..).filter(|payment| payment.address != branch_address).collect::<Vec<_>>();
|
|
||||||
}
|
|
||||||
|
|
||||||
let mut plans = self.add_outputs(utxos, key_for_any_change);
|
|
||||||
|
|
||||||
log::info!("scheduling {} new payments", payments.len());
|
|
||||||
|
|
||||||
// Add all new payments to the list of pending payments
|
|
||||||
self.payments.extend(payments);
|
|
||||||
let payments_at_start = self.payments.len();
|
|
||||||
log::info!("{} payments are now scheduled", payments_at_start);
|
|
||||||
|
|
||||||
// If we don't have UTXOs available, don't try to continue
|
|
||||||
if self.utxos.is_empty() {
|
|
||||||
log::info!("no utxos currently available");
|
|
||||||
return plans;
|
|
||||||
}
|
|
||||||
|
|
||||||
// Sort UTXOs so the highest valued ones are first
|
|
||||||
self.utxos.sort_by(|a, b| a.balance().amount.0.cmp(&b.balance().amount.0).reverse());
|
|
||||||
|
|
||||||
// We always want to aggregate our UTXOs into a single UTXO in the name of simplicity
|
|
||||||
// We may have more UTXOs than will fit into a TX though
|
|
||||||
// We use the most valuable UTXOs to handle our current payments, and we return aggregation TXs
|
|
||||||
// for the rest of the inputs
|
|
||||||
// Since we do multiple aggregation TXs at once, this will execute in logarithmic time
|
|
||||||
let utxos = self.utxos.drain(..).collect::<Vec<_>>();
|
|
||||||
let mut utxo_chunks =
|
|
||||||
utxos.chunks(N::MAX_INPUTS).map(<[<N as Network>::Output]>::to_vec).collect::<Vec<_>>();
|
|
||||||
|
|
||||||
// Use the first chunk for any scheduled payments, since it has the most value
|
|
||||||
let utxos = utxo_chunks.remove(0);
|
|
||||||
|
|
||||||
// If the last chunk exists and only has one output, don't try aggregating it
|
|
||||||
// Set it to be restored to UTXO set
|
|
||||||
let mut to_restore = None;
|
|
||||||
if let Some(mut chunk) = utxo_chunks.pop() {
|
|
||||||
if chunk.len() == 1 {
|
|
||||||
to_restore = Some(chunk.pop().unwrap());
|
|
||||||
} else {
|
|
||||||
utxo_chunks.push(chunk);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
for chunk in utxo_chunks.drain(..) {
|
|
||||||
log::debug!("aggregating a chunk of {} inputs", chunk.len());
|
|
||||||
plans.push(Plan {
|
|
||||||
key: self.key,
|
|
||||||
inputs: chunk,
|
|
||||||
payments: vec![],
|
|
||||||
change: Some(N::change_address(key_for_any_change).unwrap()),
|
|
||||||
scheduler_addendum: (),
|
|
||||||
})
|
|
||||||
}
|
|
||||||
|
|
||||||
// We want to use all possible UTXOs for all possible payments
|
|
||||||
let mut balance = utxos.iter().map(|output| output.balance().amount.0).sum::<u64>();
|
|
||||||
|
|
||||||
// If we can't fulfill the next payment, we have encountered an instance of the UTXO
|
|
||||||
// availability problem
|
|
||||||
// This shows up in networks like Monero, where because we spent outputs, our change has yet to
|
|
||||||
// re-appear. Since it has yet to re-appear, we only operate with a balance which is a subset
|
|
||||||
// of our total balance
|
|
||||||
// Despite this, we may be ordered to fulfill a payment which is our total balance
|
|
||||||
// The solution is to wait for the temporarily unavailable change outputs to re-appear,
|
|
||||||
// granting us access to our full balance
|
|
||||||
let mut executing = vec![];
|
|
||||||
while !self.payments.is_empty() {
|
|
||||||
let amount = self.payments[0].balance.amount.0;
|
|
||||||
if balance.checked_sub(amount).is_some() {
|
|
||||||
balance -= amount;
|
|
||||||
executing.push(self.payments.pop_front().unwrap());
|
|
||||||
} else {
|
|
||||||
// Doesn't check if other payments would fit into the current batch as doing so may never
|
|
||||||
// let enough inputs become simultaneously availabile to enable handling of payments[0]
|
|
||||||
break;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
// Now that we have the list of payments we can successfully handle right now, create the TX
|
|
||||||
// for them
|
|
||||||
if !executing.is_empty() {
|
|
||||||
plans.push(self.execute(utxos, executing, key_for_any_change));
|
|
||||||
} else {
|
|
||||||
// If we don't have any payments to execute, save these UTXOs for later
|
|
||||||
self.utxos.extend(utxos);
|
|
||||||
}
|
|
||||||
|
|
||||||
// If we're instructed to force a spend, do so
|
|
||||||
// This is used when an old multisig is retiring and we want to always transfer outputs to the
|
|
||||||
// new one, regardless if we currently have payments
|
|
||||||
if force_spend && (!self.utxos.is_empty()) {
|
|
||||||
assert!(self.utxos.len() <= N::MAX_INPUTS);
|
|
||||||
plans.push(Plan {
|
|
||||||
key: self.key,
|
|
||||||
inputs: self.utxos.drain(..).collect::<Vec<_>>(),
|
|
||||||
payments: vec![],
|
|
||||||
change: Some(N::change_address(key_for_any_change).unwrap()),
|
|
||||||
scheduler_addendum: (),
|
|
||||||
});
|
|
||||||
}
|
|
||||||
|
|
||||||
// If there's a UTXO to restore, restore it
|
|
||||||
// This is done now as if there is a to_restore output, and it was inserted into self.utxos
|
|
||||||
// earlier, self.utxos.len() may become `N::MAX_INPUTS + 1`
|
|
||||||
// The prior block requires the len to be `<= N::MAX_INPUTS`
|
|
||||||
if let Some(to_restore) = to_restore {
|
|
||||||
self.utxos.push(to_restore);
|
|
||||||
}
|
|
||||||
|
|
||||||
txn.put(scheduler_key::<D, _>(&self.key), self.serialize());
|
|
||||||
|
|
||||||
log::info!(
|
|
||||||
"created {} plans containing {} payments to sign, with {} payments pending scheduling",
|
|
||||||
plans.len(),
|
|
||||||
payments_at_start - self.payments.len(),
|
|
||||||
self.payments.len(),
|
|
||||||
);
|
|
||||||
plans
|
|
||||||
}
|
|
||||||
|
|
||||||
pub fn consume_payments<D: Db>(&mut self, txn: &mut D::Transaction<'_>) -> Vec<Payment<N>> {
|
|
||||||
let res: Vec<_> = self.payments.drain(..).collect();
|
|
||||||
if !res.is_empty() {
|
|
||||||
txn.put(scheduler_key::<D, _>(&self.key), self.serialize());
|
|
||||||
}
|
|
||||||
res
|
|
||||||
}
|
|
||||||
|
|
||||||
// Note a branch output as having been created, with the amount it was actually created with,
|
|
||||||
// or not having been created due to being too small
|
|
||||||
pub fn created_output<D: Db>(
|
|
||||||
&mut self,
|
|
||||||
txn: &mut D::Transaction<'_>,
|
|
||||||
expected: u64,
|
|
||||||
actual: Option<u64>,
|
|
||||||
) {
|
|
||||||
log::debug!("output expected to have {} had {:?} after fees", expected, actual);
|
|
||||||
|
|
||||||
// Get the payments this output is expected to handle
|
|
||||||
let queued = self.queued_plans.get_mut(&expected).unwrap();
|
|
||||||
let mut payments = queued.pop_front().unwrap();
|
|
||||||
assert_eq!(expected, payments.iter().map(|payment| payment.balance.amount.0).sum::<u64>());
|
|
||||||
// If this was the last set of payments at this amount, remove it
|
|
||||||
if queued.is_empty() {
|
|
||||||
self.queued_plans.remove(&expected);
|
|
||||||
}
|
|
||||||
|
|
||||||
// If we didn't actually create this output, return, dropping the child payments
|
|
||||||
let Some(actual) = actual else { return };
|
|
||||||
|
|
||||||
// Amortize the fee amongst all payments underneath this branch
|
|
||||||
{
|
|
||||||
let mut to_amortize = actual - expected;
|
|
||||||
// If the payments are worth less than this fee we need to amortize, return, dropping them
|
|
||||||
if payments.iter().map(|payment| payment.balance.amount.0).sum::<u64>() < to_amortize {
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
while to_amortize != 0 {
|
|
||||||
let payments_len = u64::try_from(payments.len()).unwrap();
|
|
||||||
let per_payment = to_amortize / payments_len;
|
|
||||||
let mut overage = to_amortize % payments_len;
|
|
||||||
|
|
||||||
for payment in &mut payments {
|
|
||||||
let to_subtract = per_payment + overage;
|
|
||||||
// Only subtract the overage once
|
|
||||||
overage = 0;
|
|
||||||
|
|
||||||
let subtractable = payment.balance.amount.0.min(to_subtract);
|
|
||||||
to_amortize -= subtractable;
|
|
||||||
payment.balance.amount.0 -= subtractable;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
// Drop payments now below the dust threshold
|
|
||||||
let payments = payments
|
|
||||||
.into_iter()
|
|
||||||
.filter(|payment| payment.balance.amount.0 >= N::DUST)
|
|
||||||
.collect::<Vec<_>>();
|
|
||||||
// Sanity check this was done properly
|
|
||||||
assert!(actual >= payments.iter().map(|payment| payment.balance.amount.0).sum::<u64>());
|
|
||||||
|
|
||||||
// If there's no payments left, return
|
|
||||||
if payments.is_empty() {
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
|
|
||||||
self.plans.entry(actual).or_insert(VecDeque::new()).push_back(payments);
|
|
||||||
|
|
||||||
// TODO2: This shows how ridiculous the serialize function is
|
|
||||||
txn.put(scheduler_key::<D, _>(&self.key), self.serialize());
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
impl<N: UtxoNetwork<Scheduler = Self>> SchedulerTrait<N> for Scheduler<N> {
|
|
||||||
type Addendum = ();
|
|
||||||
|
|
||||||
/// Check if this Scheduler is empty.
|
|
||||||
fn empty(&self) -> bool {
|
|
||||||
Scheduler::empty(self)
|
|
||||||
}
|
|
||||||
|
|
||||||
/// Create a new Scheduler.
|
|
||||||
fn new<D: Db>(
|
|
||||||
txn: &mut D::Transaction<'_>,
|
|
||||||
key: <N::Curve as Ciphersuite>::G,
|
|
||||||
network: NetworkId,
|
|
||||||
) -> Self {
|
|
||||||
Scheduler::new::<D>(txn, key, network)
|
|
||||||
}
|
|
||||||
|
|
||||||
/// Load a Scheduler from the DB.
|
|
||||||
fn from_db<D: Db>(
|
|
||||||
db: &D,
|
|
||||||
key: <N::Curve as Ciphersuite>::G,
|
|
||||||
network: NetworkId,
|
|
||||||
) -> io::Result<Self> {
|
|
||||||
Scheduler::from_db::<D>(db, key, network)
|
|
||||||
}
|
|
||||||
|
|
||||||
/// Check if a branch is usable.
|
|
||||||
fn can_use_branch(&self, balance: Balance) -> bool {
|
|
||||||
Scheduler::can_use_branch(self, balance)
|
|
||||||
}
|
|
||||||
|
|
||||||
/// Schedule a series of outputs/payments.
|
|
||||||
fn schedule<D: Db>(
|
|
||||||
&mut self,
|
|
||||||
txn: &mut D::Transaction<'_>,
|
|
||||||
utxos: Vec<N::Output>,
|
|
||||||
payments: Vec<Payment<N>>,
|
|
||||||
key_for_any_change: <N::Curve as Ciphersuite>::G,
|
|
||||||
force_spend: bool,
|
|
||||||
) -> Vec<Plan<N>> {
|
|
||||||
Scheduler::schedule::<D>(self, txn, utxos, payments, key_for_any_change, force_spend)
|
|
||||||
}
|
|
||||||
|
|
||||||
/// Consume all payments still pending within this Scheduler, without scheduling them.
|
|
||||||
fn consume_payments<D: Db>(&mut self, txn: &mut D::Transaction<'_>) -> Vec<Payment<N>> {
|
|
||||||
Scheduler::consume_payments::<D>(self, txn)
|
|
||||||
}
|
|
||||||
|
|
||||||
/// Note a branch output as having been created, with the amount it was actually created with,
|
|
||||||
/// or not having been created due to being too small.
|
|
||||||
// TODO: Move this to Balance.
|
|
||||||
fn created_output<D: Db>(
|
|
||||||
&mut self,
|
|
||||||
txn: &mut D::Transaction<'_>,
|
|
||||||
expected: u64,
|
|
||||||
actual: Option<u64>,
|
|
||||||
) {
|
|
||||||
Scheduler::created_output::<D>(self, txn, expected, actual)
|
|
||||||
}
|
|
||||||
|
|
||||||
fn refund_plan<D: Db>(
|
|
||||||
&mut self,
|
|
||||||
_: &mut D::Transaction<'_>,
|
|
||||||
output: N::Output,
|
|
||||||
refund_to: N::Address,
|
|
||||||
) -> Plan<N> {
|
|
||||||
let output_id = output.id().as_ref().to_vec();
|
|
||||||
let res = Plan {
|
|
||||||
key: output.key(),
|
|
||||||
// Uses a payment as this will still be successfully sent due to fee amortization,
|
|
||||||
// and because change is currently always a Serai key
|
|
||||||
payments: vec![Payment { address: refund_to, data: None, balance: output.balance() }],
|
|
||||||
inputs: vec![output],
|
|
||||||
change: None,
|
|
||||||
scheduler_addendum: (),
|
|
||||||
};
|
|
||||||
log::info!("refund plan for {} has ID {}", hex::encode(output_id), hex::encode(res.id()));
|
|
||||||
res
|
|
||||||
}
|
|
||||||
|
|
||||||
fn shim_forward_plan(output: N::Output, to: <N::Curve as Ciphersuite>::G) -> Option<Plan<N>> {
|
|
||||||
Some(Plan {
|
|
||||||
key: output.key(),
|
|
||||||
payments: vec![Payment {
|
|
||||||
address: N::forward_address(to).unwrap(),
|
|
||||||
data: None,
|
|
||||||
balance: output.balance(),
|
|
||||||
}],
|
|
||||||
inputs: vec![output],
|
|
||||||
change: None,
|
|
||||||
scheduler_addendum: (),
|
|
||||||
})
|
|
||||||
}
|
|
||||||
|
|
||||||
fn forward_plan<D: Db>(
|
|
||||||
&mut self,
|
|
||||||
_: &mut D::Transaction<'_>,
|
|
||||||
output: N::Output,
|
|
||||||
to: <N::Curve as Ciphersuite>::G,
|
|
||||||
) -> Option<Plan<N>> {
|
|
||||||
assert_eq!(self.key, output.key());
|
|
||||||
// Call shim as shim returns the actual
|
|
||||||
Self::shim_forward_plan(output, to)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
@@ -1,212 +0,0 @@
|
|||||||
use std::io;
|
|
||||||
|
|
||||||
use scale::{Encode, Decode};
|
|
||||||
|
|
||||||
use transcript::{Transcript, RecommendedTranscript};
|
|
||||||
use ciphersuite::group::GroupEncoding;
|
|
||||||
use frost::curve::Ciphersuite;
|
|
||||||
|
|
||||||
use serai_client::primitives::Balance;
|
|
||||||
|
|
||||||
use crate::{
|
|
||||||
networks::{Output, Network},
|
|
||||||
multisigs::scheduler::{SchedulerAddendum, Scheduler},
|
|
||||||
};
|
|
||||||
|
|
||||||
#[derive(Clone, PartialEq, Eq, Debug)]
|
|
||||||
pub struct Payment<N: Network> {
|
|
||||||
pub address: N::Address,
|
|
||||||
pub data: Option<Vec<u8>>,
|
|
||||||
pub balance: Balance,
|
|
||||||
}
|
|
||||||
|
|
||||||
impl<N: Network> Payment<N> {
|
|
||||||
pub fn transcript<T: Transcript>(&self, transcript: &mut T) {
|
|
||||||
transcript.domain_separate(b"payment");
|
|
||||||
transcript.append_message(b"address", self.address.to_string().as_bytes());
|
|
||||||
if let Some(data) = self.data.as_ref() {
|
|
||||||
transcript.append_message(b"data", data);
|
|
||||||
}
|
|
||||||
transcript.append_message(b"coin", self.balance.coin.encode());
|
|
||||||
transcript.append_message(b"amount", self.balance.amount.0.to_le_bytes());
|
|
||||||
}
|
|
||||||
|
|
||||||
pub fn write<W: io::Write>(&self, writer: &mut W) -> io::Result<()> {
|
|
||||||
// TODO: Don't allow creating Payments with an Address which can't be serialized
|
|
||||||
let address: Vec<u8> = self
|
|
||||||
.address
|
|
||||||
.clone()
|
|
||||||
.try_into()
|
|
||||||
.map_err(|_| io::Error::other("address couldn't be serialized"))?;
|
|
||||||
writer.write_all(&u32::try_from(address.len()).unwrap().to_le_bytes())?;
|
|
||||||
writer.write_all(&address)?;
|
|
||||||
|
|
||||||
writer.write_all(&[u8::from(self.data.is_some())])?;
|
|
||||||
if let Some(data) = &self.data {
|
|
||||||
writer.write_all(&u32::try_from(data.len()).unwrap().to_le_bytes())?;
|
|
||||||
writer.write_all(data)?;
|
|
||||||
}
|
|
||||||
|
|
||||||
writer.write_all(&self.balance.encode())
|
|
||||||
}
|
|
||||||
|
|
||||||
pub fn read<R: io::Read>(reader: &mut R) -> io::Result<Self> {
|
|
||||||
let mut buf = [0; 4];
|
|
||||||
reader.read_exact(&mut buf)?;
|
|
||||||
let mut address = vec![0; usize::try_from(u32::from_le_bytes(buf)).unwrap()];
|
|
||||||
reader.read_exact(&mut address)?;
|
|
||||||
let address = N::Address::try_from(address).map_err(|_| io::Error::other("invalid address"))?;
|
|
||||||
|
|
||||||
let mut buf = [0; 1];
|
|
||||||
reader.read_exact(&mut buf)?;
|
|
||||||
let data = if buf[0] == 1 {
|
|
||||||
let mut buf = [0; 4];
|
|
||||||
reader.read_exact(&mut buf)?;
|
|
||||||
let mut data = vec![0; usize::try_from(u32::from_le_bytes(buf)).unwrap()];
|
|
||||||
reader.read_exact(&mut data)?;
|
|
||||||
Some(data)
|
|
||||||
} else {
|
|
||||||
None
|
|
||||||
};
|
|
||||||
|
|
||||||
let balance = Balance::decode(&mut scale::IoReader(reader))
|
|
||||||
.map_err(|_| io::Error::other("invalid balance"))?;
|
|
||||||
|
|
||||||
Ok(Payment { address, data, balance })
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
#[derive(Clone, PartialEq)]
|
|
||||||
pub struct Plan<N: Network> {
|
|
||||||
pub key: <N::Curve as Ciphersuite>::G,
|
|
||||||
pub inputs: Vec<N::Output>,
|
|
||||||
/// The payments this Plan is intended to create.
|
|
||||||
///
|
|
||||||
/// This should only contain payments leaving Serai. While it is acceptable for users to enter
|
|
||||||
/// Serai's address(es) as the payment address, as that'll be handled by anything which expects
|
|
||||||
/// certain properties, Serai as a system MUST NOT use payments for internal transfers. Doing
|
|
||||||
/// so will cause a reduction in their value by the TX fee/operating costs, creating an
|
|
||||||
/// incomplete transfer.
|
|
||||||
pub payments: Vec<Payment<N>>,
|
|
||||||
/// The change this Plan should use.
|
|
||||||
///
|
|
||||||
/// This MUST contain a Serai address. Operating costs may be deducted from the payments in this
|
|
||||||
/// Plan on the premise that the change address is Serai's, and accordingly, Serai will recoup
|
|
||||||
/// the operating costs.
|
|
||||||
//
|
|
||||||
// TODO: Consider moving to ::G?
|
|
||||||
pub change: Option<N::Address>,
|
|
||||||
/// The scheduler's additional data.
|
|
||||||
pub scheduler_addendum: <N::Scheduler as Scheduler<N>>::Addendum,
|
|
||||||
}
|
|
||||||
impl<N: Network> core::fmt::Debug for Plan<N> {
|
|
||||||
fn fmt(&self, fmt: &mut core::fmt::Formatter<'_>) -> Result<(), core::fmt::Error> {
|
|
||||||
fmt
|
|
||||||
.debug_struct("Plan")
|
|
||||||
.field("key", &hex::encode(self.key.to_bytes()))
|
|
||||||
.field("inputs", &self.inputs)
|
|
||||||
.field("payments", &self.payments)
|
|
||||||
.field("change", &self.change.as_ref().map(ToString::to_string))
|
|
||||||
.field("scheduler_addendum", &self.scheduler_addendum)
|
|
||||||
.finish()
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
impl<N: Network> Plan<N> {
|
|
||||||
pub fn transcript(&self) -> RecommendedTranscript {
|
|
||||||
let mut transcript = RecommendedTranscript::new(b"Serai Processor Plan ID");
|
|
||||||
transcript.domain_separate(b"meta");
|
|
||||||
transcript.append_message(b"network", N::ID);
|
|
||||||
transcript.append_message(b"key", self.key.to_bytes());
|
|
||||||
|
|
||||||
transcript.domain_separate(b"inputs");
|
|
||||||
for input in &self.inputs {
|
|
||||||
transcript.append_message(b"input", input.id());
|
|
||||||
}
|
|
||||||
|
|
||||||
transcript.domain_separate(b"payments");
|
|
||||||
for payment in &self.payments {
|
|
||||||
payment.transcript(&mut transcript);
|
|
||||||
}
|
|
||||||
|
|
||||||
if let Some(change) = &self.change {
|
|
||||||
transcript.append_message(b"change", change.to_string());
|
|
||||||
}
|
|
||||||
|
|
||||||
let mut addendum_bytes = vec![];
|
|
||||||
self.scheduler_addendum.write(&mut addendum_bytes).unwrap();
|
|
||||||
transcript.append_message(b"scheduler_addendum", addendum_bytes);
|
|
||||||
|
|
||||||
transcript
|
|
||||||
}
|
|
||||||
|
|
||||||
pub fn id(&self) -> [u8; 32] {
|
|
||||||
let challenge = self.transcript().challenge(b"id");
|
|
||||||
let mut res = [0; 32];
|
|
||||||
res.copy_from_slice(&challenge[.. 32]);
|
|
||||||
res
|
|
||||||
}
|
|
||||||
|
|
||||||
pub fn write<W: io::Write>(&self, writer: &mut W) -> io::Result<()> {
|
|
||||||
writer.write_all(self.key.to_bytes().as_ref())?;
|
|
||||||
|
|
||||||
writer.write_all(&u32::try_from(self.inputs.len()).unwrap().to_le_bytes())?;
|
|
||||||
for input in &self.inputs {
|
|
||||||
input.write(writer)?;
|
|
||||||
}
|
|
||||||
|
|
||||||
writer.write_all(&u32::try_from(self.payments.len()).unwrap().to_le_bytes())?;
|
|
||||||
for payment in &self.payments {
|
|
||||||
payment.write(writer)?;
|
|
||||||
}
|
|
||||||
|
|
||||||
// TODO: Have Plan construction fail if change cannot be serialized
|
|
||||||
let change = if let Some(change) = &self.change {
|
|
||||||
change.clone().try_into().map_err(|_| {
|
|
||||||
io::Error::other(format!(
|
|
||||||
"an address we said to use as change couldn't be converted to a Vec<u8>: {}",
|
|
||||||
change.to_string(),
|
|
||||||
))
|
|
||||||
})?
|
|
||||||
} else {
|
|
||||||
vec![]
|
|
||||||
};
|
|
||||||
assert!(serai_client::primitives::MAX_ADDRESS_LEN <= u8::MAX.into());
|
|
||||||
writer.write_all(&[u8::try_from(change.len()).unwrap()])?;
|
|
||||||
writer.write_all(&change)?;
|
|
||||||
self.scheduler_addendum.write(writer)
|
|
||||||
}
|
|
||||||
|
|
||||||
pub fn read<R: io::Read>(reader: &mut R) -> io::Result<Self> {
|
|
||||||
let key = N::Curve::read_G(reader)?;
|
|
||||||
|
|
||||||
let mut inputs = vec![];
|
|
||||||
let mut buf = [0; 4];
|
|
||||||
reader.read_exact(&mut buf)?;
|
|
||||||
for _ in 0 .. u32::from_le_bytes(buf) {
|
|
||||||
inputs.push(N::Output::read(reader)?);
|
|
||||||
}
|
|
||||||
|
|
||||||
let mut payments = vec![];
|
|
||||||
reader.read_exact(&mut buf)?;
|
|
||||||
for _ in 0 .. u32::from_le_bytes(buf) {
|
|
||||||
payments.push(Payment::<N>::read(reader)?);
|
|
||||||
}
|
|
||||||
|
|
||||||
let mut len = [0; 1];
|
|
||||||
reader.read_exact(&mut len)?;
|
|
||||||
let mut change = vec![0; usize::from(len[0])];
|
|
||||||
reader.read_exact(&mut change)?;
|
|
||||||
let change =
|
|
||||||
if change.is_empty() {
|
|
||||||
None
|
|
||||||
} else {
|
|
||||||
Some(N::Address::try_from(change).map_err(|_| {
|
|
||||||
io::Error::other("couldn't deserialize an Address serialized into a Plan")
|
|
||||||
})?)
|
|
||||||
};
|
|
||||||
|
|
||||||
let scheduler_addendum = <N::Scheduler as Scheduler<N>>::Addendum::read(reader)?;
|
|
||||||
Ok(Plan { key, inputs, payments, change, scheduler_addendum })
|
|
||||||
}
|
|
||||||
}
|
|
||||||
Reference in New Issue
Block a user