Menu
BIP-340 Final

BIP 340: Schnorr Signatures for secp256k1

BIP 340 Schnorr signatures for secp256k1 — official test vectors plus a runnable Python verifier, and why Schnorr beats ECDSA in Taproot.

Type Bitcoin Improvement Proposal
Number 340
Status Final
Authors Pieter Wuille, Jonas Nick, Tim Ruffing
Original https://github.com/bitcoin/bips/blob/master/bip-0340.mediawiki

BIP-340: Schnorr Signatures for secp256k1

BIP-340 defines Schnorr signatures for Bitcoin, activated with Taproot in November 2021. Schnorr signatures are simpler and more efficient than ECDSA.

Why Schnorr?

Advantages Over ECDSA

FeatureECDSASchnorr
Signature size71-73 bytes64 bytes
VerificationSlowerFaster
Batch verificationNoYes
Key aggregationComplex (MuSig)Native
Provable securityAssumedProven

Key Benefits

  1. Smaller signatures: 64 bytes vs 71-73 bytes
  2. Batch verification: Verify multiple signatures faster than individually
  3. Native multisig: n-of-n looks like single sig
  4. Simpler math: Easier to analyze and implement

Signature Scheme

Key Generation

Same as ECDSA—secp256k1 curve:

Private key: d (256-bit scalar)
Public key: P = d × G

Public Key Encoding

BIP-340 uses x-only public keys (32 bytes instead of 33):

  • Only x-coordinate stored
  • y-coordinate implicitly even
  • If y is odd, negate private key
def lift_x(x):
    """Recover point from x-coordinate"""
    y_sq = (x**3 + 7) % p
    y = modular_sqrt(y_sq, p)
    if y % 2 != 0:
        y = p - y
    return (x, y)

Signing Algorithm

def schnorr_sign(message, private_key):
    # Ensure private key yields even y
    d = private_key
    P = d * G
    if P.y % 2 != 0:
        d = n - d

    # Generate nonce deterministically
    t = xor(bytes(d), tagged_hash("BIP0340/aux", aux_rand))
    k = int(tagged_hash("BIP0340/nonce", t || bytes(P.x) || message)) % n
    if k == 0:
        raise Error("Invalid nonce")

    R = k * G
    if R.y % 2 != 0:
        k = n - k

    # Challenge
    e = int(tagged_hash("BIP0340/challenge", bytes(R.x) || bytes(P.x) || message)) % n

    # Signature
    s = (k + e * d) % n

    return bytes(R.x) || bytes(s)

Verification Algorithm

def schnorr_verify(message, public_key_x, signature):
    P = lift_x(public_key_x)
    r = int(signature[:32])
    s = int(signature[32:])

    if r >= p or s >= n:
        return False

    e = int(tagged_hash("BIP0340/challenge", bytes(r) || bytes(P.x) || message)) % n

    R = s * G - e * P

    if R.y % 2 != 0:
        return False
    if R.x != r:
        return False

    return True

Tagged Hashes

BIP-340 uses domain-separated hashes:

def tagged_hash(tag, message):
    tag_hash = sha256(tag.encode())
    return sha256(tag_hash + tag_hash + message)

Tags used:

  • BIP0340/aux - Auxiliary randomness
  • BIP0340/nonce - Nonce generation
  • BIP0340/challenge - Signature challenge

Batch Verification

Verify n signatures faster than n individual verifications:

def batch_verify(messages, public_keys, signatures):
    # Parse all signatures
    points = []
    for i, (m, pk, sig) in enumerate(zip(messages, public_keys, signatures)):
        P = lift_x(pk)
        r, s = parse_signature(sig)
        e = challenge(r, P.x, m)
        R = lift_x(r)

        # Random coefficient
        a = random_scalar() if i > 0 else 1

        points.append((a, R))
        points.append((a * e, P))
        points.append((a * s, -G))

    # Single multi-scalar multiplication
    result = multi_scalar_mult(points)
    return result == INFINITY

Speedup: ~2x for 100 signatures.

Key Aggregation (MuSig)

Multiple parties create a joint public key:

P_agg = P_1 + P_2 + ... + P_n

