TeachMeBitcoin

========================

From TeachMeBitcoin, the free encyclopedia Reading time: 4 min

9. Building a Custom Script from Scratch in Python

This tutorial walks through building a custom Bitcoin Script from requirements, testing it, and preparing it for deployment.

Goal: Build a Hash Preimage Puzzle

We want to create a script that pays to "anyone who knows the secret value that hashes to a specific SHA256 hash." This is a classic cryptographic puzzle script.

#!/usr/bin/env python3
"""
Custom Bitcoin Script: SHA256 Hash Preimage Puzzle
Anyone who knows X such that SHA256(X) = TARGET_HASH can spend this output.
"""

import hashlib
import struct

def sha256(data: bytes) -> bytes:
    return hashlib.sha256(data).digest()

def hash160(data: bytes) -> bytes:
    return hashlib.new('ripemd160', sha256(data)).digest()

# ========================
# STEP 1: Define the secret
# ========================
SECRET = b"The secret to unlock this Bitcoin puzzle"
TARGET_HASH = sha256(SECRET)
print(f"Secret: {SECRET}")
print(f"Target SHA256: {TARGET_HASH.hex()}")

# ========================
# STEP 2: Build the locking script
# ========================
def build_locking_script(target_hash: bytes) -> bytes:
    """
    Build scriptPubKey:
    OP_SHA256 <target_hash> OP_EQUAL

    Only someone who knows the preimage of target_hash can spend.
    """
    assert len(target_hash) == 32, "SHA256 hash must be 32 bytes"

    script = bytearray()

    # OP_SHA256 = 0xa8
    script.append(0xa8)

    # Push 32 bytes (0x20 = decimal 32)
    script.append(0x20)
    script.extend(target_hash)

    # OP_EQUAL = 0x87
    script.append(0x87)

    return bytes(script)

locking_script = build_locking_script(TARGET_HASH)
print(f"\nLocking script ({len(locking_script)} bytes):")
print(f"  Hex: {locking_script.hex()}")
print(f"  ASM: OP_SHA256 <{TARGET_HASH.hex()[:16]}...> OP_EQUAL")

# ========================
# STEP 3: Build the unlocking script
# ========================
def build_unlocking_script(secret: bytes) -> bytes:
    """
    Build scriptSig:
    <secret_preimage>

    Just push the secret onto the stack.
    """
    script = bytearray()

    # Push the secret bytes
    secret_len = len(secret)
    if secret_len <= 75:
        script.append(secret_len)  # Direct push
    elif secret_len <= 255:
        script.append(0x4c)  # OP_PUSHDATA1
        script.append(secret_len)
    else:
        script.append(0x4d)  # OP_PUSHDATA2
        script.extend(struct.pack('<H', secret_len))

    script.extend(secret)

    return bytes(script)

unlocking_script = build_unlocking_script(SECRET)
print(f"\nUnlocking script ({len(unlocking_script)} bytes):")
print(f"  Hex: {unlocking_script.hex()}")

# ========================
# STEP 4: Simulate script execution
# ========================
def simulate_execution(unlocking: bytes, locking: bytes) -> bool:
    """
    Simulate Bitcoin Script execution.
    Returns True if script succeeds, False otherwise.
    """
    stack = []

    print("\n=== SCRIPT EXECUTION TRACE ===")

    # Parse and execute unlocking script
    print("\nPhase 1: Unlocking script")
    pos = 0
    while pos < len(unlocking):
        opcode = unlocking[pos]
        pos += 1

        if 1 <= opcode <= 75:  # Direct push
            data = unlocking[pos:pos+opcode]
            pos += opcode
            stack.append(data)
            print(f"  PUSH {len(data)} bytes: {data[:20].hex()}...")
        elif opcode == 0x4c:  # OP_PUSHDATA1
            length = unlocking[pos]
            pos += 1
            data = unlocking[pos:pos+length]
            pos += length
            stack.append(data)
            print(f"  OP_PUSHDATA1 {length} bytes")

    print(f"  Stack after Phase 1: [{len(stack[-1])} bytes on top]")

    # Parse and execute locking script
    print("\nPhase 2: Locking script")
    pos = 0
    while pos < len(locking):
        opcode = locking[pos]
        pos += 1

        if opcode == 0xa8:  # OP_SHA256
            if not stack:
                print("  ERROR: Stack underflow at OP_SHA256")
                return False
            top = stack.pop()
            result = sha256(top)
            stack.append(result)
            print(f"  OP_SHA256: {top[:10].hex()}... → {result.hex()[:16]}...")

        elif opcode == 0x20:  # Push 32 bytes
            data = locking[pos:pos+32]
            pos += 32
            stack.append(data)
            print(f"  PUSH 32 bytes: {data.hex()[:16]}...")

        elif opcode == 0x87:  # OP_EQUAL
            if len(stack) < 2:
                print("  ERROR: Stack underflow at OP_EQUAL")
                return False
            b = stack.pop()
            a = stack.pop()
            result = bytes([1]) if a == b else bytes([0])
            stack.append(result)
            print(f"  OP_EQUAL: {a.hex()[:16]}... == {b.hex()[:16]}... → {result.hex()}")

    # Final check
    if not stack:
        print("\nFINAL: Empty stack → FAIL")
        return False

    top = stack[-1]
    success = top != b'' and top != bytes([0])
    print(f"\nFINAL: Stack top = {top.hex()} → {'SUCCESS ✓' if success else 'FAIL ✗'}")
    return success

# Run the simulation
result = simulate_execution(unlocking_script, locking_script)

# ========================
# STEP 5: Test failure case
# ========================
print("\n=== TESTING WRONG SECRET ===")
wrong_unlocking = build_unlocking_script(b"wrong secret")
failure = simulate_execution(wrong_unlocking, locking_script)
print(f"Wrong secret result: {'SUCCESS' if failure else 'FAIL (expected)'}")

# ========================
# STEP 6: Wrap in P2SH for relay compatibility
# ========================
def wrap_in_p2sh(redeem_script: bytes) -> tuple:
    """
    Wrap a custom script in P2SH for standard relay.
    Returns (p2sh_scriptPubKey, p2sh_scriptSig_prefix)
    """
    script_hash = hash160(redeem_script)

    # P2SH locking script: OP_HASH160 <hash> OP_EQUAL
    p2sh_lock = bytes([0xa9, 0x14]) + script_hash + bytes([0x87])

    print(f"\nP2SH wrapping:")
    print(f"  Redeem script hash: {script_hash.hex()}")
    print(f"  P2SH scriptPubKey: {p2sh_lock.hex()}")
    print(f"  P2SH address prefix: 3... (mainnet) or 2... (testnet)")

    return p2sh_lock

p2sh_output = wrap_in_p2sh(locking_script)

Technical Insight

This topic covers essential mechanics for Chapter 12. Understanding these details is key to mastering advanced Bitcoin script constructions like Taproot and specialized covenants.

☕ Help support TeachMeBitcoin

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:

Ethereum: 0x578417C51783663D8A6A811B3544E1f779D39A85
Bitcoin: bc1q77k9e95rn669kpzyjr8ke9w95zhk7pa5s63qzz
Solana: 4ycT2ayqeMucixj3wS8Ay8Tq9NRDYRPKYbj3UGESyQ4J
Address copied to clipboard!