From 7d5573532f4034e07b0e535146e1edcdc4069f75 Mon Sep 17 00:00:00 2001 From: Vitor Pamplona Date: Thu, 28 May 2026 15:14:33 -0400 Subject: [PATCH] revert(cashu): re-enable NUT-12 DLEQ on own-mint outputs MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit 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) --- .../nip60Cashu/mintApi/CashuMintOperations.kt | 31 +++++++++++++------ 1 file changed, 22 insertions(+), 9 deletions(-) diff --git a/quartz/src/jvmAndroid/kotlin/com/vitorpamplona/quartz/nip60Cashu/mintApi/CashuMintOperations.kt b/quartz/src/jvmAndroid/kotlin/com/vitorpamplona/quartz/nip60Cashu/mintApi/CashuMintOperations.kt index 6861591154..1954cd1f55 100644 --- a/quartz/src/jvmAndroid/kotlin/com/vitorpamplona/quartz/nip60Cashu/mintApi/CashuMintOperations.kt +++ b/quartz/src/jvmAndroid/kotlin/com/vitorpamplona/quartz/nip60Cashu/mintApi/CashuMintOperations.kt @@ -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(