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
| Element | Size | Purpose |
|---|---|---|
OpBlake2b | 1 byte | Pops the top stack element and pushes its 32-byte Blake2b hash |
OpData32 | 1 byte | Pushes the next 32 bytes (the embedded hash) onto the stack |
| Redeem script hash | 32 bytes | Blake2b hash of the redeem script |
OpEqual | 1 byte | Pops 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:
The last row performs several operations. OpCheckMultiSig in detail:
num_pubkeys= Pops off the top element as the number of pub keys- Pops off the next
num_pubkeyselements as the pubkeys (their original push order is preserved) num_required_sigs= Pops off the next element as the minimum number of required signatures- Pops off the next
num_required_sigselements as the signatures. These are the signatures thatsignature_scriptpushed onto the stack at the very start - Validates each signature against the pubkeys in order. Signatures must be provided in the same order as their corresponding pubkeys.
- Pushes the boolean result onto the stack (
trueif at leastnum_required_sigsvalid signatures were found in the correct order, otherwisefalse)
If all of the above executes properly, funds are unlocked.
See Also: Transaction Structure, Addresses, The P2SH Covenant Envelope