Example tags used in Tapscript:
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:
-
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
-
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
-
Domain separation via tagged hashes — prevents sighash values from being reusable across different contexts
-
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.
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: