From 045731d60d6a82ea6a6f7354c89e639322d576eb Mon Sep 17 00:00:00 2001 From: Greg Martin Date: Wed, 8 Mar 2023 16:07:44 +0000 Subject: [PATCH] Allow selection of multiple utxos in both pad_alignment_output() and add_value(). --- src/subcommand/wallet/transaction_builder.rs | 234 +++++++++++++++++-- 1 file changed, 218 insertions(+), 16 deletions(-) diff --git a/src/subcommand/wallet/transaction_builder.rs b/src/subcommand/wallet/transaction_builder.rs index aed340be39..0376b2aa00 100644 --- a/src/subcommand/wallet/transaction_builder.rs +++ b/src/subcommand/wallet/transaction_builder.rs @@ -283,13 +283,15 @@ impl TransactionBuilder { if self.outputs[0].1 >= dust_limit { tprintln!("no padding needed"); } else { - let (utxo, size) = self.select_cardinal_utxo(dust_limit - self.outputs[0].1)?; - self.inputs.insert(0, utxo); - self.outputs[0].1 += size; - tprintln!( - "padded alignment output to {} with additional {size} sat input", - self.outputs[0].1 - ); + while self.outputs[0].1 < dust_limit { + let (utxo, size) = self.select_cardinal_utxo(dust_limit - self.outputs[0].1, true)?; // prefer smaller utxos to tidy dust outputs + self.inputs.insert(0, utxo); + self.outputs[0].1 += size; + tprintln!( + "padded alignment output to {} with additional {size} sat input", + self.outputs[0].1 + ); + } } } @@ -308,15 +310,25 @@ impl TransactionBuilder { .checked_add(estimated_fee) .ok_or(Error::ValueOverflow)?; - if let Some(deficit) = total.checked_sub(self.outputs.last().unwrap().1) { - if deficit > Amount::ZERO { + if let Some(mut deficit) = total.checked_sub(self.outputs.last().unwrap().1) { + while deficit > Amount::ZERO { + let additional_fee = self.fee_rate.fee(Self::ADDITIONAL_INPUT_VBYTES); let needed = deficit - .checked_add(self.fee_rate.fee(Self::ADDITIONAL_INPUT_VBYTES)) + .checked_add(additional_fee) .ok_or(Error::ValueOverflow)?; - let (utxo, value) = self.select_cardinal_utxo(needed)?; + let (utxo, value) = self.select_cardinal_utxo(needed, false)?; // prefer utxos that fill the needed amount + let benefit = value + .checked_sub(additional_fee) + .ok_or(Error::NotEnoughCardinalUtxos)?; self.inputs.push(utxo); self.outputs.last_mut().unwrap().1 += value; - tprintln!("added {value} sat input to cover {deficit} sat deficit"); + if benefit > deficit { + tprintln!("added {value} sat input to cover {deficit} sat deficit"); + deficit = Amount::ZERO; + } else { + tprintln!("added {value} sat input to reduce {deficit} sat deficit by {benefit} sat"); + deficit -= benefit; + } } } @@ -632,8 +644,18 @@ impl TransactionBuilder { panic!("Could not find outgoing sat in inputs"); } - fn select_cardinal_utxo(&mut self, minimum_value: Amount) -> Result<(OutPoint, Amount)> { + fn select_cardinal_utxo( + &mut self, + target_value: Amount, + prefer_under: bool, + ) -> Result<(OutPoint, Amount)> { let mut found = None; + let mut best = Amount::ZERO; + + tprintln!( + "looking for {} cardinal worth {target_value}", + if prefer_under { "smaller" } else { "bigger" } + ); let inscribed_utxos = self .inscriptions @@ -648,15 +670,38 @@ impl TransactionBuilder { let value = self.amounts[utxo]; - if value >= minimum_value { - found = Some((*utxo, value)); - break; + if prefer_under { + // prefer an output smaller than the target over one bigger than it + if best == Amount::ZERO { + found = Some((*utxo, value)); + best = value; + } else if best <= target_value { + if value <= target_value && value > best { + found = Some((*utxo, value)); + best = value; + } + } else if value <= target_value || value < best { + found = Some((*utxo, value)); + best = value; + } + } else { + // prefer an output bigger than the target over one smaller than it + if best >= target_value { + if value >= target_value && value < best { + found = Some((*utxo, value)); + best = value; + } + } else if value >= target_value || value > best { + found = Some((*utxo, value)); + best = value; + } } } let (utxo, value) = found.ok_or(Error::NotEnoughCardinalUtxos)?; self.utxos.remove(&utxo); + tprintln!("found cardinal worth {}", value); Ok((utxo, value)) } @@ -1618,4 +1663,161 @@ mod tests { }), ); } + + #[test] + fn select_outgoing_can_select_multiple_utxos() { + let mut utxos = vec![ + (outpoint(2), Amount::from_sat(3_006)), // 2. biggest utxo is selected 2nd leaving us needing 4206 more + (outpoint(1), Amount::from_sat(3_003)), // 1. satpoint is selected 1st leaving us needing 7154 more + (outpoint(5), Amount::from_sat(3_004)), + (outpoint(4), Amount::from_sat(3_001)), // 4. smallest utxo >= 1259 is selected 4th, filling deficit + (outpoint(3), Amount::from_sat(3_005)), // 3. next biggest utxo is selected 3rd leaving us needing 1259 more + (outpoint(6), Amount::from_sat(3_002)), + ]; + + let tx_builder = TransactionBuilder::new( + satpoint(1, 0), + BTreeMap::new(), + utxos.clone().into_iter().collect(), + recipient(), + [change(0), change(1)], + FeeRate::try_from(1.0).unwrap(), + Target::Value(Amount::from_sat(10_000)), + ) + .unwrap() + .select_outgoing() + .unwrap() + .add_value() + .unwrap(); + + utxos.remove(4); + utxos.remove(3); + utxos.remove(1); + utxos.remove(0); + assert_eq!( + tx_builder.utxos, + utxos.iter().map(|(outpoint, _ranges)| *outpoint).collect() + ); + assert_eq!( + tx_builder.inputs, + [outpoint(1), outpoint(2), outpoint(3), outpoint(4)] + ); // value inputs are pushed at the end + assert_eq!( + tx_builder.outputs, + [(recipient(), Amount::from_sat(3_003 + 3_006 + 3_005 + 3_001))] + ) + } + + #[test] + fn pad_alignment_output_can_select_multiple_utxos() { + let mut utxos = vec![ + (outpoint(4), Amount::from_sat(101)), // 4. smallest utxo >= 84 is selected 4th, filling deficit + (outpoint(1), Amount::from_sat(20_000)), // 1. satpoint is selected 1st leaving deficit 293 + (outpoint(2), Amount::from_sat(105)), // 2. biggest utxo <= 293 is selected 2nd leaving deficit 188 + (outpoint(5), Amount::from_sat(103)), + (outpoint(6), Amount::from_sat(10_000)), + (outpoint(3), Amount::from_sat(104)), // 3. biggest utxo <= 188 is selected 3rd leaving deficit 84 + (outpoint(7), Amount::from_sat(102)), + ]; + + let tx_builder = TransactionBuilder::new( + satpoint(1, 1), + BTreeMap::new(), + utxos.clone().into_iter().collect(), + recipient(), + [change(0), change(1)], + FeeRate::try_from(1.0).unwrap(), + Target::Value(Amount::from_sat(10_000)), + ) + .unwrap() + .select_outgoing() + .unwrap() + .align_outgoing() + .pad_alignment_output() + .unwrap(); + + utxos.remove(5); + utxos.remove(2); + utxos.remove(1); + utxos.remove(0); + assert_eq!( + tx_builder.utxos, + utxos.iter().map(|(outpoint, _ranges)| *outpoint).collect() + ); + assert_eq!( + tx_builder.inputs, + [outpoint(4), outpoint(3), outpoint(2), outpoint(1)] + ); // padding inputs are inserted at the start + assert_eq!( + tx_builder.outputs, + [ + (change(1), Amount::from_sat(101 + 104 + 105 + 1)), + (recipient(), Amount::from_sat(19_999)) + ] + ) + } + + fn select_cardinal_utxo_prefer_under_helper( + target_value: Amount, + prefer_under: bool, + expected_value: Amount, + ) { + let utxos = vec![ + (outpoint(4), Amount::from_sat(101)), + (outpoint(1), Amount::from_sat(20_000)), + (outpoint(2), Amount::from_sat(105)), + (outpoint(5), Amount::from_sat(103)), + (outpoint(6), Amount::from_sat(10_000)), + (outpoint(3), Amount::from_sat(104)), + (outpoint(7), Amount::from_sat(102)), + ]; + + let mut tx_builder = TransactionBuilder::new( + satpoint(0, 0), + BTreeMap::new(), + utxos.into_iter().collect(), + recipient(), + [change(0), change(1)], + FeeRate::try_from(1.0).unwrap(), + Target::Value(Amount::from_sat(10_000)), + ) + .unwrap(); + + assert_eq!( + tx_builder + .select_cardinal_utxo(target_value, prefer_under) + .unwrap() + .1, + expected_value + ); + } + + #[test] + fn select_cardinal_utxo_prefer_under() { + // select biggest utxo <= 104 + select_cardinal_utxo_prefer_under_helper(Amount::from_sat(104), true, Amount::from_sat(104)); + + // select biggest utxo <= 1_000 + select_cardinal_utxo_prefer_under_helper(Amount::from_sat(1_000), true, Amount::from_sat(105)); + + // select biggest utxo <= 10, else smallest > 10 + select_cardinal_utxo_prefer_under_helper(Amount::from_sat(10), true, Amount::from_sat(101)); + + // select smallest utxo >= 104 + select_cardinal_utxo_prefer_under_helper(Amount::from_sat(104), false, Amount::from_sat(104)); + + // select smallest utxo >= 1_000 + select_cardinal_utxo_prefer_under_helper( + Amount::from_sat(1000), + false, + Amount::from_sat(10_000), + ); + + // select smallest utxo >= 100_000, else biggest < 100_000 + select_cardinal_utxo_prefer_under_helper( + Amount::from_sat(100_000), + false, + Amount::from_sat(20_000), + ); + } }