Implement P2WPKH & P2WSH output spend script
What changed, and why it matters
This commit adds helper functions to create the correct unlocking script for two common Bitcoin transaction types (P2SH-P2WPKH and P2SH-P2WSH). It is a normal feature addition to the library and does not fix or introduce a security vulnerability.
No security action required. Treat as a routine feature commit.
Security signals we found
No strong security signals were identified.
Evidence from the diff
The patch introduces a new ScriptSigBufExt trait with p2sh_p2wpkh and p2sh_p2wsh constructors. These build the standard scriptSig that pushes the Segwit redeem script for P2SH-wrapped Segwit outputs. The implementation follows the expected Bitcoin protocol: 0 <20-byte pubkey hash> for P2SH-P2WPKH and 0 <32-byte script hash> for P2SH-P2WSH. Unit tests verify the redeem script structure and lengths. No security defect is present in the diff.
Changed components
bitcoin/src/blockdata/script/owned.rsbitcoin/src/blockdata/script/mod.rsbitcoin/src/blockdata/script/tests.rsInspect captured patch +64 / −4
diff --git a/bitcoin/src/blockdata/script/mod.rs b/bitcoin/src/blockdata/script/mod.rs
index 9fa8e3bb..cea6378c 100644
--- a/bitcoin/src/blockdata/script/mod.rs
+++ b/bitcoin/src/blockdata/script/mod.rs
@@ -71,7 +71,7 @@ pub use self::{
borrowed::{ScriptExt, TapScriptExt, ScriptPubKeyExt, WitnessScriptExt, ScriptSigExt},
builder::Builder,
instruction::{Instruction, Instructions, InstructionIndices},
- owned::{ScriptBufExt, ScriptPubKeyBufExt},
+ owned::{ScriptBufExt, ScriptPubKeyBufExt, ScriptSigBufExt},
push_bytes::{PushBytes, PushBytesBuf, PushBytesErrorReport},
};
#[doc(no_inline)]
diff --git a/bitcoin/src/blockdata/script/owned.rs b/bitcoin/src/blockdata/script/owned.rs
index f83d4053..c3d59126 100644
--- a/bitcoin/src/blockdata/script/owned.rs
+++ b/bitcoin/src/blockdata/script/owned.rs
@@ -7,11 +7,12 @@ use internals::ToU64 as _;
use super::{
opcode_to_verify, write_scriptint, Builder, Error, Instruction, PushBytes, ScriptBuf,
- ScriptExtPriv as _, ScriptPubKeyBuf,
+ ScriptExtPriv as _, ScriptPubKeyBuf, ScriptSigBuf, WitnessScript,
};
use crate::internal_macros;
use crate::key::{
- LegacyPublicKey, PubkeyHash, TapTweak, TweakedPublicKey, UntweakedPublicKey, WPubkeyHash,
+ FullPublicKey, LegacyPublicKey, PubkeyHash, TapTweak, TweakedPublicKey, UntweakedPublicKey,
+ WPubkeyHash,
};
use crate::opcodes::all::*;
use crate::opcodes::{self, Opcode};
@@ -234,6 +235,38 @@ crate::internal_macros::define_extension_trait! {
}
}
+crate::internal_macros::define_extension_trait! {
+ /// Extension functionality for the [`ScriptSigBuf`] type.
+ pub trait ScriptSigBufExt impl for ScriptSigBuf {
+ /// Constructs a scriptSig required to spend a P2SH-P2WPKH output.
+ ///
+ /// The scriptSig pushes the P2WPKH redeem script (`0 <20-byte-pubkey-hash>`) which
+ /// is required when spending a P2SH-wrapped Segwit output. The witness data should
+ /// be provided separately using [`WitnessExt::p2wpkh`].
+ ///
+ /// [`WitnessExt::p2wpkh`]: crate::blockdata::witness::WitnessExt::p2wpkh
+ fn p2sh_p2wpkh(pubkey: FullPublicKey) -> Self {
+ let redeem_script: super::ScriptPubKeyBuf = Builder::new().push_int_unchecked(0).push_slice(pubkey.wpubkey_hash()).into_script();
+ Builder::new().push_slice(<&PushBytes>::try_from(redeem_script.as_bytes()).expect("redeem script is 22 bytes")).into_script()
+ }
+
+ /// Constructs a scriptSig required to spend a P2SH-P2WSH output.
+ ///
+ /// The scriptSig pushes the P2WSH redeem script (`0 <32-byte-script-hash>`) which
+ /// is required when spending a P2SH-wrapped Segwit output. The witness data should
+ /// be provided separately using [`WitnessExt::p2wsh`].
+ ///
+ /// [`WitnessExt::p2wsh`]: crate::blockdata::witness::WitnessExt::p2wsh
+ fn p2sh_p2wsh(witness_script: &WitnessScript) -> Result<ScriptSigBuf, super::WitnessScriptSizeError> {
+ use super::WitnessScriptExt as _;
+
+ let hash = witness_script.wscript_hash()?;
+ let redeem_script: super::ScriptPubKeyBuf = Builder::new().push_int_unchecked(0).push_slice(hash).into_script();
+ Ok(Builder::new().push_slice(<&PushBytes>::try_from(redeem_script.as_bytes()).expect("redeem script is 34 bytes")).into_script())
+ }
+ }
+}
+
mod sealed {
pub trait Sealed {}
impl<T> Sealed for super::ScriptBuf<T> {}
diff --git a/bitcoin/src/blockdata/script/tests.rs b/bitcoin/src/blockdata/script/tests.rs
index ff25a9b5..4af492e9 100644
--- a/bitcoin/src/blockdata/script/tests.rs
+++ b/bitcoin/src/blockdata/script/tests.rs
@@ -8,8 +8,9 @@ use hex_unstable::hex;
use super::*;
use crate::consensus::encode::{deserialize, serialize};
-use crate::crypto::key::{LegacyPublicKey, XOnlyPublicKey};
+use crate::crypto::key::{FullPublicKey, LegacyPublicKey, XOnlyPublicKey};
use crate::script::borrowed::{ScriptPubKeyExt as _, ScriptPubKeyExtPriv as _, TapScriptExt as _};
+use crate::script::owned::ScriptSigBufExt as _;
use crate::script::witness_program::WitnessProgram;
use crate::script::witness_version::WitnessVersion;
use crate::{opcodes, Amount, FeeRate};
@@ -1079,3 +1080,29 @@ fn longest_witness_program() {
assert_eq!(script.witness_version(), Some(version));
}
+
+#[test]
+fn p2sh_p2wpkh_script_sig() {
+ let key = "026c468be64d22761c30cd2f12cbc7de255d592d7904b1bab07236897cc4c2e766"
+ .parse::<FullPublicKey>()
+ .unwrap();
+ let script_sig = crate::ScriptSigBuf::p2sh_p2wpkh(key);
+
+ // The scriptSig should be a single push of the 22-byte redeem script: 0014<20-byte-hash>
+ let redeem_script = script_sig.redeem_script().expect("should have redeem script");
+ assert_eq!(redeem_script.as_bytes()[0], 0x00); // witness version 0
+ assert_eq!(redeem_script.as_bytes()[1], 0x14); // push 20 bytes
+ assert_eq!(redeem_script.len(), 22);
+}
+
+#[test]
+fn p2sh_p2wsh_script_sig() {
+ let witness_script = WitnessScriptBuf::from_hex_no_length_prefix("522103e5529d8eaa3d559903adb2e881eb06c86ac2574ffa503c45f4e942e2a693b33e2102e5f10fcdcdbab211e0af6a481f5532536ec61a5fdbf7183770cf8680fe729d8152ae").unwrap();
+ let script_sig = crate::ScriptSigBuf::p2sh_p2wsh(&witness_script).expect("script is valid");
+
+ // The scriptSig should be a single push of the 34-byte redeem script: 0020<32-byte-hash>
+ let redeem_script = script_sig.redeem_script().expect("should have redeem script");
+ assert_eq!(redeem_script.as_bytes()[0], 0x00); // witness version 0
+ assert_eq!(redeem_script.as_bytes()[1], 0x20); // push 32 bytes
+ assert_eq!(redeem_script.len(), 34);
+}
Why this scored 15/100
Community notes
Notes can correct, qualify, or add evidence to the AI analysis. Every note shown here has been validated by a human moderator.
The AI analysis stands alone for now. Submit a note if you can add evidence or important context.