Building a P2SH-P2WSH 2-of-2 Multisig Transaction in Bitcoin

Shashank RMShashank RM
10 min read

Introduction

Imagine you and a friend want to share control over some Bitcoin. Neither of you should be able to spend it alone—both must agree to any transaction. This is where multisig transactions come into play.

But there’s more! Instead of using a basic multisig setup, I’ll wrap it in P2SH-P2WSH. This method gives us the security benefits of SegWit while keeping the spending conditions hidden until the coins are spent.

In this blog, I’ll walk through creating a 2-of-2 multisig transaction using P2SH-P2WSH. I won’t just throw code at you—I’ll break down every concept so you understand why each step matters.

By the end, you’ll know how to:

  • Construct a multisig redeem script.

  • Create and sign a transaction correctly.

  • Ensure it meets Bitcoin’s validation rules.


Jargon Dictionary

Before diving into the technical details, here are some key terms I’ll be using throughout this blog. Understanding these will help you follow along smoothly.

1. Multisig (Multi-Signature)

A Bitcoin script that requires multiple private keys to sign a transaction before it can be spent. In this case, I’ll use a 2-of-2 multisig, meaning both keys are required.

2. P2SH (Pay-to-Script-Hash)

A Bitcoin address type that allows complex scripts (like multisig) while keeping them hidden until the funds are spent. The sender only needs to know the hash of the script, not the full script itself.

3. P2WSH (Pay-to-Witness-Script-Hash)

A SegWit version of P2SH that improves efficiency and security. Instead of including the full script in the scriptSig, it moves it to the witness field, reducing transaction size and fees.

4. Redeem Script

The actual locking script containing the spending conditions (e.g., a 2-of-2 multisig setup). In a P2SH transaction, this script is revealed when spending the funds.

5. Witness

A separate data structure introduced by SegWit to hold signatures and script data, improving Bitcoin’s scalability and security.

6. Sighash (Signature Hash)

A cryptographic hash of the transaction that is signed by private keys. This ensures the transaction hasn’t been tampered with after signing.

7. Outpoint

A reference to a specific unspent transaction output (UTXO) being used as an input in a new transaction. It consists of a transaction ID (txid) and an index (vout).

8. Locktime

A parameter that prevents a transaction from being broadcasted before a certain block height or timestamp.

9. Sequence

A field in transaction inputs that allows time-locked spending or replaces transactions before confirmation (used in RBF - Replace-By-Fee).

10. ScriptSig

A script placed in the input of a transaction, used to unlock the funds. In P2SH, it contains the redeem script.

11. Txid (Transaction ID)

A unique identifier for a Bitcoin transaction, derived by hashing its contents.

This list will serve as a reference throughout the blog. If anything seems unclear, feel free to revisit this section!


Goal

The goal is to create a P2SH-P2WSH multisig transaction that spends from a 2-of-2 multisig address. This means:

  • The Bitcoin is locked in a script that requires both private keys to sign before it can be spent.

  • The script is wrapped inside a P2SH address, making it look like a standard transaction until it’s spent.

  • The actual spending conditions are enforced using SegWit (P2WSH), which improves efficiency and reduces fees.

Why is this important?

Understanding how to construct such a transaction helps in several ways:

  • Security: Multisig setups prevent a single point of failure, reducing the risk of losing funds due to a compromised key.

  • Efficiency: By using SegWit (P2WSH), we separate signatures from the main transaction, lowering costs and improving scalability.

  • Flexibility: P2SH allows complex spending conditions while keeping the script hidden until the funds are spent.

To achieve this, I need to:

  1. Construct a transaction that meets the given input and output requirements.

  2. Compute the sighash and sign it correctly with both private keys.

  3. Build the scriptSig and witness stack so the Bitcoin network accepts the transaction.

  4. Output the final signed transaction hex to a file.

This isn’t just about writing code—it’s about truly understanding how Bitcoin transactions work under the hood. Let’s break it down step by step.


Understanding P2SH-P2WSH Multisig Transactions

Before diving into the implementation, it’s important to understand what a P2SH-P2WSH multisig transaction is and how it works. This will provide the necessary context to follow the transaction-building process.

