TeachMeBitcoin

Conceptual model:

From TeachMeBitcoin, the free encyclopedia Reading time: 4 min

10. The Signature Hash (sighash) Algorithm

What is a Signature Hash?

The signature hash (sighash) is the data that is actually signed by the private key during transaction creation. It commits a signature to specific aspects of the transaction, ensuring that the signature authorizes exactly what was intended.

# Conceptual model:
sighash_preimage = serialize(transaction_data, sighash_flags)
z = SHA256(SHA256(sighash_preimage))
signature = ECDSA_sign(private_key, z)

The sighash type is a 1-byte value appended to the DER-encoded signature. It tells the verifier how to reconstruct the sighash preimage for verification.

Sighash Type Values

# Standard sighash types
SIGHASH_ALL            = 0x01  # Signs all inputs and outputs
SIGHASH_NONE           = 0x02  # Signs all inputs, no outputs
SIGHASH_SINGLE         = 0x03  # Signs all inputs, one output

# Modifier (OR with above)
SIGHASH_ANYONECANPAY   = 0x80  # Only signs this input (not others)

# Combined values
SIGHASH_ALL | ANYONECANPAY    = 0x81
SIGHASH_NONE | ANYONECANPAY   = 0x82
SIGHASH_SINGLE | ANYONECANPAY = 0x83

The Legacy Sighash Algorithm (Pre-SegWit)

def get_signature_hash(tx, input_index, script_code, sighash_type):
    """
    Compute sighash for legacy (non-SegWit) transactions.
    """
    # Make a copy to modify
    tx_copy = copy_transaction(tx)

    # Clear all input scripts
    for i, inp in enumerate(tx_copy.inputs):
        inp.script = b''

    # Set the current input's script
    tx_copy.inputs[input_index].script = script_code

    # Apply sighash_type modifications
    base_type = sighash_type & 0x1f
    anyone_can_pay = bool(sighash_type & 0x80)

    if base_type == SIGHASH_NONE:
        tx_copy.outputs = []
        for i, inp in enumerate(tx_copy.inputs):
            if i != input_index:
                inp.sequence = 0

    elif base_type == SIGHASH_SINGLE:
        # Only keep output at same index as input
        tx_copy.outputs = tx_copy.outputs[:input_index + 1]
        # Set outputs before index to "null" (-1 value, empty script)
        for i in range(input_index):
            tx_copy.outputs[i].value = 0xFFFFFFFFFFFFFFFF  # -1
            tx_copy.outputs[i].script = b''
        for i, inp in enumerate(tx_copy.inputs):
            if i != input_index:
                inp.sequence = 0

    if anyone_can_pay:
        tx_copy.inputs = [tx_copy.inputs[input_index]]

    # Serialize and hash
    preimage = tx_copy.serialize() + struct.pack('<I', sighash_type)
    return hashlib.sha256(hashlib.sha256(preimage).digest()).digest()

The Sighash Bug: SIGHASH_SINGLE with More Inputs than Outputs

There is a known quirk in the legacy sighash algorithm: when SIGHASH_SINGLE is used and the input index exceeds the number of outputs, the hash value defaults to 0x01 (the integer 1):

# The bug (simplified):
if base_type == SIGHASH_SINGLE and input_index >= len(tx.outputs):
    # This is a protocol quirk — returns hash of "1" as 256-bit little-endian
    return b'\x01' + b'\x00' * 31

# Because 1 in little-endian 32 bytes is:
# 0100000000000000000000000000000000000000000000000000000000000000

This means any private key can "sign" this transaction with SIGHASH_SINGLE when its input has no corresponding output — a significant quirk that has been exploited in practice.

BIP143 SegWit Sighash Algorithm

BIP143 completely redesigned the sighash algorithm for SegWit inputs. Key improvements:

def segwit_sighash(tx, input_index, script_code, value, sighash_type):
    """
    BIP143 sighash for SegWit v0 inputs.
    Commits to input amounts — hardware wallets can verify they're not overpaying fees.
    """
    anyone_can_pay = bool(sighash_type & SIGHASH_ANYONECANPAY)
    base_type = sighash_type & 0x1f

    # Compute hashPrevouts
    if not anyone_can_pay:
        outpoints = b''.join(inp.outpoint.serialize() for inp in tx.inputs)
        hash_prevouts = double_sha256(outpoints)
    else:
        hash_prevouts = b'\x00' * 32

    # Compute hashSequence
    if not anyone_can_pay and base_type not in (SIGHASH_SINGLE, SIGHASH_NONE):
        sequences = b''.join(struct.pack('<I', inp.sequence) for inp in tx.inputs)
        hash_sequence = double_sha256(sequences)
    else:
        hash_sequence = b'\x00' * 32

    # Compute hashOutputs
    if base_type not in (SIGHASH_SINGLE, SIGHASH_NONE):
        outputs = b''.join(out.serialize() for out in tx.outputs)
        hash_outputs = double_sha256(outputs)
    elif base_type == SIGHASH_SINGLE and input_index < len(tx.outputs):
        hash_outputs = double_sha256(tx.outputs[input_index].serialize())
    else:
        hash_outputs = b'\x00' * 32

    # Assemble preimage
    preimage = (
        struct.pack('<I', tx.version)
        + hash_prevouts
        + hash_sequence
        + tx.inputs[input_index].outpoint.serialize()
        + encode_script(script_code)
        + struct.pack('<Q', value)          # ← Input amount committed here
        + struct.pack('<I', tx.inputs[input_index].sequence)
        + hash_outputs
        + struct.pack('<I', tx.locktime)
        + struct.pack('<I', sighash_type)
    )
    return double_sha256(preimage)
☕ 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!