Skip to content

jmcore.btc_script

jmcore.btc_script

Bitcoin script utilities for fidelity bonds.

Uses python-bitcointx for script operations where appropriate.

Classes

BondAddressInfo dataclass

Derived address information for a fidelity bond UTXO.

Source code in jmcore/src/jmcore/btc_script.py
150
151
152
153
154
155
156
157
158
159
@dataclass(frozen=True)
class BondAddressInfo:
    """Derived address information for a fidelity bond UTXO."""

    address: str
    """Bech32 P2WSH address (e.g. bc1q... or tb1q...)"""
    scriptpubkey: bytes
    """P2WSH scriptPubKey bytes (OP_0 <32-byte-hash>)"""
    witness_script: bytes
    """The timelocked witness script: <locktime> OP_CLTV OP_DROP <pubkey> OP_CHECKSIG"""
Attributes
address: str instance-attribute

Bech32 P2WSH address (e.g. bc1q... or tb1q...)

scriptpubkey: bytes instance-attribute

P2WSH scriptPubKey bytes (OP_0 <32-byte-hash>)

witness_script: bytes instance-attribute

The timelocked witness script: OP_CLTV OP_DROP OP_CHECKSIG

Functions:

derive_bond_address(utxo_pub: bytes, locktime: int, network: str = 'mainnet') -> BondAddressInfo

Derive the P2WSH fidelity bond address from a bond proof's public key and locktime.

Given the UTXO public key and locktime from a fidelity bond proof, this reconstructs the timelocked witness script and derives the P2WSH address. This address can then be used by any backend (full node, neutrino, mempool) to look up the bond UTXO.

Args: utxo_pub: 33-byte compressed public key from the bond proof locktime: Locktime value from the bond proof (Unix timestamp) network: Bitcoin network ("mainnet", "testnet", "signet", "regtest")

Returns: BondAddressInfo with address, scriptpubkey, and witness_script

Raises: ValueError: If utxo_pub is not 33 bytes

Source code in jmcore/src/jmcore/btc_script.py
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
194
195
196
197
198
199
200
201
202
203
204
def derive_bond_address(
    utxo_pub: bytes,
    locktime: int,
    network: str = "mainnet",
) -> BondAddressInfo:
    """
    Derive the P2WSH fidelity bond address from a bond proof's public key and locktime.

    Given the UTXO public key and locktime from a fidelity bond proof, this reconstructs
    the timelocked witness script and derives the P2WSH address. This address can then be
    used by any backend (full node, neutrino, mempool) to look up the bond UTXO.

    Args:
        utxo_pub: 33-byte compressed public key from the bond proof
        locktime: Locktime value from the bond proof (Unix timestamp)
        network: Bitcoin network ("mainnet", "testnet", "signet", "regtest")

    Returns:
        BondAddressInfo with address, scriptpubkey, and witness_script

    Raises:
        ValueError: If utxo_pub is not 33 bytes
    """
    # Import here to avoid circular imports (bitcoin.py imports from btc_script.py)
    from jmcore.bitcoin import scriptpubkey_to_address

    if len(utxo_pub) != 33:
        raise ValueError(f"Invalid utxo_pub length: {len(utxo_pub)}, expected 33")

    # Reconstruct the timelocked witness script
    witness_script = mk_freeze_script(utxo_pub.hex(), locktime)

    # Hash to P2WSH scriptPubKey
    scriptpubkey = redeem_script_to_p2wsh_script(witness_script)

    # Derive bech32 address
    address = scriptpubkey_to_address(scriptpubkey, network)

    return BondAddressInfo(
        address=address,
        scriptpubkey=scriptpubkey,
        witness_script=witness_script,
    )

disassemble_script(script_bytes: bytes) -> str

Disassemble a Bitcoin script into human-readable form.

Uses python-bitcointx for proper parsing.

Args: script_bytes: Script bytes

Returns: Human-readable script representation

Source code in jmcore/src/jmcore/btc_script.py
 77
 78
 79
 80
 81
 82
 83
 84
 85
 86
 87
 88
 89
 90
 91
 92
 93
 94
 95
 96
 97
 98
 99
100
101
102
103
104
105
106
107
108
109
110
def disassemble_script(script_bytes: bytes) -> str:
    """
    Disassemble a Bitcoin script into human-readable form.

    Uses python-bitcointx for proper parsing.

    Args:
        script_bytes: Script bytes

    Returns:
        Human-readable script representation
    """
    script = CScript(script_bytes)
    parts: list[str] = []

    for op in script:
        if isinstance(op, CScriptOp):
            parts.append(str(op))
        elif isinstance(op, bytes):
            # Data push - try to interpret as number if small
            if len(op) <= 5:
                try:
                    num = _decode_scriptnum(op)
                    parts.append(str(num))
                except (ValueError, IndexError):
                    parts.append(f"<{op.hex()}>")
            else:
                parts.append(f"<{op.hex()}>")
        elif isinstance(op, int):
            parts.append(str(op))
        else:
            parts.append(repr(op))

    return " ".join(parts)

