Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
51 changes: 51 additions & 0 deletions src/types.rs
Original file line number Diff line number Diff line change
Expand Up @@ -14,6 +14,8 @@ use chain::{ChainPosition, ConfirmationBlockTime};
use core::convert::AsRef;
use core::fmt;

use crate::collections::BTreeMap;

use bitcoin::transaction::{OutPoint, Sequence, TxOut};
use bitcoin::{psbt, Weight};

Expand Down Expand Up @@ -141,6 +143,55 @@ impl Utxo {
}
}

/// The finalization status for a single PSBT input.
#[derive(Debug, PartialEq)]
pub enum FinalizeInputOutcome {

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Would it make sense to add a Display impl for FinalizeInputOutcome so callers don't have to match on the variants themselves to produce an outcome that can be logged in user interfaces?

Copy link
Copy Markdown
Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Maybe, but I lean toward leaving this out for now. I was thinking of FinalizeInputOutcome as structured data that callers can match on and render however they want, especially for UI wording, while Debug is still available for basic logging. Happy to add Display if others think that belongs in the initial API though.

/// The input was already finalized before this call.
AlreadyFinalized,
/// The input was successfully finalized during this call.
Finalized,
/// The wallet could not derive a descriptor for the input.
MissingDescriptor,
/// The wallet found the descriptor but could not construct the input satisfaction.
CouldNotSatisfy(miniscript::Error),
}

impl FinalizeInputOutcome {
/// Whether the input is finalized after this call.
pub fn is_finalized(&self) -> bool {
matches!(self, Self::AlreadyFinalized | Self::Finalized)
}
}

/// The outcome of a PSBT finalization attempt.
#[derive(Debug, PartialEq)]
pub struct FinalizePsbtOutcome {
outcomes: BTreeMap<usize, FinalizeInputOutcome>,
}

impl FinalizePsbtOutcome {
pub(crate) fn new(outcomes: BTreeMap<usize, FinalizeInputOutcome>) -> Self {
Self { outcomes }
}

/// Whether all inputs are finalized after this call.
pub fn is_finalized(&self) -> bool {

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

If outcomes is empty (e.g. a PSBT with no inputs), is_finalized() returns true, since .all() on an empty iterator is true. Is this the intended behavior or worth documenting/guarding against explicitly?

Copy link
Copy Markdown
Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I think this is intended since is_finalized() is only answering whether any input failed finalization, so if there are no inputs there is nothing to fail. This also matches the old finalize_psbt behavior where finished started as true and only changed inside the input loop. But that's my take, open to further thoughts

self.outcomes
.values()
.all(FinalizeInputOutcome::is_finalized)
}

/// Borrow the per-input finalization outcomes.
pub fn outcomes(&self) -> &BTreeMap<usize, FinalizeInputOutcome> {
&self.outcomes
}

/// Consume the collection and return the per-input finalization outcomes.
pub fn into_outcomes(self) -> BTreeMap<usize, FinalizeInputOutcome> {
self.outcomes
}
}

/// Index out of bounds error.
#[derive(Debug, Clone, Copy, PartialEq, Eq)]
pub struct IndexOutOfBoundsError {
Expand Down
126 changes: 96 additions & 30 deletions src/wallet/mod.rs
Original file line number Diff line number Diff line change
Expand Up @@ -1917,23 +1917,71 @@ impl Wallet {
}
})
.collect::<HashMap<Txid, u32>>();
let current_height = sign_options
.assume_height
.unwrap_or_else(|| self.chain.tip().height());

Ok(self
.try_finalize_psbt_with(
psbt,
Some(current_height),
|_, input| {
confirmation_heights
.get(&input.previous_output.txid)
.copied()
},
true,
)?
.is_finalized())
}

/// Attempt to finalize each input of a PSBT and return per-input finalization results.
///
/// Use this method when you need to inspect why a specific input could not be finalized. Call
/// [`FinalizePsbtOutcome::is_finalized`] on the returned value to check whether all inputs are
/// finalized after the call.
///
/// Per-input finalization failures are reported as [`FinalizeInputOutcome`] values. This method
/// only returns `Err` when the PSBT is malformed, for example if its inputs are out of bounds.
///
/// Timelock satisfaction is evaluated from the PSBT transaction fields. This method does not
/// redact or clear output metadata.
pub fn try_finalize_psbt(
&self,
psbt: &mut Psbt,
) -> Result<FinalizePsbtOutcome, IndexOutOfBoundsError> {
self.try_finalize_psbt_with(psbt, None, |_, _| None, false)
}

