TeachMeBitcoin

Example tags used in Tapscript:

From TeachMeBitcoin, the free encyclopedia Reading time: 4 min

5. Tapscript Signature Hashing (BIP 341)

The signature hash (sighash) in Tapscript is the data that gets hashed and then signed by the spending key. Getting the sighash algorithm right is critical for security — it defines exactly what a signature commits to, and therefore what a signature proves.

Why a New Sighash Algorithm

Legacy Bitcoin Script had three different sighash algorithms over its history: the original algorithm (buggy for large transactions), the SegWit v0 BIP 143 algorithm (fixed quadratic hashing), and various per-sighash-flag variants. Each fixed some problems but introduced new ones or left old ones in place.

Tapscript's sighash algorithm (defined in the signing section of BIP 341) was designed from scratch with specific goals:

  1. Commit to all input amounts — hardware wallets in legacy transactions could be tricked into signing a transaction where they didn't know the total fees

  2. Commit to all input scriptPubKeys — prevents a class of attacks where a signer doesn't know if they're signing a SegWit or legacy input

  3. Domain separation via tagged hashes — prevents sighash values from being reusable across different contexts

  4. No quadratic hashing — the algorithm is O(n) in the number of inputs

Tagged Hashes

Tapscript uses tagged hashes throughout its sighash construction. A tagged hash is:

def tagged_hash(tag: str, data: bytes) -> bytes:
    tag_hash = sha256(tag.encode())
    return sha256(tag_hash + tag_hash + data)

# Example tags used in Tapscript:
# "TapSighash"
# "TapLeaf"
# "TapBranch"
# "TapTweak"

The double-prefix of the tag hash ensures that the output of a tagged hash cannot be reused as the input to a different tagged hash operation, providing strong domain separation.

The Tapscript Sighash Algorithm

def tapscript_sighash(tx, input_index, utxos, hash_type, annex=None, tap_leaf=None):
    """
    Compute the Tapscript signature hash per BIP 341.

    tx:           the spending transaction
    input_index:  which input we are signing
    utxos:        list of all UTXOs being spent (needed for all inputs)
    hash_type:    SIGHASH flag byte
    annex:        optional annex data (BIP 341)
    tap_leaf:     tapscript leaf hash (for script path spending)
    """

    # Epoch (always 0x00 for current Tapscript)
    epoch = bytes([0x00])

    # Hash type
    hash_type_byte = bytes([hash_type])

    # Transaction data
    n_version = tx.nVersion.to_bytes(4, 'little')
    n_locktime = tx.nLockTime.to_bytes(4, 'little')

    # Commit to all inputs
    sha_prevouts = sha256(b''.join(
        inp.prevout.serialize() for inp in tx.vin
    ))

    # Commit to all input amounts (NEW vs legacy)
    sha_amounts = sha256(b''.join(
        utxo.nValue.to_bytes(8, 'little') for utxo in utxos
    ))

    # Commit to all input scriptPubKeys (NEW vs legacy)
    sha_scriptpubkeys = sha256(b''.join(
        ser_string(utxo.scriptPubKey) for utxo in utxos
    ))

    # Commit to all sequences
    sha_sequences = sha256(b''.join(
        inp.nSequence.to_bytes(4, 'little') for inp in tx.vin
    ))

    # Commit to all outputs
    sha_outputs = sha256(b''.join(
        out.serialize() for out in tx.vout
    ))

    # Spend type: key path vs script path, annex presence
    spend_type = 0x00
    if annex: spend_type |= 0x01
    if tap_leaf: spend_type |= 0x02
    spend_type_byte = bytes([spend_type])

    # Input-specific data
    this_input = tx.vin[input_index]
    input_data = (
        utxos[input_index].nValue.to_bytes(8, 'little') +
        ser_string(utxos[input_index].scriptPubKey) +
        this_input.nSequence.to_bytes(4, 'little')
    )

    # Optional: annex hash
    annex_data = b''
    if annex:
        annex_data = sha256(ser_string(annex))

    # Optional: tap leaf hash (for script path)
    tap_leaf_data = b''
    if tap_leaf:
        tap_leaf_data = tap_leaf  # 32-byte hash

    # Assemble preimage
    preimage = (
        epoch + hash_type_byte + n_version + n_locktime +
        sha_prevouts + sha_amounts + sha_scriptpubkeys +
        sha_sequences + sha_outputs + spend_type_byte +
        input_data + annex_data + tap_leaf_data
    )

    return tagged_hash("TapSighash", preimage)

Sighash Flags in Tapscript

Tapscript supports four sighash flag values:

SIGHASH_DEFAULT    = 0x00  (new in Tapscript — same as ALL but 1 byte shorter signature)
SIGHASH_ALL        = 0x01
SIGHASH_NONE       = 0x02
SIGHASH_SINGLE     = 0x03
SIGHASH_ANYONECANPAY = 0x80  (can be OR'd with above)

SIGHASH_DEFAULT (0x00) is new to Tapscript. It is identical to SIGHASH_ALL in what it commits to, but the signature is 64 bytes instead of 65 bytes (the sighash byte is omitted, saving 1 byte per signature). This is the recommended sighash type for most Tapscript use cases.

Technical Insight

This topic covers essential mechanics for Chapter 11. 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!