§ 02

§ 02 · Cryptography

Schnorr signatures

64 bytes that prove you hold the key, for one exact message

A signature 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 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.

Schnorr signature

Sign with the private key, verify with the public key, then break it

P Public key (x-only)

41a72a7b46ce0ab003c631b38d980ade640612963d3e884949a16bf961e28456

m Message digest

bcd410d351ce06c9bdfb6e0cf733d2ba1eeb2854d49d542288096232b29523ac

σ Signature: R.x ‖ s

9af473ce79bba67a44d3a7833ae89e8301f05d1705e533b8887fd36cc2c8e560b9db803321cf0aab34993413b309f4895914117da4aba9dc27b07a90b1fea960

64 bytes: the x-coordinate of a one-time point R, then the number s. Pearl’s default signer picks R deterministically (RFC 6979), so the same key and message always give the same signature.

Flip one bit in the

Signature valid

The equation s·G = R + e·P holds, so whoever made this signature knew the private key for P, and the message is exactly this one.

How verification works

The verifier never sees the private key. It recomputes a challenge number e from R, P and the message, then checks one equation on the curve:

e = TaggedHash(“BIP0340/challenge”, R.x ‖ P.x ‖ m) mod n

R.x
9af473ce79bba67a44d3a7833ae89e8301f05d1705e533b8887fd36cc2c8e560
P.x
41a72a7b46ce0ab003c631b38d980ade640612963d3e884949a16bf961e28456
m
bcd410d351ce06c9bdfb6e0cf733d2ba1eeb2854d49d542288096232b29523ac
e
e5e183f0c353ca76e8e0e571ef42443a2b5a4002e7d8751fafee3ec41f237715
s
b9db803321cf0aab34993413b309f4895914117da4aba9dc27b07a90b1fea960

s·G = R + e·P

Only someone who knows d (P = d·G) could have chosen s = k + e·d so that this balances, and e can’t be known before R is fixed. Signing a different message or with a different key changes e, which is why every flip above fails.

Runs in your browser · nothing is sent src/lib/keys.ts · BIP-340, Pearl’s RFC 6979 signer

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:

  1. Pick a secret nonce

    Choose a one-time secret number k, the signing nonce, and compute the point R = k·G.

  2. Make the challenge

    Hash R’s x-coordinate, the public key and the message with the BIP0340/challenge tagged hash. Read the result as a number e.

  3. Mix the key in

    Compute s = k + e·d, modulo the curve’s order n. The signature is (R.x, s).

e=Hchallenge(Rx‖P‖m),s=k+e⋅d(modn)e = H_{\text{challenge}}(R_x \,\|\, P \,\|\, m), \qquad s = k + e \cdot d \pmod n

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:

s⋅G=R+e⋅Ps \cdot G \;=\; R + e \cdot P
Verify
Why it holds: s·G = (k + e·d)·G = k·G + e·(d·G) = R + e·P.

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:

s1−s2=(k+e1d)−(k+e2d)=(e1−e2)ds_1 - s_2 = (k + e_1 d) - (k + e_2 d) = (e_1 - e_2)\, d

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.

A typical payment: 1 input, 2 outputs 205 bytes
01000000 00 01 01 5939557091bae792be66f26cbd83fc78f16a690a854109e8a1d115e253812133 ab020000 00 ffffffff 02 b0359f1100000000 22 5120d7ab02c96a342bdddb5d07b9f3ebd6c04e077ff41418110b9a1e5997439416b2 00c2eb0b00000000 22 512062e9da3682aab288392cf7218bafc36287288abb3ce200c5d1a50aa234073b2b 01 40 4686c7014749649b25555874c7772ecb7a44e85454893ed5888db11ebcdc82a22d74ce1a1671d02810f5ba1dca3277409e95a653ba9644099f7da26be7a86ba2 00000000
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.
txid
d919e02a4adfd739ceef31d98803f0ddd24e1b1fa67b0fe572cdec42c1b384c9
Size
205 bytes · 154 vbytes
Block
130,825
Fee
7,915 grains

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:

JavaScript
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.

The codex

Suggested

Up and down arrows move through the results, Enter opens one, Escape closes. Anywhere on the site, slash or Command K opens this search, and the square brackets [ and ] go to the previous and next page.