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
| Feature | ECDSA | Schnorr |
|---|---|---|
| Signature size | 71-73 bytes | 64 bytes |
| Verification | Slower | Faster |
| Batch verification | No | Yes |
| Key aggregation | Complex (MuSig) | Native |
| Provable security | Assumed | Proven |
Key Benefits
- Smaller signatures: 64 bytes vs 71-73 bytes
- Batch verification: Verify multiple signatures faster than individually
- Native multisig: n-of-n looks like single sig
- 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 randomnessBIP0340/nonce- Nonce generationBIP0340/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:
- Round 1: Exchange nonce commitments
- 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
| Attack | Mitigation |
|---|---|
| Related-key | Use random auxiliary data |
| Nonce reuse | Deterministic nonce |
| Key cancellation | MuSig key aggregation |
For Agents
When to Use Schnorr
- All Taproot (P2TR) transactions
- Efficient multisig via MuSig2
- Batch verification scenarios
Libraries
| Language | Library |
|---|---|
| JavaScript | @noble/secp256k1 |
| Python | python-secp256k1, bip340 ref |
| Rust | secp256k1, 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"
}
}