§ 02 · Cryptography
Schnorr signatures
64 bytes that prove you hold the key, for one exact message
On this page 7 sections
A signatureSignature: Proof, made with a private key, that the key's owner approved one exact transaction. Anyone can check it with the public key, and it is useless for any other transaction. proves that whoever holds a private key approved one exact message, without revealing the key. Change a single bit of the message and the signature stops being valid.
Pearl signs every spend with Schnorr signaturesSchnorr signature: The signature scheme Pearl uses to spend coins: 64 bytes, defined in Bitcoin's BIP-340. Pearl transactions never use the older ECDSA signatures. as defined in Bitcoin’s BIP-340. They are short (64 bytes), simple, and built on the elliptic-curve arithmetic from the previous page.
Try it
Sign a message, verify it, then flip one bit and watch verification fail.
What’s inside a signature?
Two 32-byte numbers, back to back. Here is a real BIP-340 signature made with Pearl’s own code, for the message SHA-256(“Pearl”), with BIP-340’s optional extra randomness set to the number 1:
| Part | Value |
|---|---|
| Private key d | 0c28fca386c7a227600b2fe50b7cae11ec86d3bf1fbe471be89827e19d72aa1d |
| Public key P (x-only) | d0de0aaeaefad02b8bdc8a01a1b8b11c696bd3d66a2c5f10780d95b7df42645c |
| Message m | 72bc556112e9538a2433f0a9b87b032eae43c69e367e9dca1b176929dcf20ed5 |
| R.x (first 32 bytes) | 85df688c0be1ff7cf23866497fda839fbb6009b820451ff3600caa2cf32dc490 |
| s (last 32 bytes) | 80e136b1f442d43275a394cf4b007d7031e052499ad02984cea0bbd3179af2db |
(This test key is public. Never use it for real coins.)
How is a signature made?
With private key d and public key P = d·G:
-
Pick a secret nonce
Choose a one-time secret number k, the signing nonceSigning nonce: A secret one-time number used inside each Schnorr signature. It must never repeat for different messages, or the private key can be worked out. Not the same thing as a mining nonce., and compute the point R = k·G.
-
Make the challenge
Hash R’s x-coordinate, the public key and the message with the
BIP0340/challengetagged hash. Read the result as a number e. -
Mix the key in
Compute s = k + e·d, modulo the curve’s order n. The signature is (R.x, s).
The nonce hides the key: s mixes d with a random-looking k, so s alone says nothing about d. The challenge ties everything to this message and this key.
How is it checked?
The verifier knows P, m and the signature, so it can recompute e. It does not know k or d. It checks one equation:
Only someone who knew d could have produced an s that makes both sides match.
The fine print of BIP-340
- Public keys are x-only. If d·G has an odd y, the signer uses n − d instead, so P always has an even y.
- R must have an even y too; if k·G doesn’t, the signer uses n − k.
- The verifier computes R′ = s·G − e·P and accepts only if R′ is not infinity, has an even y, and its x equals the signature’s first 32 bytes.
- s must be below n, and R.x must be below p.
Why must the nonce never repeat?
Because two signatures with the same k give the key away. Subtract them:
Everything except d is public, so d = (s₁ − s₂) / (e₁ − e₂). The same happens if k is ever guessable. This has drained real wallets on other chains.
BIP-340 suggests deriving k from the private key, the message and some fresh randomness. Pearl’s transaction signer, inherited from btcd, derives it deterministically with RFC 6979 instead: the same key and message always give the same k, and a different message always gives a different one.
Both methods give valid BIP-340 signatures, and a verifier can’t tell them apart. For the key and message above, Pearl’s default signer produces a different but equally valid signature, starting c3a52ccdb5b9875d….
Where do signatures appear in a Pearl transaction?
In the witness. A normal Pearl spend uses the Taproot key path, and its whole witness is a single 64-byte Schnorr signature (65 bytes if it names a sighash type other than the default).
The message being signed isn’t the raw transaction. It’s a 32-byte digest built with the TapSighash tagged hash, covering the inputs, outputs and amounts. See sighash.
| Field | Bytes | Value |
|---|---|---|
| Version version | 4 @0 | 1 int32 little-endian. Pearl wallets and pearld coinbases use version 1; relay policy accepts 1-2. |
| Marker marker | 1 @4 | 0x00 Segwit marker (BIP-144): says witness data follows the outputs. |
| Flag flag | 1 @5 | 0x01 Witness flag; 0x01 is the only value Pearl accepts. |
| Input count vin.length | 1 @6 | 1 CompactSize number of inputs. |
| Previous txid vin[0].txid | 32 @7 | 33218153e215d1a1e80941850a696af178fc83bd6cf266be92e7ba9170553959 The transaction being spent, stored byte-reversed (internal order); shown here in normal display order. |
| Output index vin[0].vout | 4 @39 | 683 Which output of that transaction is spent (uint32 LE). |
| scriptSig length vin[0].scriptSig.length | 1 @43 | 0 Always 0 for non-coinbase inputs in Pearl: every spend uses the witness instead. |
| Sequence vin[0].sequence | 4 @44 | 0xffffffff Final (0xffffffff): no RBF signal, no relative locktime, and locktime is ignored for this input. Pearl wallets use this. |
| Output count vout.length | 1 @48 | 2 CompactSize number of outputs. |
| Amount vout[0].value | 8 @49 | 295,646,640 grains (2.95646640 PRL) int64 little-endian, in grains (1 PRL = 100,000,000 grains). |
| Script length vout[0].scriptPubKey.length | 1 @57 | 34 CompactSize length of the locking script. |
| Locking script vout[0].scriptPubKey | 34 @58 | prl1p674s9jt2xs4amk6aq7ul867kcp8qwll5zsvpzzu6revewsu5z6eqv9d5z4 OP_1 OP_PUSHBYTES_32 <output key Q>: a Taproot output. |
| Amount vout[1].value | 8 @92 | 200,000,000 grains (2.00000000 PRL) int64 little-endian, in grains (1 PRL = 100,000,000 grains). |
| Script length vout[1].scriptPubKey.length | 1 @100 | 34 CompactSize length of the locking script. |
| Locking script vout[1].scriptPubKey | 34 @101 | prl1pvt5a5d5z42egswfv7uscht7rv2rj3z4m8n3qp3w35592ydq88v4seuhn70 OP_1 OP_PUSHBYTES_32 <output key Q>: a Taproot output. |
| Witness items witness[0].length | 1 @135 | 1 Number of witness stack items for input 0. |
| Schnorr signature length witness[0][0].length | 1 @136 | 64 CompactSize length of this witness item. |
| Schnorr signature witness[0][0] | 64 @137 | 4686c7014749649b25555874c7772ecb7a44e85454893ed5888db11ebcdc82a22d74ce1a1671d02810f5ba1dca3277409e95a653ba9644099f7da26be7a86ba2 64-byte BIP-340 signature (R.x ‖ s) with SIGHASH_DEFAULT: a Taproot key-path spend. |
| Locktime locktime | 4 @201 | 0 0: no time lock. |
In this real payment, the witness is one item of 40 (64) bytes: R.x = 4686c7014749649b…1ebcdc82a2, then s = 2d74ce1a1671d028…6be7a86ba2.
Why Schnorr rather than ECDSA?
Schnorr signatures are linear: the equation s = k + e·d adds up nicely. That makes several things possible that ECDSA makes hard:
- Key aggregation. Several people can combine keys and produce one ordinary-looking signature (MuSig2).
- Batch verification. Many signatures can be checked together, faster than one by one.
- A clean security proof, and a fixed 64-byte size with no special encoding.
Code: sign and verify in the browser
With the same library this site uses:
import { schnorr } from '@noble/curves/secp256k1.js';
const sig = schnorr.sign(msg32, privKey, auxRand); // 64 bytes: R.x ‖ s
schnorr.verify(sig, msg32, xOnlyPubKey); // true
msg32[31] ^= 1;
schnorr.verify(sig, msg32, xOnlyPubKey); // false: one bit changed In pearld, signing is schnorr.Sign in
signature.go (node/btcec/schnorr/signature.go:376-530 at v1.5.2, on GitHub), called with no options by
txscript/sign.go (node/txscript/sign.go:37-108 at v1.5.2, on GitHub), so the RFC 6979 nonce is used.