Pay-To-Script Hash (P2SH)
Notes on Kaspa's P2SHOverview
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:
- Execute
signature_script:- Push
signature_1onto the stack - Push
signature_2onto the stack - Push
redeem_scriptonto the stack
- Push
- Execute
script_public_key:- Execute
OpBlake2b- pops the top element (the rawredeem_script), hashes it, and pushes the resulting 32 byte hash onto the stack - Push the 32-byte
redeem_scripthash (the one embedded inscript_public_key) - Execute
OpEqual- pops the top two elements (the tworedeem_scripthashes) off the stack and checks equality
- Execute
- Execute
redeem_script- Push 2 onto the stack (number of required sigs)
- Push pubkey_1 onto the stack
- Push pubkey_2 onto the stack
- Push pubkey_3 onto the stack
- Push 3 onto the stack (number of pub keys)
- Execute
OpCheckMultiSig: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.