Kaspalytics Learn is under (heavy) construction. Check back regularly!
Updated: Aug 14, 2026

Pay-To-Script Hash (P2SH)

How P2SH locks funds to the hash of a redeem script on Kaspa - the script public key and signature script layouts, execution order, and a worked 2-of-3 multisig example.

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.

In summary:

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.

Script Public Key

Kaspa’s P2SH script public key standard is 35 bytes in the following format:

OpBlake2b OpData32 <32-byte redeem script hash> OpEqual

ElementSizePurpose
OpBlake2b1 bytePops the top stack element and pushes its 32-byte Blake2b hash
OpData321 bytePushes the next 32 bytes (the embedded hash) onto the stack
Redeem script hash32 bytesBlake2b hash of the redeem script
OpEqual1 bytePops the top two elements, pushes true if equal, otherwise fails

Signature Script

<push op> <data/signature(s)> ... <push op> <redeem script bytes>

  • Each signature/data element is its own data push.
  • The serialized redeem script is the final data push. It is not executed during this pass - it is treated as bytes on the stack.
  • The redeem script provided at spend time must match exactly the redeem script that was hashed at lock time.

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.

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, one stack state at a time:

Executes
<signature_1>
signature_script begins
sig_1
<signature_2>
sig_1sig_2
<redeem_script>
the whole script, pushed as bytes - not executed
sig_1sig_2redeem
- stack saved -
the engine snapshots the stack here, before script_public_key runs
sig_1sig_2redeem
OpBlake2b
script_public_key begins - pops redeem, pushes its 32-byte hash
sig_1sig_2H(redeem)
OpData32 <hash>
the hash committed to at lock time
sig_1sig_2H(redeem)H(committed)
OpEqual
pops both hashes, pushes true - or the spend fails here
sig_1sig_2true
- stack restored, redeem popped -
the snapshot comes back and the redeem element is taken off it. That true is discarded with the rest - the P2SH check and the redeem script do not share a stack
sig_1sig_2
OpData1 2
redeem_script begins - required signatures
sig_1sig_22
<pubkey_1>
sig_1sig_22pk_1
<pubkey_2>
sig_1sig_22pk_1pk_2
<pubkey_3>
sig_1sig_22pk_1pk_2pk_3
OpData1 3
total public keys
sig_1sig_22pk_1pk_2pk_33
OpCheckMultiSig
consumes all of it, pushes the verdict
true
The redeem script is pushed as data and only becomes code after the hash check passes. The hash check runs on its own stack, which is discarded, so the signatures the redeem script verifies are the ones pushed at the start.

The last row performs several operations. OpCheckMultiSig in detail:

  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.

See Also: Transaction Structure, Addresses, The P2SH Covenant Envelope

Kaspalytics strives to provide accurate data - our highest level of effort is given to data validation and maintenance. However, we cannot guarantee 100% accuracy. Data is subject to inaccuracies and change.

Contact | © 2026 Kaspalytics