diff --git a/src/input.rs b/src/input.rs index b949b01..31e652d 100644 --- a/src/input.rs +++ b/src/input.rs @@ -87,6 +87,7 @@ enum PlanOrPsbtInput { /// Takes precedence over the sequence implied by `plan.relative_timelock` /// when computing [`Input::sequence`]. sequence_override: Option, + sighash_type: Option, }, PsbtInput { psbt_input: Box, @@ -153,6 +154,7 @@ impl PlanOrPsbtInput { PlanOrPsbtInput::Plan { plan, sequence_override, + .. } => sequence_override.or_else(|| { plan.relative_timelock .map(|relative_timelock| relative_timelock.to_sequence()) @@ -161,6 +163,13 @@ impl PlanOrPsbtInput { } } + pub fn sighash_type(&self) -> Option { + match self { + PlanOrPsbtInput::Plan { sighash_type, .. } => *sighash_type, + PlanOrPsbtInput::PsbtInput { psbt_input, .. } => psbt_input.sighash_type, + } + } + pub fn satisfaction_weight(&self) -> usize { match self { PlanOrPsbtInput::Plan { plan, .. } => plan.satisfaction_weight(), @@ -291,6 +300,7 @@ impl Input { plan: PlanOrPsbtInput::Plan { plan: Box::new(plan), sequence_override: None, + sighash_type: None, }, status, is_coinbase, @@ -312,6 +322,7 @@ impl Input { plan: PlanOrPsbtInput::Plan { plan: Box::new(plan), sequence_override: None, + sighash_type: None, }, status, is_coinbase, @@ -598,6 +609,26 @@ impl Input { self.plan.sequence() } + /// Sighash type. + pub fn sighash_type(&self) -> Option { + self.plan.sighash_type() + } + + /// Set the sighash type for this input. + /// + /// This overrides any sighash type already attached to the input — whether carried in via + /// [`Input::from_psbt_input`] (as [`psbt::Input::sighash_type`]) or set by a prior call. + /// + /// Accepts anything convertible into [`psbt::PsbtSighashType`], including the standard + /// `EcdsaSighashType` and `TapSighashType` from `rust-bitcoin`. + pub fn set_sighash_type(&mut self, sighash_type: impl Into) { + let new = Some(sighash_type.into()); + match &mut self.plan { + PlanOrPsbtInput::Plan { sighash_type, .. } => *sighash_type = new, + PlanOrPsbtInput::PsbtInput { psbt_input, .. } => psbt_input.sighash_type = new, + } + } + /// The weight in witness units needed for satisfying the [`Input`]. /// /// The satisfaction weight is the combined size of the fully satisfied input's witness @@ -629,6 +660,7 @@ impl Input { PlanOrPsbtInput::Plan { plan, sequence_override, + .. } => { if let Some(required) = plan.absolute_timelock { if sequence == Sequence::MAX { diff --git a/src/selection.rs b/src/selection.rs index 48cb851..c97b662 100644 --- a/src/selection.rs +++ b/src/selection.rs @@ -39,14 +39,6 @@ pub struct PsbtParams { /// [`non_witness_utxo`]: bitcoin::psbt::Input::non_witness_utxo pub mandate_full_tx_for_segwit_v0: bool, - /// Sighash type to be used for each input. - /// - /// This option only applies to [`Input`]s that include a plan, as otherwise the given PSBT - /// input can be expected to set a specific sighash type. Defaults to `None` which will not - /// set an explicit sighash type for any input. (In that case the sighash will typically - /// cover all of the outputs). - pub sighash_type: Option, - /// Apply BIP-326 anti-fee-sniping (AFS) protection, using the given block height. /// /// * `None` (default) — no AFS is applied. @@ -84,7 +76,6 @@ impl Default for PsbtParams { version: transaction::Version::TWO, min_locktime: absolute::LockTime::ZERO, mandate_full_tx_for_segwit_v0: true, - sighash_type: None, anti_fee_sniping: None, } } @@ -253,7 +244,7 @@ impl Selection { } } - psbt_input.sighash_type = params.sighash_type; + psbt_input.sighash_type = plan_input.sighash_type(); continue; } @@ -743,4 +734,44 @@ mod tests { "should return UnsupportedVersion error for version < 2" ); } + + #[test] + fn test_set_sighash_type_propagates_to_psbt() -> anyhow::Result<()> { + use bitcoin::psbt::PsbtSighashType; + use bitcoin::TapSighashType; + + let mut input = setup_test_input(2_000)?; + + // Inputs start without a sighash type set. + assert_eq!(input.sighash_type(), None); + + input.set_sighash_type(TapSighashType::Single); + assert_eq!( + input.sighash_type(), + Some(PsbtSighashType::from(TapSighashType::Single)), + ); + + // A subsequent set overrides the prior value. + input.set_sighash_type(TapSighashType::None); + assert_eq!( + input.sighash_type(), + Some(PsbtSighashType::from(TapSighashType::None)), + ); + + // `create_psbt` propagates the per-input sighash type into the PSBT input. + let selection = Selection { + inputs: vec![input], + outputs: vec![Output::with_script( + ScriptBuf::new(), + Amount::from_sat(9_000), + )], + }; + let psbt = selection.create_psbt(PsbtParams::default())?; + assert_eq!( + psbt.inputs[0].sighash_type, + Some(PsbtSighashType::from(TapSighashType::None)), + ); + + Ok(()) + } }