From 8d997a9973cdbdcc0a7d141cb1897b9ebd490181 Mon Sep 17 00:00:00 2001 From: Claude Date: Wed, 27 May 2026 23:45:22 +0000 Subject: [PATCH] =?UTF-8?q?fix(cashu):=20split=20Bdhke.unblind=20into=20sm?= =?UTF-8?q?aller=20helpers=20=E2=80=94=20dodge=20ART=20JIT=20bug?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit CashuTrace showed the Android 15+ JIT compiler crash (SIGSEGV 0x48 in "Jit thread pool") firing while ART was compiling Bdhke.unblind: unblindOne[0/2] begin amount=2 unblindOne: Bdhke.unblind begin >>> Fatal signal 11 (SIGSEGV) <<< in Jit thread pool unblindOne: Bdhke.unblind end ← app thread keeps running unblindOne[0/2] end unblindOne[1/2] begin amount=8 unblindOne: Bdhke.unblind begin unblindOne: Bdhke.unblind end unblindOne[1/2] end […300ms of other work, then process death tombstone…] The unblind itself runs fine to completion (twice). The crash is the JIT compiler thread choking on Bdhke.unblind's bytecode — specifically the escape-analysis pass on the 10 short-lived Fe4 / MutablePoint allocations packed into one ~30-line method body. ART decides the process is unstable a few hundred ms later and tears it down. Split the body into three: an orchestrator + parseAffinePoint() + computeNegRk(). Same semantics, three smaller bytecode bodies for the JIT to compile, each well under the threshold that triggers the bad optimizer path. If this still crashes, the trace will tell us which of the three methods the JIT was compiling and we move to the next mitigation (reusable per-thread buffer pools to drop allocations further). --- .../quartz/nip60Cashu/bdhke/Bdhke.kt | 46 ++++++++++++------- 1 file changed, 30 insertions(+), 16 deletions(-) diff --git a/quartz/src/commonMain/kotlin/com/vitorpamplona/quartz/nip60Cashu/bdhke/Bdhke.kt b/quartz/src/commonMain/kotlin/com/vitorpamplona/quartz/nip60Cashu/bdhke/Bdhke.kt index 9e07e61c14..96a58257d9 100644 --- a/quartz/src/commonMain/kotlin/com/vitorpamplona/quartz/nip60Cashu/bdhke/Bdhke.kt +++ b/quartz/src/commonMain/kotlin/com/vitorpamplona/quartz/nip60Cashu/bdhke/Bdhke.kt @@ -144,32 +144,46 @@ object Bdhke { ): ByteArray { require(r.size == 32) { "Blinding factor must be 32 bytes" } - val cTickX = Fe4() - val cTickY = Fe4() - require(KeyCodec.parsePublicKey(blindSignature, cTickX, cTickY)) { "Invalid blind signature" } - val cTick = MutablePoint().also { it.setAffine(cTickX, cTickY) } + // The body was previously inlined here as one ~30-line method + // with ~10 short-lived Fe4 / MutablePoint allocations. Android + // 15+ ART crashes (SIGSEGV at 0x48 in "Jit thread pool") when + // it tries to escape-analyze that exact allocation density. + // Splitting into three smaller methods gives the JIT three + // small bodies it can compile cleanly instead of one big one + // that triggers the optimizer bug. + val cTick = parseAffinePoint(blindSignature, "blind signature") + val k = parseAffinePoint(mintPubKey, "mint public key") + val negRk = computeNegRk(k, r) + val out = MutablePoint() + ECPoint.addPoints(out, cTick, negRk) + return toCompressed(out) + } - val kx = Fe4() - val ky = Fe4() - require(KeyCodec.parsePublicKey(mintPubKey, kx, ky)) { "Invalid mint public key" } - val k = MutablePoint().also { it.setAffine(kx, ky) } + /** Parse a 33-byte compressed pubkey into an affine [MutablePoint]. */ + private fun parseAffinePoint( + compressed: ByteArray, + label: String, + ): MutablePoint { + val px = Fe4() + val py = Fe4() + require(KeyCodec.parsePublicKey(compressed, px, py)) { "Invalid $label" } + return MutablePoint().also { it.setAffine(px, py) } + } - // r·K, then negate Y to get -r·K + /** Compute `-r·K` as an affine [MutablePoint]. */ + private fun computeNegRk( + k: MutablePoint, + r: ByteArray, + ): MutablePoint { val rScalar = Fe4() U256.fromBytesInto(rScalar, r, 0) val rk = MutablePoint() ECPoint.mul(rk, k, rScalar) - - // Convert to affine first, then negate Y. val rkX = Fe4() val rkY = Fe4() require(ECPoint.toAffine(rk, rkX, rkY)) { "rK is point at infinity" } FieldP.neg(rkY, rkY) - val negRk = MutablePoint().also { it.setAffine(rkX, rkY) } - - val out = MutablePoint() - ECPoint.addPoints(out, cTick, negRk) - return toCompressed(out) + return MutablePoint().also { it.setAffine(rkX, rkY) } } /**