Schnorr signature operations for Bitcoin according to BIP340
Provides comprehensive Schnorr signature functionality including:
- Deterministic and randomized signature generation
- Public key derivation from private keys
- Signature verification with proper point validation
- Integration with Bitcoin's Taproot upgrade
Key advantages over ECDSA:
- Linear signature aggregation
- Smaller signature size (64 bytes vs 71-73 bytes for ECDSA)
- Batch verification for improved performance
- Eliminates signature malleability
- Enables more sophisticated multi-signature schemes
Example
// Basic Schnorr signature workflow
const privateKey = "L1vHfV6GUbMJSvFaqjnButzwq5x4ThdFaotpUgsfScwMNKjdGVuS";
const message = "Hello Schnorr!";
// Sign the message
const signature = schnorr_sig.sign(privateKey, message);
// Get the public key
const publicKey = schnorr_sig.retrieve_public_key(privateKey);
// Verify the signature
const isValid = schnorr_sig.verify(signature, message, publicKey);
console.log(isValid); // true
Methods
(static) retrieve_public_key(private_keyopt) → {Uint8Array}
Derives the Schnorr public key from a private key according to BIP340
Computes the x-only public key representation used in BIP340:
- Compute the full public key point P = d*G
- If P.y is odd, negate d to make P.y even
- Return only the x-coordinate (32 bytes)
The x-only representation saves space and simplifies signature verification while maintaining the same security properties as full public keys.
Parameters:
| Name | Type | Attributes | Default | Description |
|---|---|---|---|---|
private_key |
string |
<optional> |
"L1vHfV6GUbMJSvFaqjnButzwq5x4ThdFaotpUgsfScwMNKjdGVuS" | WIF-encoded private key |
Throws:
-
If private key is invalid
- Type
- Error
Returns:
32-byte x-only public key for use with Schnorr signatures
- Type
- Uint8Array
Example
// Get public key for Schnorr operations
const privateKey = "L1vHfV6GUbMJSvFaqjnButzwq5x4ThdFaotpUgsfScwMNKjdGVuS";
const publicKey = schnorr_sig.retrieve_public_key(privateKey);
console.log(publicKey.length); // 32 bytes (x-only)
// Use in signature verification
const message = "Taproot transaction";
const signature = schnorr_sig.sign(privateKey, message);
const verified = schnorr_sig.verify(signature, message, publicKey);
console.log(verified); // true
// Compare with different private key
const otherPrivKey = "KwDiBf89QgGbjEhKnhXJuH7LrciVrZi3qYjgd9M7rFU73sVHnoWn";
const otherPubKey = schnorr_sig.retrieve_public_key(otherPrivKey);
console.log(Buffer.from(publicKey).equals(Buffer.from(otherPubKey))); // false
(static) sign(private_keyopt, msgopt, auxRandopt) → {Uint8Array}
Creates a Schnorr signature for a given message using BIP340 specification
The signing process follows BIP340:
- Parse and validate the private key
- Compute the public key P = d*G (where d is private key)
- Generate nonce k using auxiliary randomness (prevents side-channel attacks)
- Compute R = k*G and ensure R.y is even (BIP340 requirement)
- Compute challenge e = SHA256(R.x || P || m)
- Compute signature s = (k + e*d) mod n
- Return signature as 64-byte array: R.x || s
Parameters:
| Name | Type | Attributes | Default | Description |
|---|---|---|---|---|
private_key |
string |
<optional> |
"L1vHfV6GUbMJSvFaqjnButzwq5x4ThdFaotpUgsfScwMNKjdGVuS" | WIF-encoded private key |
msg |
string |
<optional> |
"Hello world" | Message to sign (will be UTF-8 encoded) |
auxRand |
Uint8Array |
<optional> |
randomBytes(32) | 32 bytes of auxiliary randomness for nonce generation |
Throws:
-
If private key is invalid or signing fails
- Type
- Error
Returns:
64-byte Schnorr signature (32-byte R.x + 32-byte s)
- Type
- Uint8Array
Example
// Sign with default randomness
const signature = schnorr_sig.sign(privateKey, "Hello Bitcoin!");
console.log(signature.length); // 64 bytes
// Sign with custom auxiliary randomness
const customAux = new Uint8Array(32).fill(0xaa);
const deterministicSig = schnorr_sig.sign(privateKey, "Hello Bitcoin!", customAux);
// Multiple signatures of same message with different aux data will differ
const sig1 = schnorr_sig.sign(privateKey, "test", new Uint8Array(32).fill(1));
const sig2 = schnorr_sig.sign(privateKey, "test", new Uint8Array(32).fill(2));
console.log(Buffer.from(sig1).equals(Buffer.from(sig2))); // false
(static) verify(sig, msgopt, public_key) → {boolean}
Verifies a Schnorr signature against a message and public key
The verification process implements BIP340 algorithm:
- Parse the 64-byte signature into R.x (32 bytes) and s (32 bytes)
- Validate that R.x and s are valid field elements
- Compute challenge e = SHA256(R.x || P || m)
- Compute point S = sG - eP
- Verify that S.x == R.x and S.y is even
This verification is more efficient than ECDSA and allows for batch verification when verifying multiple signatures simultaneously.
Parameters:
| Name | Type | Attributes | Default | Description |
|---|---|---|---|---|
sig |
Uint8Array | Buffer | 64-byte Schnorr signature to verify |
||
msg |
string |
<optional> |
"Hello World" | Original message that was signed |
public_key |
Uint8Array | Buffer | 32-byte x-only public key (BIP340 format) |
Returns:
True if signature is valid, false otherwise
- Type
- boolean
Example
// Standard verification
const signature = schnorr_sig.sign(privateKey, "Hello Schnorr!");
const publicKey = schnorr_sig.retrieve_public_key(privateKey);
const isValid = schnorr_sig.verify(signature, "Hello Schnorr!", publicKey);
console.log(isValid); // true
// Invalid signature detection
const invalidSig = new Uint8Array(64); // All zeros
const isInvalid = schnorr_sig.verify(invalidSig, "test", publicKey);
console.log(isInvalid); // false
// Wrong message detection
const wrongMsg = schnorr_sig.verify(signature, "Wrong message", publicKey);
console.log(wrongMsg); // false