revert(cashu): re-enable NUT-12 DLEQ on own-mint outputs

Restores the per-signature `Bdhke.verifyDleq` check in
`CashuMintOperations.unblindOne` that was removed earlier on this branch
under the (wrong) belief that `verifyDleq`'s allocation density was
triggering the ART JIT crash. The real bug was `uLtInline`
inline-expansion in U256/ScalarN/FieldP (see preceding commit); with
that fixed, `verifyDleq` runs at 2048 iterations/process in the
regression suite (`r_verifyDleq_2048`) with no JIT trouble.

The skip-DLEQ commit argued "a malicious mint can just refuse the
request" so the check buys "fail fast vs fail-at-next-spend, not actual
security." That undersells NUT-12: a key-substituting mint produces
*unspendable* proofs, and pre-emptive DLEQ catches that at mint-receive
time, before the user considers the operation done. Without the check,
the failure surfaces at the next swap — by which point a sender has
already considered the payment complete and any sent token is dead.

Third-party proof verification (incoming cashu tokens, nutzap redeems)
continues to go through `verifyDleqCarol` / `verifyTokenDleq` —
unchanged, still the harder untrust boundary.

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
This commit is contained in:
Vitor Pamplona
2026-05-28 15:14:33 -04:00
co-authored by Claude Opus 4.7
parent 1f3630c4ae
commit 7d5573532f
@@ -675,15 +675,28 @@ class CashuMintOperations(
val mintPubKey = mintPubKeyHex.hexToByteArray()
val cTickBytes = signature.cTick.hexToByteArray()
// NUT-12 DLEQ on the mint's response is intentionally NOT
// verified here. unblindOne only runs for outputs we asked the
// mint to sign (swap, mint-from-LN, swap-to-locked) — a
// malicious mint could just refuse the request, so the DLEQ
// check on our own outputs offers little marginal security
// beyond "fail fast vs. fail-at-next-spend". Third-party proofs
// (incoming cashu tokens, nutzap redeems) continue to be
// Carol-verified via [verifyTokenDleq] — that's where the
// untrust boundary actually lives.
// NUT-12 DLEQ verification. When the mint emits a DLEQ proof
// alongside the blind signature, we MUST verify it before we
// treat the resulting proof as valid — without this check, a
// malicious or buggy mint can hand us a junk C' that fails only
// at spend time, by which point a sender already considers the
// payment complete. Older mints omit the dleq field entirely;
// we accept those silently for backwards compatibility.
signature.dleq?.let { dleq ->
val ok =
Bdhke.verifyDleq(
e = dleq.e.hexToByteArray(),
s = dleq.s.hexToByteArray(),
blindedMessage = output.bTick,
blindSignature = cTickBytes,
mintPubKey = mintPubKey,
)
if (!ok) {
throw MintProtocolException(
"NUT-12 DLEQ verification failed for amount ${output.amount} — mint signature does not match its published keyset key",
)
}
}
val c =
Bdhke.unblind(