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.
From Lesson 1: a Proof is {amount, id, secret, C}. Today we learn where C comes from. Three keeps-it-secret rules drive everything:
k — its private key for one denomination — and publishes K = kG. (G is the secp256k1 generator point you know from onchain Bitcoin.)x — the random secret string. Private until the moment of spending, when it's revealed on purpose.r — the blinding factor. Private forever. This one number is what unlinkability rests on.| # | Who | Equation | Crosses the wire? |
|---|---|---|---|
| 1 | Wallet | Y = hash_to_curve(x) — map the secret string to a curve point | no |
| 2 | Wallet | B_ = Y + rG — blind it | yes → mint (as BlindedMessage) |
| 3 | Mint | C_ = kB_ — sign blindly | yes → wallet (as BlindSignature) |
| 4 | Wallet | C = C_ − rK — unblind | no (stored in the Proof) |
| 5 | Mint, at spend time | check C == k·hash_to_curve(x), then record x as spent | wallet 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.
hash_to_curve is load-bearingSuppose 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.
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.
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.
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).