a8 = OP_SHA256, 20 = push 32 bytes, 87 = OP_EQUAL
9. How to Write and Test Custom Scripts
Understanding Script Constraints
Before writing a custom script, understand the consensus constraints:
-
Scripts are limited to 10,000 bytes (per scriptPubKey + scriptSig + redeem script).
-
The stack can hold at most 1,000 items.
-
Each stack element is limited to 520 bytes.
-
Non-standard scripts won't be relayed by default nodes (use
-acceptnonstdtxn=1in regtest). -
Some opcodes are disabled:
OP_CAT,OP_SUBSTR,OP_LEFT,OP_RIGHT,OP_INVERT,OP_AND,OP_OR,OP_XOR,OP_2MUL,OP_2DIV,OP_MUL,OP_DIV,OP_MOD,OP_LSHIFT,OP_RSHIFT.
Writing a Hash Preimage Lock
A simple custom script that requires knowing a secret preimage:
scriptPubKey: OP_SHA256 <32-byte-hash> OP_EQUAL
scriptSig: <preimage>
Build and test:
import hashlib
secret = b"my_secret_preimage"
hash_value = hashlib.sha256(secret).hexdigest()
script_pubkey = "a8" + "20" + hash_value + "87"
# a8 = OP_SHA256, 20 = push 32 bytes, 87 = OP_EQUAL
script_sig = "12" + secret.hex() # 0x12 = 18 bytes
# Adjust push length to match actual secret length
print(f"scriptPubKey: {script_pubkey}")
print(f"scriptSig: {script_sig}")
Test with btcdeb:
btcdeb "[OP_SHA256 $(echo -n "my_secret_preimage" | sha256sum | cut -d' ' -f1) OP_EQUAL]" \
--scriptSig "$(echo -n "my_secret_preimage" | xxd -p -c 256)"
Writing a Time-Locked Script (CLTV)
OP_CHECKLOCKTIMEVERIFY (CLTV) rejects spending until a certain block height or timestamp:
scriptPubKey:
<locktime> OP_CHECKLOCKTIMEVERIFY OP_DROP
OP_DUP OP_HASH160 <pubkeyhash> OP_EQUALVERIFY OP_CHECKSIG
In hex (locktime = block 800000 = 0x000C3500, little-endian = 0x0035C000):
locktime_hex = (800000).to_bytes(4, 'little').hex() # "003dc00c"
cltv_script = (
"04" + locktime_hex + # push 4-byte locktime
"b1" + # OP_CHECKLOCKTIMEVERIFY
"75" + # OP_DROP
"76" + # OP_DUP
"a9" + # OP_HASH160
"14" + pubkeyhash + # push 20-byte hash
"88" + # OP_EQUALVERIFY
"ac" # OP_CHECKSIG
)
Encoding as P2SH
Wrap your custom script in P2SH for a standard-looking address:
import hashlib
def hash160(data: bytes) -> bytes:
return hashlib.new('ripemd160', hashlib.sha256(data).digest()).digest()
redeem_script_bytes = bytes.fromhex(cltv_script)
script_hash = hash160(redeem_script_bytes).hex()
p2sh_scriptpubkey = "a9" + "14" + script_hash + "87"
print(f"P2SH scriptPubKey: {p2sh_scriptpubkey}")
Testing in Regtest
# Start regtest with non-standard scripts enabled
bitcoind -regtest -acceptnonstdtxn=1 -daemon
# Send to the custom P2SH address
bitcoin-cli -regtest sendtoaddress <p2sh-address> 1.0
# Mine a block
bitcoin-cli -regtest generatetoaddress 1 $(bitcoin-cli -regtest getnewaddress)
# Create spending transaction with redeem script in scriptSig
# Then testmempoolaccept to verify before broadcasting
Pro Tip
When debugging scripts, always start with a high-level disassembly before diving into the stack trace. Tools like bitcoin-cli decodescript are your first line of defense in identifying standard script patterns.
TeachMeBitcoin is an ad-free, open-source educational repository curated by a passionate team of Bitcoin researchers and educators for public benefit. If you found our articles helpful, please consider supporting our hosting and ongoing content updates with a clean donation: