TeachMeBitcoin

Both of these verify correctly for the same message and public key:

From TeachMeBitcoin, the free encyclopedia Reading time: 4 min

23. Low-S Signature Requirement (BIP 66)

The Problem: Signature Malleability

Bitcoin's ECDSA signatures have a mathematical property: for any valid signature (r, s), the signature (r, n - s) is also valid, where n is the order of the secp256k1 group. This means every ECDSA signature has two valid forms — the original (r, s) and the "negated-S" version (r, n-s).

n = 0xFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFEBAAEDCE6AF48A03BBFD25E8CD0364141

# Both of these verify correctly for the same message and public key:
(r, s)         → valid ECDSA signature
(r, n - s)     → also valid ECDSA signature

Why This Matters for Transaction IDs

In legacy transactions (pre-SegWit), the scriptSig is part of the transaction data used to compute the TXID. Since the scriptSig contains the DER-encoded signature, swapping s for n-s changes the DER encoding, which changes the TXID:

Original transaction:
  TXID_1 = HASH256(version + inputs_with_sig_s + outputs + locktime)

Malleable transaction (same content, different sig):
  TXID_2 = HASH256(version + inputs_with_sig_(n-s) + outputs + locktime)

TXID_1 ≠ TXID_2 but both are valid and spend the same UTXO!

This was exploited in the MtGox exchange hack and created problems for payment channels and other protocols that relied on TXID stability.

BIP 66: Strict DER Encoding

BIP 66 (activated July 2015) introduced strict DER encoding rules, which among other requirements mandated that the s value must be in the "lower half" of the group order:

# BIP 66 Low-S requirement:
s <= n/2

# Where:
n = 0xFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFEBAAEDCE6AF48A03BBFD25E8CD0364141
n/2 = 0x7FFFFFFFFFFFFFFFFFFFFFFFFFFFFFFF5D576E7357A4501DDFE92F46681B20A0

# If s > n/2, normalize it:
s_normalized = n - s  (which is < n/2)

Why n/2 is the Cutoff

The group of valid s values is [1, n-1]. For every s > n/2, there exists a "smaller" valid signature n-s < n/2. The Low-S rule picks the canonical representative from each pair, eliminating ambiguity:

# Before BIP 66:
Both (r, s) and (r, n-s) are acceptable → 2 valid signatures per key/message pair

# After BIP 66 (Low-S rule):
Only (r, s) where s <= n/2 is acceptable → 1 valid signature per key/message pair

Implementing Low-S Enforcement

SECP256K1_ORDER = 0xFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFEBAAEDCE6AF48A03BBFD25E8CD0364141
HALF_ORDER = SECP256K1_ORDER // 2

def normalize_signature(r: int, s: int):
    """Ensure signature uses low-S form."""
    if s > HALF_ORDER:
        s = SECP256K1_ORDER - s
    return r, s

def is_low_s(s: int) -> bool:
    return s <= HALF_ORDER

def verify_bip66_signature(sig_bytes: bytes) -> bool:
    """Verify signature is valid DER and uses low-S."""
    r, s = parse_der_signature(sig_bytes[:-1])  # strip sighash byte
    if not is_low_s(s):
        raise ScriptError("BIP 66 violation: high-S signature")
    return True

BIP 66 Enforcement Levels

Non-standard:    High-S signatures are non-standard (won't be relayed by default)
SCRIPT_VERIFY_LOW_S: Consensus enforcement flag (activated at BIP 66 activation)

# In Bitcoin Core:
# "Low-S" is enforced as a consensus rule since block 363,724 (July 2015)
# Prior transactions with high-S remain valid (they were mined under old rules)

SegWit and TXID Malleability

SegWit (BIP 141, activated August 2017) solves signature malleability more fundamentally by separating witness data (including signatures) from the transaction data used to compute the TXID:

Legacy TXID:
  txid = HASH256(version + vin + vout + locktime)
  → scriptSig (with signature) is inside vin → TXID is malleable

SegWit TXID (wtxid for witness, txid for non-witness):
  txid  = HASH256(version + vin_without_witness + vout + locktime)
  wtxid = HASH256(version + marker + flag + vin + vout + witness + locktime)
  → txid does NOT include signature → TXID is not malleable even with high-S

Schnorr Signatures: No Malleability

BIP 340 Schnorr signatures don't have the s-negation malleability issue because:

# Schnorr signature (R, s):
# R is a curve point (represented as x-coordinate, 32 bytes)
# s is a scalar mod n

# There is no (R, n-s) equivalent that also verifies
# The verification equation: s*G = R + H(R||P||m)*P
# Negating s gives:        -s*G = -(R + H(R||P||m)*P)
# Which would verify against -P (a different public key)
# So high-S Schnorr signatures simply don't verify → no malleability
☕ 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!