From 6afd006737e3cc689db177617f9626b0055436d1 Mon Sep 17 00:00:00 2001 From: Matthew Date: Mon, 6 Apr 2026 08:41:59 -0500 Subject: [PATCH 1/5] feat: expose detailed try_finalize_psbt outcomes --- src/types.rs | 51 +++++++++++ src/wallet/mod.rs | 90 +++++++++++++++---- tests/wallet.rs | 216 +++++++++++++++++++++++++++++++++++++++++++++- 3 files changed, 337 insertions(+), 20 deletions(-) diff --git a/src/types.rs b/src/types.rs index 28d7e1f4..b0e5398e 100644 --- a/src/types.rs +++ b/src/types.rs @@ -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}; @@ -141,6 +143,55 @@ impl Utxo { } } +/// The finalization status for a single PSBT input. +#[derive(Debug, PartialEq)] +pub enum FinalizeInputOutcome { + /// 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, +} + +impl FinalizePsbtOutcome { + pub(crate) fn new(outcomes: BTreeMap) -> Self { + Self { outcomes } + } + + /// Whether all inputs are finalized after this call. + pub fn is_finalized(&self) -> bool { + self.outcomes + .values() + .all(FinalizeInputOutcome::is_finalized) + } + + /// Borrow the per-input finalization outcomes. + pub fn outcomes(&self) -> &BTreeMap { + &self.outcomes + } + + /// Consume the collection and return the per-input finalization outcomes. + pub fn into_outcomes(self) -> BTreeMap { + self.outcomes + } +} + /// Index out of bounds error. #[derive(Debug, Clone, Copy, PartialEq, Eq)] pub struct IndexOutOfBoundsError { diff --git a/src/wallet/mod.rs b/src/wallet/mod.rs index 60d1ccef..577fa2e4 100644 --- a/src/wallet/mod.rs +++ b/src/wallet/mod.rs @@ -1917,8 +1917,51 @@ impl Wallet { } }) .collect::>(); + let current_height = sign_options + .assume_height + .unwrap_or_else(|| self.chain.tip().height()); + + Ok(self + .try_finalize_psbt_with(psbt, |_, input| { + Some(( + current_height, + confirmation_heights + .get(&input.previous_output.txid) + .copied(), + )) + })? + .is_finalized()) + } + + /// Finalize a PSBT and return per-input finalization results. Use this method when you need to + /// inspect why a specific input could not be finalized. + /// + /// The method should only return `Err` when the PSBT is malformed, for example if its inputs + /// are out of bounds. + pub fn try_finalize_psbt( + &self, + psbt: &mut Psbt, + ) -> Result { + self.try_finalize_psbt_with(psbt, |_, _| None) + } + + fn try_finalize_psbt_with( + &self, + psbt: &mut Psbt, + mut wallet_timelocks: F, + ) -> Result + where + F: FnMut(usize, &bitcoin::TxIn) -> Option<(u32, Option)>, + { + 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() { let psbt_input = &psbt @@ -1926,14 +1969,9 @@ impl Wallet { .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. @@ -1953,14 +1991,22 @@ 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, confirmation_height)) = + wallet_timelocks(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 @@ -1978,23 +2024,29 @@ impl Wallet { if !tmp_input.witness.is_empty() { psbt_input.final_script_witness = Some(tmp_input.witness); } + 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 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. diff --git a/tests/wallet.rs b/tests/wallet.rs index 47afa502..fae2a04f 100644 --- a/tests/wallet.rs +++ b/tests/wallet.rs @@ -10,7 +10,10 @@ use bdk_wallet::psbt::PsbtUtils; use bdk_wallet::signer::{SignOptions, SignerError, SignersContainer}; use bdk_wallet::test_utils::*; use bdk_wallet::KeychainKind; -use bdk_wallet::{AddressInfo, Balance, PersistedWallet, Update, Wallet, WalletTx}; +use bdk_wallet::{ + AddressInfo, Balance, FinalizeInputOutcome, IndexOutOfBoundsError, PersistedWallet, Update, + Wallet, WalletTx, +}; use bitcoin::constants::COINBASE_MATURITY; use bitcoin::hashes::Hash; use bitcoin::script::PushBytesBuf; @@ -1606,6 +1609,217 @@ fn test_try_finalize_sign_option() { } } +#[test] +fn test_try_finalize_psbt_outcomes() { + { + let (mut wallet, _) = get_funded_wallet_single(get_test_wpkh()); + let addr = wallet.next_unused_address(KeychainKind::External); + let mut builder = wallet.build_tx(); + builder.drain_to(addr.script_pubkey()).drain_wallet(); + let mut psbt = builder.finish().unwrap(); + + let is_final = wallet + .sign( + &mut psbt, + SignOptions { + try_finalize: false, + ..Default::default() + }, + ) + .unwrap(); + assert!(!is_final); + assert!( + psbt.outputs + .iter() + .any(|output| !output.bip32_derivation.is_empty()), + "expected wallet-owned outputs to retain derivation data before finalization" + ); + + let finalized = wallet.try_finalize_psbt(&mut psbt).unwrap(); + + assert!(finalized.is_finalized()); + assert_matches!( + finalized.outcomes().get(&0), + Some(FinalizeInputOutcome::Finalized) + ); + assert!( + psbt.inputs[0].final_script_sig.is_some() + || psbt.inputs[0].final_script_witness.is_some() + ); + assert!(psbt + .outputs + .iter() + .all(|output| output.bip32_derivation.is_empty())); + + let finalized = wallet.try_finalize_psbt(&mut psbt).unwrap(); + + assert!(finalized.is_finalized()); + assert_matches!( + finalized.outcomes().get(&0), + Some(FinalizeInputOutcome::AlreadyFinalized) + ); + } + + { + let (mut wallet, _) = get_funded_wallet_single(get_test_wpkh()); + let addr = wallet.next_unused_address(KeychainKind::External); + let mut builder = wallet.build_tx(); + builder.add_recipient(addr.script_pubkey(), Amount::from_sat(25_000)); + let mut psbt = builder.finish().unwrap(); + + let dud_input = bitcoin::psbt::Input { + witness_utxo: Some(TxOut { + value: Amount::from_sat(100_000), + script_pubkey: miniscript::Descriptor::::from_str( + "wpkh(025476c2e83188368da1ff3e292e7acafcdb3566bb0ad253f62fc70f07aeee6357)", + ) + .unwrap() + .script_pubkey(), + }), + ..Default::default() + }; + + psbt.inputs.push(dud_input); + psbt.unsigned_tx.input.push(bitcoin::TxIn::default()); + + let finalized = wallet.try_finalize_psbt(&mut psbt).unwrap(); + + assert!(!finalized.is_finalized()); + assert_matches!( + finalized.outcomes().get(&0), + Some(FinalizeInputOutcome::CouldNotSatisfy( + bdk_wallet::miniscript::Error::MissingSig(_) + )) + ); + assert_matches!( + finalized.outcomes().get(&1), + Some(FinalizeInputOutcome::MissingDescriptor) + ); + } +} + +#[test] +fn test_try_finalize_psbt_returns_index_out_of_bounds_for_malformed_psbt() { + { + let (mut wallet, _) = get_funded_wallet_single(get_test_wpkh()); + let addr = wallet.next_unused_address(KeychainKind::External); + let mut builder = wallet.build_tx(); + builder.drain_to(addr.script_pubkey()).drain_wallet(); + let mut psbt = builder.finish().unwrap(); + + psbt.inputs.clear(); + + let err = wallet.try_finalize_psbt(&mut psbt).unwrap_err(); + + assert_eq!(err, IndexOutOfBoundsError::new(0, 0)); + } + + { + let (mut wallet, _) = get_funded_wallet_single(get_test_wpkh()); + let addr = wallet.next_unused_address(KeychainKind::External); + let mut builder = wallet.build_tx(); + builder.drain_to(addr.script_pubkey()).drain_wallet(); + let mut psbt = builder.finish().unwrap(); + + wallet + .sign( + &mut psbt, + SignOptions { + try_finalize: false, + ..Default::default() + }, + ) + .unwrap(); + assert!(psbt.inputs[0].final_script_sig.is_none()); + assert!(psbt.inputs[0].final_script_witness.is_none()); + + psbt.unsigned_tx + .input + .push(psbt.unsigned_tx.input[0].clone()); + + let err = wallet.try_finalize_psbt(&mut psbt).unwrap_err(); + + assert_eq!(err, IndexOutOfBoundsError::new(1, 1)); + assert!(psbt.inputs[0].final_script_sig.is_none()); + assert!(psbt.inputs[0].final_script_witness.is_none()); + } +} + +#[test] +fn test_try_finalize_psbt_uses_psbt_timelocks() { + { + let (mut wallet, _) = get_funded_wallet_single(get_test_single_sig_cltv()); + let addr = wallet.next_unused_address(KeychainKind::External); + let mut builder = wallet.build_tx(); + builder.add_recipient(addr.script_pubkey(), Amount::from_sat(25_000)); + let mut psbt = builder.finish().unwrap(); + + wallet + .sign( + &mut psbt, + SignOptions { + try_finalize: false, + ..Default::default() + }, + ) + .unwrap(); + + let mut valid_psbt = psbt.clone(); + assert!(wallet + .try_finalize_psbt(&mut valid_psbt) + .unwrap() + .is_finalized()); + + psbt.unsigned_tx.lock_time = absolute::LockTime::from_height(0).unwrap(); + + let finalized = wallet.try_finalize_psbt(&mut psbt).unwrap(); + + assert!(!finalized.is_finalized()); + assert_matches!( + finalized.outcomes().get(&0), + Some(FinalizeInputOutcome::CouldNotSatisfy(_)) + ); + assert!(psbt.inputs[0].final_script_sig.is_none()); + assert!(psbt.inputs[0].final_script_witness.is_none()); + } + + { + let (mut wallet, _) = get_funded_wallet_single(get_test_single_sig_csv()); + let addr = wallet.next_unused_address(KeychainKind::External); + let mut builder = wallet.build_tx(); + builder.add_recipient(addr.script_pubkey(), Amount::from_sat(25_000)); + let mut psbt = builder.finish().unwrap(); + + wallet + .sign( + &mut psbt, + SignOptions { + try_finalize: false, + ..Default::default() + }, + ) + .unwrap(); + + let mut valid_psbt = psbt.clone(); + assert!(wallet + .try_finalize_psbt(&mut valid_psbt) + .unwrap() + .is_finalized()); + + psbt.unsigned_tx.input[0].sequence = Sequence::MAX; + + let finalized = wallet.try_finalize_psbt(&mut psbt).unwrap(); + + assert!(!finalized.is_finalized()); + assert_matches!( + finalized.outcomes().get(&0), + Some(FinalizeInputOutcome::CouldNotSatisfy(_)) + ); + assert!(psbt.inputs[0].final_script_sig.is_none()); + assert!(psbt.inputs[0].final_script_witness.is_none()); + } +} + #[test] fn test_taproot_try_finalize_sign_option() { let (mut wallet, _) = get_funded_wallet_single(get_test_tr_with_taptree()); From 50a1a0f55ce5a6e1d15ef7dc2515807fd69ba7a3 Mon Sep 17 00:00:00 2001 From: Matthew Date: Wed, 10 Jun 2026 14:25:40 -0500 Subject: [PATCH 2/5] fix: preserve opaque psbt input fields on finalize --- src/wallet/mod.rs | 26 ++++++++++++++---------- tests/wallet.rs | 50 +++++++++++++++++++++++++++++++++++++++++++++++ 2 files changed, 66 insertions(+), 10 deletions(-) diff --git a/src/wallet/mod.rs b/src/wallet/mod.rs index 577fa2e4..a4e3c183 100644 --- a/src/wallet/mod.rs +++ b/src/wallet/mod.rs @@ -2009,21 +2009,27 @@ impl Wallet { 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) => { diff --git a/tests/wallet.rs b/tests/wallet.rs index fae2a04f..ba7a073e 100644 --- a/tests/wallet.rs +++ b/tests/wallet.rs @@ -1698,6 +1698,56 @@ fn test_try_finalize_psbt_outcomes() { } } +#[test] +fn test_try_finalize_psbt_preserves_opaque_input_fields() { + let (mut wallet, _) = get_funded_wallet_single(get_test_wpkh()); + let addr = wallet.next_unused_address(KeychainKind::External); + let mut builder = wallet.build_tx(); + builder.drain_to(addr.script_pubkey()).drain_wallet(); + let mut psbt = builder.finish().unwrap(); + + wallet + .sign( + &mut psbt, + SignOptions { + try_finalize: false, + ..Default::default() + }, + ) + .unwrap(); + + let proprietary_key = bitcoin::psbt::raw::ProprietaryKey { + prefix: b"bdk-test".to_vec(), + subtype: 0, + key: vec![1, 2, 3], + }; + let proprietary_value = vec![4, 5, 6]; + let unknown_key = bitcoin::psbt::raw::Key { + type_value: 0x42, + key: vec![7, 8, 9], + }; + let unknown_value = vec![10, 11, 12]; + + psbt.inputs[0] + .proprietary + .insert(proprietary_key.clone(), proprietary_value.clone()); + psbt.inputs[0] + .unknown + .insert(unknown_key.clone(), unknown_value.clone()); + + let finalized = wallet.try_finalize_psbt(&mut psbt).unwrap(); + + assert!(finalized.is_finalized()); + assert_eq!( + psbt.inputs[0].proprietary.get(&proprietary_key), + Some(&proprietary_value) + ); + assert_eq!( + psbt.inputs[0].unknown.get(&unknown_key), + Some(&unknown_value) + ); +} + #[test] fn test_try_finalize_psbt_returns_index_out_of_bounds_for_malformed_psbt() { { From 7f8cbcaf394cd0a1c1c934203c3298e000092d47 Mon Sep 17 00:00:00 2001 From: Matthew Date: Wed, 10 Jun 2026 14:31:29 -0500 Subject: [PATCH 3/5] fix: preserve output metadata in try_finalize_psbt --- src/wallet/mod.rs | 26 +++++++++++++++----------- tests/wallet.rs | 20 ++++++++++++++------ 2 files changed, 29 insertions(+), 17 deletions(-) diff --git a/src/wallet/mod.rs b/src/wallet/mod.rs index a4e3c183..4525bed1 100644 --- a/src/wallet/mod.rs +++ b/src/wallet/mod.rs @@ -1922,14 +1922,18 @@ impl Wallet { .unwrap_or_else(|| self.chain.tip().height()); Ok(self - .try_finalize_psbt_with(psbt, |_, input| { - Some(( - current_height, - confirmation_heights - .get(&input.previous_output.txid) - .copied(), - )) - })? + .try_finalize_psbt_with( + psbt, + |_, input| { + Some(( + current_height, + confirmation_heights + .get(&input.previous_output.txid) + .copied(), + )) + }, + true, + )? .is_finalized()) } @@ -1942,13 +1946,14 @@ impl Wallet { &self, psbt: &mut Psbt, ) -> Result { - self.try_finalize_psbt_with(psbt, |_, _| None) + self.try_finalize_psbt_with(psbt, |_, _| None, false) } fn try_finalize_psbt_with( &self, psbt: &mut Psbt, mut wallet_timelocks: F, + clear_output_derivations: bool, ) -> Result where F: FnMut(usize, &bitcoin::TxIn) -> Option<(u32, Option)>, @@ -2043,9 +2048,8 @@ impl Wallet { } } - // Clear derivation paths from outputs. let finalized = FinalizePsbtOutcome::new(outcomes); - if finalized.is_finalized() { + if clear_output_derivations && finalized.is_finalized() { for output in &mut psbt.outputs { output.bip32_derivation.clear(); output.tap_key_origins.clear(); diff --git a/tests/wallet.rs b/tests/wallet.rs index ba7a073e..0204cfd5 100644 --- a/tests/wallet.rs +++ b/tests/wallet.rs @@ -1628,10 +1628,15 @@ fn test_try_finalize_psbt_outcomes() { ) .unwrap(); assert!(!is_final); + let output_bip32_derivations = psbt + .outputs + .iter() + .map(|output| output.bip32_derivation.clone()) + .collect::>(); assert!( - psbt.outputs + output_bip32_derivations .iter() - .any(|output| !output.bip32_derivation.is_empty()), + .any(|derivation| !derivation.is_empty()), "expected wallet-owned outputs to retain derivation data before finalization" ); @@ -1646,10 +1651,13 @@ fn test_try_finalize_psbt_outcomes() { psbt.inputs[0].final_script_sig.is_some() || psbt.inputs[0].final_script_witness.is_some() ); - assert!(psbt - .outputs - .iter() - .all(|output| output.bip32_derivation.is_empty())); + assert_eq!( + psbt.outputs + .iter() + .map(|output| output.bip32_derivation.clone()) + .collect::>(), + output_bip32_derivations + ); let finalized = wallet.try_finalize_psbt(&mut psbt).unwrap(); From 8a7b5de61946fef9cd0c586bc4e0cdc1a5372ac4 Mon Sep 17 00:00:00 2001 From: Matthew Date: Wed, 10 Jun 2026 14:37:01 -0500 Subject: [PATCH 4/5] docs: clarify try_finalize_psbt behavior --- src/wallet/mod.rs | 14 ++++++++++---- 1 file changed, 10 insertions(+), 4 deletions(-) diff --git a/src/wallet/mod.rs b/src/wallet/mod.rs index 4525bed1..e4f2ff25 100644 --- a/src/wallet/mod.rs +++ b/src/wallet/mod.rs @@ -1937,11 +1937,17 @@ impl Wallet { .is_finalized()) } - /// Finalize a PSBT and return per-input finalization results. Use this method when you need to - /// inspect why a specific input could not be finalized. + /// Attempt to finalize each input of a PSBT and return per-input finalization results. /// - /// The method should only return `Err` when the PSBT is malformed, for example if its inputs - /// are out of bounds. + /// 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, From ec3a683f0d9247c6e533914e84d83b68ad09d3ce Mon Sep 17 00:00:00 2001 From: Matthew Date: Mon, 15 Jun 2026 13:55:06 -0500 Subject: [PATCH 5/5] refactor: clarify try_finalize_psbt timelock inputs --- src/wallet/mod.rs | 22 ++++++++++------------ 1 file changed, 10 insertions(+), 12 deletions(-) diff --git a/src/wallet/mod.rs b/src/wallet/mod.rs index e4f2ff25..d08e63ef 100644 --- a/src/wallet/mod.rs +++ b/src/wallet/mod.rs @@ -1924,13 +1924,11 @@ impl Wallet { Ok(self .try_finalize_psbt_with( psbt, + Some(current_height), |_, input| { - Some(( - current_height, - confirmation_heights - .get(&input.previous_output.txid) - .copied(), - )) + confirmation_heights + .get(&input.previous_output.txid) + .copied() }, true, )? @@ -1952,17 +1950,18 @@ impl Wallet { &self, psbt: &mut Psbt, ) -> Result { - self.try_finalize_psbt_with(psbt, |_, _| None, false) + self.try_finalize_psbt_with(psbt, None, |_, _| None, false) } fn try_finalize_psbt_with( &self, psbt: &mut Psbt, - mut wallet_timelocks: F, + current_height: Option, + mut confirmation_height_for_input: F, clear_output_derivations: bool, ) -> Result where - F: FnMut(usize, &bitcoin::TxIn) -> Option<(u32, Option)>, + F: FnMut(usize, &bitcoin::TxIn) -> Option, { let tx = &psbt.unsigned_tx; if psbt.inputs.len() < tx.input.len() { @@ -2002,9 +2001,8 @@ impl Wallet { match desc { Some(desc) => { let mut tmp_input = bitcoin::TxIn::default(); - let satisfy_result = if let Some((current_height, confirmation_height)) = - wallet_timelocks(n, input) - { + let satisfy_result = if let Some(current_height) = current_height { + let confirmation_height = confirmation_height_for_input(n, input); desc.satisfy( &mut tmp_input, (