Breaking it Down: P2SH, P2WSH, and Multisig

  1. Multisig (Multi-Signature) Transactions

    • Instead of a single private key controlling the funds, multisig requires multiple keys to authorize a transaction.

    • In this case, I’m working with a 2-of-2 multisig, meaning both private keys must sign the transaction for it to be valid.

  2. Pay-to-Script-Hash (P2SH)

    • Instead of directly revealing the full locking script (redeem script) in the scriptPubKey, a hashed version of the script is stored.

    • This makes the transaction look like a regular payment, keeping the complexity hidden until it’s spent.

  3. Pay-to-Witness-Script-Hash (P2WSH)

    • Segregated Witness (SegWit) improves Bitcoin's efficiency by separating the signature data from the transaction structure.

    • P2WSH is the SegWit equivalent of P2SH but stores a SHA256 hash of the redeem script instead of a RIPEMD-160(SHA256) hash.

    • This reduces fees and prevents third-party malleability.

How This Transaction Works

  1. Funds were originally locked in a P2SH address that contains a P2WSH script (which itself contains the 2-of-2 multisig conditions).

  2. To spend the funds, I need to provide:

    • A valid witness stack (signatures + the multisig script).

    • A proper scriptSig, which includes the redeem script that was originally hashed.

  3. Bitcoin nodes verify the transaction by:

    • Checking that the provided redeem script hashes to the expected P2SH address.

    • Confirming the witness script hash matches the P2WSH address.

    • Ensuring both signatures are correct for the multisig script.

By constructing a valid scriptSig and witness stack, I can successfully spend the locked funds.


Constructing the Transaction

Now that I’ve covered the fundamental concepts, it’s time to break down how to construct the transaction step by step.

  • Defining the Inputs and Outputs

    Every Bitcoin transaction consists of inputs (where the BTC comes from) and outputs (where the BTC goes).

    • Input:

      • The UTXO (unspent transaction output) I’m spending has:

        • TXID: 0000000000000000000000000000000000000000000000000000000000000000

        • VOUT (Output Index): 0

        • Sequence Number: 0xffffffff (default for immediate spending)

    • Output:

      • I’m sending 0.001 BTC to the Bitcoin address 325UUecEQuyrTd28Xs2hvAxdAjHM7XzqVF

Constructing the Transaction Structure

Using the bitcoin library in Rust, I define the transaction:

let mut tx = Transaction {
    version: 2,
    lock_time: LockTime::ZERO,
    input: vec![TxIn {
        previous_output: OutPoint {
            txid: Txid::all_zeros(),
            vout: 0,
        },
        script_sig: ScriptBuf::new(), // Will be set later
        sequence: Sequence::MAX,
        witness: Witness::default(),
    }],
    output: vec![TxOut {
        value: 100000, // 0.001 BTC in satoshis
        script_pubkey: Address::from_str("325UUecEQuyrTd28Xs2hvAxdAjHM7XzqVF")
            .unwrap()
            .require_network(Network::Bitcoin)
            .unwrap()
            .script_pubkey(),
    }],
};

This defines:

  • Transaction version: 2 (standard)

  • Locktime: 0 (spendable immediately)

  • One input: Spending from the given TXID and output index

  • One output: Sending 0.001 BTC to the specified address

The scriptSig and witness data are still empty at this stage, as they will be added after signing.

Signing the Transaction

Now that the transaction structure is ready, the next step is to sign the input using the two private keys. Since this is a P2SH-P2WSH transaction, I need to:

  • Generate a sighash – a unique hash of the transaction input that will be signed.

  • Sign the sighash with both private keys to produce valid ECDSA signatures.

  • Format the signatures properly before adding them to the witness stack.

1. Creating the Witness Script

The witness script is the actual script that enforces the 2-of-2 multisig condition. It was already provided in hex format:

let script_bytes = hex::decode(
    "5221032ff8c5df0bc00fe1ac2319c3b8070d6d1e04cfbf4fedda499ae7b775185ad53b21039bbc8d24f89e5bc44c5b0d1980d6658316a6b2440023117c3c03a4975b04dd5652ae"
).unwrap();
let witness_script = Script::from_bytes(&script_bytes);

This script ensures that two valid signatures from the corresponding public keys are required to spend the UTXO.

2. Generating the Sighash

To sign the transaction, I first need to generate the sighash (hash of the transaction data) that both private keys will sign. This is done using SighashCache:

let sighash = SighashCache::new(&tx)
    .segwit_signature_hash(
        0, // Input index
        &witness_script,
        tx.output[0].value,
        EcdsaSighashType::All, // SIGHASH_ALL (default)
    )
    .unwrap();

3. Signing the Transaction

