diff --git a/processor/src/wallet.rs b/processor/src/wallet.rs index f31b8cb5..5e0928fc 100644 --- a/processor/src/wallet.rs +++ b/processor/src/wallet.rs @@ -100,6 +100,101 @@ impl CoinDb for MemCoinDb { } } +fn select_inputs(inputs: &mut Vec) -> (Vec, u64) { + // Sort to ensure determinism. Inefficient, yet produces the most legible code to be optimized + // later + inputs.sort_by(|a, b| a.amount().cmp(&b.amount())); + + // Select the maximum amount of outputs possible + let res = inputs.split_off(inputs.len() - C::MAX_INPUTS.min(inputs.len())); + // Calculate their sum value, minus the fee needed to spend them + let sum = res.iter().map(|input| input.amount()).sum(); + // sum -= C::MAX_FEE; // TODO + (res, sum) +} + +fn select_outputs( + payments: &mut Vec<(C::Address, u64)>, + value: &mut u64 +) -> Vec<(C::Address, u64)> { + // Prioritize large payments which will most efficiently use large inputs + payments.sort_by(|a, b| a.1.cmp(&b.1)); + + // Grab the payments this will successfully fund + let mut outputs = vec![]; + let mut p = payments.len(); + while p != 0 { + p -= 1; + if *value >= payments[p].1 { + *value -= payments[p].1; + // Swap remove will either pop the tail or insert an element that wouldn't fit, making it + // always safe to move past + outputs.push(payments.swap_remove(p)); + } + // Doesn't break in this else case as a smaller payment may still fit + } + + outputs +} + +// Optimizes on the expectation selected/inputs are sorted from lowest value to highest +fn refine_inputs( + selected: &mut Vec, + inputs: &mut Vec, + mut remaining: u64 +) { + // Drop unused inputs + let mut s = 0; + while remaining > selected[s].amount() { + remaining -= selected[s].amount(); + s += 1; + } + // Add them back to the inputs pool + inputs.extend(selected.drain(.. s)); + + // Replace large inputs with smaller ones + for s in (0 .. selected.len()).rev() { + for i in 0 .. inputs.len() { + // Doesn't break due to inputs no longer being sorted + // This could be made faster if we prioritized small input usage over transaction size/fees + // TODO: Consider. This would implicitly consolidate inputs which would be advantageous + if selected[s].amount() < inputs[i].amount() { + continue; + } + + // If we can successfully replace this input, do so + let diff = selected[s].amount() - inputs[i].amount(); + if remaining > diff { + remaining -= diff; + + let old = selected[s].clone(); + selected[s] = inputs[i].clone(); + inputs[i] = old; + } + } + } +} + +fn select_inputs_outputs( + inputs: &mut Vec, + outputs: &mut Vec<(C::Address, u64)> +) -> (Vec, Vec<(C::Address, u64)>) { + if inputs.len() == 0 { + return (vec![], vec![]); + } + + let (mut selected, mut value) = select_inputs::(inputs); + + let outputs = select_outputs::(outputs, &mut value); + if outputs.len() == 0 { + inputs.extend(selected); + return (vec![], vec![]); + } + + refine_inputs::(&mut selected, inputs, value); + (selected, outputs) +} + pub struct Wallet { db: D, coin: C, @@ -196,53 +291,20 @@ impl Wallet { // Payments is the first set of TXs in the schedule // As each payment re-appears, let mut payments = schedule[payment] where the only input is // the source payment - // let (mut payments, schedule) = payments; + // let (mut payments, schedule) = schedule(payments); let mut payments = payments; - payments.sort_by(|a, b| a.1.cmp(&b.1).reverse()); let mut txs = vec![]; for (keys, outputs) in self.keys.iter_mut() { - // Select the highest value outputs to minimize the amount of inputs needed - outputs.sort_by(|a, b| a.amount().cmp(&b.amount()).reverse()); - while outputs.len() != 0 { - // Select the maximum amount of outputs possible - let mut input_bound = C::MAX_INPUTS.min(outputs.len()); - - // Calculate their sum value, minus the fee needed to spend them - let mut sum = outputs[0 .. input_bound].iter().map(|input| input.amount()).sum::(); - // sum -= C::MAX_FEE; // TODO - - // Grab the payments this will successfully fund - let mut these_payments = vec![]; - let mut p = 0; - while p < payments.len() { - if sum >= payments[p].1 { - sum -= payments[p].1; - these_payments.push(payments.remove(p)); - } else { - // Doesn't break in this else case as a smaller payment may still fit - p += 1; - } - } - - // Move to the next set of keys if none of these outputs remain significant - if these_payments.len() == 0 { + let (inputs, outputs) = select_inputs_outputs::(outputs, &mut payments); + // If we can no longer process any payments, move to the next set of keys + if outputs.len() == 0 { + debug_assert_eq!(inputs.len(), 0); break; } - // Drop any uneeded inputs - while sum > outputs[input_bound - 1].amount() { - sum -= outputs[input_bound - 1].amount(); - input_bound -= 1; - } - - // TODO: Replace any high value inputs with low value inputs, if we can - - // We now have a minimal effective outputs/payments set - // Take ownership while removing these candidates from the provided list - let inputs = outputs.drain(.. input_bound).collect(); - + // Create the transcript for this transaction let mut transcript = Transcript::new(b"Serai Processor Wallet Send"); transcript.append_message( b"canonical_height", @@ -256,12 +318,13 @@ impl Wallet { b"index", &u64::try_from(txs.len()).unwrap().to_le_bytes() ); + let tx = self.coin.prepare_send( keys.clone(), transcript, acknowledged_height, inputs, - &these_payments + &outputs ).await?; // self.db.save_tx(tx) // TODO txs.push(tx);