Scripts¶
A ScriptPublicKey is the lock on every output. Most of the time it's
generated for you — pay_to_address_script builds the standard
pay-to-pubkey lock, and both the
Generator (see Transaction Generator) and the high-level
Wallet (see Wallet) API use it under the hood. This page
is for the cases where you need a non-standard lock: multisig redeem
scripts, KRC‑20 / inscription envelopes, time-locked spends, covenant
prototypes.
The fluent builder is
ScriptBuilder; the
opcode set lives in
Opcodes.
When you'd reach for this¶
- Multisig. Building the redeem script behind an
M-of-Npay-to-script-hash address. Worked example:examples/transactions/multisig.py. - KRC‑20 / inscription envelopes. Embedding token-protocol JSON
in a commit-reveal script. Worked example:
examples/transactions/krc20_deploy.py. - Custom locking conditions. Time-locked or hash-locked spends and covenant prototypes. Advanced — you should already know what opcodes you need.
Building a script¶
ScriptBuilder is a chained-builder: every method returns the
builder so you can compose a script in one expression.
from kaspa import Opcodes, ScriptBuilder
script = ScriptBuilder()\
.add_data(pubkey_xonly_hex)\
.add_op(Opcodes.OpCheckSig)
print(script.to_string()) # hex of the assembled script
The four input families:
add_op(op)/add_ops([op, ...])— push a single opcode or a sequence.opis anOpcodesenum member or its integer value.add_data(bytes_or_hex)— push raw bytes (signatures, public keys, payloads). The builder picks the rightOP_PUSHDATA*variant for the size automatically. Accepts a hex string,bytes, or a list of ints.add_i64(n)— push a signed integer. Used forMandNin multisig, and for sequence / lock-time scalars.add_lock_time(daa_score)/add_sequence(seq)— push DAA-score and sequence values, for time-locked spends.
For the full opcode catalog see the
Opcodes reference.
Wrapping in P2SH¶
The output side commits to the hash of the redeem script, not the
script itself. create_pay_to_script_hash_script produces the
locking
ScriptPublicKey;
pay_to_script_hash_script
is the equivalent free function when you have the redeem-script bytes
directly:
from kaspa import address_from_script_public_key
spk = script.create_pay_to_script_hash_script()
p2sh_address = address_from_script_public_key(spk, "testnet-10")
Place spk on a TransactionOutput, or share p2sh_address so
funds can be sent to it. See address_from_script_public_key for the network argument.
When spending a P2SH output, the input's signature_script must
reveal the redeem script and a satisfying signature. For the
single-signature case, the convenience helper writes the witness for
you (create_input_signature, encode_pay_to_script_hash_signature_script, fill_input):
sig = pending.create_input_signature(input_index=0, private_key=key)
witness = script.encode_pay_to_script_hash_signature_script(sig)
pending.fill_input(0, witness)
pay_to_script_hash_signature_script(redeem_script, signature)
is the equivalent functional form when you no longer have the original
builder in scope. For multisig — where the witness needs more than
one signature — you build the signature_script manually with
ScriptBuilder; see the example linked below.
Two real shapes¶
Multisig redeem (M-of-N)¶
create_multisig_address produces the same lockup as a one-shot helper; this section
shows what's happening underneath:
redeem = ScriptBuilder()\
.add_i64(2)\
.add_data(pub_a.to_x_only_public_key().to_string())\
.add_data(pub_b.to_x_only_public_key().to_string())\
.add_data(pub_c.to_x_only_public_key().to_string())\
.add_i64(3)\
.add_op(Opcodes.OpCheckMultiSig)
spk = redeem.create_pay_to_script_hash_script()
This is a 2-of-3 Schnorr multisig: integer M, the public keys
(XOnlyPublicKey for Schnorr), integer N, then Opcodes.OpCheckMultiSig. ECDSA
multisig uses Opcodes.OpCheckMultiSigECDSA and full (compressed) PublicKeys
instead. The mass calculator needs to know about the multiple
signatures the input will eventually hold — pass sig_op_count=N per
input and minimum_signatures=M to the
Generator (see Transaction Generator). See
Signing → Multisig for how
those fields feed mass.
The spending side (collecting M signatures and packing them into
each input's signature_script) is non-trivial. The full flow is in
examples/transactions/multisig.py.
KRC‑20 / inscription envelope¶
import json
script = ScriptBuilder()\
.add_data(pub.to_x_only_public_key().to_string())\
.add_op(Opcodes.OpCheckSig)\
.add_op(Opcodes.OpFalse)\
.add_op(Opcodes.OpIf)\
.add_data(b"kasplex")\
.add_i64(0)\
.add_data(json.dumps(payload, separators=(",", ":")).encode())\
.add_op(Opcodes.OpEndIf)
The OpFalse / OpIf block is unreachable execution — a
conventional way to embed protocol data without affecting whether
the script can be satisfied. The reveal-stage input later spends a
P2SH output committing to this script, putting the embedded payload
on-chain.
The two-stage commit/reveal flow itself (fund the P2SH address in a
commit transaction, then spend it back to yourself in a reveal
transaction once the commit is mature) lives in
examples/transactions/krc20_deploy.py.
The managed Wallet wraps this as
accounts_commit_reveal /
accounts_commit_reveal_manual, keyed by a
CommitRevealAddressKind.
Inspecting an unknown script¶
When you hold a ScriptPublicKey from somewhere else — a UTXO
returned by get_utxos_by_addresses, an output read off-chain — the
classification predicates
(is_script_pay_to_pubkey,
is_script_pay_to_pubkey_ecdsa,
is_script_pay_to_script_hash)
tell you which lockup family it is:
from kaspa import (
is_script_pay_to_pubkey,
is_script_pay_to_pubkey_ecdsa,
is_script_pay_to_script_hash,
)
script_bytes = utxo.script_public_key.script
if is_script_pay_to_pubkey(script_bytes):
... # Schnorr P2PK
elif is_script_pay_to_pubkey_ecdsa(script_bytes):
... # ECDSA P2PK
elif is_script_pay_to_script_hash(script_bytes):
... # P2SH — needs the redeem script to spend
Use them to pick the signing path, filter UTXOs the wallet can actually spend, or audit a transaction's outputs.
SighashType and advanced flows¶
Scripts and sighash variants interact when you write protocols that
intentionally let signed transactions be amended.
SighashType.All — the
default — commits to every input and every output and is the only
one ordinary scripts should use. The _None, Single, and
*AnyOneCanPay variants exist for coinjoins and partial co-signing
flows; don't reach for them without a spec to follow. See
Signing → SighashType.
What this page didn't cover¶
- The full multisig orchestration (deriving cosigner keys, exchanging
partial signatures, submission):
examples/transactions/multisig.py. - Commit-reveal mechanics (two-stage submission, fee budget, the
maturity gate between commit and reveal):
examples/transactions/krc20_deploy.py. - The full opcode catalog:
Opcodesreference.