Menu
BIP-bip-340 Final

BIP-340: Schnorr Signatures

Schnorr signature scheme for Bitcoin. Simpler, smaller, and more efficient than ECDSA with native key aggregation.

Type Bitcoin Improvement Proposal
Number bip-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

Vector 1

Secret Key: 0000000000000000000000000000000000000000000000000000000000000003
Public Key: F9308A019258C31049344F85F89D5229B531C845836F99B08601F113BCE036F9
Message: 0000000000000000000000000000000000000000000000000000000000000000
Signature: E907831F80848D1069A5371B402410364BDF1C5F8307B0084C55F1CE2DCA8215...

Vector 2 (Empty message)

Secret Key: 0B432B2677937381AEF05BB02A66ECD012773062CF3FA2549E44F58ED2401710
Message: (empty)
Signature: ...

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"
  }
}