mk_freeze_script(pubkey_hex: str, locktime: int) -> bytes

Create a timelocked script using OP_CHECKLOCKTIMEVERIFY.

Script format: OP_CHECKLOCKTIMEVERIFY OP_DROP OP_CHECKSIG

Args: pubkey_hex: Compressed public key as hex string (33 bytes) locktime: Unix timestamp for the locktime

Returns: Script as bytes

Source code in jmcore/src/jmcore/btc_script.py
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
def mk_freeze_script(pubkey_hex: str, locktime: int) -> bytes:
    """
    Create a timelocked script using OP_CHECKLOCKTIMEVERIFY.

    Script format: <locktime> OP_CHECKLOCKTIMEVERIFY OP_DROP <pubkey> OP_CHECKSIG

    Args:
        pubkey_hex: Compressed public key as hex string (33 bytes)
        locktime: Unix timestamp for the locktime

    Returns:
        Script as bytes
    """
    pubkey_bytes = bytes.fromhex(pubkey_hex)
    if len(pubkey_bytes) != 33:
        raise ValueError(f"Invalid pubkey length: {len(pubkey_bytes)}, expected 33")

    # Use python-bitcointx to build the script
    script = CScript([locktime, OP_CHECKLOCKTIMEVERIFY, OP_DROP, pubkey_bytes, OP_CHECKSIG])
    return bytes(script)

parse_freeze_script(script_bytes: bytes) -> tuple[int, bytes]

Parse a canonical JoinMarket fidelity bond witness script.

Returns the locktime and compressed public key from <locktime> OP_CLTV OP_DROP <pubkey> OP_CHECKSIG. Noncanonical script number or push encodings are rejected by reconstructing the script with :func:mk_freeze_script and requiring an exact byte match.

Source code in jmcore/src/jmcore/btc_script.py
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
def parse_freeze_script(script_bytes: bytes) -> tuple[int, bytes]:
    """Parse a canonical JoinMarket fidelity bond witness script.

    Returns the locktime and compressed public key from
    ``<locktime> OP_CLTV OP_DROP <pubkey> OP_CHECKSIG``. Noncanonical script
    number or push encodings are rejected by reconstructing the script with
    :func:`mk_freeze_script` and requiring an exact byte match.
    """
    try:
        operations = list(CScript(script_bytes))
    except Exception as exc:
        raise ValueError(f"Invalid fidelity bond script: {exc}") from exc

    if len(operations) != 5:
        raise ValueError("Fidelity bond script must contain exactly five operations")
    locktime_bytes, op_cltv, op_drop, pubkey, op_checksig = operations
    if not isinstance(locktime_bytes, bytes):
        raise ValueError("Fidelity bond locktime must use a script-number data push")
    if op_cltv != OP_CHECKLOCKTIMEVERIFY or op_drop != OP_DROP:
        raise ValueError("Fidelity bond script is missing OP_CLTV OP_DROP")
    if op_checksig != OP_CHECKSIG:
        raise ValueError("Fidelity bond script is missing OP_CHECKSIG")
    if not isinstance(pubkey, bytes) or len(pubkey) != 33 or pubkey[0] not in (0x02, 0x03):
        raise ValueError("Fidelity bond script must contain a compressed public key")

    locktime = _decode_scriptnum(locktime_bytes)
    if locktime <= 0:
        raise ValueError("Fidelity bond locktime must be positive")
    if mk_freeze_script(pubkey.hex(), locktime) != script_bytes:
        raise ValueError("Fidelity bond script uses a noncanonical encoding")
    return locktime, pubkey

redeem_script_to_p2wsh_script(redeem_script: bytes) -> bytes

Convert a redeem script to P2WSH scriptPubKey.

Args: redeem_script: The redeem script bytes

Returns: P2WSH scriptPubKey (OP_0 <32-byte-hash>)

Source code in jmcore/src/jmcore/btc_script.py
135
136
137
138
139
140
141
142
143
144
145
146
147
def redeem_script_to_p2wsh_script(redeem_script: bytes) -> bytes:
    """
    Convert a redeem script to P2WSH scriptPubKey.

    Args:
        redeem_script: The redeem script bytes

    Returns:
        P2WSH scriptPubKey (OP_0 <32-byte-hash>)
    """
    script_hash = hashlib.sha256(redeem_script).digest()
    script = CScript([OP_0, script_hash])
    return bytes(script)