Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Select multiple utxos #2303

Merged
merged 12 commits into from
Jul 30, 2023
Prev Previous commit
Next Next commit
more verbose but clearer logic
raphjaph committed Jul 29, 2023
commit 1e17800ff8a10b80048125b6b2f92e431039f7f8
85 changes: 51 additions & 34 deletions src/subcommand/wallet/transaction_builder.rs
Original file line number Diff line number Diff line change
@@ -38,7 +38,10 @@ use {
blockdata::{locktime::absolute::LockTime, witness::Witness},
Amount, ScriptBuf,
},
std::collections::{BTreeMap, BTreeSet},
std::{
cmp::{max, min},
collections::{BTreeMap, BTreeSet},
},
};

#[derive(Debug, PartialEq)]
@@ -267,6 +270,7 @@ impl TransactionBuilder {
);

let sat_offset = self.calculate_sat_offset();

if sat_offset == 0 {
tprintln!("outgoing is aligned");
} else {
@@ -297,13 +301,16 @@ impl TransactionBuilder {
.unwrap()
.script_pubkey()
.dust_value();

if self.outputs[0].1 >= dust_limit {
tprintln!("no padding needed");
} else {
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
let (utxo, size) = self.select_cardinal_utxo(dust_limit - self.outputs[0].1, true)?;

self.inputs.insert(0, utxo);
self.outputs[0].1 += size;

tprintln!(
"padded alignment output to {} with additional {size} sat input",
self.outputs[0].1
@@ -330,15 +337,21 @@ impl TransactionBuilder {
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(additional_fee)
.ok_or(Error::ValueOverflow)?;
let (utxo, value) = self.select_cardinal_utxo(needed, false)?; // prefer utxos that fill the needed amount

let (utxo, value) = self.select_cardinal_utxo(needed, false)?;

let benefit = value
.checked_sub(additional_fee)
.ok_or(Error::NotEnoughCardinalUtxos)?;

self.inputs.push(utxo);

self.outputs.last_mut().unwrap().1 += value;

if benefit > deficit {
tprintln!("added {value} sat input to cover {deficit} sat deficit");
deficit = Amount::ZERO;
@@ -660,16 +673,19 @@ impl TransactionBuilder {
panic!("Could not find outgoing sat in inputs");
}

/// Cardinal UTXOs are those that contain no inscriptions and can therefore
/// be used to pad transactions. Sometimes multiple of these UTXOs are needed
/// and depending on the context we want to select either ones above or
/// under (when trying to consolidate dust outputs) the target value.
fn select_cardinal_utxo(
&mut self,
target_value: Amount,
prefer_under: bool,
) -> Result<(OutPoint, Amount)> {
let mut found = None;
let mut best = Amount::ZERO;

if self.max_inputs.is_some() && self.inputs.len() == self.max_inputs.unwrap() {
return Err(Error::TooManyInputs(self.max_inputs.unwrap()));
if let Some(max_inputs) = self.max_inputs {
if self.inputs.len() == max_inputs {
return Err(Error::TooManyInputs(max_inputs));
}
}

tprintln!(
@@ -683,42 +699,43 @@ impl TransactionBuilder {
.map(|satpoint| satpoint.outpoint)
.collect::<BTreeSet<OutPoint>>();

let mut best_match = None;
for utxo in &self.utxos {
if inscribed_utxos.contains(utxo) {
continue;
}

let value = self.amounts[utxo];

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;
let current_value = self.amounts[utxo];

let (_, best_value) = match best_match {
Some(prev) => prev,
None => {
best_match = Some((*utxo, current_value));
(*utxo, current_value)
}
};

let abs_diff = |a: Amount, b: Amount| -> Amount { max(a, b) - min(a, b) };
let is_closer = abs_diff(current_value, target_value) < abs_diff(best_value, target_value);

let not_preference_but_closer = if prefer_under {
best_value > target_value && is_closer
} 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;
}
best_value < target_value && is_closer
};

let is_preference_and_closer = if prefer_under {
current_value <= target_value && is_closer
} else {
current_value >= target_value && is_closer
};

if is_preference_and_closer || not_preference_but_closer {
best_match = Some((*utxo, current_value))
}
}

let (utxo, value) = found.ok_or(Error::NotEnoughCardinalUtxos)?;
let (utxo, value) = best_match.ok_or(Error::NotEnoughCardinalUtxos)?;

self.utxos.remove(&utxo);
tprintln!("found cardinal worth {}", value);