Single signature valid for aggregate key:

  • Looks like single-sig on chain
  • All n parties must participate
  • Enables efficient n-of-n multisig

MuSig2 Protocol

Improved 2-round protocol:

  1. Round 1: Exchange nonce commitments
  2. Round 2: Exchange nonces, create partial signatures
# Simplified MuSig2
def musig2_sign(private_keys, message):
    # Each party generates two nonces
    R1, R2 = generate_nonces()

    # Aggregate nonces
    R_agg = sum(R1_i) + b * sum(R2_i)

    # Each party creates partial signature
    s_i = k_i + e * a_i * d_i

    # Aggregate
    s = sum(s_i)

    return (R_agg.x, s)

Implementation

JavaScript (noble-secp256k1)

import * as secp from '@noble/secp256k1';

// Sign
const signature = await secp.schnorr.sign(messageHash, privateKey);

// Verify
const isValid = await secp.schnorr.verify(signature, messageHash, publicKey);

Python (reference implementation)

from bip340 import schnorr_sign, schnorr_verify

# Sign
sig = schnorr_sign(msg, seckey, aux_rand)

# Verify
valid = schnorr_verify(msg, pubkey, sig)

Test Vectors

These are the official BIP-340 test vectors. Every conforming implementation must reproduce them. A signature is 64 bytes: R.x (bytes 0–31) followed by s (bytes 32–63).

Vector 0 — valid (secret key …0003, all-zero message):

Public Key: F9308A019258C31049344F85F89D5229B531C845836F99B08601F113BCE036F9
Message:    0000000000000000000000000000000000000000000000000000000000000000
Signature:  E907831F80848D1069A5371B402410364BDF1C5F8307B0084C55F1CE2DCA821525F66A4A85EA8B71E482A74F382D2CE5EBEEE8FDB2172F477DF4900D310536C0
Result:     valid

Vector 3 — valid (all-FF message; fails if the message is reduced modulo p or n):

Public Key: 25D1DFF95105F5253C4022F628A996AD3A0D95FBF21D468A1B33F8C160D8F517
Message:    FFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFF
Signature:  7EB0509757E246F19449885651611CB965ECC1A187DD51B64FDA1EDC9637D5EC97582B9CB13DB3933705B32BA982AF5AF25FD78881EBB32771FC5922EFC66EA3
Result:     valid

Vector 5 — invalid (public key not on the curve — a verifier must reject it):

Public Key: EEFDEA4CDB677750A420FEE807EACF21EB9898AE79B9768766E4FAA04A2D4A34
Message:    243F6A8885A308D313198A2E03707344A4093822299F31D0082EFA98EC4E6C89
Signature:  6CFF5C3BA86C69EA4B7376F31A9BCB4F74C1976089B2D9963DA2E5543E17776969E89B4C5564D00349106B8497785DD7D1D713A8AE82B32FA79D5F7FC407D39B
Result:     rejected

Verify it yourself

Pure-Python BIP-340 verification — standard library only, no dependencies. It checks all three vectors above against their expected results:

"""BIP-340 Schnorr verification, checked against the official test vectors."""
import hashlib

p = 0xFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFEFFFFFC2F
n = 0xFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFEBAAEDCE6AF48A03BBFD25E8CD0364141
G = (0x79BE667EF9DCBBAC55A06295CE870B07029BFCDB2DCE28D959F2815B16F81798,
     0x483ADA7726A3C4655DA4FBFC0E1108A8FD17B448A68554199C47D08FFB10D4B8)

def tagged_hash(tag, msg):
    t = hashlib.sha256(tag.encode()).digest()
    return hashlib.sha256(t + t + msg).digest()

def point_add(P1, P2):
    if P1 is None: return P2
    if P2 is None: return P1
    if P1[0] == P2[0] and P1[1] != P2[1]: return None
    if P1 == P2:
        lam = 3 * P1[0] * P1[0] * pow(2 * P1[1], p - 2, p) % p
    else:
        lam = (P2[1] - P1[1]) * pow(P2[0] - P1[0], p - 2, p) % p
    x3 = (lam * lam - P1[0] - P2[0]) % p
    return (x3, (lam * (P1[0] - x3) - P1[1]) % p)