Next, I use the secp256k1 library to sign the sighash with both private keys:

let msg = Message::from_slice(&sighash[..]).unwrap();
let sig1 = secp.sign_ecdsa(&msg, &keypair1.secret_key());
let sig2 = secp.sign_ecdsa(&msg, &keypair2.secret_key());

These signatures prove that both private key holders authorize the spending of the UTXO.

4. Formatting the Signatures

Bitcoin signatures must be in DER format and include the sighash type (SIGHASH_ALL in this case).

let mut sig1_der = sig1.serialize_der().to_vec();
sig1_der.push(EcdsaSighashType::All as u8);

let mut sig2_der = sig2.serialize_der().to_vec();
sig2_der.push(EcdsaSighashType::All as u8);

These signatures are now properly formatted and ready to be included in the witness stack.

Constructing the Witness Stack

Now that I have the signatures, the next step is to properly construct the witness stack. The witness stack is a sequence of data elements that are used to satisfy the spending conditions of a SegWit transaction.

For a 2-of-2 multisig P2SH-P2WSH transaction, the witness stack must include:

  • An OP_0 placeholder (due to a known quirk in OP_CHECKMULTISIG).

  • The two valid ECDSA signatures in the correct order.

  • The witness script, which serves as the redeem script.

1. Adding Elements to the Witness Stack

In Rust, I construct the witness stack by pushing these elements in the correct order:

tx.input[0].witness.push(&[]); // OP_0 (CHECKMULTISIG workaround)
tx.input[0].witness.push(&sig2_der); // Signature 2
tx.input[0].witness.push(&sig1_der); // Signature 1
tx.input[0].witness.push(&witness_script.as_bytes()); // Witness script
  • The OP_0 is added first because OP_CHECKMULTISIG in Bitcoin has an off-by-one bug that requires an extra dummy element.

  • Signature 2 is added before Signature 1 because Bitcoin expects signatures in the same order as their corresponding public keys in the witness script.

  • The witness script is added last to complete the stack.

Once this witness stack is correctly structured, the transaction is ready to be broadcasted.

Finalizing and Broadcasting the Transaction

With the witness stack constructed, the last step is to serialize the transaction into its raw hexadecimal form and save it to a file. This hex-encoded transaction can then be broadcast to the Bitcoin network.

1. Serializing the Transaction

Bitcoin transactions need to be in a binary format, but they are often represented in hexadecimal for readability and transmission. In Rust, I use Bitcoin's serialization utilities to convert the transaction into hex:

let tx_hex = hex::encode(serialize(&tx));

Here’s what happens:

  • serialize(&tx): Converts the transaction into a byte array.

  • hex::encode(...): Converts the byte array into a human-readable hexadecimal string.

2. Writing the Transaction Hex to a File

To store the transaction hex, I write it to a file named out.txt:

fs::write("out.txt", tx_hex).unwrap();

This ensures that the generated transaction can be easily retrieved and broadcasted later using a Bitcoin node or other tools like bitcoin-cli sendrawtransaction.


Conclusion and Key Takeaways

In this blog, I walked through the process of creating a P2SH-P2WSH 2-of-2 multisig transaction, covering each step in detail—from constructing the transaction to signing and finalizing it.

Here are the key takeaways:

  • P2SH-P2WSH transactions allow complex spending conditions to be hidden inside a redeem script while benefiting from SegWit improvements.

  • The witness script defines the spending conditions, in this case, a 2-of-2 multisig setup.

  • ECDSA signatures are generated using the two private keys and included in the witness stack.

  • Bitcoin’s sighash mechanism ensures signatures commit to specific parts of the transaction, preventing tampering.

  • The witness stack must be structured correctly to pass script verification, including an OP_0 workaround for OP_CHECKMULTISIG.

  • The final transaction hex is saved and can be broadcasted to the Bitcoin network.

This method of transaction creation is useful in real-world applications, including multi-party wallets, smart contracts, and advanced Bitcoin scripting techniques.

By understanding how to build these transactions manually, You gained a deeper insight into Bitcoin’s scripting system and transaction validation rules.


That wraps up this guide! If you found this helpful or have any questions, feel free to drop a comment. Happy coding! 🚀

0
Subscribe to my newsletter

Read articles from Shashank RM directly inside your inbox. Subscribe to the newsletter, and don't miss out.

Written by

Shashank RM
Shashank RM

Sharing my learnings on Full Stack, Web3 and DevOps with the Community.