Lesson 0002 · ~15 minutesBDHKE by hand: the math that makes a mint blind

The win for today: you'll run the actual blind-signature protocol — real secp256k1, the same curve as Bitcoin — step by step in this page, and be able to say precisely which values cross the wire, which stay secret, and why the algebra makes the mint blind. This is the crypto core of NUT-00; everything you code in a wallet (blinding outputs, unblinding signatures) is these five equations.

The cast, and their secrets

From Lesson 1: a Proof is {amount, id, secret, C}. Today we learn where C comes from. Three keeps-it-secret rules drive everything:

The five equations

#WhoEquationCrosses the wire?
1WalletY = hash_to_curve(x) — map the secret string to a curve pointno
2WalletB_ = Y + rG — blind ityes → mint (as BlindedMessage)
3MintC_ = kB_ — sign blindlyyes → wallet (as BlindSignature)
4WalletC = C_ − rK — unblindno (stored in the Proof)
5Mint, at spend timecheck C == k·hash_to_curve(x), then record x as spentwallet reveals (x, C)

Why does step 4 produce a valid signature? Substitute and watch the blinding cancel:

C = C_ − rK
  = k·B_ − rK          (step 3)
  = k(Y + rG) − r(kG)  (step 2, and K = kG)
  = kY + krG − krG
  = kY                 ← the mint's key applied to your secret's point,
                          though the mint never saw Y

The mint saw only B_ = Y + rG. Since r is uniformly random and never revealed, B_ is a uniformly random point — it carries zero information about Y. At spend time the mint sees (x, C = kY), and has no way to connect Y to any particular Y + rG it once signed. That's the whole trick: same DH-style multiplication you know from key exchange, used as a signature.

Why hash_to_curve is load-bearing

Suppose the wallet could choose the point Y directly instead of deriving it from a hash. Pick Y = aG for a scalar a you know. Then the "signature" is C = kY = a(kG) = aK — computable from the mint's public key, no mint involved. Free money. Forcing Y = hash_to_curve(x) means nobody knows the discrete log of Y, so kY can only come from the holder of k. The exact construction (NUT-00):

msg_hash = SHA256("Secp256k1_HashToCurve_Cashu_" || x)
Y = decompress_point("02" || SHA256(msg_hash || counter))
    // counter: uint32 little-endian, from 0, incremented until the
    // 32 bytes are a valid x-coordinate (~2 tries on average)

Note it's also why one keypair k exists per amount: the signature kY encodes nothing about value, so value must be encoded in which key signed. A mint using one key for 1-sat and 64-sat tokens would let you mint at 1 sat and melt at 64.

Run it yourself

Below is the real protocol on real secp256k1 (check the page source — ~90 lines, including spec-exact hash_to_curve). Step through it; watch where each value lands. Red border = secret, green = public/revealed. The wire log is what a network capture of wallet↔mint HTTP traffic would show.

👛 Wallet (Alice)

🏦 Mint (Bob)

📡 Wire log

nothing sent yet

Do this before moving on: run the full flow once, then hit “Spend same proof again” and “Attempt forgery.” Then reset and run it once more, saying each equation out loud before clicking. That second pass is the one that sticks.

Check yourself

Go deeper

Primary source: the cryptography section of NUT-00 — you can now read it line by line; every symbol should be familiar. Cross-check with the cheat sheet's BDHKE table, which mirrors today's notation.

Two threads we deliberately left hanging, both are future lessons: how does the wallet know the mint used the right k and isn't tagging users with a rogue key? (DLEQ proofs, NUT-12) — and what if I lose my wallet DB of secrets? (deterministic secrets from a seed, NUT-13).