def point_mul(P, k):
    R = None
    while k:
        if k & 1: R = point_add(R, P)
        P = point_add(P, P); k >>= 1
    return R

def lift_x(x):
    if x >= p: return None
    y2 = (pow(x, 3, p) + 7) % p
    y = pow(y2, (p + 1) // 4, p)
    if pow(y, 2, p) != y2: return None
    return (x, y if y % 2 == 0 else p - y)

def schnorr_verify(msg: bytes, pubkey: bytes, sig: bytes) -> bool:
    if len(pubkey) != 32 or len(sig) != 64:
        return False
    P = lift_x(int.from_bytes(pubkey, "big"))
    if P is None:
        return False
    r = int.from_bytes(sig[0:32], "big")
    s = int.from_bytes(sig[32:64], "big")
    if r >= p or s >= n:
        return False
    e = int.from_bytes(tagged_hash("BIP0340/challenge", sig[0:32] + pubkey + msg), "big") % n
    R = point_add(point_mul(G, s), point_mul(P, n - e))
    if R is None or R[1] % 2 != 0 or R[0] != r:
        return False
    return True

# (public key, message, signature, expected result) — official BIP-340 vectors 0, 3, 5
vectors = [
    ("F9308A019258C31049344F85F89D5229B531C845836F99B08601F113BCE036F9",
     "0000000000000000000000000000000000000000000000000000000000000000",
     "E907831F80848D1069A5371B402410364BDF1C5F8307B0084C55F1CE2DCA821525F66A4A85EA8B71E482A74F382D2CE5EBEEE8FDB2172F477DF4900D310536C0", True),
    ("25D1DFF95105F5253C4022F628A996AD3A0D95FBF21D468A1B33F8C160D8F517",
     "FFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFF",
     "7EB0509757E246F19449885651611CB965ECC1A187DD51B64FDA1EDC9637D5EC97582B9CB13DB3933705B32BA982AF5AF25FD78881EBB32771FC5922EFC66EA3", True),
    ("EEFDEA4CDB677750A420FEE807EACF21EB9898AE79B9768766E4FAA04A2D4A34",
     "243F6A8885A308D313198A2E03707344A4093822299F31D0082EFA98EC4E6C89",
     "6CFF5C3BA86C69EA4B7376F31A9BCB4F74C1976089B2D9963DA2E5543E17776969E89B4C5564D00349106B8497785DD7D1D713A8AE82B32FA79D5F7FC407D39B", False),
]

for pubkey_hex, msg_hex, sig_hex, expected in vectors:
    result = schnorr_verify(bytes.fromhex(msg_hex), bytes.fromhex(pubkey_hex), bytes.fromhex(sig_hex))
    print(f"{'PASS' if result == expected else 'FAIL'}  expected={expected}  got={result}")

Expected output:

PASS  expected=True  got=True
PASS  expected=True  got=True
PASS  expected=False  got=False

Security Properties

Proven Security

Schnorr security reduces to the Discrete Logarithm Problem (DLP) in the Random Oracle Model.

Resistance to Attacks

AttackMitigation
Related-keyUse random auxiliary data
Nonce reuseDeterministic nonce
Key cancellationMuSig key aggregation

For Agents

When to Use Schnorr

  • All Taproot (P2TR) transactions
  • Efficient multisig via MuSig2
  • Batch verification scenarios

Libraries

LanguageLibrary
JavaScript@noble/secp256k1
Pythonpython-secp256k1, bip340 ref
Rustsecp256k1, k256

Signature Format

64 bytes total:
- bytes 0-31: R.x (x-coordinate of nonce point)
- bytes 32-63: s (scalar)

Machine-Readable Summary

{
  "bip": 340,
  "title": "Schnorr Signatures for secp256k1",
  "status": "final",
  "signature_size": 64,
  "public_key_size": 32,
  "features": ["batch-verification", "key-aggregation", "provable-security"],
  "hash_tags": ["BIP0340/aux", "BIP0340/nonce", "BIP0340/challenge"],
  "libraries": {
    "javascript": "@noble/secp256k1",
    "rust": "secp256k1",
    "python": "bip340"
  }
}