Updated: 5/9/2026

Pay-To-Script Hash (P2SH)

Notes on Kaspa's P2SH

Overview

P2SH is a standard template for locking and unlocking (spending) UTXOs on the UTXO-based networks, which includes Kaspa.

Redeem Script: The raw spending-condition script. It contains the conditions that must be satisfied to spend the UTXO. Signatures and other input data are not part of the redeem script. They are supplied separately at spend time as part of signature_script.

The essence is:

Locking: Funds are locked to the hash of the redeem script. This reduces disk usage (relative to storing the full script) and keeps actual script contents private until spent. The hash of the redeem script is part of the value stored in the script_public_key field of the UTXO.

Unlocking: To unlock, the spender provides any required data and signatures, along with the full redeem script (serialized as a data push), in the signature_script field of the transaction input. The signatures/data and the redeem script are separate elements. The redeem script provided at spend time must match exactly the redeem script that was hashed at lock time. Script contents are exposed as part of the unlocking process, required so the network can validate the transaction.

The above summary omits some detail. Additional script opcodes are part of the standard P2SH template and are required in the script_public_key and signature_script fields. For example: pushing data onto the stack, validating script hashes match, etc. More detail below.

Deeper Dive

Kaspa’s P2SH script public key standard is 35 bytes in the following format: OpBlake2b OpData32 <32-byte redeem script hash> OpEqual

Signature Script format: <push op> <data/signature(s)> ... <push op> <redeem script bytes>

Note the redeem script is included as a single data push - it is not executed during the first pass, it is treated as bytes on the stack.

Execution

The Kaspa script engine runs the two scripts back to back, sharing a single stack: signature_script first, then script_public_key.

1.) signature_script runs

Each signature/data element is pushed onto the stack, then the serialized redeem script is pushed as the top element.

2.) script_public_key runs

2.1. OpBlake2b pops the top element (the serialized redeem script) and pushes its 32-byte Blake2b hash.

2.2. OpData32 pushes the 32-byte hash embedded in the locking script. OpEqual pops both hashes and pushes true if they match, otherwise false which results in script failure.

3.) Redeem script execution

Because this is a P2SH spend, Kaspa’s script engine then deserializes the redeem script and executes it against the remaining stack. Which are the signatures and data left over after the redeem script element was popped. The redeem script must leave a truthy value on top of the stack for the spend to be valid.

This two-phase design is what lets the locking script stay small (just a hash commitment) while still allowing arbitrary spending conditions to be enforced at spend time.

Concrete Example: 2 of 3 Multisig

Building the redeem script

A redeem script is generated for the Multisig configuration (spending constraints). The following is a snippet in Python taken from the (Kaspa Python SDK multisig example):

redeem_script = ScriptBuilder()\
	.add_i64(2)\                        # The number of required signatures
	.add_data(pubkey_1)\                # One of the public keys 
	.add_data(pubkey_2)\                # One of the public keys 
	.add_data(pubkey_3)\                # One of the public keys 
	.add_i64(3)\                        # Total number of pub keys
	.add_op(Opcodes.OpCheckMultiSig)    # Check multisig

NOTE: The add_i64() and add_data() methods of the ScriptBuilder class abstract away something important. Under the hood, they identify the correct data push opcode based on data size of the passed arg, and add this push opcode to the script. This reduces effort for developers but obfuscates an important concept in this example.

Plain funding UTXOs are spend to create outputs which lock

Locking funds

Funds can then be locked to the multisig redeem script above:

# Create the P2SH locking script
# This hashes the redeem script AND builds the P2SH standard script public key (spk) template mentioned above
spk = redeem_script.create_pay_to_script_hash_script()

# Encode the P2SH spk to an address
address = address_from_script_public_key(spk, network="testnet")

The address can then be shared with a sender, and the sender sends funds to this address. More than likely the sender would paste the address into their wallet application, and the sending application will decode the address back to the P2SH script_public_key. The script_public_key is then used on the transaction’s outputs to lock the outputs to this multisig P2SH script.

Unlocking funds

When ready to spend, the raw (unhashed) multisig redeem script is embedded in the signature_script of the inputs spending the UTXOs, as well as the required signatures:

signature_script = ScriptBuilder()\
	.add_data(signature_1)\
	.add_data(signature_2)\
	.add_data(redeem_script)

NOTE: Again note that this code is from Kaspa’s Python SDK, which handles data push opcodes behind the scenes.

The full script execution is:

  1. Execute signature_script:
    1. Push signature_1 onto the stack
    2. Push signature_2 onto the stack
    3. Push redeem_script onto the stack
  2. Execute script_public_key:
    1. Execute OpBlake2b - pops the top element (the raw redeem_script), hashes it, and pushes the resulting 32 byte hash onto the stack
    2. Push the 32-byte redeem_script hash (the one embedded in script_public_key)
    3. Execute OpEqual - pops the top two elements (the two redeem_script hashes) off the stack and checks equality
  3. Execute redeem_script
    1. Push 2 onto the stack (number of required sigs)
    2. Push pubkey_1 onto the stack
    3. Push pubkey_2 onto the stack
    4. Push pubkey_3 onto the stack
    5. Push 3 onto the stack (number of pub keys)
    6. Execute OpCheckMultiSig:
      1. num_pubkeys = Pops off the top element as the number of pub keys
      2. Pops off the next num_pubkeys elements as the pubkeys (their original push order is preserved)
      3. num_required_sigs = Pops off the next element as the minimum number of required signatures
      4. Pops off the next num_required_sigs elements as the signatures. These are the signatures that signature_script pushed onto the stack at the very start
      5. Validates each signature against the pubkeys in order. Signatures must be provided in the same order as their corresponding pubkeys.
      6. Pushes the boolean result onto the stack (true if at least num_required_sigs valid signatures were found in the correct order, otherwise false)

If all of the above executes properly, funds are unlocked.