mirror of
https://github.com/serai-dex/serai.git
synced 2025-03-12 09:26:51 +00:00
Remove vast swaths of legacy code in the processor
This commit is contained in:
parent
6e9cb74022
commit
2da24506a2
6 changed files with 1 additions and 2276 deletions
|
@ -102,10 +102,6 @@ pub mod sign {
|
|||
Shares { id: SignId, shares: HashMap<Participant, Vec<u8>> },
|
||||
// Re-attempt a signing protocol.
|
||||
Reattempt { id: SignId },
|
||||
/* TODO
|
||||
// Completed a signing protocol already.
|
||||
Completed { session: Session, id: [u8; 32], tx: Vec<u8> },
|
||||
*/
|
||||
}
|
||||
|
||||
impl CoordinatorMessage {
|
||||
|
@ -118,7 +114,6 @@ pub mod sign {
|
|||
CoordinatorMessage::Preprocesses { id, .. } |
|
||||
CoordinatorMessage::Shares { id, .. } |
|
||||
CoordinatorMessage::Reattempt { id, .. } => id.session,
|
||||
// TODO CoordinatorMessage::Completed { session, .. } => *session,
|
||||
}
|
||||
}
|
||||
}
|
||||
|
@ -131,8 +126,6 @@ pub mod sign {
|
|||
Preprocesses { id: SignId, preprocesses: Vec<Vec<u8>> },
|
||||
// Signed shares for the specified signing protocol.
|
||||
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::Shares { id, .. } => (1, 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];
|
||||
|
@ -406,8 +394,6 @@ impl ProcessorMessage {
|
|||
// Unique since SignId
|
||||
sign::ProcessorMessage::Preprocesses { id, .. } => (1, 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];
|
||||
|
|
|
@ -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 })
|
||||
}
|
||||
}
|
Loading…
Reference in a new issue