Skip to content

Commit

Permalink
Allow selection of multiple utxos in both pad_alignment_output() and …
Browse files Browse the repository at this point in the history
…add_value().
  • Loading branch information
Greg Martin committed Mar 8, 2023
1 parent a4ee361 commit 045731d
Showing 1 changed file with 218 additions and 16 deletions.
234 changes: 218 additions & 16 deletions src/subcommand/wallet/transaction_builder.rs
Original file line number Diff line number Diff line change
Expand Up @@ -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
);
}
}
}

Expand All @@ -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;
}
}
}

Expand Down Expand Up @@ -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
Expand All @@ -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))
}
Expand Down Expand Up @@ -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),
);
}
}

0 comments on commit 045731d

Please sign in to comment.