Files
amethyst/quartz/src
Claude 70e04523ea feat(cashu): NUT-12 DLEQ verification on every blind signature
Without DLEQ, a malicious or buggy mint can hand us a C' that doesn't
correspond to its published keyset key — we accept it, store the proof
as kind:7375, and only discover the forgery when we try to spend the
proof. By that point any sender already considers the payment done.

NUT-12 fixes this by having the mint return (e, s) alongside each C',
proving in zero knowledge that it used the same private key k for both
C' = k·B' and its published A = k·G. The wallet now verifies this on
every signature it accepts.

What's new:

- Bdhke.verifyDleq(e, s, B', C', A) — Alice-side check:
    R1 = sG - eA
    R2 = sB' - eC'
    e' = SHA256(R1 || R2 || A || C')   (compressed, 33-byte each)
    return e' == e
  Uses the existing ECPoint primitives; point negation via the same
  affine-Y flip pattern already used in unblind(). Defensive against
  malformed inputs — wrong sizes, off-curve points, zero/oversized
  scalars all return false (no exception escapes to callers).

- Bdhke.signFull(B', k, r') — mint-side counterpart, test-only.
  Lets round-trip tests exercise both halves without standing up a
  real mint. Optional r' argument keeps the suite deterministic.

- DleqProofDto on BlindSignatureDto.dleq — optional, parsed from the
  same JSON the mint already returns. Carries e, s, and an optional
  blinding-factor r (only ever non-null in proofs that travel between
  WALLETS — Carol verification, NUT-12 §3, not used here yet).

- CashuMintOperations.unblindOne now verifies the DLEQ proof when
  present. Mismatch → MintProtocolException, aborting the swap/mint
  before any proof event is published. Older mints that don't emit
  the dleq field still pass through (backwards compatible).

Tests: 9 cases on the verify/sign round-trip — happy path, wrong
mint pubkey, tampered C / e / s, wrong-length inputs, zero scalars,
and a determinism check that different DLEQ nonces produce different
proofs that nonetheless both verify.

Two bugs fixed during implementation worth flagging:
1. Secp256k1.pubkeyCreate returns 65-byte UNCOMPRESSED format. The
   first cut of signFull was passing that straight into the hash
   input, overrunning into the C' slot. Now compresses to 33 bytes
   first.
2. Initial verifyDleq rejected `e` scalars whose 32-byte value
   exceeded n. sha256 output can technically be >= n with
   probability ~2^-128 — vanishingly rare in practice, but the
   check was also rejecting valid proofs spuriously through a
   different path. ScalarN.isValid is only applied to `s` now;
   `e` only needs to be non-zero.

https://claude.ai/code/session_01MdWddiar819f8XYt5N8BjP
2026-05-27 15:17:44 +00:00
..
2026-05-12 19:27:12 -04:00