fn try_finalize_psbt_with<F>(
&self,
psbt: &mut Psbt,
current_height: Option<u32>,
mut confirmation_height_for_input: F,
Comment thread
oleonardolima marked this conversation as resolved.
clear_output_derivations: bool,
Comment thread
oleonardolima marked this conversation as resolved.
) -> Result<FinalizePsbtOutcome, IndexOutOfBoundsError>
where
F: FnMut(usize, &bitcoin::TxIn) -> Option<u32>,
{
let tx = &psbt.unsigned_tx;
if psbt.inputs.len() < tx.input.len() {
return Err(IndexOutOfBoundsError::new(
psbt.inputs.len(),
psbt.inputs.len(),
));
}

let mut finished = true;
let mut outcomes = BTreeMap::new();

for (n, input) in tx.input.iter().enumerate() {

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

If psbt.inputs.len() < tx.input.len() but not 0, this loop will partially mutate earlier inputs before hitting the IndexOutOfBoundsError on a later one. Is it expected to have a partially-mutated PSBT on Err , or should length be validated up front before any mutation starts?

Copy link
Copy Markdown
Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Agree I think validating up front is cleaner here. 6afd006 adds a guard before the loop so a PSBT with missing input metadata returns IndexOutOfBoundsError before any inputs are mutated, plus a regression test for the partial mutation case.

let psbt_input = &psbt
.inputs
.get(n)
.ok_or(IndexOutOfBoundsError::new(n, psbt.inputs.len()))?;
if psbt_input.final_script_sig.is_some() || psbt_input.final_script_witness.is_some() {
outcomes.insert(n, FinalizeInputOutcome::AlreadyFinalized);
continue;
}
let confirmation_height = confirmation_heights
.get(&input.previous_output.txid)
.copied();
let current_height = sign_options
.assume_height
.unwrap_or_else(|| self.chain.tip().height());

// - Try to derive the descriptor by looking at the txout. If it's in our database, we
// know exactly which `keychain` to use, and which derivation index it is.
Expand All @@ -1953,48 +2001,66 @@ impl Wallet {
match desc {
Some(desc) => {
let mut tmp_input = bitcoin::TxIn::default();
match desc.satisfy(
&mut tmp_input,
(
PsbtInputSatisfier::new(psbt, n),
After::new(Some(current_height), false),
Older::new(Some(current_height), confirmation_height, false),
),
) {
let satisfy_result = if let Some(current_height) = current_height {
let confirmation_height = confirmation_height_for_input(n, input);
desc.satisfy(
&mut tmp_input,
(
PsbtInputSatisfier::new(psbt, n),
After::new(Some(current_height), false),
Older::new(Some(current_height), confirmation_height, false),
),
)
} else {
desc.satisfy(&mut tmp_input, PsbtInputSatisfier::new(psbt, n))
};

match satisfy_result {
Ok(_) => {
let length = psbt.inputs.len();
// Set the UTXO fields, final script_sig and witness
// and clear everything else.
let psbt_input = psbt
.inputs
.get_mut(n)
.ok_or(IndexOutOfBoundsError::new(n, length))?;
let original = mem::take(psbt_input);
psbt_input.non_witness_utxo = original.non_witness_utxo;
psbt_input.witness_utxo = original.witness_utxo;
if !tmp_input.script_sig.is_empty() {
psbt_input.final_script_sig = Some(tmp_input.script_sig);
}
if !tmp_input.witness.is_empty() {
psbt_input.final_script_witness = Some(tmp_input.witness);
}
let final_script_sig =
(!tmp_input.script_sig.is_empty()).then_some(tmp_input.script_sig);
let final_script_witness =
(!tmp_input.witness.is_empty()).then_some(tmp_input.witness);

// BIP174 finalization clears input metadata except UTXOs, final scripts,
// and opaque fields the finalizer does not understand.
*psbt_input = bitcoin::psbt::Input {
non_witness_utxo: original.non_witness_utxo,
witness_utxo: original.witness_utxo,
final_script_sig,
final_script_witness,
proprietary: original.proprietary,
unknown: original.unknown,
..Default::default()
};
outcomes.insert(n, FinalizeInputOutcome::Finalized);
}
Err(err) => {
outcomes.insert(n, FinalizeInputOutcome::CouldNotSatisfy(err));
}
Err(_) => finished = false,
}
}
None => finished = false,
None => {
outcomes.insert(n, FinalizeInputOutcome::MissingDescriptor);
}
}
}

// Clear derivation paths from outputs.
if finished {
let finalized = FinalizePsbtOutcome::new(outcomes);
if clear_output_derivations && finalized.is_finalized() {
for output in &mut psbt.outputs {
output.bip32_derivation.clear();
output.tap_key_origins.clear();
}
}

Ok(finished)
Ok(finalized)
}

/// Return the secp256k1 context used for all signing operations.
Expand Down
Loading